Advertisement
kronicmage

Super quick monad uses

Mar 12th, 2020
922
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Haskell 11.03 KB | None | 0 0
  1. import Control.Monad
  2.  
  3. -- I recently wrote a bunch of code at work today that was a relatively common real worl use
  4. -- of monads. Thought I'd share
  5.  
  6. -- Suppose you want to do the following:
  7. -- 1. Take an integer as input
  8. -- 2. Verify that it's a certain form (i.e. a multiple of 7 maybe) (in real world, this was verifying a checksum)
  9. -- 3. Check that a corresponding element exists in a database
  10. -- 4. Verify that the value attached to that element in the database is also a valid form
  11. --
  12. -- In something like python, you would probably do something like this all day:
  13. -- > def f(x):
  14. -- >     if valid_input(x):
  15. -- >          if x in database:
  16. -- >               value = database[x]
  17. -- >               if valid_output(value):
  18. -- >                    return value
  19. -- >     raise SomeException
  20. --
  21. -- You can probably see that as checks and conditions and complexity grows with this kind of thing,
  22. -- it can quickly get unwieldy. The natural response for this in langauges like Python, of course,
  23. -- is to ask for forgiveness rather than permission: just do the computations and catch exceptions later.
  24. --
  25. -- The problem with that approach, especially with larger code bases, and multiple engineers working
  26. -- on the same codebase, is that it becomes increasingly difficult to tell when something will throw an
  27. -- error, which errors will be thrown, when to catch errors (are these errors already handled lower/higher
  28. -- on the stack?), and in particular, it's almost impossible to guarantee that your code will not crash.
  29. --
  30. -- In Haskell, we are a statically typed language. Everything that a function does should have a
  31. -- type signature that corresponds to what to expect.
  32. -- Say you have a constant integer in haskell. What would that look like?
  33. -- > x :: Int
  34. -- > x = 5
  35. -- In Java, for comparison, that would be
  36. -- > int x = 5;
  37. -- But in Java, what about an integer that you read from stdin? It would also just have a type signature of `int`.
  38. -- Haskell is special in that since `x :: Int` looks like a constant, you would need some special
  39. -- type to denote that x is not a constant, for instance, `x :: IO Int`
  40. --
  41. -- Back to our earlier example: in Haskell, throwing exceptions willy nilly is bad practice.
  42. -- You want your type signatures to represent something that is failable. Because ultimately, with
  43. -- a properly constructed type definition, then successful compilation => never crashing!!!
  44.  
  45. -- So for our example monad, we have a data type that can either hold the successful result
  46. -- of a computation, or an object describing an error that has occured.
  47. data Exceptable a = Error String | Result a deriving (Show)
  48.  
  49. -- For this to be a useful exception type, it should be something that essentially
  50. -- allows you to run functions on it as normal if we got a valid result.
  51. -- But when there's an error, it should carry that error through to the end.
  52. instance Monad Exceptable where
  53.     return = Result
  54.     (Error  x) >>= f = Error x
  55.     (Result y) >>= f = f y
  56.  
  57. -- for compatibilty reasons, I also need to define the Applicative and Functor instance
  58. -- I won't explain those for now, but you can look it up for interest.
  59.  
  60. instance Applicative Exceptable where
  61.     pure  = return
  62.     (<*>) = ap
  63. instance Functor Exceptable where fmap f x = x >>= (return . f)
  64.  
  65. -- Now, back to monads.
  66. -- The core of monadic programming style, are functions of the form `(a -> m b)`
  67. -- The secret is that monads make it extremely easy to chain and sequence
  68. -- functions of that form together. So it is extremely easy for separate developers
  69. -- to build smaller building blocks, and then compose our way up to bigger building blocks
  70. -- For instance, we can turn each of our 4 steps into a separate (a -> m b) function,
  71. -- and then chain it all together
  72. -- (note: I'm gonna write really contrived versions of what actually happened just for the sake of
  73. -- simplicity and time)
  74.  
  75. -- In realworld, this would have been a proper input checksum check lol
  76. validate_input :: Int -> Exceptable Int
  77. validate_input x = if x `mod` 7 == 0 then Result x else Error "Bad input"
  78.  
  79. -- this was verifying that the entry reference format was valid in the real world
  80. -- so if it was valid, it would return it as a result. If it was invalid, throw error
  81. -- (in real world we have some higher level cleaner ways of doing this kind of check on booleans,
  82. -- but the real world version wasn't a simple boolean check anyway so this is just for simplicity)
  83. valid_reference :: Int -> Exceptable Int
  84. valid_reference 14 = Result 14
  85. valid_reference 21 = Result 21
  86. valid_reference x  = Error "Bad database reference"
  87.  
  88. -- pretend that this was something that read from database
  89. retrieve_from_database :: Int -> Exceptable String
  90. retrieve_from_database 14 = Result "data!"
  91. retrieve_from_database x  = Error "Database permissions error"
  92.  
  93. -- so now, with these (a -> m b) functions in hand, it's time to chain them!
  94. -- suppose we got ourselves some input data that came from elsewhere in the program:
  95.  
  96. input :: Exceptable Int
  97. input = Result 5
  98.  
  99. -- then we can bind it to all of our smaller components to get what we want!
  100. output :: Exceptable String
  101. output = input >>= validate_input >>= valid_reference >>= retrieve_from_database
  102. -- > Error "Bad input"
  103.  
  104. -- so we were able to do what the big if statement chain did in python, but
  105. -- in practically one line!
  106. -- In order to achieve the same level of run time safety in Python, you would need all sorts
  107. -- of nesting, or stack management shenanigans. Lots of layers.
  108. -- The joy of monads is that they are able to keep everything flat!
  109. -- The input to (>>=) is (m a). The output of (>>=) is a (m b). Nowhere do we have (m (m (m a))) or whatever
  110. -- By keeping things flat, and making outputs of >>= the same as the input of bind, it becomes possible
  111. -- to chain lots of smaller components together in one go
  112. -- Composability is key!
  113.  
  114. -- For instance, we could have combined input sanitation and format validation in one go:
  115. validate_input_and_format :: Int -> Exceptable Int
  116. validate_input_and_format = validate_input >=> valid_reference
  117. -- (>=>) is (.) but for functions (a -> m b) instead of (a -> b)
  118. -- f >=> g = \x -> f x >>= g
  119. -- So our output computation can be a bit smaller:
  120. output2 :: Exceptable String
  121. output2 = input >>= validate_input_and_format >>= retrieve_from_database
  122.  
  123. -- or, we could just make output a function directly! like so...
  124. output_func :: Int -> Exceptable String -- note the (a -> m b) form!
  125. output_func input = validate_input input >>= valid_reference >>= retrieve_from_database
  126. -- ...which is equivalent to...
  127. output_func2 :: Int -> Exceptable String
  128. output_func2 = validate_input >=> valid_reference >=> retrieve_from_database
  129.  
  130. -- so now we can just run:
  131. -- > output_func2 7
  132. -- -> Error "Bad database reference"
  133. -- > output_func2 14
  134. -- -> Result "data!"
  135.  
  136. -- So as we get non-error data from other parts of the program, we can just >>= that data into output_func
  137. -- and get more data. Composability of effectful actions is the core idea of monads.
  138.  
  139. -- I ask you again to consider what the python equivalent of this would be.
  140. -- To get this kind of simplicity, flatness, and composability in Python, you would need
  141. -- a lot of exception throwing and catching (since if statement chains can't get you this
  142. -- level of flatness). And that just gets messy in large scale.
  143. -- Sure, if you were to make something similar to this super small program in that manner,
  144. -- then you would be fine. But with large code bases, that quickly breaks down.
  145. -- Monads let you keep things clean and flat, no matter how large your code gets
  146.  
  147. -- There's a lot more I can talk about monads. There is another reason that they're used -
  148. -- they have a strong theoretical background from category theory. A lot of higher level
  149. -- control structures of monads are lifted straight from once-theoretical category theory
  150. -- constructs. By using the math so directly, we benefit from all the theorems and results
  151. -- that they provide.
  152.  
  153. -- A lot more things are monads than one may realize. For example, lists are monads:
  154. -- > instance Monad [] where
  155. -- >     return x = [x]
  156. -- >     lst >>= f = concatMap f lst
  157. -- (note: concatMap is just flatMap (a.k.a. map function, then flatten list))
  158. -- And list comprehensions are just monad structures!
  159.  
  160. -- for example, a basic comprehension:
  161. comprehension_a = [(x, y) | x <- [3, 4, 5], y <- [1, 2, 3], x + y == 6]
  162. -- has the following monadic form:
  163. comprehension_b = [3, 4, 5] >>= \x -> [1, 2, 3] >>= \y -> guard (x + y == 6) >> return (x, y)
  164. -- in haskell, we have syntax sugar for this called do-notation:
  165. comprehension_c = do
  166.     x <- [3, 4, 5]
  167.     y <- [1, 2, 3]
  168.     guard (x + y == 6)
  169.     return (x, y)
  170. -- where ([letter] <- [val]) is sugar for [val] >>= \[letter] -> ...
  171. -- and every other line has implicit `>>` between them
  172.  
  173. -- another surprising one: functions are monads! If you know continuation passing style,
  174. -- then I can tell you that CPS is equivalent to the monad instance of functions. In pseudo-haskell,
  175. -- > instance Monad (r ->) where
  176. -- >     return x = \_ -> x
  177. -- >     f >>= g  = \r -> g (f r) r
  178. -- Why is this the implementation you may ask? Because this is the only implementation that follows
  179. -- the monad laws from math. That's beyond the scope of this right now though
  180.  
  181. -- So since so many things are monads, that lets us write really general code as control flow structures!
  182. -- For example:
  183. generalized_cartesian :: Monad m => m a -> m b -> m (a, b)
  184. generalized_cartesian xs ys = do
  185.     x <- xs
  186.     y <- ys
  187.     return (x, y)
  188.  
  189. -- > generalized_cartesian [1, 2] [3, 4]
  190. -- -> [(1, 3), (1, 4), (2, 3), (2, 4)]
  191. -- > generalized_cartesian (Result 3) (Result 4)
  192. -- -> Result (3, 4)
  193. -- And of course, the function monad :)
  194. -- > generalized_cartesian (\x -> x*x) (\x -> x+x) $ 5
  195. -- -> (25, 10)
  196.  
  197. -- In general, monads let you structure a potentially stateful computation,
  198. -- by letting you separate the context/structure of a computation from its execution
  199. -- In principle, its possible to write a monad such that the following do block is valid code
  200. -- and behaves like an assembly language:
  201. -- > do
  202. -- >     add 3 0 3
  203. -- >     mov 3 4
  204. -- >     label "asdf"
  205. -- >     jlz 3 "asdf"
  206. -- Implementation is an exercise for the reader :).
  207.  
  208. -- So long as every monad instance follows the monad laws, then we achieve ultimate control
  209. -- over state, effects, and the order in which things are run. We can do so in such a manner
  210. -- that avoids aribtrary control flow (who knows where you'll end up when you throw an exception???),
  211. -- that is statically typed (you can tell when something can error/change state just from the type!!!),
  212. -- and that can be always flat and doesn't need to nest (unlike big if-chains!)
  213. -- Furthermore, the same mathematical structure that allows you to do this has applications in many
  214. -- more areas that allow us to write really really general combinators that can sequence computations of
  215. -- any kind, as long as they are monads!
  216.  
  217. -- Sorry if this doesn't make sense it's kinda late when I'm writing this lmao
  218. -- But that's the gist of why monads are nice
  219. -- Ask me if you have any questions
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement