Introduction to Haskell Programming
A brief introduction to haskell programming.
1. Introduction
- Haskell is a purely functional programming language.
- You cannot set a variable to something and then set it to something else later: No side-effects
- If a function is called twice with the same parameters, it's guaranteed to return the same result: Referential transparency
- Haskell is lazy: unless specifically told otherwise, Haskell won't execute functions and calculate things until it's really forced to show you a result. Allow infinite data structures
- Statically typed: Its type system that has type inference.
- Most widely used Haskell compiler: GHC
2. Starting out
Some basic ideas
Haskell can be used as a calculator
Some infix function | Meaning |
---|---|
+, -, *, / | Arithmetic |
==, /=, <, >, <=, >= | Equality |
&&, || | Logical |
++ | List concatenation |
: | List cons |
!! | List index access |
Some prefix function | Meaning |
---|---|
not | Logical |
succ | Increase by 1 |
max, min | Comparison |
head | First element of a list |
tail | The list without the head element |
last | Last element of a list |
init | The list without the last element |
length | Get length of a list |
null | Check if a list is empty |
reverse | Reverse a list |
take | Extracts a number of elements from a list |
drop | Drop a number of elements from a list |
minimum | Get min of a list |
maximum | Get max of a list |
sum | Get sum of a list |
product | Get product of a list |
elem | Check if an item is in a list |
cycle | Cycles a list into an infinite list |
repeat | Repeat an element into an infinite list |
replicate | Repeat an element for a number of times |
fst | Get the first component of a pair tuple |
snd | Get the second component of a pair tuple |
zip | Zips two lists to list of joining pairs |
compare | return GT (greater than), LT (lesser than) or EQ |
read | Takes a string and returns a type of Read class |
fromIntegral | Takes an integral and turns it into a more general number |
Change prefix function to infix function:
1 | (div 42 5) == (42 `div` 5) |
Change back:
1 | ((+) 42 5) == (42 + 5) |
First function saved as the file doubleMe.hs:
1 | doubleMe x = x + x |
1 | ghci> :l doubleMe |
if
statement is an expression which means that it
returns a value thus the else
part of the if
statements in Haskell is mandatory
1 | sumDoubledUs x y = doubleMe x + doubleMe y |
Functions in Haskell don't have to be in any particular order. Thus
the order of doubleMe
and sumDoubleUs
is not
important
We usually use ' to either denote a strict version of a function (one that isn't lazy) or a slightly modified version of a function or a variable.
1 | doubleSmallNumber' x = (if x > 100 then x else x*2) + 1 |
Intro to list
List is homogeneous data structure \(\rightarrow\) stores elements with same type
1 | ghci> let lostNumbers = [4,8,15,16,23,42] |
To concatenate two lists:
1 | ghci> [1,2,3,4] ++ [9,10,11,12] |
To use ++
operator on long strings is however slow.
++
takes in two lists, so, even if you want to append one
element to the end of a list with ++
, you have to surround
it with square brackets so it becomes a list
To append an element at the beginning of a list: :
operator
1 | ghci> 'A':" SMALL CAT" |
[1,2,3]
is actually just syntactic sugar for
1:2:3:[]
Strings are lists, "hello"
is just syntactic sugar for
['h','e','l','l','o']
. Thus, we can use list functions on
them
To get an element out of a list by index: !!
operator,
the indices start at 0
1 | ghci> "Steve Buscemi" !! 6 |
Lists can contain lists. The lists within a list can be of different lengths but they can't be of different types
1 | ghci> let b = [[1], [2,3], [4,5,6]] |
Lists can be compared in lexicographical order
1 | ghci> [3,2,1] > [2,10,100] |
Texas ranges
A faster way to generate a list with a certain pattern
1 | [1..20] == [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] |
List comprehension
Similar to set comprehensions. They are used for building more specific sets out of general sets
The part before the pipe is called the output function
1 | [ x*2 | x <- [1..10], x*2 >= 12] == [12,14,16,18,20] |
It can be used to combines a list of adjectives and a list of nouns:
1 | ghci> let nouns = ["hobo", "frog", "pope"] |
We can implement our own length
:
1 | length' xs = sum [1 | _ <- xs] |
Here _
means that we don't care what the output is
To work on strings:
1 | removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']] |
Tuples
Tuples are similar to lists. The fundamental differences are:
- Tuples have fixed size
- Tuples is NOT homogeneous.
- Tuples define its own type.
[(1,2),(8,11,5),(4,5)]
or[(1,2),("One",2)]
are not valid syntactically because they have different types
Two useful functions that operate on pairs:
1 | ghci> fst (8, 11) |
A cool function that produces a list of pairs: zip
. It
takes two lists and then zips them together into one list by joining the
matching elements into pairs
1 | ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"] |
A cool example:
1 | ghci> let rightTriangles = [(a,b,c) | c <- [1..20], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2] |
3. Types and Typeclasses
Some basic types
A type is a kind of label that every expression has. It tells us in which category of things that expression fits. A type is written in capital case
Type | Range |
---|---|
Int | 2147483647 to -2147483648 on 32-bit machine |
Integer | No boundary |
Float | Single precision |
Double | Double precision |
Bool | True or False |
Char | A character |
Unlike Java or Pascal, Haskell has type inference. Haskell can infer some type based on context
1 | ghci> :t 'a' |
::
is read as "has type of"
Types of functions
1 | removeNonUppercase :: [Char] -> [Char] |
The parameters are separated with ->
and there's no
special distinction between the parameters and the return type
Type variables
For example:
1 | ghci> : t head |
Here a
is a type variable which means that
a
can be of any type. Functions that have type variables
are called polymorphic functions. Another example:
1 | ghci> :t fst |
Intro to typeclasses
A typeclass is a sort of interface that defines some behaviour. If a type is a part of a typeclass, that means that it supports and implements the behaviour the typeclass describes
1 | ghci> :t (==) |
Everything before the =>
symbol is called a class
constraint. The type of those two values must be a member of the
Eq
class. This was the class constraint
The Eq
typeclass provides an interface for testing for
equality. Any type where it makes sense to test for equality between two
values of that type should be a member of the Eq
class
Some basic typeclasses:
Eq
is used for types that support equality testing1
2
3
4
5
6ghci> 5 == 5
True
ghci> 'a' == 'a'
True
ghci> 3.432 == 3.432
TrueOrd
members have an ordering. The be a member ofOrd
, a type must first have membership in the prestigious and exclusiveEq
club1
2
3
4
5
6
7
8ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool
ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 `compare` 3
GTShow
members can be presented as strings1
2
3
4
5
6ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"Read
members values can be extract from a string1
2
3
4ghci> read "True" || False
True
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]But we cannot run for example
read "4"
, the GHCI will give an error because it doesn't know what we want in return. It knows we want some type that is part of theRead
class, it doesn't know which oneThus we must use explicit type annotations, which is a way of explicitly saying what the type of expression should be. We do that by adding
::
at the end of the expression and then specifying a type1
2
3
4
5
6
7
8ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
Enum
members are sequentially ordered types1
2
3
4
5
6ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> succ 'B'
'C'Bounded
members have an upper and a lower bound1
2
3
4
5
6ghci> minBound :: Int
-9223372036854775808
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: (Bool, Int, Char)
(True,9223372036854775807,'\1114111')Num
members have the property of being able to act like numbers1
2
3
4
5
6
7
8
9
10ghci> 20 :: Int
20
ghci> 20 :: Integer
20
ghci> 20 :: Float
20.0
ghci> 20 :: Double
20.0
ghci> :t (*)
(*) :: Num a => a -> a -> aIntegral
members represent integers.Num
includes all numbers, including real numbers and integral numbers,Integral
includes only integral (whole) numbers. In this typeclass areInt
andInteger
Floating
members includes only floating point numbers, soFloat
andDouble
A very useful function for dealing with numbers is
fromIntegral
. It has a type declaration of
fromIntegral :: (Num b, Integral a) => a -> b
. That's
useful when you want integral and floating point types to work together
nicely. For example:
1 | fromIntegral (length [1,2,3,4]) + 3.2 == 7.2 |
4. Syntax in Functions
Pattern matching
Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns
1 | lucky :: (Integral a) => a -> String |
When you call lucky
, the patterns will be checked from
top to bottom and when it conforms to a pattern, the corresponding
function body will be used
Some examples:
1 | factorial :: (Integral a) => a -> a |
We can also pattern match in list comprehensions. Should a pattern match fail, it will just move on to the next element
1 | ghci> let xs = [(1,3), (4,5), (6,7), (10,12)] |
The following examples show some pattern matches in list comprehensions
1 | head' :: [a] -> a |
There is also a thing called as patterns. Those are a handy way of breaking something up according to a pattern and binding it to names whilst still keeping a reference to the whole thing
You can do that by putting a name and an @
in front of a
pattern. For instance, the pattern xs@(x:y:ys)
. This
pattern will match exactly the same things as x:y:ys
but
you can easily get the whole list via xs
1 | capital :: String -> String |
You can't use ++
in pattern matches. If you tried to
pattern match against (xs ++ ys)
, Haskell will not be able
to decide what would be in the first and what would be in the second
list
Guards
Guards are a way of testing whether some property of a value (or several of them) are true or false. The guars are a lot more readable when you have several conditions
1 | bmiTell :: (RealFloat a) => a -> a -> String |
otherwise
is defined simply as
otherwise = True
and catches everything. We put the keyword
where
after the guards and then we define several names or
functions. These names are visible only across the guards. All the names
in where
should be aligned at a single column, otherwise
Haskell will get confused
Let
let
bindings let you bind to variables anywhere and are
expressions themselves but are local. let
bindings bind
values to names, it can be used for pattern matching
1 | cylinder :: (RealFloat a) => a -> a -> a |
The names that you define in the let
part are accessible
to the expression after the in
part
The difference between let
and where
is
that let
bindings are expressions themselves.
where
bindings are just syntactic constructs. We can do the
following with let
:
1 | ghci> 4 * (if 10 > 5 then 10 else 0) + 2 |
The in
part can be omitted when defining functions and
constants, then the names will be visible throughout the entire
interactive session
Case expressions
Case expressions are expressions. We can evaluate expressions based
on the possible cases of the value of a variable, we can also do pattern
matching. We can rewrite the head'
above in the following
way
1 | head' :: [a] -> a |
The syntax for case expressions is pretty simple:
1 | case expression of pattern -> result |
Whereas pattern matching on function parameters can only be done when defining functions, case expressions can be used pretty much anywhere. For instance:
1 | describeList :: [a] -> String |
5. Recursion
Some recursion examples:
1 | maximum' :: (Ord a) => [a] -> a |
6. Higher order functions
Haskell functions can take functions as parameters and return functions as return values. A function that does either of those is called a higher order function
Curried functions
Every function in Haskell officially only takes one parameter. All the functions that accept several parameters have been curried functions. For example:
max :: (Ord a) => a -> a -> a
is the same as
max :: (Ord a) => a -> (a -> a)
The following two calls are equivalent:
1 | ghci> max 4 5 |
If we call a function with too few parameters, we get back a partially applied function. Using partial application is a neat way to create functions on the fly so we can pass them to another function or to seed them with some data. For example:
1 | multThree :: (Num a) => a -> a -> a -> a |
Then
1 | ghci> let multTwoWithNine = multThree 9 |
Another way of writing it
1 | compareWithHundred :: (Num a, Ord a) => a -> Ordering |
Section an infix function
To section an infix function, simply surround it with parentheses and only supply a parameter on one side:
1 | divideByTen :: (Floating a) => a -> a |
Some higher-orderism is in order
Functions can take functions as parameters and also return functions. Note that the parentheses in the following examples are mandatory. They indicate that the first parameter are functions
1 | applyTwice :: (a -> a) -> a -> a |
Applying:
1 | ghci> applyTwice (+3) 10 |
Maps and filters
map
takes a function and a list and applies that
function to every element in the list, producing a new list. Its
definition:
1 | map :: (a -> b) -> [a] -> [b] |
filter
is a function that takes a predicate (a predicate
is a function that tells whether somethign is true or not) and a list
and then returns the list of elements that satisfy the predicate. Its
definition:
1 | filter :: (a -> Bool) -> [a] -> [a] |
With them, we can implement quicksort easily:
1 | quicksort :: (Ord a) => [a] -> [a] |
filter
doesn't work on infinite lists while
takeWhile
does. Eg. find the sum of all odd squares that
are smaller than 10,000
1 | ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..]))) |
Generate Collatz sequences
1 | chain :: (Integral a) => a -> [a] |
Applying
1 | ghci> chain 10 |
Another example is:
1 | ghci> let listOfFuns = map (*) [0..] |
What happens here is that the number in the list is applied to the
function *
, which has a type of
(Num a) => a -> a -> a
. Applying only one
parameter to a function that takes two parameters returns a function
that takes one parameter. If we map *
over the list
[0..]
, we get back a list of functions that only take one
parameter. So this function produces a list like
[(0*), (1*), (2*), (3*), (4*), ...]
. Thus the fourth
element is (4*)
and it gives 20 when it applied to 5
Lambda
Lambdas are basically anonymous function. To make a lambda, we write
a \
and then we write the parameters, separated by spaces.
After that comes a ->
and then the function body. We
usually surround them by parentheses, because otherwise they extend all
the way to the right
1 | map (\(a,b) -> a + b) [(1,2),(3,5),(6,3),(2,6),(2,5)] |
Foldl
foldl
- fold a list from left. It needs a binary
function. The binary function is applied between the starting value and
the head of the list. That produces a new accumulator value and the
binary function is called with that value and the next element, etc. For
example:
1 | sum' :: (Num a) => [a] -> a |
foldr
- fold a list from right. The starting value and
the list swapped places. To generate a list as the result, right fold
append is faster. Also only right fold work on infinite list.
The accumulator value (and hence, the result) of a fold can be of any type. It can be a number, a boolean or even a new list. For example:
1 | map' :: (a -> b) -> [a] -> [b] |
Folds can be used to implement any function where you traverse a list once, element by element, and then return something based on that
foldl1
and foldr1
is similar but do not
require starting value and assume that the starting value equal with the
first inputted element from the list. Examples:
1 | maximum' :: (Ord a) => [a] -> a |
scanl
and scanr
are similar but they report
all the intermediate accumulated states in the form of a list.
scanl1
and scanr1
are analogous.
Function application with
$
The function application operator $
has the lowest
precedence and it is right-associative. It can help us by saving lots of
parenthesis
1 | ($) :: (a -> b) -> a -> b |
$
means that function application can be treated just
like another function. That way, we can for instance, map function
application over a list of functions
1 | ghci> map ($ 3) [(4+), (10*), (^2), sqrt] |
Function composition
The .
function define function composition \((f\circ g)(x)=f(g(x))\)
1 | (.) :: (b -> c) -> (a -> b) -> a -> c |
Examples
1 | ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24] |
If you want to rewrite an expression with a lot of parentheses by
using function composition, you can start by putting the last parameter
of the innermost function after a $
and then just composing
all the other function calls, writing them without their last parameter
and putting dots between them
Thus
replicate 100 (product (map (*3) (zipWith max [1,2,3,4,5] [4,5,6,7,8])))
becomes
replicate 100 . product . map (*3) . zipWith max [1,2,3,4,5] $ [4,5,6,7,8]
7. Modules
1 | import Data.List |
In ghci
:
1 | ghci> :m + Data.List Data.Map Data.Set |
Import only some functions
1 | import Data.List (nub, sort) |
Import everything but some functions
1 | import Data.List hiding (nub) |
Qualified import
1 | import qualified Data.Map as M |
Data.List
foldl'
and foldr'
are stricter versions of
their respective lazy incarnations. When using lazy folds on really big
lists, you might often get a stack overflow error. The stricter versions
will not.
length
, take
, drop
,
splitAt
, !!
and replicate
all
take an Int
as one of their parameters. To use the more
general type Num
, Data.list
has
genericLength
, genericTake
,
genericDrop
, genericSplitAt
,
genericIndex
and genericReplicate
.
The nub
, delete
, union
,
intersect
and group
functions all have their
more general counterparts called nubBy
,
deleteBy
, unionBy
, intersectBy
and groupBy
. They take in a function for comparison the
inputted elements.
Similarly, the sort
, insert
,
maximum
and minimum
have their more general
equivalents sortBy
, insertBy
,
maximumBy
and minimumBy
. They take in function
for comparison as well.
1 | ghci> intersperse '.' "MONKEY" |
Data.Char
Data.Char
has the following trivial functions.
isControl
, isSpace
, isLower
,
isUpper
, isAlpha
, isAlphaNum
,
isPrint
, isDigit
, isOctDigit
,
isHexDigit
, isLetter
, isMark
,
isNumber
, isPunctuation
,
isSymbol
, isSeparator
, isAscii
,
isAsciiUpper
, isAsciiLower
,
toUpper
, toLower
, toTitle
,
digitToInt
, intToDigit
, ord
,
chr
.
1 | ghci> generalCategory ' ' |
Data.Map
Trivial list map implementation
1 | findKey :: (Eq k) => k -> [(k,v)] -> v |
But Data.Map
is a lot faster. It can be imported by
1 | import qualified Data.Map as Map |
Usage:
1 | ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")] |
Data.Set
Imported
1 | import qualified Data.Set as Set |
Usage
1 | ghci> Set.null Set.empty |
Your own modules
Example:
1 | module Geometry |
Or organized by files and directories. For example if we have a
folder called Geometry
. There are three files in the folder
- Sphere.hs
, Cuboid.hs
, and
Cube.hs
.
In Sphere.hs
:
1 | module Geometry.Sphere |
In Cuboid.hs
:
1 | module Geometry.Cuboid |
In Cube.hs
:
1 | module Geometry.Cube |
Usage:
1 | import qualified Geometry.Sphere as Sphere |
8. Make your own types and typeclasses
Algebraic data types
How boolean type is defined in the standard library:
1 | data Bool = False | True |
Another example:
1 | data Shape = Circle Float Float Float | Rectangle Float Float Float Float |
To improve the previous example, let's make an intermediate data type
that defines a point in two-dimensional space and add
deriving (Show)
at the end a data declaration. Haskell will
automatically make the type part of the Show
typeclass
1 | data Point = Point Float Float deriving (Show) |
How about a function that nudges a shape? It takes a shape, the amount to move it on the x axis and the amount to move it on the y axis and then returns a new shape that has the same dimensions, only it's located somewhere else
1 | nudge :: Shape -> Float -> Float -> Shape |
If we don't want to deal directly with points, we can make some auxiliary functions that create shapes of some size at the zero coordinates and then nudge those:
1 | baseCircle :: Float -> Shape |
If we wanted to export the functions and types that we defined here in a module, we could start it off like this:
1 | module Shapes |
By doing Shape(..)
, we exported all the value
constructors for shape
, so that means that whoever imports
our module can make shapes by using the Rectangle
and
Circle
value constructors. It's the same as writing
Shape (Rectangle, Circle)
We could also opt not to export any value constructors for
Shape
by just writing Shape
in the export
statement. That way, someone importing our module could only make shapes
by using the auxiliary function functions baseCircle
and
baseRect
Data.Map
uses that approach. You cannot create a map by
doing Map.Map [(1,2), (3,4)]
because it doesn't export that
value constructor. However, you can make a mapping by using one of the
auxiliary functions like Map.fromList
. Not exporting the
value constructors of a data types makes them more abstract in such a
way that we hide their implementation
Record syntax
If the data type we want to create is complicated, then to create it
with the previous method will be unreadable. For example:
data Person = Person String string Int Float String String deriving (Show)
Then, instead, we can do the following:
1 | data Person = Person { firstName :: String |
Type parameters
Don't put type constraints into data declarations even if it seems to make sense, because you'll have to put them into the function type declarations either way.
1 | data Maybe a = Nothing | Just a |
Derived instances
1 | data Person = Person { firstName :: String |
Type synonyms
1 | type String = [Char] |
The names of the types and values are independent of each other. We only use a type constructor in a type declaration or a type signature. We only use a value constructor in actual code. So they can be the same.
Another useful data type is
1 | data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) |
Usage:
1 | ghci> :t Right 'a' |
This type can be used instead of Maybe
to get real
information about the returned value.
For example a locker simulator:
1 | import qualified Data.Map as Map |
Recursive data structures
For example
1 | data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord) |
Which can be written in the record syntax as:
1 | data List a = Empty | Cons { listHead :: a, listTail :: List a} deriving (Show, Read, Eq, Ord) |
Here Cons
is another word for :
.
:
is actually a constructor that takes a value and another
list and returns a list.
We can define functions to be automatically infix by making them comprised of only special characters. We can also do the same with constructors
1 | infixr 5 :-: |
When we define functions as operators, we can use that to give them a
fixity. A fixity states how tightly the operator binds and whether it's
left-associative infixl
or right-associative
infixr
.
When deriving Show
for our type, Haskell will still
display it as if the constructor was a prefix function.
Binary search tree
An example of implementation of the binary search tree
1 | data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) |
Typeclasses
For example:
1 | class Eq a where |
The Functor typeclass
Types with the Functor
typeclass is basically those
things that can be mapped over, i.e. the list type is part of the
Functor
typeclass.
For example:
1 | class Functor f where |
9. Input and Output
Hello, world!
In file helloworld.hs
1 | main = putStrLn "Hello, world" |
To compile and run:
1 | ghc --make helloworld |
The type of the function putStrLn
:
1 | ghci> :t putStrLn |
A more useful example:
1 | main = do |
Each of these steps is an I/O action. By putting them together with
do
syntax, we glued them into one I/O action. The action
that we got has a type of IO ()
, because that's the type of
the last I/O action inside. The last action cannot be bound to a name
like the first two.
The type of getLine
1 | ghci> :t getLine |
The getLine
perform the I/O action and then bind its
result value to name
. getLine
has a type of
IO String
, so name
will have a type of
String
.
Another example:
1 | main = do |
Using return
doesn't cause the I/O do block to end in
execution or anything like that. The return
makes I/O
actions that don't really do anything except have an encapsulated result
and that result is thrown away because it isn't bound to a name. For
example:
1 | main = do |
return
is sort of the opposite to <-
.
While return
takes a value and wraps it up in a box,
<-
takes a box (and performs it) and takes the value out
of it, binding it to a name.
There are more I/O functions:
putStr
putChar
print
==putStrLn . show
getChar
when
: it looks like a control flow statement, but it's actually a normal function. It takes a boolean value and an I/O action. If that boolean value isTrue
, it returns the same I/O action that we supplied to it. If it isFalse
, it returns thereturn ()
.1
2
3
4
5
6
7import Control.Monad
main = do
c <- getChar
when (c /= ' ') $ do
putChar c
mainsequence
takes a list of I/O actions and returns an I/O actions that will perform those actions one after the other. The result contained in that I/O action will be a list of the results of all the I/O actions that were performed.1
2
3main = do
rs <- sequence [map print [1,2,3,4,5]]
print rsmapM
: takes a function and a list, maps the function over the list and then sequences itmapM_
does the same, only it throws away the result later. We usually usemapM_
when we don't care what result our sequenced I/O actions have.forM
: likemapM
, only that it has its parameters switched around.
forever
: takes an I/O action and returns an I/O action that just repeats the I/O action it got forever.1
2
3
4
5
6
7import Control.Monad
import Data.Char
main = forever $ do
putStr "Give me some input: "
l <- getLine
putStrLn $ map toUpper l
Files and streams
getContents
is an I/O action that reads everything from
the standard input until it encounters an end-of-file character. Its
type is getContents :: IO String
. getContents
goes a lazy I/O, it doesn't read all of the input at once. For example:
1 | import Data.Char |
There is a function called interact
that takes a
function of type String -> String
as a parameter and
returns an I/O action that will take some input, run that function on it
and then print out the function's result. For example to filter only
lines shorter than 10 characters:
1 | main = interact shortLinesOnly |
Which can be reduced to one line:
1 | main = interact $ unlines . filter ((< 10) . length) . lines |
For file read, there is a function called hGetContents
.
It takes a Handle
, so it knows which file to get the
contents from and returns an IO String
- an I/O action that
holds as its result the contents of the file. hGetContents
won't attempt to read the file at once and store it in memory, but it
will read it as needed. For example:
1 | import System.IO |
Another way of doing this is with withFile
:
1 | import System.IO |
The withFile
can be implemented in the following way:
1 | withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a |
Other functions to read and write file: * readFile
has a
type signature of readFile :: FilePath -> IO String
.
readFile
takes a path to a file and return an I/O action
that will read that file (lazily) and bind its contents to something as
a string. * writeFile
has a type of
writeFile :: FilePath -> String -> IO ()
. It takes a
path to a file and a string to write to that file and returns an I/O
action that will do the writing. * appendFile
has a type
signature that's just like writeFile
, only
appendFile
doesn't truncate the file to zero length if it
already exists but it appends stuff to it.
You can control how exactly buffering is done by using the
hSetBuffering
function. It takes a handle and a
BufferMode
and returns an I/O action that sets the
buffering. BufferMode
is a simple enumeration data type and
the possible values it can hold are: NoBuffering
,
LineBuffering
or BlockBuffering (Maybe Int)
.
The Maybe Int
is for how big the chunk should be, in bytes.
If it is Nothing
, then the operating system determines the
chunk size. NoBuffering
means that it will be read one
character at a time. For example:
1 | main = do |
We can also use hFlush
, which is a function that takes a
handle and returns an I/O action that will flush the buffer of the file
associated with the handle.
Command line arguments
The System.Environment
module has two cool I/O actions.
One is getArgs
, which has a type of
getArgs :: IO [String]
and is an I/O action that will get
the arguments that the program was run with and have as its contained
result a list with the arguments. getProgName
has a type of
getProgName :: IO String
and is an I/O action that contains
the program name:
1 | import System.Environment |
A simple todo list apps
1 | import System.Environment |
Bytestrings
Bytestrings are sort of like lists, only each element is one byte (or
8 bits) in size. Bytestrings come in two flavors: strict and lazy ones.
Strict bytestrings reside in Data.ByteString
. A strict
bytestring represents a series of bytes in an array. The upside is that
there's less overhead because there are no thunks involved. The downside
is that they're likely to fill your memory up faster.
The other variety of bytestrings resides in
Data.ByteString.Lazy
. If you evaluatea byte in a lazy
bytestring, the first 64K will be evalated. After that, it's just a
promise for the rest of the chunks.
For example:
1 | import qualified Data.ByteString.Lazy as B |
Exceptions
Pure code can throw exceptions, but they can only be caught in the
I/O part of our code. But for a good practice, don't mix exceptions and
pure code. Take advatange of Haskell's powerful type system and use
types like Either
and Maybe
to represent
results that may have failed.
An example:
1 | import System.Environment |
With exception:
1 | import System.Environment |
The predicates that act on IOError
are: *
isAlreadyExistError
* isDoesNotExistError
*
isAlreadyInUseError
* isFullError
*
isEOFError
* isIllegalOperation
*
isPermissionError
* isUserError
10. Functors, Applicative, Functors and Monoids
Functors redux
Functors are things that can be mapped over, like lists,
Maybe
s, trees, and such. In Haskell, they are described by
the typeclass Functor
, which has only one typeclass method,
namely fmap
, which has a type of
fmap :: (a -> b) -> f a -> f b
.
Let's see how IO
is an instance of Functor
.
When we fmap
a function over an I/O action, we want to get
back an I/O action that does the same thing, but has our function
applied over its result value.
1 | instance Functor IO where |
The result of mapping something over an I/O action will be an I/O action, so right off the bat we use do syntax to glue two actions and make a new one.
Another instance of Functor
that we've been dealing with
all along without knowing was (->) r
.
1 | instance Functor ((->) r) where |
Its type is then
fmap :: (a -> b) -> (r -> a) -> (r -> b)
which reminds us about function compositions. Thus it can be written as:
1 | instance Functor ((->) r) where |
Law of Functor
- If we map the
id
function over a functor, the functor that we get back should be the same as the original functor. - Composing two functions and then mapping the resulting function over a functor should be the same as first mapping one function over the functor and then mapping the other one.
Applicative Functors
Applicative functors are represented in Haskell by the
Applicative
typeclass, found in the
Control.Applicative
module.
1 | class (Functor f) => Applicative f where |
It starts the definition of the Applicative
class and it
also introduces a class constraint. It says that if we want to make a
type onstructor part of the Applicative
typeclass, it has
to be in Functor
first. The first method it defines is
called pure
. pure
should take a value of any
type and return an applicative functor with that value inside it. The
<*>
function has a type declaration of
f (a -> b) -> f a -> f b
. It's a sort of a beefed
up fmap
. <*>
takes a fnctor that has a
function in it and another functor and sort of extracts that function
from the first functor and then maps it over the second one.
For example, Applicative
instance implementation for
Maybe
:
1 | instance Applicative Maybe where |
Applicative
instance implementaton for lists:
1 | instance Applicative [] where |
Control.Applicative
also exports a function called
<$>
, which is just fmap
as an infix
operator:
1 | (<$>) :: (Functor f) => (a -> b) -> f a -> f b |
Monoids
A monoid is when you have an associative binary function and a value which acts as an identity with respect to that function. When something acts as an identity with respect to a function, it means hat when called with that function and some other value, the result is always equal to that other value.
It is defined as:
1 | class Monoid m where |
The Monoid
type class is defined in
Data.Monoid
. List, product, and sum are all monoids. For
example:
1 | instance Num a => Monoid (Product a) where |
Monoid instance for
Ordering
1 | instance Monoid Ordering where |
Maybe
the Monoid
1 | instance Monoid a => Monoid (Maybe a) where |
11. Monads
Monads are just beefed up applicative functors, much like applicative functors are only beefed up functors.
An applicative value can be seeen as a value with an added context.
The Applicative
type class allowed us tp use normal
functions on these values with context and the context was preserved.
For example, Maybe a
values represent computations that
might have failed, [a]
values represent computations that
have several results (non-deterministic computations), IO a
values represent values that have side-effects, etc.
Monads are a natural extension of applicative functors. If you have a
value with a context, m a
, how do you apply it to a
function that takes a normal a
and returns a value with a
context? So essentially, we will want this function:
1 | (>>=) :: (Monad m) => m a -> (a -> m b) -> m b |
Monads are just applicative functors that support
>>=
. The >>=
function is
pronounced as bind.
Maybe
Monad
Maybe
is a monad. >>=
would take a
Maybe a
value and a function of type
a -> Maybe b
and somehow apply the function to the
Maybe a
. For example
1 | ghci> (\x -> Just (x+1)) 1 |
Instead of calling it >>=
, let's call it
applyMaybe
for now. It takes a Maybe a
and a
function that returns a Maybe b
and manages to apply that
function to the Maybe a
:
1 | applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b |
The monad type class
1 | class Monad m where |
- We don't need to write
class (Applicative m) => Monad m where
because every monad is an applicative functor by default. return
is the same aspure
, only with a different name. It takes a value and puts it in a minimal default context that still holds that value. In other words, it takes something and wraps it in a monad.- The next function is
>>=
, or bind. It's like tunction application, only instead of taking a normal value and feeding it to a normal function, it takes a monadic value and feeds it to a function that takes a normal value but returns monadic value. - The
>>
function comes with a default implementation and we pretty much never implement it when makingMonad
instances.>>
is used when we want to pass some value to a function that ignores its parameter and always just returns some predetermined value. fail
function will not be used explicitly in our code. Instead, it's used by Haskell to enable failure in a special syntactic construct for monads that we'll meet later.
Maybe
monad:
1 | instance Monad Maybe where |
do notation
The do
notation isn't just for IO
, but can
be used for any monad. Its principle is the same, gluing together
monadic values in sequence.
The following example:
1 | ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y))) |
Can be written as:
1 | foo :: Maybe String |
In a do
expression, every line is a monadic value.
1 | routine :: Maybe Soup |
The list monad
The Monad
instance for lists:
1 | instance Monad [] where |
Another example of propagation of the non-determinism:
1 | ghci> [1,2] >>= \n -> ['a', 'b'] >>= \ch -> return (n, ch) |
It is much clearer with do
notation:
1 | listOfTuples :: [(Int, Char)] |
This is similar to list comprehensions. In fact, list comprehensions are just syntactic sugar for using lists as monads.
1 | ghci> [ (n, ch) | n <- [1,2], ch <- ['a', 'b']] |
Monad laws
- Left identity: If we take a value, put it in a default context with
return
and then feed it to a function by using>>=
, it's the same as just taking the value and applying the function to it. Formally:return x >>= f
is the same thing asf x
- Right identity: If we have a monadic value and we use
>>=
to feed it toreturn
, the result is our original monadic value. Formally:m >>= return
is no different than justm
- Associativity: When we have a chain of monadic function applications
with
>>=
, it shouldn't matter how they're nested. Formally:- Doing
(m >>= f) >>= g
is just like doingm >>= (\x -> f x >>= g)
- Doing
Add log to code with the Writer type
1 | newtype Writer w a = Writer { runWriter :: (a, w) } |
The apply monad
1 | instance Monad ((->) r) where |
Stateful computations
1 | newtype State s a = State { runState :: s -> (a, s) } |