Lecture 4
Monads and IO
Monad: The Idea
Maybe, yes? Maybe, no?
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = case ma of
Nothing -> Nothing
Just a -> case mb of
Nothing -> Nothing
Just b -> Just (a + b)
π₯Ίππ Awkward pattern matching
andThen :: Maybe Int -> (Int -> Maybe Int) -> Maybe Int
andThen ma f = case ma of
Nothing -> Nothing
Just x -> f x
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = andThen ma (\a -> andThen mb (\b -> Just (a + b)))
π Ordinary helper function
π Refactoring
andThen :: Maybe a -> (a -> Maybe b) -> Maybe b
andThen ma f = case ma of
Nothing -> Nothing
Just x -> f x
Either way
eitherPlus :: Either String Int -> Either String Int -> Either String Int
eitherPlus ea eb = case ea of
Left err -> Left err
Right a -> case eb of
Left err -> Left err
Right b -> Right (a + b)
π₯Ίππ Awkward pattern matching strikes again
andThen :: Either String Int
-> (Int -> Either String Int)
-> Either String Int
andThen ea f = case ea of
Left err -> Left err
Right x -> f x
eitherPlus :: Either String Int -> Either String Int -> Either String Int
eitherPlus ea eb = andThen ea (\a -> andThen eb (\b -> Right (a + b)))
π Ordinary helper function
π Refactoring
andThen :: Either e a
-> (a -> Either e b)
-> Either e b
andThen ea f = case ea of
Left err -> Left err
Right x -> f x
Multiple combinations
listPlus :: [Int] -> [Int] -> [Int]
listPlus la lb = case la of
[] -> []
a : as -> case lb of
[] -> []
bs -> map (+ a) bs ++ listPlus as bs
π₯Ίππ Even more awkward pattern matching
andThen :: [Int] -> (Int -> [Int]) -> [Int]
andThen l f = case l of
[] -> []
x : xs -> f x ++ andThen xs f
listPlus :: [Int] -> [Int] -> [Int]
listPlus la lb = andThen la (\a -> andThen lb (\b -> [a + b]))
π Ordinary helper function
π Refactoring
andThen :: [a] -> (a -> [b]) -> [b]
andThen l f = case l of
[] -> []
x : xs -> f x ++ andThen xs f
π The function became slower after refactoring!
Is there a pattern?
andThen :: Maybe a -> (a -> Maybe b) -> Maybe b
andThen :: Either e a -> (a -> Either e b) -> Either e b
andThen :: [a] -> (a -> [b]) -> [b]
π Very similar type signatures
maybePlus ma mb = andThen ma (\a -> andThen mb (\b -> Just (a + b)))
eitherPlus ea eb = andThen ea (\a -> andThen eb (\b -> Right (a + b)))
listPlus la lb = andThen la (\a -> andThen lb (\b -> [a + b]))
π Almost the same implementations
What to do when we see a pattern?
1. Nothing
2. Recognize
4. Abstract
5. Put in a library
3. Copy-paste
Monad is a pattern
π Monad is a generalization of the "andThen" pattern
How to generalize? π€
We need:
- Polymorphic andThen
- Polymorphic constructor
maybePlus ma mb = andThen ma (\a -> andThen mb (\b -> Just (a + b)))
eitherPlus ea eb = andThen ea (\a -> andThen eb (\b -> Right (a + b)))
listPlus la lb = andThen la (\a -> andThen lb (\b -> [a + b]))
Monad
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
π§βπ What we can already learn from the definition
π« What we can't see from the above
- Monad is a typeclass
- It has two methods
- The second method is an operator (it's called bind)
- It's a typeclass for type constructors like Maybe and not e.g. Int
- Instances implemementations
- Laws
- How to use this abstraction efficiently
- Why it's called Monad
Just a typeclass
Instances
instance Monad Maybe where
return :: a -> Maybe a
return x = Just x
(>>=) :: Maybe a
-> (a -> Maybe b)
-> Maybe b
Nothing >>= _ = Nothing
Just x >>= f = f x
βΉοΈ Maybe
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
instance Monad (Either e) where
return :: a -> Either e a
return x = Right x
(>>=) :: Either e a
-> (a -> Either e b)
-> Either e b
Left e >>= _ = Left e
Right x >>= f = f x
instance Monad [] where
return :: a -> [a]
return x = [x]
(>>=) :: [a] -> (a -> [b]) -> [b]
l >>= f = concatMap f l
βΉοΈ Either
βΉοΈ List
Laws
instance Monad Maybe where
return :: a -> Maybe a
return x = Just x
...
π Correct Monad instance for Maybe
Left identity
Right identity
Associativity
instance Monad Maybe where
return :: a -> Maybe a
return x = Nothing
...
π Incorrect Monad instance for Maybe
Generalizing
andThen :: Maybe a -> (a -> Maybe b) -> Maybe b
andThen :: Either e a -> (a -> Either e b) -> Either e b
andThen :: [a] -> (a -> [b]) -> [b]
π Similar chaining functions
(>>=) :: Monad m => m a -> (a -> m b) -> m b
π Similar constructors
Just :: a -> Maybe a
Right :: a -> Either e a
(:[]) :: a -> [a] -- robot monkey operator
return :: Monad m => a -> m a
Refactoring
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = andThen ma (\a -> andThen mb (\b -> Just (a + b)))
0οΈβ£ Starting with the basics
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = ma `andThen` (\a -> mb `andThen` (\b -> Just (a + b)))
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = ma `andThen` \a -> mb `andThen` \b -> Just (a + b)
1οΈβ£ Using infix form of andThen
2οΈβ£ Removing redundant ()
maybePlus :: Maybe Int -> Maybe Int -> Maybe Int
maybePlus ma mb = ma >>= \a -> mb >>= \b -> return (a + b)
monadPlus :: Monad m => m Int -> m Int -> m Int
monadPlus ma mb = ma >>= \a -> mb >>= \b -> return (a + b)
3οΈβ£ Replacing `andThen` with >>=Β and Just with return
4οΈβ£Generalizing the type and changing the name
Actually, in Haskell...
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
class Applicative m => Monad m where
return :: a -> m a
return = pure
(>>=) :: m a -> (a -> m b) -> m b
βΉοΈ Monad is a part of FAMily
π©βπ¬ Everyone uses pure instead of return nowadays
And that's the Monad!
Real World
Purity π§
map :: (a -> b) -> [a] -> [b]
π Pure functions β functions without side-effects that depend only on their explicit input arguments.
π₯ Benefits of pure functions
1. Determinism
4. Composability
2. Easier reasoning
5. Optimizations
6. Parallelism
3. Simpler testing
π§ Purity + Laziness π¦₯
getLine :: String
π€ How would a function that reads a line would look like?
ghci> getTwoLines !! 1
???????
π€π€ How about reading two lines??
getTwoLines :: [String]
getTwoLInes = [getLine, getLine]
π€π€π€ How this should work???
Conclusion
π‘ We can't have side effects in Haskell without changing at least something. Otherwise, we'll lose the remainings of our sanity while working with the language.
Input/Output (IO)
data IO a = ...
βΉοΈ Opaque data type IO
βΉοΈ Functions with side-effects
getLine :: IO String
π©βπ¬ getLine is a function that returns a value of type String and also performs some side-effects
βΉοΈ IO has the Monad instance
instance Monad IO where
return :: a -> IO a
(>>=) :: IO a -> (a -> IO b) -> IO b
...
π©βπ¬ The Monad instance for IO allows to chain effectful actions
getTwoLines :: IO [String]
getTwoLines =
getLine >>= \line1 -> getLine >>= \line2 -> pure [line1, line2]
Common question
How to get String from IO String?
Concept of IO
String
IO String
How to run IO?
βΉοΈ A helper function to print to the terminal
putStrLn :: String -> IO ()
π©βπ¬ putStrLn is a function that takes a value of type String, prints it the terminal and doesn't return any meaningful value.
ghci> () -- a unit type
()
ghci> :t ()
() :: ()
ghci> putStrLn "Hi! π"
Hi! π
ghci> :t getLine
getLine :: IO String
ghci> getLine
Alice
"Alice"
ghci> fmap length getLine
Bob
3
How to run Haskell programs?
module Main where
main :: IO ()
main = ... your program goes here ...
π©βπ¬ Haskell program starts with the main function in the module Main
module Main where
main :: IO ()
main = putStrLn "Hello, world!"
Hello World
Printing twice
βWe want to print two different lines
putStrLn :: String -> IO ()
π©βπ¬ We use the bind operator (>>=) and ignore the putStrLn resutl
module Main where
main :: IO ()
main = putStrLn "Hello, Alice!" >>= \_ -> putStrLn "Hello, Bob!"
(>>) :: Monad m => m a -> m b -> m b
action1 >> action2 = action1 >>= \_ -> action2
βΉοΈ A helper function
main :: IO ()
main = putStrLn "Hello, Alice!" >> putStrLn "Hello, Bob!"
βΉοΈ Slightly shorter version
do
π do-notation β syntax sugar for >>=,Β >> and let-in
example1 =
fun1 >>= \result -> fun2 result
example1 = do
result <- fun1
fun2 result
βΉοΈ >>= rule
example1 = fun1 >>= fun2
Eta-reduced example1 with >>=
βΉοΈ >>Β rule
example2 = fun1 >> fun2
example2 = do
fun1
fun2
βΉοΈ let-in rule
example3 = let x = f y in fun x
example3 = do
let x = f y
fun x
Example with do
βοΈ Write a program that reads a line and prints its reverse
main :: IO ()
main =
getLine >>= \line ->
let rev = reverse line in
putStrLn rev
main :: IO ()
main = do
line <- getLine
let rev = reverse line
putStrLn rev
π Classic Monad Style
βοΈ Common mistake #1
let line = getLine
rev <- reverse line
βοΈ Common mistake #2
π do-Style
Cabal
Cabal
π Cabal β format for describing structure of Haskell packages.
my-project/
βββ app/
β βββ Main.hs
βββ src/
β βββ MyModule.hs
β βββ AnotherModule.hs
βββ my-project.cabal
cabal-version: 2.4
name: my-project
version: 1.0.0.0
library
hs-source-dirs: src
exposed-modules: MyModule
AnotherModule
build-depends: base ^>= 4.14
executable my-project
hs-source-dirs: app
main-is: Main.hs
build-depends: my-project
, base
π A Haskell package
π Files with the .cabalΒ file extension describe the package.Β
π cabal-install β a Haskell build tool that works with Cabal packages.
Typical main
module Main (main) where
import qualified MyModule
main :: IO ()
main = MyModule.main
build-depends
library
hs-source-dirs: src
exposed-modules: MyModule
build-depends: base ^>= 4.14
, containers ^>= 0.6
, text ^>= 1.2
, time ^>= 1.9
π The build-depends field in the .cabal file specifies external Haskell packages and their versions you want to use.
Common Haskell packages
π¦ base β standard Haskell library
π¦ containers β Map and Set data structures
π¦ text β efficient UTF-16 strings (UTF-8 since version 2.0)
π¦ bytestring β byte arrays
π¦ time β time, clocks and calendar data types and functions
π¦ aeson β JSON parsing
βΉοΈ Hackage has 16K+ packages
Functional π½, Imperative π
readFile :: FilePath -> IO Text
countWords :: Text -> IO (Map Text Int)
printWord :: Map Text Int -> IO ()
π©βπ¬ Haskell allows controlling side effects. Separate pure logic from effectful computations and require IO only when needed.
βοΈ Write a program that finds the most frequent word in a file
π’ Imperative style
π€Έ Functional style
readFile :: FilePath -> IO Text
countWords :: Text -> Map Text Int
findFrequent :: Map Text Int -> Maybe (Text, Int)
displayResult :: Maybe (Text, Int) -> Text
putStrLn :: Text -> IO ()
More sources
Lecture 4: Monads and IO
By Haskell Beginners 2022
Lecture 4: Monads and IO
Fundamentals of Functional Programming and Haskell
- 2,866