Skip to main content

Deep Dive into Typeclasses - Haskell Notes 4

Free2018-05-12#Functional_Programming#Haskell typeclass#Haskell Functor#Haskell类型类#Haskell kind#Haskell类型的类型#Haskell函子

A very interesting type system.

0. Typeclass vs. Class

A Typeclass is the equivalent of an interface definition in Haskell, used to declare a set of behaviors.

In OOP, a Class is an object template used to describe real-world entities and encapsulate their internal state. In FP, there is no such thing as internal state, so Class in a functional context refers to an interface. Deriving from a class (deriving (SomeTypeclass)) means possessing the behaviors defined by that class, which is analogous to implementing an interface in OOP and thus possessing the behaviors defined by that interface.

1. Declaration

The class keyword is used to define a new typeclass:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  x == y = not (x /= y)
  x /= y = not (x == y)

Here, a is a type variable, and a concrete type is provided when defining an instance. The first two type declarations are the behaviors defined by the interface (described by defining function types). The last two function implementations are optional, describing the relationship between these two functions via indirect recursive definition, so providing an implementation for just one function is sufficient (this approach is called a minimal complete definition).

P.S. In the GHCi environment, you can use the :info <typeclass> command to see which functions the class defines and which types belong to that class.

2. Implementation

The instance keyword is used to define an instance of a specific typeclass:

instance Eq TrafficLight where
  Red == Red = True
  Green == Green = True
  Yellow == Yellow = True
  _ == _ = False

Here, the type variable a in class Eq a is replaced with the concrete TrafficLight type, and the == function is implemented (implementing /= simultaneously is unnecessary because the relationship between the two is declared in the Eq class).

Try making a custom type a member of the Show class:

data Answer = Yes | No | NoExcuse
instance Show Answer where
  show Yes = "Yes, sir."
  show No = "No, sir."
  show NoExcuse = "No excuse, sir."

Give it a try:

> Yes
Yes, sir.

P.S. In the GHCi environment, you can use the :info <type> command to see which typeclasses a type belongs to.

Subclasses

Similarly, there is the concept of a subclass, which refers to a constraint that to become a member of class B, one must first be a member of class A:

class (Eq a) => Num a where
-- ...

This requires members of the Num class to first be members of the Eq class. Syntactically, it just looks like an additional type constraint. Similarly, another example:

instance (Eq m) => Eq (Maybe m) where
  Just x == Just y = x == y
  Nothing == Nothing = True
  _ == _ = False

Here, the type variable m in Maybe m must be a member of the Eq class before Maybe m itself can be a member of the Eq class.

3. Functor

Functor (sounds impressive) is also a typeclass, representing something that can be mapped over.

class Functor f where
  fmap :: (a -> b) -> f a -> f b

fmap takes a function that maps a to b, and a parameter of type f a, and returns a value of type f b.

This might look a bit confusing; type f a refers to a type with a type parameter, such as Maybe, List, etc. For example:

mapMaybe :: Eq t => (t -> a) -> Maybe t -> Maybe a
mapMaybe f m
  | m == Nothing = Nothing
  | otherwise = Just (f x)
  where (Just x) = m

Here, Maybe t -> Maybe a is an example of f a -> f b. Give it a try:

> mapMaybe (> 0) (Just 3)
Just True

map a to b here refers to converting Maybe Num to Maybe Bool:

Just 3 :: Num a => Maybe a
Just True :: Maybe Bool

Therefore, the behavior defined by Functor is to keep the outer type unchanged (f a, where f is the type constructor), while allowing the inner type to change via mapping (the fmap function) (changing f a to f b, where a and b are concrete types).

In the context of a List, this means allowing the contents of a List to be mapped to another List, where the type of the contents in the new List can differ. Regardless, the result of fmap is still a List a (where a is a type variable).

This sounds very natural, as List already belongs to the Functor class, and:

map :: (a -> b) -> [a] -> [b]

This is precisely a concrete implementation of the fmap :: (a -> b) -> f a -> f b type definition. In fact, this map is that fmap:

