Haskell from 0 to IO (Maybe Hero)
This guide references some syntax and patterns used when writing programs in the Haskell language. A text editor and the GHC compiler are required to run the code, but online environments are also an option.
Haskell expects programs have an entrypoint called
main, which later is explained in detail, but for now we will create a file named
Program.hs and write inside the following code:
-- Comments are written like this = print "hola"main
Check if code can be interpreted:
Check if code can be compiled and executed:
ghc -o Program Program.hs ./Program
Some system also require to add the
-dynamic option (eg. Arch Linux).
Haskell definitions indicate a type with
:: and their value with
num is defined with type
Int and value
num :: Int -- type = 9 -- definition num = print nummain
= symbol means equality in both ways, this means that
num can be replaced by
Detailed definitions are done using
let..in.., which has a
let section with local values accessed by the
in section to calculate a final value.
= num let x = 5 -- define x = 10 -- define y y in x + y -- use them = print nummain
Other way to have local definitions is to attach a
where section, the following code is equivalent to the previous one:
= x + y -- use definitions num where = 5 -- define x x = 10 -- define y y = print nummain
Carefully designed types reject unwanted values by making them unrepresentable.
type keyword indicates an alias to an existing type.
String is an alias to a list of
type String = [Char]
data keyword is used to define custom types.
Booleans are represented in this way:
data Bool = False | True
We can apply conditionals over booleans like this:
b :: Bool = True b s :: String = if b then "True" else "False" s = print smain
Ordering type is used to compare things:
data Ordering = LT | EQ | GT
Handling each possible case for a type is called
pattern matching, and ideally all of them should be handled
ord :: Ordering ord = LT s :: String = case ord of s LT -> "Less Than" EQ -> "Equal" GT -> "Greater Than" = print smain
Maybe type is parametrized and represents the existence of something with a generic type
t, avoiding the use of
null at all.
data Maybe t = Nothing | Just t
Pattern matching also works with parametrized types:
mInt :: Maybe Int = Just 9 mInt num :: Int = case mInt of num Just n -> n Nothing -> 0 = print nummain
Either type has 2 parameters and represents the existence of a value with type
e or a value with type
data Either e t = Left e | Right t
We can use
Either String t to represent an error message when a result of type
t cannot be obtained.
err :: Either String Int = Left "Could not obtain the number"err
When we see an arrow
-> in a type, we know it is a function.
Every function receives an
a and gives a
b as result.
f :: a -> b
Functions indicate their body with
f :: Int -> Int = x + 3 f x = print (f 5)main
The same function can be implemented inline as a lambda
f :: Int -> Int = \x -> x + 3 f = print (f 5)main
We can “combine” functions using the
. operator, called
composition, so that if we have
g . f then
f will produce an intermediate result to be taken by
g to produce a final result:
f :: Int -> Int = x + 3 f x g :: Int -> Int = x * 5 g x h :: Int -> Int = g . f -- be careful with the order h = print (h 2)main
There is also an
$ operator, called “application”, usually used to change precedence and avoid extra parenthesis. You can think of it as having parenthesis at both sides.
Here we have equivalent
main implementations, choose the one you prefer.
f :: Int -> Int = x + 1 f x -- all of these are equivalent = print . f $ 3 + 4 main1 = (print . f) $ (3 + 4) main2 = (print . f) (3 + 4) main3 = print (f (3 + 4)) main4 = main1main
A function can give a function as result and we can use this mechanism to make new definitions:
f :: Int -> (Int -> Int) = \y -> x + y f x = f 5 add5 = print (add5 6)main
Parenthesis in that type signature can be omitted, and we can also evaluate the
f function with all the parameters at once:
f :: Int -> Int -> Int = \y -> x + y f x = print (f 5 6)main
We can also move the
y parameter to the left side, just to make it easier to read:
f :: Int -> Int -> Int = x + y f x y = print (f 5 6)main
A function can receive a function as parameter, but then those parenthesis are required to maintain precedence. We don’t know what the
h function does, but we know it can be used over an
g :: (Int -> Int) -> Int = h 3 g h = x + 2 f x = print (g f) -- g consumes f functionmain
Pattern matching can also be used to define a function piece-by-piece
fib :: Int -> Int 0 = 0 fib 1 = 1 fib = fib (x - 1) + fib (x - 2) fib x = print (fib 10)main
When types are generic, function body can only use known operations.
a could be any type, so
x can only be returned as-is.
id' :: a -> a = x id' x = print (id' 5)main
We can define a set of operations, then types could implement them, that is called
As example a type which fulfils the
Eq typeclass will have all its functions available.
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool /=) x y = not (x == y) (
We can see that
b to implement
Eq, because it needs operations from that set.
class Eq b => Ord b where compare :: b -> b -> Ordering <), (<=), (>=), (>) :: b -> b -> Bool (max, min :: b -> b -> b
Typeclass implementation is done via instances for each type.
Here we define
Eq for the
(/=) is already defined based on
instance Eq Bool where ==) True True = True (==) False False = True (==) _ _ = False (
Num typeclasses, so inside
isPositive we can use number and comparison operations over
isPositive :: (Ord t, Num t) => t -> Bool = compare 0 xisPositive x
Now we are ready to inspect the type of the
main function we wrote at the beginning.
main :: IO () = print "hola"main
IO a type represents a set of instructions that will be executed by the runtime of Haskell, with something of type
a as result.
In the case of main
(), which is called unit, and its only possible value is
This means that the
main function produces a set of instructions to be executed by the runtime when the program is launched.
We know that
print "hola" type is also
IO () because it should have the same type that
main has to be valid code, and we also know that
"hola" is a
We could think that
print :: String -> IO (), but we have been using
C a => a -> IO () with some unknown constraint C.
That constraint is the
Show typeclass we can see here:
class Show a where show :: a -> String -- plus other definitions
show function takes something and produces a
String, then that function is the missing piece.
Then we can infer that
Show a => a -> IO (), so
a is converted to an
String which is printed.
This is the definition of the
print :: Show a => a -> IO () print x = putStrLn (show x)
As we can see, it uses
show to obtain an
String, which will be consumed by the
putStrLn function, and that is the one that has the
String -> IO () type we thought before.
We will see soon how to write bigger programs using
IO a type, but first we should talk a bit more about typeclasses.
Some typeclasses define a set of associated laws which cannot be checked by the compiler, but the code must follow them to preserve the logic.
Haskell relies on developers to check that their code adheres to the laws, which could be done via mathematical proofs, but there are also tools to generate informal tests to check properties (eg. QuickCheck).
We can take as example the
Eq typeclass we saw before:
class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool
Eq implementation should follow these laws:
x == x = True
x == y = y == x
- Transitivity: if
x == y && y == z = True, then
x == z = True
- Substitution: if
x == y = True, then
f x == f y = True
x /= y = not (x == y)
We can see that our previous
Eq Bool instance follows reflexivity law, because by definition agrees with
x == x form:
==) True True = True (==) False False = True(
Given that our implementation is valid, we can always replace
x == x with
True when we see it, which is useful to simplify our code.
Typeclass laws help us to refactor the code and make it better using known properties.
There are many typeclasses defined in the Haskell libraries, the Typeclassopedia is a good place to start learning more details about the standard typeclasses, but I will mention here some of the most common ones and their laws, just as reference, there is no need to memorize them now because they will become familiar as time passes.
Types which fulfil
Semigroupapi should implement
(<>)function, also called
class Semigroup a where (<>) :: a -> a -> a -- other definitions...
The following property, called associativity, should be true for any valid
(x <> y) <> z = x <> (y <> z)
We can use
(<>)function to take to things of the same type and produce a combined result also of the same type.
= "hola" s1 = "mundo" s2 = print (s1 <> s2)main
Semigroupinstance defines how those things are combined, in this case
Types which fulfil
class Functor t where fmap :: (a -> b) -> t a -> t b -- other definitions...
The following properties should be true for any valid
fmap id == id
fmap (f . g) == fmap f . fmap g
We can use
fmapover a parametrized type
t ato apply a function
a -> bwhich takes things of type
ato produce things of type
fmapis applied over a parametrized
List Intto apply
ffunction which will add 3 to each integer inside the list, obtaining a new list with the same shape but new values.
xs :: [Int] = [1, 2, 3] xs f :: Int -> Int = x + 3 f x = print (fmap f xs)main
fmapbehavior depends on the specific parametrized type we are working with, eg. in the case of data structures usually allows us to apply a function over each element preserving the structure shape.
Types which fulfil
Applicativeapi should implement the required functions (ie.
(<*>), etc) and must have a
Functorinstance as well, so the
fmapfunction will be available as well.
class Functor t => Applicative t where pure :: a -> t a (<*>) :: t (a -> b) -> t a -> t b -- other definitions...
The following properties should be true for any
pure id <*> v = v
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
pure f <*> pure x = pure (f x)
u <*> pure y = pure ($ y) <*> u
purefunction is really useful when working with a parametrized type
Maybe a, etc) because it allows us to take something of type
aand generate a value of type
t ain a predefined way.
= pure ()main
This example shows a program which does nothing, but it is interesting anyway because we can see how
IO afrom an
a, which in this case is the unit type.
Any type which implements
Monadwill have a
bind, it should also implement
Functorapi as well, so we also have the
fmapfunctions available for
class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b -- other definitions...
When we see
mf >>= kwe know
kconsumes something of type
mf :: m aand
k :: (a -> m b)), so we can say
kis a continuation, because it could be the next piece to be executed.
Keep in mind that the following properties are required for any valid
pure a >>= k = k a
mf >>= pure = mf
mf >>= (\x -> k x >>= h) = (mf >>= k) >>= h
>>=function is useful when we have something of a parametrized type
t aand we want to process the values of type
awith the condition that in the end we should produce something of type
f :: Int -> String = "n = " <> show n f n mInt :: Maybe Int = Nothing mInt = print (mInt >>= (pure . f))main
In the example we have
Maybe Intand we would like to process that
Intwith the function
fto obtain an
String, so we use the bind function
>>=to do handle this and give
pure . fas continuation, so it conforms with the expected type
Int -> Maybe String.
The parametrized type
Maybe ahas a bind implementation which is intelligent enough to note that the
Int) doesn’t exist, because
Nothing, so bind avoids calling
pure . fas the continuation expects the
Intto be there.
We can se that
pure . fuses
pureto conform with
Int -> Maybe Inttype, and it could have consumed an
mInthad it (eg.
mInt = Just 4).
As we can see, bind mechanism and meaning are related to the parametrized type which implements the
Monadinstance, so we need to understand that type very well before learning about the inner working of a certain typeclass instance.
Finally, as promised, we can see how to write bigger programs using
IO a type.
First we can see a piece of code which uses
(>>=) operator to obtain a
String written by the user and then prints it to the console.
= getLine >>= putStrLnmain
We can rewrite it using an explicit parameter named
line, which is produced by
getLine subroutine and passed to the continuation (remember that when we see something like
mf >>= k, then
k is the continuation).
= getLine >>= (\line -> putStrLn line)main
As this gets tiring really quickly, Haskell defines a special syntax called
do-notation, which we can use to write equivalent code in a more familiar style.
Like in 2nd example
getLine result is available as
= do main <- getLine line putStrLn line
As a final example we have an imperative-style program which asks the user for an input and then iterates over the elements of a list printing the user input each time.
import Control.Monad (forM_) = [1..10] xs = do main <- getLine line $ \x -> do forM_ xs putStrLn (line <> show x)
There are other ways to write this program, but this can feel familiar to programmers which already know other languages.