#
Emulator Trace & Contract Monads
#
Monads
Lecture 4, Part 2
This part is not necessarily specific to Plutus. This is an overview of how monads work in a functional programming language like Haskell.
#
Pure Functions
To understand how monads work, we'll compare to Java. Imagine this method.
public static int foo() {
// Some code. Maybe it asks the user to input a number on the console and then
// returns it.
}
foo()
foo()
It doesn't take any argument, and returns an int. It's entirely possible that
the two calls to foo() return a different int. The function could be doing
some IO which changes what int will be returned. These IO operations are called
side-effects.
Haskell is a purely functional language. This situation cannot happen in Haskell. Consider this function.
foo :: Int
foo = -- Some code
foo
foo
foo will always return the same number. We don't need to know what the code
inside it does to know that. If we give a function the same arguments, it will
always return the same result. Side-effects cannot happen.
#
The IO Monad
Without side-effects, your program can't really do anything. It can't affect the world. Side-effects like input into the program and output like printing a result to console are necessary to have a useful program.
Haskell has a way to do side-effects called the IO monad.
foo :: IO Int
foo = -- Some code
The IO Int type represents an action to compute an int which can invoke
side-effects. You can think of it like a recipe. It's a list of instructions for
what a computer should do to end up with an int as a result.
foo is actually still pure here. When foo is evaluated, the "recipe" won't
actually be executed. The result of evaluating foo is just the recipe to
produce the int.
The only way to actually execute the recipe is in the main function. In the executable's main entry point. Note that the REPL (GHCi) also allows you to execute IO actions.
#
Hello World in Haskell
main :: IO ()
main = putStrLn "Hello, World!"
The function name must be main and the type must be IO (). That type is
again a recipe that can do some side-effects and return unit () which means it
returns nothing.
putStrLn is a function that takes a string and returns IO (). That return
type matches what main is expecting.
:t putStrLn
putStrLn :: String -> IO ()
You could execute the hello world program in the REPL by typing it directly.
putStrLn "Hello, World!"
"Hello, World!"
#
getLine
There are many IO actions in Haskell. Here's one example. There's a function
called getLine. It waits for user input from the keyboard.
:t getLine
getLine :: IO String
getLine --Haskell is now waiting for user input on the console.
Hello
"Hello"
You can combine these primitive IO actions into bigger, more complex actions to make your program useful.
#
Functors
View information about IO. It has a Functor instance.
:i IO
type IO :: * -> *
newtype IO a
= ghc-prim-0.6.1:GHC.Types.IO (ghc-prim-0.6.1:GHC.Prim.State#
ghc-prim-0.6.1:GHC.Prim.RealWorld
-> (# ghc-prim-0.6.1:GHC.Prim.State#
ghc-prim-0.6.1:GHC.Prim.RealWorld,
a #))
-- Defined in ‘ghc-prim-0.6.1:GHC.Types’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monoid a => Monoid (IO a) -- Defined in ‘GHC.Base’
instance Semigroup a => Semigroup (IO a) -- Defined in ‘GHC.Base’
instance MonadFail IO -- Defined in ‘Control.Monad.Fail’
Functor is an important type in Haskell.
:t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
We are particularly interested when f is IO.
#
Functor Example with toUpper
toUpper works on Chars.
:t toUpper
toUpper :: Char -> Char
toUpper 'q'
'Q'
Since a String is just a list of Chars, we can map over a String with
toUpper.
:t map toUpper
map toUpper :: [Char] -> [Char]
map toUpper "haskell"
"HASKELL"
Note how map toUpper has a good type signature to use as the first argument to
fmap.
Remember that getLine's type is IO String. That matches the second argument
to fmap.
Finally, we can use fmap with these arguments.
:t fmap (map toUpper) getLine
fmap (map toUpper) getLine :: IO [Char]
fmap (map toUpper) getLine
hello world
"HELLO WORLD"
We were able to map toUpper over the input given by getLine.
#
>>
You can perform IO actions in sequence with >>
putStrLn "Hello" >> putStrLn "World"
Hello
World
If the first action has a return result, it will be ignored.
#
>>= (Bind)
If you need to use the result of the first action, you can use bind >>=. The
type signature must match this:
:t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
It uses any monad, but we are interested in IO so imagine m above is IO.
Given the first argument that has side-effects a: IO a
And given the second argument that is a function takes an a and returns IO b: (a -> m b)
>>= would give us IO b.
#
Bind Example with getLine
-- Look at the type signature
:t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
-- getLine matches the first argument
:t getLine
getLine :: IO String
-- putStrLn matches the second argument
:t putStrLn
putStrLn :: String -> IO ()
-- Bind them
getLine >>= putStrLn
Hello World
Hello World
#
Another Bind Example
-- In a file called src/Week04/Notes.hs
module Notes where
bar :: IO ()
bar = getLine >>= \s ->
getLine >>= \t ->
putStrLn (s ++ t)
-- In the REPL
:l src/Week04/Notes.hs
bar
Hello
World
HelloWorld
#
Return
:t return
return :: Monad m => a -> m a
Again, it can be used with any monad, but we're interested in IO so think IO
when you see m above.
return takes an argument a and returns IO a.
return "Hello World" :: IO String
"Hello World"
#
The Maybe Type
We're done working with IO for right now. Now we focus on Maybe.
:i Maybe
type Maybe :: * -> *
data Maybe a = Nothing | Just a
-- more info
It has two constructors Nothing or Just a. It can take either Nothing or
a with a Just constructor.
#
readMaybe
You can read a String as an Int with read
read "42" :: Int
42
But you get an error when you try to read something that cannot be parsed as
an Int.
read "abc" :: Int
*** Exception: Prelude.read: no parse
It's better to use readMaybe
import Text.Read (readMaybe)
readMaybe "42" :: Maybe Int
Just 42
readMaybe "abc" :: Maybe Int
Nothing
Instead of a direct Int or an error, we now get Just with the Integer or
Nothing.
#
Maybe.hs
Most of my notes are comments in the file.
#
Either.hs
:i Either
type Either :: * -> * -> *
data Either a b = Left a | Right b
-- more info
The two constructors are Left and Right. Either will be one of those.
Left "Hello World" :: Either String Int
Left "Haskell"
Right 42 :: Either String Int
Right 42
The problem with Maybe is that when there's an error, only Nothing is
returned. There's no place for an error message. We can use Either to solve
that problem.
The convention is the Left corresponds to an error while Right corresponds
to the expected value.
I wrote more notes as comments in the file.
#
Writer.hs
Here's another example. This one can produce computations that also output logs.
Most of my notes are comments in the file.
#
Monad.hs
With all of the above explained, we can get a better understanding of what a monad is. It's a combination of a computation and real-world side-effects.
Whenever there's a possibility of a computation with side-effects, we also have the ability to do the computation without side-effects.
Computations like...
>>=MaybeEitherWriter
have counterparts that return the computation without side-effects. These are...
returnJustRight(\a -> Writer a [])
The definition of a monad is the combination of these two features.
- the ability to bind two computations together
- the ability to construct a computation from a pure value without making use of the potential side-effects
Some more notes of my notes are comments in the file.
#
Applicative
Monad is actually a subclass of Applicative.
:i Applicative
type Applicative :: (* -> *) -> Constraint
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
-- more info
Notice that pure has the same signature as Monad's return.
The standard way to implement Functor and Applicative is to import helper
functions from Control.Monad
instance Functor Writer where
fmap = liftM
instance Applicative Writer where
pure = return
(<*>) = ap
instance Monad Writer where
return a = Writer a []
(>>=) = bindWriter
#
Superpowers
Another way to think about monads is that they are a way to do a computation withe a side-effect or a superpower.
IO's superpower is having real-world side-effects like...
- keyboard or file input
- console output or writing to a file
Maybe's superpower is the ability to fail without throwing an exception.
Either's superpower is the ability to fail with an error message.
Our custom Writer's superpower is to log strings.
There's a saying in the Haskell community:
Haskell has an overloaded semi-colon
In imperitave languages, commands are executed in order separated by a
semi-colon. In a sense, bind >>= is like a semi-colon. It binds together
monadic computations together like a semi-colon would in an imperitave language.
But in Haskell, this "semi-colon" is programmable. We can say what to do when the semi-colon is reached. It comes with a way to tell the computer how to combine two computations.
#
do Notation
If you have multiple lines all bound together:
threeInts mx my mz =
mx >>= \k ->
my >>= \l ->
mz >>= \m ->
let s = k + l + m in return s
Haskell has a special notation called do notation that makes things a little
more simple:
threeInts' mx my mz = do
k <- mx
l <- my
m <- mz
return k + l + m
This does the same as the first example, just with some syntactic sugar. It's a
matter of taste. For smaller number of binds, people tend to write them
explicitly. For larger number of binds, they use do.
You can also use let with this notation
threeInts' mx my mz = do
k <- mx
l <- my
m <- mz
let s = k + l + m
return s
#
Multiple Side-Effects
Often you'll want to do several side-effects. Like failing with an error message and logging. There are several approaches.
- monad transformers to build custom monads
- effect systems to tailor make monads that have the exact features you want them to have
Plutus uses effect systems for the Contract and Trace monads.
You don't have to know everything about how an effect system works to use them effectively. You just have to understand what each can do.