instance Functor [] where
  fmap = map

Both Maybe and List belong to the Functor class. What do they have in common?

They both act like containers. And the behavior defined by fmap is exactly mapping the contents (values) inside the container and then putting them back in.

There are also some special scenarios, such as Either:

data Either a b = Left a | Right b 	-- Defined in ‘Data.Either’

Either's type constructor has two type parameters, while f in fmap :: (a -> b) -> f a -> f b only accepts one. Therefore, Either's fmap requires the left type to be fixed:

mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

The left side is not mapped because mapping could change its type, and Either a (the f in fmap :: (a -> b) -> f a -> f b) cannot change, so it's treated just like Nothing. For example:

> mapEither show (Right 3)
Right "3"
> mapEither show (Left 3)
Left 3

Another similar one is Map:

-- 给Data.Map起了别名Map
data Map.Map k a -- ...

When mapping over Map k v, k should not change, so only the values are mapped:

mapMap :: Ord k => (t -> a) -> Map.Map k t -> Map.Map k a
mapMap f m = Map.fromList (map (\(k ,v) -> (k, f v)) xs)
  where xs = Map.toList m

For example:

> mapMap (+1) (Map.insert 'a' 2 Map.empty)
fromList [('a',3)]
> mapMap (+1) Map.empty
fromList []

P.S. These simple implementations can be verified for correctness by comparing them with the standard library implementation, for example:

> fmap (+1) (Map.insert 'a' 2 Map.empty )
fromList [('a',3)]

P.S. Additionally, implementing a Functor requires following certain rules, such as not wanting the order of elements in a List to change, or wanting a binary search tree to preserve its structural properties.

4. Kind

Values (including functions) participate in operations, and types are attributes of values. Thus, values can be classified by type. Through this attribute carried by a value, some of its properties can be inferred. Similarly, a kind is the type of a type, acting as a classification for types.

In the GHCi environment, you can view the type of a type using the :kind command, for example:

> :k Int
Int :: *
> :k Maybe
Maybe :: * -> *
> :k Maybe Int
Maybe Int :: *
> :k Either
Either :: * -> * -> *
> :k Either Bool
Either Bool :: * -> *
> :k Either Bool Int
Either Bool Int :: *

Int :: * indicates that Int is a concrete type. Maybe :: * -> * indicates that Maybe takes one concrete type as a parameter and returns a concrete type. Either :: * -> * -> * indicates that Either takes two concrete types as parameters and returns a concrete type. This is similar to function calls and features currying, allowing for partial application.

There are even stranger kinds, for example:

data Frank a b  = Frank {frankField :: b a} deriving (Show)

The type of the parameter frankField for the value constructor Frank is restricted to b a. Thus, b is * -> * and a is a concrete type *. Therefore, the kind of the Frank type constructor is:

Frank :: * -> (* -> *) -> *

Where the first * is parameter a, the middle * -> * is parameter b, and the final * indicates the return of a concrete type. It can be populated as follows:

> :t Frank {frankField = Just True}
Frank {frankField = Just True} :: Frank Bool Maybe
> :t Frank {frankField = "hoho"}
Frank {frankField = "hoho"} :: Frank Char []

Looking back at the Functor implementation for Either:

> :k Either
Either :: * -> * -> *
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b

The kind of Either is * -> * -> * (requires two concrete type parameters), while fmap wants f to be * -> * (requires only one concrete type parameter). Therefore, Either should be partially applied by filling in one parameter to make it * -> *. Thus, the implementation of mapEither is:

mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

Either a is a standard * -> *. For example:

> :k Either Int
Either Int :: * -> *

P.S. You can also apply this to typeclasses, for example:

> :k Functor
Functor :: (* -> *) -> Constraint
> :k Eq
Eq :: * -> Constraint

Where Constraint is also a kind, representing that it must be an instance of a certain class (i.e., a type constraint, often seen to the left of => in function signatures), such as Num. See What does has kind 'Constraint' mean in Haskell for details.

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment