diff --git a/part1.html b/part1.html index ee92b72..655f919 100644 --- a/part1.html +++ b/part1.html @@ -160,12 +160,11 @@ code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warni
Nothing is a constructor.
empty = []
size xs = length xs
sameSize x y = size x == size y
-The Haskell 2010 allows only a very specific type of class instance. Let’s look at some instances that aren’t allowed. The examples use the Size class:
class Size a where
- size :: a -> IntWe saw a Size [a] instance above. Why not define an instance just for lists of booleans?
instance Size [Bool] where
- size bs = length (filter id bs) -- count Trueserror:
- • Illegal instance declaration for ‘Size [Bool]’
- (All instance types must be of the form (T a1 ... an)
- where a1 ... an are *distinct type variables*,
- and each type variable appears at most once in the instance head.
- Use FlexibleInstances if you want to disable this.)
- • In the instance declaration for ‘Size [Bool]’
-Dang. As the error tries to tell us, we can only define instances where all type parameters are different type variables. That is, we can define instance Size (Either a b) but we can’t define:
instance Size (Either String a) – since String is not a type variableinstance Size (Either a a) – since the type variables aren’t differentinstance Size [[a]] – since [a] is not a type variableWhy is this? This rule guarantees that it’s simple for the compiler to look up the correct type class instance. It can just look at what the top-level type constructor is, and then pick an instance.
-The GHC Haskell implementation has many extensions to the type class system – we’ll get back to some of them in part 2 of this course.
-Did you notice how in the previous example we gave sameSize the same definition in both instances? This is a very common occurrence, and it’s why Haskell classes can have default implementations. As a first example, here’s an Example type class for giving example values of types.
class Example a where
- example :: a -- the main example for the type `a`
- examples :: [a] -- a short list of examples
- examples = [example] -- ...defaulting to just the main example
-
-instance Example Int where
- example = 1
- examples = [0,1,2]
-
-instance Example Bool where
- example = Trueclass Example a where
+ example :: a -- the main example for the type `a`
+ examples :: [a] -- a short list of examples
+ examples = [example] -- ...defaulting to just the main example
+
+instance Example Int where
+ example = 1
+ examples = [0,1,2]
+
+instance Example Bool where
+ example = TrueHere’s how Example works. Note how the default implementation of examples got used in the Bool case but not in the Int case. Also note the need for explicit type signatures to tell GHCi which instance we’re interested in. Without them, we would get an “Ambiguous type variable” error.
Prelude> example :: Bool
-True
-Prelude> example :: Int
-1
-Prelude> examples :: [Bool]
-[True]
-Prelude> examples :: [Int]
-[0,1,2]Prelude> example :: Bool
+True
+Prelude> example :: Int
+1
+Prelude> examples :: [Bool]
+[True]
+Prelude> examples :: [Int]
+[0,1,2]The standard type classes use lots of default implementations to make implementing the classes easy. Here is the standard definitions for Eq (formatted for readability).
class Eq a where
- (==) :: a -> a -> Bool
- x == y = not (x /= y)
-
- (/=) :: a -> a -> Bool
- x /= y = not (x == y)class Eq a where
+ (==) :: a -> a -> Bool
+ x == y = not (x /= y)
+
+ (/=) :: a -> a -> Bool
+ x /= y = not (x == y)Note how both operations have a default implementation in terms of the other. This means we could define an Eq instance with no content at all, but the resulting functions would just recurse forever. In practice, we want to define at least one of == and /=.
When there are lots of default implementations, it can be hard to know which functions you need to implement yourself. For this reason class documentation usually mentions the minimal complete definition. For Eq, the docs say “Minimal complete definition: either == or /=.”
Let’s look at Ord next. Ord has 7 operations, all with default implementations in terms of each other. By the way, note the quirky way of defining multiple type signatures at once. It’s okay, it’s a feature of Haskell, this is how Ord is defined in the standard. (We’ll get back to what the (Eq a) => part means soon.)
class (Eq a) => Ord a where
- compare :: a -> a -> Ordering
- (<), (<=), (>=), (>) :: a -> a -> Bool
- max, min :: a -> a -> a
-
- compare x y | x == y = EQ
- | x <= y = LT
- | otherwise = GT
-
- x <= y = compare x y /= GT
- x < y = compare x y == LT
- x >= y = compare x y /= LT
- x > y = compare x y == GT
-
- max x y | x <= y = y
- | otherwise = x
- min x y | x <= y = x
- | otherwise = yclass (Eq a) => Ord a where
+ compare :: a -> a -> Ordering
+ (<), (<=), (>=), (>) :: a -> a -> Bool
+ max, min :: a -> a -> a
+
+ compare x y | x == y = EQ
+ | x <= y = LT
+ | otherwise = GT
+
+ x <= y = compare x y /= GT
+ x < y = compare x y == LT
+ x >= y = compare x y /= LT
+ x > y = compare x y == GT
+
+ max x y | x <= y = y
+ | otherwise = x
+ min x y | x <= y = x
+ | otherwise = yWith this definition it’s really hard to know what the minimal complete definition is. Luckily the docs tell us “Minimal complete definition: either compare or <=.”
As a final word on default implementations, if there is never a need to override the default definition, the function can be moved out of the class for simplicity. Consider a class like Combine below:
class Combine a where
- combine :: a -> a -> a
- combine3 :: a -> a -> a -> a
- combine3 x y z = combine x (combine y z)class Combine a where
+ combine :: a -> a -> a
+ combine3 :: a -> a -> a -> a
+ combine3 x y z = combine x (combine y z)It’s hard to think of a case where combine3 would be given any other definition, so why not move it out of the class:
class Combine a where
- combine :: a -> a -> a
-
-combine3 :: Combine a => a -> a -> a -> a
-combine3 x y z = combine x (combine y z)class Combine a where
+ combine :: a -> a -> a
+
+combine3 :: Combine a => a -> a -> a -> a
+combine3 x y z = combine x (combine y z)As an example, here are the Eq and Ord instances for a simple pair type. Note how the definition uses the minimal complete definition rules by only defining == and <=.
data IntPair = IntPair Int Int
- deriving Show
-
-instance Eq IntPair where
- IntPair a1 a2 == IntPair b1 b2 = a1==b1 && a2==b2
-
-instance Ord IntPair where
- IntPair a1 a2 <= IntPair b1 b2
- | a1<b1 = True
- | a1>b1 = False
- | otherwise = a2<=b2*Main> (IntPair 1 2) < (IntPair 2 3)
-True
-*Main> (IntPair 1 2) > (IntPair 2 3)
-False
-*Main> compare (IntPair 1 2) (IntPair 2 3)
-LT
-*Main Data.List> sort [IntPair 1 1,IntPair 1 4,IntPair 2 1,IntPair 2 2]
-[IntPair 1 1,IntPair 1 4,IntPair 2 1,IntPair 2 2]data IntPair = IntPair Int Int
+ deriving Show
+
+instance Eq IntPair where
+ IntPair a1 a2 == IntPair b1 b2 = a1==b1 && a2==b2
+
+instance Ord IntPair where
+ IntPair a1 a2 <= IntPair b1 b2
+ | a1<b1 = True
+ | a1>b1 = False
+ | otherwise = a2<=b2*Main> (IntPair 1 2) < (IntPair 2 3)
+True
+*Main> (IntPair 1 2) > (IntPair 2 3)
+False
+*Main> compare (IntPair 1 2) (IntPair 2 3)
+LT
+*Main Data.List> sort [IntPair 1 1,IntPair 1 4,IntPair 2 1,IntPair 2 2]
+[IntPair 1 1,IntPair 1 4,IntPair 2 1,IntPair 2 2]As we’ve seen many times already, deriving is a way to get automatically generated class instances. The Read and Show classes should pretty much always be derived to get the standard behaviour. The derived instance for Eq is typically what you want. It requires constructors and fields to match.
The derived Ord instance might not be what you want. It orders constructors left-to-right, and then compares fields inside constructors left-to-right. An example:
data Person = Dead | Alive String Int
- deriving (Show, Eq, Ord)Prelude> Dead < Alive "Bob" 35 -- constructors are ordered left-to-right
-True
-Prelude> Alive "Barbara" 35 < Alive "Clive" 17 -- names are compared before ages
-True
-Prelude> Alive "Clive" 17 < Alive "Clive" 30 -- finally, ages are compared if names match
-Truedata Person = Dead | Alive String Int
+ deriving (Show, Eq, Ord)Prelude> Dead < Alive "Bob" 35 -- constructors are ordered left-to-right
+True
+Prelude> Alive "Barbara" 35 < Alive "Clive" 17 -- names are compared before ages
+True
+Prelude> Alive "Clive" 17 < Alive "Clive" 30 -- finally, ages are compared if names match
+TrueYou can use the :info command in GHCi to get the contents and instances of a class. These days the info even includes the minimal complete definition (see the MINIMAL pragma). For example:
Prelude> :info Num
-class Num a where
- (+) :: a -> a -> a
- (-) :: a -> a -> a
- (*) :: a -> a -> a
- negate :: a -> a
- abs :: a -> a
- signum :: a -> a
- fromInteger :: Integer -> a
- {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
- -- Defined in ‘GHC.Num’
-instance Num Word -- Defined in ‘GHC.Num’
-instance Num Integer -- Defined in ‘GHC.Num’
-instance Num Int -- Defined in ‘GHC.Num’
-instance Num Float -- Defined in ‘GHC.Float’
-instance Num Double -- Defined in ‘GHC.Float’Prelude> :info Num
+class Num a where
+ (+) :: a -> a -> a
+ (-) :: a -> a -> a
+ (*) :: a -> a -> a
+ negate :: a -> a
+ abs :: a -> a
+ signum :: a -> a
+ fromInteger :: Integer -> a
+ {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
+ -- Defined in ‘GHC.Num’
+instance Num Word -- Defined in ‘GHC.Num’
+instance Num Integer -- Defined in ‘GHC.Num’
+instance Num Int -- Defined in ‘GHC.Num’
+instance Num Float -- Defined in ‘GHC.Float’
+instance Num Double -- Defined in ‘GHC.Float’Both classes and instances can form hierarchies. This means that a class or instance depends on another class or instance.
-Let’s try to define an Eq instance for a simple pair type:
data Pair a = MakePair a a
- deriving Show
-
-instance Eq (Pair a) where
- (MakePair x y) == (MakePair a b) = x==a && y==berror:
- • No instance for (Eq a) arising from a use of ‘==’
- Possible fix: add (Eq a) to the context of the instance declaration
- • In the first argument of ‘(&&)’, namely ‘x == a’
- In the expression: x == a && y == b
- In an equation for ‘==’:
- (MakePair x y) == (MakePair a b) = x == a && y == bdata Pair a = MakePair a a
+ deriving Show
+
+instance Eq (Pair a) where
+ (MakePair x y) == (MakePair a b) = x==a && y==berror:
+ • No instance for (Eq a) arising from a use of ‘==’
+ Possible fix: add (Eq a) to the context of the instance declaration
+ • In the first argument of ‘(&&)’, namely ‘x == a’
+ In the expression: x == a && y == b
+ In an equation for ‘==’:
+ (MakePair x y) == (MakePair a b) = x == a && y == bThe compiler is trying to tell us that our Eq (Pair a) instance needs an Eq a instance to work. How can we compare pairs of values of type a if we can’t compare values of type a? To solve this we need to add a type constraint to the instance declaration, just like we’ve added type constraints to function definitions.
instance Eq a => Eq (Pair a) where
- (MakePair x y) == (MakePair a b) = x==a && y==binstance Eq a => Eq (Pair a) where
+ (MakePair x y) == (MakePair a b) = x==a && y==bNow we can compare pairs, as long as the element type is comparable. However, we can’t compare, say, pairs of functions, since functions don’t have an Eq instance.
-Prelude> MakePair 1 1 == MakePair 1 1
-True
-Prelude> MakePair reverse reverse == MakePair reverse reverse
-
-<interactive>:15:1: error:
- • No instance for (Eq ([a0] -> [a0])) arising from a use of ‘==’
- (maybe you haven't applied a function to enough arguments?)
- • In the expression:
- MakePair reverse reverse == MakePair reverse reverse
- In an equation for ‘it’:
- it = MakePair reverse reverse == MakePair reverse reversePrelude> MakePair 1 1 == MakePair 1 1
+True
+Prelude> MakePair reverse reverse == MakePair reverse reverse
+
+<interactive>:15:1: error:
+ • No instance for (Eq ([a0] -> [a0])) arising from a use of ‘==’
+ (maybe you haven't applied a function to enough arguments?)
+ • In the expression:
+ MakePair reverse reverse == MakePair reverse reverse
+ In an equation for ‘it’:
+ it = MakePair reverse reverse == MakePair reverse reverseLet’s continue with another example. Here’s a simple type class and an instance
-class Check a where
- check :: a -> Bool
-
-instance Check Int where
- check x = x > 0class Check a where
+ check :: a -> Bool
+
+instance Check Int where
+ check x = x > 0Now we can write a function that checks a list. We use the standard library function and :: [Bool] -> Bool that checks if a list is all Trues.
checkAll :: Check a => [a] -> Bool
-checkAll xs = and (map check xs)checkAll :: Check a => [a] -> Bool
+checkAll xs = and (map check xs)In order to turn this into a Check [a] instance, we need to add a constraint to the instance declaration. Our Check [a] is based on the Check a instance.
instance Check a => Check [a] where
- check xs = and (map check xs)instance Check a => Check [a] where
+ check xs = and (map check xs)This means that our Check [a] instance is only valid when there is a corresponding Check a instance. For example, if we try to invoke a Check [Bool] instance, we get an error about the missing Check Bool instance:
Prelude> check [True,False]
-
-<interactive>:1:1: error:
- • No instance for (Check Bool) arising from a use of ‘check’
- • In the expression: check [True, False]
- In an equation for ‘it’: it = check [True, False]Prelude> check [True,False]
+
+<interactive>:1:1: error:
+ • No instance for (Check Bool) arising from a use of ‘check’
+ • In the expression: check [True, False]
+ In an equation for ‘it’: it = check [True, False]Also, if we try to define Check [a] instance without the constraint, we get an error (with a pretty good suggestion!)
• No instance for (Check a) arising from a use of ‘check’
- Possible fix:
- add (Check a) to the context of the instance declaration • No instance for (Check a) arising from a use of ‘check’
+ Possible fix:
+ add (Check a) to the context of the instance declarationIf you think about it, this instance hierarchy allows us to circumvent the limitation that we can’t make a Check [Int] instance.
Finally, sometimes multiple constraints are needed. Consider for example the Eq instance for Either:
instance (Eq a, Eq b) => Eq (Either a b) where
- Left x == Left y = x==y
- Right x == Right y = x==y
- _ == _ = Falseinstance (Eq a, Eq b) => Eq (Either a b) where
+ Left x == Left y = x==y
+ Right x == Right y = x==y
+ _ == _ = FalseRespectively, a class can depend on another class. This is useful for instance when you want to use functions from another class in your default implementations:
-class Size a where
- size :: a -> Int
-
-class Size a => SizeBoth a where
- sizeBoth :: a -> a -> Int
- sizeBoth x y = size x + size yclass Size a where
+ size :: a -> Int
+
+class Size a => SizeBoth a where
+ sizeBoth :: a -> a -> Int
+ sizeBoth x y = size x + size yIn cases like this we say SizeBoth is a subclass of Size. Note again the confusion with object oriented programming. Examples of subclasses in the standard library include:
class Eq a => Ord a where
- ...
-class Num a => Fractional a where
- ...class Eq a => Ord a where
+ ...
+class Num a => Fractional a where
+ ...Another way to look at subclasses is that if you have a class Main a => Sub a, you must provide an instance Main MyType in order to be able to declare instance Sub MyType.
What are the functions in the Eq class?
Nothing is a constructor.
Given the following definition of the class BitOperations
class BitOperations a where
- bitNot :: a -> a
- bitNot x = bitNand x x
- bitAnd :: a -> a -> a
- bitAnd x y = bitNot (bitOr (bitNot x) (bitNot y))
- bitOr :: a -> a -> a
- bitOr x y = bitNot (bitAnd (bitNot x) (bitNot y))
- bitNand :: a -> a -> a
- bitNand x y = bitNot (bitAnd x y)class BitOperations a where
+ bitNot :: a -> a
+ bitNot x = bitNand x x
+ bitAnd :: a -> a -> a
+ bitAnd x y = bitNot (bitOr (bitNot x) (bitNot y))
+ bitOr :: a -> a -> a
+ bitOr x y = bitNot (bitAnd (bitNot x) (bitNot y))
+ bitNand :: a -> a -> a
+ bitNand x y = bitNot (bitAnd x y)which set of operations is not a minimal complete definition of BitOperations?
Fractional, I also get an instance for
If I define an instance for Num, I also get an instance for Fractional
Num, I also get an instance for F
7.1 Modeling with Boxes
Sometimes you don’t need a new type, but instead can just reuse a standard type. For example, repesenting car register plate numbers with String. However, if your code is full of Strings, it can be easy to accidentally mix up e.g. a car’s model and registration in a function like registerCar :: String -> String -> CarRegistry -> CarRegistry.
For situations like this it’s common to create a new type that just contains a String (a “boxed” string):
-data Plate = Plate String
- deriving (Show, Eq)
+data Plate = Plate String
+ deriving (Show, Eq)
We can now give registerCar a slightly nicer type, String -> Plate -> CarRegistry -> CarRegistry. Additionally, we can restrict the operations that are possible on Plates to a subset of those that are possible on strings. For example, there is no need to combine the register plate numbers of two cars. Thus we don’t need to offer a function concatPlates :: Plate -> Plate -> Plate. We can also define a smart constructor for Plate that checks that the register number is in the correct format:
-parsePlate :: String -> Maybe Plate
-parsePlate string
- | correctPlateNumber string = Just (Plate string)
- | otherwise = Nothing
+parsePlate :: String -> Maybe Plate
+parsePlate string
+ | correctPlateNumber string = Just (Plate string)
+ | otherwise = Nothing
Here’s another example: representing money. If we just store money as Ints, the compiler won’t protect us from mistakes like multiplying money with money. If instead we implement our own Money type that wraps Int, we get type safety. Additionally, we can encapsulate the fact that money is represented as an integer amount of cents.
-data Money = Money Int
- deriving Show
-
-renderMoney :: Money -> String
-renderMoney (Money cents) = show (fromIntegral cents / 100)
-
-(+!) :: Money -> Money -> Money
-(Money a) +! (Money b) = Money (a+b)
-
-scale :: Money -> Double -> Money
-scale (Money a) x = Money (round (fromIntegral a * x))
-
-addVat :: Money -> Money
-addVat m = m +! scale m 0.24
-renderMoney (Money 100 +! Money 150)
- ==> "2.5"
-
-scale (Money 299) 0.24
- ==> Money 72
-
-addVat (Money 299)
- ==> Money 371
+data Money = Money Int
+ deriving Show
+
+renderMoney :: Money -> String
+renderMoney (Money cents) = show (fromIntegral cents / 100)
+
+(+!) :: Money -> Money -> Money
+(Money a) +! (Money b) = Money (a+b)
+
+scale :: Money -> Double -> Money
+scale (Money a) x = Money (round (fromIntegral a * x))
+
+addVat :: Money -> Money
+addVat m = m +! scale m 0.24
+renderMoney (Money 100 +! Money 150)
+ ==> "2.5"
+
+scale (Money 299) 0.24
+ ==> Money 72
+
+addVat (Money 299)
+ ==> Money 371
Note! If you’re familiar with Object-Oriented Programming, this is a bit like encapsulation.
7.2 Modeling with Cases
Haskell’s algebraic datatypes are really powerful at modeling things based on cases. It’s often useful to think of types as defining the set of possible cases, and functions handling those cases (often via pattern matching). Let’s look at two examples.
Since it’s so easy to define custom types in Haskell, it’s quite convenient to use more descriptive types instead of booleans or strings. Consider a list of persons. In some other language if you wanted to sort the persons into ascending order by name you might use a call like sortPersons(persons, "name", true). In Haskell you can do this instead:
-data Person = Person {name :: String, age :: Int}
- deriving Show
-
-data SortOrder = Ascending | Descending
-data SortField = Name | Age
-
-sortByField :: SortField -> [Person] -> [Person]
-sortByField Name ps = sortBy (comparing name) ps
-sortByField Age ps = sortBy (comparing age) ps
-
-sortPersons :: SortField -> SortOrder -> [Person] -> [Person]
-sortPersons field Ascending ps = sortByField field ps
-sortPersons field Descending ps = reverse (sortByField field ps)
-
-persons = [Person "Fridolf" 73, Person "Greta" 60, Person "Hans" 65]
-sortPersons Name Ascending persons
- ==> [Person {name = "Fridolf", age = 73},Person {name = "Greta", age = 60},Person {name = "Hans", age = 65}]
-sortPersons Age Descending persons
- ==> [Person {name = "Fridolf", age = 73},Person {name = "Hans", age = 65},Person {name = "Greta", age = 60}]
+data Person = Person {name :: String, age :: Int}
+ deriving Show
+
+data SortOrder = Ascending | Descending
+data SortField = Name | Age
+
+sortByField :: SortField -> [Person] -> [Person]
+sortByField Name ps = sortBy (comparing name) ps
+sortByField Age ps = sortBy (comparing age) ps
+
+sortPersons :: SortField -> SortOrder -> [Person] -> [Person]
+sortPersons field Ascending ps = sortByField field ps
+sortPersons field Descending ps = reverse (sortByField field ps)
+
+persons = [Person "Fridolf" 73, Person "Greta" 60, Person "Hans" 65]
+sortPersons Name Ascending persons
+ ==> [Person {name = "Fridolf", age = 73},Person {name = "Greta", age = 60},Person {name = "Hans", age = 65}]
+sortPersons Age Descending persons
+ ==> [Person {name = "Fridolf", age = 73},Person {name = "Hans", age = 65},Person {name = "Greta", age = 60}]
Note how you can’t accidentally typo the field name (unlike with strings), and how you don’t need to remember whether true refers to ascending or descending order.
Let’s move on to the next example. Many Haskell functions don’t work with empty lists (consider head []). If you’re writing code that needs to track whether lists are possibly empty or guaranteed to not be empty, you can use the NonEmpty type from the Data.List.NonEmpty module.
Consider the definition of NonEmpty:
-data NonEmpty a = a :| [a]
+data NonEmpty a = a :| [a]
Here the type represents a lack of cases. The type NonEmpty a will always consist of a value of type a, and some further as, collected in a list. Here are some example values of NonEmpty Int:
-1 :| [2,3,4]
-1 :| []
+1 :| [2,3,4]
+1 :| []
By the way, this is also an example of an infix constructor. We’ve already met another infix constructor earlier, the list constructor (:). Any operator that begins with a colon (the : character) can be used as an infix constructor. We can pattern match on (:|) just like on (:), as you’ll see in the examples below.
Here are the functions that convert between normal lists and nonempty lists. Note how we can’t have a function [a] -> NonEmpty a, but must instead use Maybe to represent the possibility that the list was, indeed, empty. Note also how toList has only one equation, we can’t have a toList [] situation due to the type NonEmpty.
-nonEmpty :: [a] -> Maybe (NonEmpty a)
-nonEmpty [] = Nothing
-nonEmpty (x:xs) = Just (x :| xs)
-
-toList :: NonEmpty a -> [a]
-toList (x :| xs) = x : xs
-nonEmpty [1,2,3] ==> Just (1 :| [2,3])
-nonEmpty [1] ==> Just (1 :| [])
-nonEmpty [] ==> Nothing
-toList (1 :| [2,3]) ==> [1,2,3]
+nonEmpty :: [a] -> Maybe (NonEmpty a)
+nonEmpty [] = Nothing
+nonEmpty (x:xs) = Just (x :| xs)
+
+toList :: NonEmpty a -> [a]
+toList (x :| xs) = x : xs
+nonEmpty [1,2,3] ==> Just (1 :| [2,3])
+nonEmpty [1] ==> Just (1 :| [])
+nonEmpty [] ==> Nothing
+toList (1 :| [2,3]) ==> [1,2,3]
Here are head and last implemented for NonEmpty:
-neHead (x :| _) = x
-neLast (x :| []) = x
-neLast (_ :| xs) = last xs
-neHead (1:|[2,3]) ==> 1
-neLast (1:|[2,3]) ==> 3
+neHead (x :| _) = x
+neLast (x :| []) = x
+neLast (_ :| xs) = last xs
+neHead (1:|[2,3]) ==> 1
+neLast (1:|[2,3]) ==> 3
By the way, these functions are available as Data.List.NonEmpty.head and Data.List.NonEmpty.last along with many other useful functions.
In summary, if you write types that represent all possible cases for your values, and then write functions that handle those cases, your code will be simple and correct.
7.3 Monoids
@@ -3780,197 +3757,197 @@ If I define an instance for Num, I also get an instance for F
A pattern that comes up surprisingly often in functional programming is the monoid (not to be confused with a monad!). Explanations of monoids are often very mathematical, but the idea is simple: combining things.
7.3.1 Associative Operations
Many functions and operators we use are associative. This is just a fancy way of saying they don’t need parentheses. For example, all of these expressions have the value 16 because addition is associative:
-(1 + 3) + (5 + 7)
- 1 + (3 + (5 + 7))
- 1 + 3 + 5 + 7
+(1 + 3) + (5 + 7)
+ 1 + (3 + (5 + 7))
+ 1 + 3 + 5 + 7
Examples of associative operations are easy to come by in Haskell. For example the ++ operator for catenating lists is associative: it doesn’t matter whether you do ([1] ++ [2,3]) ++ [4] or [1] ++ ([2,3] ++ [4]) – the result is [1,2,3,4].
Another great example is the function composition operator. Both (head . tail) . tail and head . (tail . tail) compute the third element of a list.
However not all operators are associative. The most familiar examples are subtraction and exponentiation. (1-2)-3 is -4 but 1-(2-3) is 2. Similarly, (2^3)^2 is 64 while 2^(3^2) is 512. One needs to be careful with parentheses when using operators that are not associative.
Another operator that’s not associative is the list constructor, (:). This time the reason is even more fundamental: while True:(False:[]) is ok, (True:False):[] does not even type! In order for an operation to be associative, it needs to take two arguments of the same type.
In addition to operators, functions can also be associative. The syntax looks a bit different, but a function f is associative if these are the same:
-f x (f y z)
-f (f x y) z
+f x (f y z)
+f (f x y) z
Two widely-used associative functions are the min and max functions:
-min 2 (min 1 3) ==> 1
-min (min 2 1) 3 ==> 1
-
-max 2 (max 1 3) ==> 3
-max (max 2 1) 3 ==> 3
+min 2 (min 1 3) ==> 1
+min (min 2 1) 3 ==> 1
+
+max 2 (max 1 3) ==> 3
+max (max 2 1) 3 ==> 3
7.3.2 Semigroups
Mathematically speaking, an associative function (or operator) forms a semigroup. Haskell has a type class Semigroup (defined in the module Data.Semigroup) that can be used when a type has one clear associative operation.
-class Semigroup a where
- -- An associative operation.
- (<>) :: a -> a -> a
+class Semigroup a where
+ -- An associative operation.
+ (<>) :: a -> a -> a
Lists are an instance of Semigroup with (++) as (<>):
-[1] <> [2,3] <> [4] ==> [1,2,3,4]
+[1] <> [2,3] <> [4] ==> [1,2,3,4]
Types that have multiple different associative operators usually aren’t made an instance of Semigroup. An example is Int, which has many associative functions like +, * and max. Instead, the Haskell standard library uses boxing (see earlier in this lecture). Here are the definitions for Sum and Product:
-data Sum a = Sum a
-instance Num a => Semigroup (Sum a) where
- Sum a <> Sum b = Sum (a+b)
-
-data Product a = Product a
-instance Num a => Semigroup (Product a) where
- Product a <> Product b = Product (a*b)
+data Sum a = Sum a
+instance Num a => Semigroup (Sum a) where
+ Sum a <> Sum b = Sum (a+b)
+
+data Product a = Product a
+instance Num a => Semigroup (Product a) where
+ Product a <> Product b = Product (a*b)
By the way, this is another benefit of boxing things: being able to declare different type class instances!
Note how the Num a constraint lets us use Num operations like + and * on the contained type a. We can have values like Sum "abc" :: Sum String, but they won’t have a Semigroup instance!
Similarly, we have box types Min and Max. Let’s play around in GHCi a bit:
-Prelude> import Data.Semigroup
-Prelude Data.Semigroup> Product (2::Int) <> Product 3 <> Product 1
-Product {getProduct = 6}
-Prelude Data.Semigroup> Sum 3 <> Sum 5 <> Sum 7
-Sum {getSum = 15}
-Prelude Data.Semigroup> Product 2 <> Product 3 <> Product 1
-Product {getProduct = 6}
-Prelude Data.Semigroup> Min 4 <> Min 3 <> Min 5
-Min {getMin = 3}
-Prelude Data.Semigroup> Max 4 <> Max 3 <> Max 5
-Max {getMax = 5}
+Prelude> import Data.Semigroup
+Prelude Data.Semigroup> Product (2::Int) <> Product 3 <> Product 1
+Product {getProduct = 6}
+Prelude Data.Semigroup> Sum 3 <> Sum 5 <> Sum 7
+Sum {getSum = 15}
+Prelude Data.Semigroup> Product 2 <> Product 3 <> Product 1
+Product {getProduct = 6}
+Prelude Data.Semigroup> Min 4 <> Min 3 <> Min 5
+Min {getMin = 3}
+Prelude Data.Semigroup> Max 4 <> Max 3 <> Max 5
+Max {getMax = 5}
7.3.3 Finally, Monoids
If we listen to the mathematicians for a moment again, a monoid is a semigroup with a neutral element. A neutral element is a zero: an element that does nothing when combined with other elements. Here are some examples:
--- 0 is the neutral element of (+)
-3 + 0 ==> 3
-0 + 3 ==> 3
-
--- 1 is the neutral element of (*)
-1 * 5 ==> 5
-5 * 1 ==> 5
-
--- [] is the neutral element of (++)
-[] ++ [1,2] ==> [1,2]
-[1,2] ++ [] ==> [1,2]
+-- 0 is the neutral element of (+)
+3 + 0 ==> 3
+0 + 3 ==> 3
+
+-- 1 is the neutral element of (*)
+1 * 5 ==> 5
+5 * 1 ==> 5
+
+-- [] is the neutral element of (++)
+[] ++ [1,2] ==> [1,2]
+[1,2] ++ [] ==> [1,2]
The Haskell type class Monoid (from the module Data.Monoid) represents monoids.
-class Semigroup a => Monoid a where
- -- The neutral element
- mempty :: a
+class Semigroup a => Monoid a where
+ -- The neutral element
+ mempty :: a
Here are the Monoid instances corresponding to our three examples of neutral elements:
-instance Num a => Monoid (Sum a) where
- mempty = Sum 0
-
-instance Num a => Monoid (Product a) where
- mempty = Product 1
-
-instance Monoid [] where
- mempty = []
+instance Num a => Monoid (Sum a) where
+ mempty = Sum 0
+
+instance Num a => Monoid (Product a) where
+ mempty = Product 1
+
+instance Monoid [] where
+ mempty = []
So, what is a monoid for a programmer? A type forms a monoid if there’s a way of combining two elements of the type together so that parenthesis don’t matter, and there’s a also an “empty element” that can be combined with things without changing them. When thought of like this, monoids come up in programming quite often!
7.3.4 Why?
What use is this Monoid class? Can’t we just write 1 + 2 instead of Sum 1 <> Sum 2? We can, yes, but some library functions work on all Monoid types.
The reason we want both a neutral element and an associative binary operator is that those are the exact two things we need in order to reduce or fold multiple elements into one value. This is the job of:
-mconcat :: Monoid a => [a] -> a
+mconcat :: Monoid a => [a] -> a
Sidenote: one way to define mconcat is foldr (<>) mempty. Do you remember foldr?
Let’s look at why we need the properties of Monoid to implement mconcat. Firstly, we need mempty to handle empty lists:
-mconcat [] :: Sum Int ==> Sum 0
+mconcat [] :: Sum Int ==> Sum 0
Secondly, we need associativity to be able to reduce a list [x,y,z] to a unique value. If <> were not associative, we would have two possible values for mconcat [x,y,z], namely (x<>y)<>z and x<>(y<>z).
The most useful Monoid function is foldMap:
-foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
+foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
That type signature looks scary, but concrete cases are simpler:
-foldMap Max [1::Int,4,2] ==> Max 4
-foldMap Product [1::Int,4,2] ==> Product 8
--- We need the ::Int to avoid an "Ambiguous type variable" error when printing the result
+foldMap Max [1::Int,4,2] ==> Max 4
+foldMap Product [1::Int,4,2] ==> Product 8
+-- We need the ::Int to avoid an "Ambiguous type variable" error when printing the result
Let’s break down that type. We know that an example of a Foldable t => t a type is [a], so we can rewrite the type as
-foldMap' :: Monoid m => (a -> m) -> [a] -> m
+foldMap' :: Monoid m => (a -> m) -> [a] -> m
We can build this function out of functions we already know:
-foldMap' f xs = mconcat (map f xs)
+foldMap' f xs = mconcat (map f xs)
Oh, by the way, thanks to the (Monoid a, Monoid b) => Monoid (a,b) instance we can even compute the maximum and product in one pass:
-foldMap (\x -> (Max x, Product x)) [1::Int,4,2] ==> (Max 4, Product 8)
+foldMap (\x -> (Max x, Product x)) [1::Int,4,2] ==> (Max 4, Product 8)
Note, you don’t need to use monoids in your own code, but you’ll eventually bump into them when using Haskell libraries so it’s good to know what they are.
7.3.5 How?
Due to various historical and performance reasons, the definition of the Monoid and Semigroup classes aren’t just
-class Semigroup a where
- (<>) :: a -> a -> a
-class Semigroup a => Monoid a where
- mempty :: a
+class Semigroup a where
+ (<>) :: a -> a -> a
+class Semigroup a => Monoid a where
+ mempty :: a
Although you can mostly pretend they are. The actual definitions are:
-class Semigroup a where
- -- | An associative operation.
- (<>) :: a -> a -> a
-
- -- Combine elements of a nonempty list with <>
- sconcat :: NonEmpty a -> a
- sconcat as = ... -- default implementation omitted
-
- -- Combine a value with itself using <>, n times
- stimes :: Integral b => b -> a -> a
- stimes n x = ... -- default implementation omitted
-class Semigroup a => Monoid a where
- mempty :: a
-
- mappend :: a -> a -> a
- mappend = (<>)
-
- -- Combine elements of a list with <>
- mconcat :: [a] -> a
- mconcat = ... -- default implementation omitted
+class Semigroup a where
+ -- | An associative operation.
+ (<>) :: a -> a -> a
+
+ -- Combine elements of a nonempty list with <>
+ sconcat :: NonEmpty a -> a
+ sconcat as = ... -- default implementation omitted
+
+ -- Combine a value with itself using <>, n times
+ stimes :: Integral b => b -> a -> a
+ stimes n x = ... -- default implementation omitted
+class Semigroup a => Monoid a where
+ mempty :: a
+
+ mappend :: a -> a -> a
+ mappend = (<>)
+
+ -- Combine elements of a list with <>
+ mconcat :: [a] -> a
+ mconcat = ... -- default implementation omitted
As you can see, all the operations except <> and mempty have default definitions, so a normal Monoid instance declaration looks just like this:
-instance Semigroup MyType where
- x <> y = ...
-
-instance Monoid MyType where
- mempty = ...
+instance Semigroup MyType where
+ x <> y = ...
+
+instance Monoid MyType where
+ mempty = ...
7.4 Open and Closed Abstractions
A question novice Haskell programmers often ask (or at least should ask!) is: when should I use type classes? This section offers one answer.
Let’s look at a concrete example. A vehicle can be either a car or an airplane. We can model this with algebraic datatypes (as we’ve seen earlier in this chapter), but also with type classes. Here’s the datatype version:
-data Vehicle = Car String | Airplane String
-
-sound :: Vehicle -> String
-sound (Car _) = "brum brum"
-sound (Airplane _) = "zooooom"
+data Vehicle = Car String | Airplane String
+
+sound :: Vehicle -> String
+sound (Car _) = "brum brum"
+sound (Airplane _) = "zooooom"
Here’s the class version. Note how each case gets its own datatype, which are collected together in a type class.
-data Car = Car String
-data Airplane = Airplane String
-
-class VehicleClass a where
- sound :: a -> String
-
-instance VehicleClass Car where
- sound (Car _) = "brum brum"
-
-instance VehicleClass Airplane where
- sound (Airplane _) = "zooooom"
+data Car = Car String
+data Airplane = Airplane String
+
+class VehicleClass a where
+ sound :: a -> String
+
+instance VehicleClass Car where
+ sound (Car _) = "brum brum"
+
+instance VehicleClass Airplane where
+ sound (Airplane _) = "zooooom"
What is the difference between these solutions? The data-based solution is closed, meaning the set of cases is fixed and we can handle all of them in one place. The class-based solution is open, meaning we can add new cases, even in other modules.
An open abstraction is nice when we want extensibility. In the class-based solution, another module could define a bike:
-data Bike = Bike String
-
-instance VehicleClass Bike where
- sound (Bike _) = "whirrr"
+data Bike = Bike String
+
+instance VehicleClass Bike where
+ sound (Bike _) = "whirrr"
A closed abstraction is good when we want to know that we’ve handled all cases, consider for example the function canCollide which checks whether two vehicles can collide:
-canCollide :: Vehicle -> Vehicle -> Bool
-canCollide (Car _) (Car _) = True
-canCollide (Airplane _) (Airplane _) = True
-canCollide _ _ = False
+canCollide :: Vehicle -> Vehicle -> Bool
+canCollide (Car _) (Car _) = True
+canCollide (Airplane _) (Airplane _) = True
+canCollide _ _ = False
This would be very hard to implement reliably in the class-based solution. Consider for example how collision checks between Bikes and Cars would get handled.
7.5 Modeling with Languages
Sometimes it’s useful to implement a mini programming language for describing parts of your software. The fancy term for these is an Embedded Domain-Specific Language (EDSL). Haskell is well suited to modeling and interpreting languages. The expressions of the language are represented using (often recursive) algebraic data types. The language can be interpreted (that is, evaluated or run) by a recursive function.
Here’s an example of a language for describing price computations for products in a web shop.
-data Discount = DiscountPercent Int -- A percentage discount
- | DiscountConstant Int -- A constant discount
- | MinimumPrice Int -- Set a minimum price
- | ForCustomer String Discount -- Discounts can be conditional
- | Many [Discount] -- Apply a number of discounts in row
+data Discount = DiscountPercent Int -- A percentage discount
+ | DiscountConstant Int -- A constant discount
+ | MinimumPrice Int -- Set a minimum price
+ | ForCustomer String Discount -- Discounts can be conditional
+ | Many [Discount] -- Apply a number of discounts in row
The language is interpreted by the function applyDiscount that takes a customer name, a price, a discount, and returns a price.
-applyDiscount :: String -> Int -> Discount -> Int
-applyDiscount _ price (DiscountPercent percent) = price - (price * percent) `div` 100
-applyDiscount _ price (DiscountConstant discount) = price - discount
-applyDiscount _ price (MinimumPrice minPrice) = max price minPrice
-applyDiscount customer price (ForCustomer target discount)
- | customer == target = applyDiscount customer price discount
- | otherwise = price
-applyDiscount customer price (Many discounts) = go price discounts
- where go p [] = p
- go p (d:ds) = go (applyDiscount customer p d) ds
+applyDiscount :: String -> Int -> Discount -> Int
+applyDiscount _ price (DiscountPercent percent) = price - (price * percent) `div` 100
+applyDiscount _ price (DiscountConstant discount) = price - discount
+applyDiscount _ price (MinimumPrice minPrice) = max price minPrice
+applyDiscount customer price (ForCustomer target discount)
+ | customer == target = applyDiscount customer price discount
+ | otherwise = price
+applyDiscount customer price (Many discounts) = go price discounts
+ where go p [] = p
+ go p (d:ds) = go (applyDiscount customer p d) ds
Here we apply a discount chain of -50%, -$30 with a minimum price of $35:
-applyDiscount "Bob" 120 (DiscountPercent 50)
- ==> 60
-applyDiscount "Bob" 60 (DiscountConstant 30)
- ==> 30
-applyDiscount "Bob" 30 (MinimumPrice 35)
- ==> 35
-applyDiscount "Bob" 120 (Many [DiscountPercent 50, DiscountConstant 30, MinimumPrice 35])
- ==> 35
+applyDiscount "Bob" 120 (DiscountPercent 50)
+ ==> 60
+applyDiscount "Bob" 60 (DiscountConstant 30)
+ ==> 30
+applyDiscount "Bob" 30 (MinimumPrice 35)
+ ==> 35
+applyDiscount "Bob" 120 (Many [DiscountPercent 50, DiscountConstant 30, MinimumPrice 35])
+ ==> 35
Here we have different discounts for Ssarah and Yvonne:
-applyDiscount "Yvonne" 100 (Many [ForCustomer "Yvonne" (DiscountConstant 10), ForCustomer "Ssarah" (DiscountConstant 20)])
- ==> 90
-applyDiscount "Ssarah" 100 (Many [ForCustomer "Yvonne" (DiscountConstant 10), ForCustomer "Ssarah" (DiscountConstant 20)])
- ==> 80
+applyDiscount "Yvonne" 100 (Many [ForCustomer "Yvonne" (DiscountConstant 10), ForCustomer "Ssarah" (DiscountConstant 20)])
+ ==> 90
+applyDiscount "Ssarah" 100 (Many [ForCustomer "Yvonne" (DiscountConstant 10), ForCustomer "Ssarah" (DiscountConstant 20)])
+ ==> 80
As you can see, even a simple Discount type can generate complex behaviours because it is self-referential (recursive). Using Discount we are able to represent the discount logic of our webshop as data instead of writing code.
There are multiple reasons for representing logic as data instead of code. Unlike code, data can easily be stored in a file or database, or even transmitted over the network. We can also use the same data for multiple purposes, for exaple we could visualize the discount chains in an administration user interface.
@@ -3982,103 +3959,103 @@ If I define an instance for Num, I also get an instance for F
8.1 A Taste of IO
This course has been centered around pure functional programming. We’ve done lots of arithmetic, reversed lists, worked with binary trees, but so far we haven’t been able to affect the world outside our GHCi.
Things like reading input, writing to a file, or talking over the network are side effects. Side effects can’t be represented with pure functional code. A function like
-readInputFromTheUser :: String -> String
+readInputFromTheUser :: String -> String
can’t be pure, because if it were, readInputFromUser "What is your name?" would always have to return the same result. However, representing side-effects and impurity in a pure language is possible. There are many ways of doing it, and the Haskell way is to use Monads.
Monads are reputedly difficult to understand. That is probably because they are so abstract. I think it’s best to focus on practical and concrete cases first. Here’s a taste of the IO Monad, which you can use for all sorts of side effects in Haskell.
Let’s start!
-Prelude> :t getLine
-getLine :: IO String
-Prelude> line <- getLine
+Prelude> :t getLine
+getLine :: IO String
+Prelude> line <- getLine
another line
-Prelude> :t line
-line :: String
-Prelude> line
-"another line"
-Prelude> reverse line
-"enil rehtona"
+Prelude> :t line
+line :: String
+Prelude> line
+"another line"
+Prelude> reverse line
+"enil rehtona"
What we’ve seen here is the IO action getLine. It has type IO String. This means that GHCi can execute the action to produce a value of type String. When we enter line <- getLine into GHCi, we mean:
Execute the IO action getLine, and give the result the name line.
After we’ve received line, it’s a pure String value and we can work with it normally.
Some IO actions take parameters. For example putStrLn :: String -> IO () takes a String and returns an IO action that prints that string. The () type is a special type that only has one value, (). In this case IO () means that this IO always produces the same empty value (). You can run IO actions by just
-Prelude> :t putStrLn
-putStrLn :: String -> IO ()
-Prelude> :t putStrLn "hello"
-putStrLn "hello" :: IO ()
-Prelude> val <- putStrLn "hello"
-hello
-Prelude> val
-()
+Prelude> :t putStrLn
+putStrLn :: String -> IO ()
+Prelude> :t putStrLn "hello"
+putStrLn "hello" :: IO ()
+Prelude> val <- putStrLn "hello"
+hello
+Prelude> val
+()
If you don’t need the return value of an IO action, you can run it in GHCi without the <-:
-Prelude> putStrLn "hello"
-hello
+Prelude> putStrLn "hello"
+hello
You can build your own IO actions by combining other actions with do-notation. A do block lists IO actions that are executed in order.
-printTwoThings :: IO ()
-printTwoThings = do
- putStrLn "Hello!"
- putStrLn "How are you?"
-
-greet :: IO ()
-greet = do
- putStrLn "What's your name?"
- name <- getLine
- putStrLn ("Hello, " ++ name)
-Prelude> printTwoThings
-Hello!
-How are you?
-Prelude> greet
-What's your name?
-Seraphim
-Hello, Seraphim
+printTwoThings :: IO ()
+printTwoThings = do
+ putStrLn "Hello!"
+ putStrLn "How are you?"
+
+greet :: IO ()
+greet = do
+ putStrLn "What's your name?"
+ name <- getLine
+ putStrLn ("Hello, " ++ name)
+Prelude> printTwoThings
+Hello!
+How are you?
+Prelude> greet
+What's your name?
+Seraphim
+Hello, Seraphim
8.1.1 What About Purity?
It feels as if we can just do side effects where ever we want with these IO actions. However, it’s important to remember the distinction between defining an IO action and executing it.
Let’s try to print while mapping over a list
-printAndIncrement :: Int -> Int
-printAndIncrement x = x+1
- where action = putStrLn "got a number!"
-Prelude> map printAndIncrement [1,2,3]
-[2,3,4]
+printAndIncrement :: Int -> Int
+printAndIncrement x = x+1
+ where action = putStrLn "got a number!"
+Prelude> map printAndIncrement [1,2,3]
+[2,3,4]
This didn’t print anything, because even though we defined our action, it wasn’t given to GHCi for execution. Because printAndIncrement returns an Int, it can’t return an action. Ok, let’s try another approach:
-Prelude> length (map putStrLn ["string1","string2"])
-2
+Prelude> length (map putStrLn ["string1","string2"])
+2
That didn’t print anything either! Let’s see why:
-Prelude> :t map putStrLn ["string1","string2"]
-map putStrLn ["string1","string2"] :: [IO ()]
-Prelude> :t length (map putStrLn ["string1","string2"])
-length (map putStrLn ["string1","string2"]) :: Int
+Prelude> :t map putStrLn ["string1","string2"]
+map putStrLn ["string1","string2"] :: [IO ()]
+Prelude> :t length (map putStrLn ["string1","string2"])
+length (map putStrLn ["string1","string2"]) :: Int
We generated a list of IO actions and computed the length of the list. Defining IO actions is pure, it’s running them that causes side effects. Since the type of our expression was Int, no IO actions could land in GHCi and be executed.
If we instead return an IO action, it does get run:
-Prelude> :t head (map putStrLn ["string1","string2"])
-head (map putStrLn ["string1","string2"]) :: IO ()
-Prelude> head (map putStrLn ["string1","string2"])
-string1
+Prelude> :t head (map putStrLn ["string1","string2"])
+head (map putStrLn ["string1","string2"]) :: IO ()
+Prelude> head (map putStrLn ["string1","string2"])
+string1
Here too, the code that produces the action putStrLn "string1" is pure, it’s only after the IO action is executed by GHCi that we see the printed string. And as you can see, the other IO action, putStrLn "string2", never got run.
If this feels complicated, don’t worry. We’ll get back to this on part 2 of the course.
8.1.2 What About Haskell Programs?
We know that GHCi can run IO actions. What about actual Haskell programs? The way Haskell programs work is that the IO action called main gets executed when the program is run. Recall our example program from Lecture 1.
-module Gold where
-
--- The golden ratio
-phi :: Double
-phi = (sqrt 5 + 1) / 2
-
-polynomial :: Double -> Double
-polynomial x = x^2 - x - 1
-
-f x = polynomial (polynomial x)
-
-main = do
- print (polynomial phi)
- print (f phi)
+module Gold where
+
+-- The golden ratio
+phi :: Double
+phi = (sqrt 5 + 1) / 2
+
+polynomial :: Double -> Double
+polynomial x = x^2 - x - 1
+
+f x = polynomial (polynomial x)
+
+main = do
+ print (polynomial phi)
+ print (f phi)
Here we see some pure code and a main IO action that prints two things (print is just putStrLn combined with show).
We can place this code in a file called Gold.hs, compile it into an executable, and run it:
-$ ghc -main-is Gold Gold.hs
-[1 of 1] Compiling Gold ( Gold.hs, Gold.o )
-Linking Gold ...
-$ ./Gold
-0.0
--1.0
+$ ghc -main-is Gold Gold.hs
+[1 of 1] Compiling Gold ( Gold.hs, Gold.o )
+Linking Gold ...
+$ ./Gold
+0.0
+-1.0
8.2 Summary
So far, we’ve learned about Haskell’s syntax and types, quite a bit of functional programming and about some language features like type classes.
We’ve also seen some type-oriented programming, and even gotten a taste of I/O in Haskell.