Take a walk on the functional side: yes, we need monads
Apr 6, 2024
It’s not always practical to rewrite a large codebase in a functional language. But it’s natural for a large codebase, especially in finance, to have a functional core, as functional programming is a natural fit to express data transformations. We can use functional programming techniques to improve the quality and maintainability of our code. Even for Python-exclusive projects, I often require my team to write core functions in a functional style (a quant
module). Following SOLID principles, we can write core functions that are small, single-purpose, and pure, which makes them easier to test and reason about. The rest of the codebase can then be written in an imperative style, calling these core functions as needed.
In this post, I’ll provide some insights into incorporating elements of functional programming into a large imperative codebase, using Haskell and Python as an example. I assume you are a professional developer with a basic understanding of functional programming concepts.
We often refer to the term functional programming, meaning the following programming techniques:
- Composing functions for structuring programs and using recursion instead of loops
- Purity, so that the result of a function is fully determined once its parameters have been fixed
- Absence of side effects (doing literally nothing except evaluating the result)
- Immutability (the inability to change the value of a variable)
- Referential transparency (the ability to replace a variable with its value without introducing any effects)
- Equational reasoning (the ability to reason about functions and their results)
Some other not necessarily exclusive features of functional programming languages include:
- Higher-order functions (functions that can take other functions as arguments or return them as results)
- Lazy evaluation (the ability to delay the evaluation of an expression until its value is actually needed)
- Pattern matching (a way to match values against patterns and destructure them)
- Algebraic data types (a way to define data types by combining other data types)
- Type inference (the ability to deduce the type of an expression from its context)
- Type classes (a way to define interfaces for types)
- Monads (a way to structure computations that may have side effects)
- Functors, applicatives, and monoids (type classes that generalize common patterns of function composition)
Syntax
Composition
g :: X -> Y
f :: Y -> Z
f . g :: X -> Z
Note the composition order. By default A -> B -> C
means A -> (B -> C)
. If you want to describe the type (A -> B) -> C
, you need the parentheses.
Guarded equations
abs:: Float -> Float
abs n | n >= 0 = n
| otherwise = -n
bmiClassifier :: (Fractional a, Ord a) => a -> a -> String
bmiClassify weight height
| bmi <= 18.5 = "underweight"
| bmi <= 25.0 = "normal"
| bmi <= 30.0 = "overweight"
| otherwise = "obese"
where bmi = weight / height ^ 2
The first arrow (=>
) separates the type constraints (here, (Fractional a, Ord a)) from the rest of the type signature. In Haskell, all functions are considered to be curried by default. This means that a function that takes multiple arguments is actually a series of functions, each taking a single argument and returning another function (except for the final function, which returns the result). This currying is a fundamental aspect of how functions are represented and applied in Haskell, leading to its characteristic arrow-heavy type signatures.
Pattern matching
sumList :: Num a => [a] -> a
sumList xs = case xs of
[] -> 0
(x:xs') -> x + sumList xs'
In recursion, the base case is the empty list []
, and the recursive case is (x:xs')
. The colon :
is the list constructor, which separates the head x
from the tail xs'
. The base case returns 0
, and the recursive case returns the sum of the head x
and the sum of the tail sumList xs'
.
In Haskell, the apostrophe ('
) in the variable name xs'
(pronounced “exs prime”) doesn’t have a special syntactic meaning; it’s simply part of the variable name. Haskell allows apostrophes in identifiers, and they are often used by Haskell programmers to denote a related or modified version of a variable. In this context, xs’ is a new list derived from the original list xs by removing its head element.
describeList :: [a] -> String
describeList xs = case xs of
[] -> "The list is empty."
[x] -> "The list has one element."
xs -> "The list has multiple elements."
You can also write in a direct pattern matching style:
sumList :: Num a => [a] -> a
sumList [] = 0 -- Base case: the sum of an empty list is 0
sumList (x:xs) = x + sumList xs -- Recursive case: add the head to the sum of the tail
bmiClassifier weight height = case () of
_ | bmi <= 18.5 -> "underweight"
| bmi <= 25.0 -> "normal"
| bmi <= 30.0 -> "overweight"
| otherwise -> "obese"
where bmi = weight / height ^ 2
bmiClassifier weight height =
let bmi = weight / height ^ 2
in case () of
_ | bmi <= 18.5 -> "underweight"
| bmi <= 25.0 -> "normal"
| bmi <= 30.0 -> "overweight"
| otherwise -> "obese"
The unit value is denoted by ()
and is the only value of the unit type, also written as ()
. The unit type and value are somewhat analogous to void in other programming languages, but with some key differences due to Haskell’s purely functional nature.
printMessage :: String -> IO ()
printMessage msg = putStrLn msg
In Haskell, the underscore (_
) is used as a wildcard pattern that matches anything without binding it to a variable. In the context of your code snippet, the underscore is used in a somewhat unconventional but perfectly valid way to facilitate a case expression that’s solely used to enable pattern guards for expressing conditional logic. This technique is a bit of a Haskell idiom for structuring code that depends on multiple conditions.
-- also works
bmiClassifier :: (RealFloat a) => a -> a -> String
bmiClassifier weight height =
if bmi <= 18.5 then "underweight"
else if bmi <= 25.0 then "normal"
else if bmi <= 30.0 then "overweight"
else "obese"
where bmi = weight / height ^ 2
let Floating
Let floating is an optimization technique where let bindings are moved (“floated”) to a different scope. This can be done to either:
Move computations closer to where they are used (floating inwards), to avoid unnecessary computation in code paths where the values are not used. Move computations to a common scope (floating outwards) where multiple branches of code can share the result, to avoid redundant computation.
without let floating
calculate :: Int -> Int -> Int
calculate x y =
if x > 10
then let a = x * 2
in a + y
else let a = x * 3
in a - y
-- with let floating
calculate :: Int -> Int -> Int
calculate x y =
let a = if x > 10 then x * 2 else x * 3
in if x > 10 then a + y else a - y
Lambda lifting
Lambda lifting is a transformation that converts local function definitions into top-level functions. This transformation is useful for making functions available to other functions that need them, and for improving code readability by separating concerns.
-- without lambda lifting
outerFunction x y =
let
innerFunction z = x + y + z
in
innerFunction 10
-- with lambda lifting
innerFunction x y z = x + y + z
outerFunction x y = innerFunction x y 10
in practice, lambda lifting is part of the compilation process for functional languages and is handled automatically by the compiler.
Conventions
- Haskell is case-sensitive, so
myVar
andmyvar
are different variables. - Functions and variables must begin with a lowercase letter.
- Data types and type classes must begin with an uppercase letter.
- Using
'
for modified or temporary values - Using
_
for unused variables - In type signatures, single lowercase letters (often starting from
a
) are used for type variables. It’s common to usea, b, c
for generic types, and specific letters for types with constraints (e.g., m for monads, f for functors). mk
- not a built-in keyword or function. However, it is commonly used as a prefix in naming conventions for functions or constructors that “make” or construct something, especially in the context of data types or abstract data types (ADTs).show
- a built-in function in Haskell that converts a value to a string and is often used in function names to indicate that the function returns a string representation of a value.unsafe
- prefix used to warn that a function violates some of Haskell’s safety guarantees, such as type safety or purity. For example, unsafePerformIO allows IO operations in a non-IO context, which can lead to unpredictable results if used improperly.try
- indicates functions that perform actions which may fail, often returning a Maybe or Either type.Most Python functions probably need this prefix to be accurate.lift
- prefix used in functions that lift a function or value from one context to another, such as lifting a function from a monad to a monad transformer.
Syntax differences against Python
Operators
a && b
returns the boolean value True
if both a
and b
do, and False
otherwise.
(&&) :: Bool -> Bool -> Bool
(||) :: Bool -> Bool -> Bool
(++) :: [a] -> [a] -> [a]
Infix operators
We can declare new operators easily; for example:
(+++) :: Int -> Int -> Int
x +++ y = if even x then y else x + y
Indexing
List-indexing operation (!!
). Given a list xs
and an index n
, the expression xs!!n
returns the element of xs
at position n
, counting from 0
.
Quotes
Single quotes means single character, double quotes means character array (string). In Haskell, 'c'
is a single character ( Char
), and "c"
is a list of characters ( [Char]
).
Lambda expressions
A lambda expression \n -> 2*n+1
. This is equivalent to the Python lambda expression lambda n: 2*n+1.
Indentation vs braces
This is sort of a mess in Haskell. You can use indentation or braces and semicolons to separate blocks of code.
Code which is part of some expression should be indented further in than the beginning of that expression (even if the expression is not the leftmost element of the line).
main :: IO ()
main = do {
putStrLn "What is your name?";
name <- getLine;
putStrLn $ "Hello, " ++ name ++ "!"
}
-- also works
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn $ "Hello, " ++ name ++ "!"
roots :: (Float,Float,Float) -> (Float,Float)
roots (a,b,c)
| a == 0 = error "not quadratic"
| disc < 0 = error "complex roots"
| otherwise = ((-b-r)/e, (-b+r)/e)
where {disc = b*b - 4*a*c; r = sqrt d; e = 2*a}
-- also works
where disc = b*b - 4*a*c
r = sqrt d
e = 2*a
-- but not
where disc = b*b - 4*a*c
r = sqrt d
e = 2*a
Strictness
A Haskell function f
is said to be strict if f undefined = undefined
, and nonstrict otherwise.
Data structures
Lists
We can have lists over any type but we cannot mix different types in the same list. The operator (:) :: a -> [a] -> [a]
, pronounced ‘cons’, is a constructor for lists.
In Haskell, data types are used to define the structure and representation of data in a program. They allow you to create custom types to suit the needs of your application.
data Person = Person { name :: String, age :: Int }
alice :: Person
alice = Person { name = "Alice", age = 30 }
In particular, the data type for List
is defined as:
data List a = Nil | Cons a (List a)
This definition is a recursive data type, where Nil
represents an empty list and Cons
represents a list with a head element of type a
and a tail that is another list of type a
.
List notation, such as [1,2,3]
, is in fact an abbreviation for a more basic form 1:2:3:[]
.
As a result of above:
- Finite lists are represented as
Nil
orCons x xs
, wherex
is the head of the list andxs
is the tail. - Partial lists are represented as
Nil
orCons x (delay xs)
, wheredelay
is a function that delays the evaluation of its argument. - Infinite lists are represented as
Cons x (delay xs)
, wherex
is the head of the list andxs
is the tail.
iterate
is a function in Haskell that generates an infinite list by repeatedly applying a function to a starting value. Its type signature is:
iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)
The function map
then applies f
to each element of the list [x1, x2, ..., xn]
, producing a new list [f x1, f x2, ..., f xn]
of type [b]
.
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs st
Enumerations
Cool tricks over lists of integers. As a matter of fact, enumerations are not restricted to integers, but to members of yet another type class Enum
[1..10] -- [1,2,3,4,5,6,7,8,9,10]
[1,3..10] -- [1,3,5,7,9]
[1..] -- infinite list
['a'..'z'] -- "abcdefghijklmnopqrstuvwxyz"
['a'..'z'] ++ ['A'..'Z'] -- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Successor and predecessor
succ :: Enum a => a -> a
pred :: Enum a => a -> a
succ 5 -- 6
pred 5 -- 4
List comprehensions
List comprehensions are a way to generate lists in Haskell by specifying a predicate and a set of generators. The syntax is similar to set builder notation in mathematics.
[x^2 | x <- [1..5]] -- [1,4,9,16,25]
[(x,y) | x <- [1,2], y <- [3,4]] -- [(1,3),(1,4),(2,3),(2,4)]
-- Pythagorean triples
triads :: Int -> [(Int,Int,Int)]
triads n = [(x,y,z) | x <- [1..n], y <- [1..n],
z <- [1..n], x*x+y*y==z*z]
triads 10 -- [(3,4,5),(4,3,5),(6,8,10),(8,6,10)]
-- divisors
divisors x = [d | d <- [2..x-1], x `mod` d == 0]
divisors 12 -- [2,3,4,6]
-- coprime
coprime x y= disjoint (divisors x) (divisors y)
coprime 12 25 -- True
import qualified Data.Set as Set
-- Define two sets
let setA = Set.fromList [1, 2, 3]
let setB = Set.fromList [4, 5, 6]
-- Check if disjoint (i.e., their intersection is empty)
let disjoint = Set.null (Set.intersection setA setB)
Functors
Functor is a type class that represents types that can be mapped over. The fmap
function is used to apply a function to the elements of a functor.
Functor laws of maps are:
map id = id
map (f . g) = map f . map g
Rename the type of the function to fmap
:
class Functor f where
fmap :: (a -> b) -> f a -> f b
One of the simplest examples of a functor in Haskell is the Maybe type, which is used to represent values that might be missing. The Maybe type is defined as:From experience, most Python bugs in production are caused by careless handling of None
and exceptions – this is in a way encouraged by Python by virtue of being a dynamic language. You essentially are tolerating leaky code that break in production in unexpected ways. If you want to have a more pleasant computational infrastructure, I don’t think you can get around without implementing a Maybe
monad (to get around None
that’s plaguing your code) or introducing exceptionless programming patterns in Python. In other words, implement monads (see below).
data Maybe a = Nothing | Just a
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
main = do
print $ fmap (*2) (Just 100) -- Outputs: Just 200
print $ fmap (*2) Nothing -- Outputs: Nothing
print $ fmap show (Just 123) -- Outputs: Just "123"
-- A function that might fail to return a value
safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing -- Division by zero is not allowed
safeDivide x y = Just (x `div` y)
-- Using `Maybe` values
example1 :: Maybe Int
example1 = Just 5
example2 :: Maybe Int
example2 = safeDivide 10 0 -- This will be Nothing
example3 :: Maybe Int
example3 = safeDivide 10 2 -- This will be Just 5
-- Pattern matching on `Maybe` values
printMaybe :: Show a => Maybe a -> String
printMaybe Nothing = "No value"
printMaybe (Just x) = "The value is " ++ show x
main :: IO ()
main = do
putStrLn $ printMaybe example1 -- Outputs: The value is 5
putStrLn $ printMaybe example2 -- Outputs: No value
putStrLn $ printMaybe example3 -- Outputs: The value is 5
zip
and zipWith
zip :: [a] -> [b] -> [(a,b)]
zip (x:xs) (y:ys) = (x,y): zip xs ys
zip _ _ = []
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (x:xs) (y:ys)=fxy: zipWith f xs ys
zipWith f _ _ = []
-- zipWith is cool - we should have it in Python
main = print $ zipWith (+) [1, 2, 3] [4, 5, 6]
-- Output: [5, 7, 9]
main = print $ zipWith (\x y -> x * 2 + y) [1, 2, 3] [4, 5, 6]
-- Output: [6, 9, 12]
main = print $ zipWith (\c n -> replicate n c) ['a', 'b', 'c'] [1, 2, 3]
-- Output: ["a","bb","ccc"]
Some key points to note about the type signature of zipWith
:
(a -> b -> c)
- This is the type of the first argument tozipWith
. It indicates thatzipWith
expects a function as its first parameter. This function itself takes two arguments: one of type a and another of typeb
, and it returns a value of typec
. The typesa
,b
, andc
can be any types, which makeszipWith
a polymorphic function. This means thatzipWith
can work with functions that operate on integers, characters, strings, custom data types, or any other types, as long as the types of the lists you provide match the expected input types of the function.-> [a]
- This part of the signature tells us that the second argument tozipWith
is a list of elements of typea
. This should match the type of the first argument of the function provided as the first parameter tozipWith
.-> [b]
- Similarly, this indicates that the third argument tozipWith
is a list of elements of typeb
. This should match the type of the second argument of the function provided as the first parameter to zipWith.-> [c]
- Finally, the return type ofzipWith
is a list of elements of typec
. This matches the return type of the function provided as the first parameter to zipWith. The resulting list contains the results of applying the provided function to each pair of elements from the two input lists.
But when you read (a -> b -> c)
, you should think a function that takes two arguments of types a
and b
and returns a value of type c
.
Higher-order functions
($): Function application operator
It is used to apply a function to an argument. It’s useful for reducing the need for parentheses because it has low, right-associative precedence.
($) :: (a -> b) -> a -> b
-- Without $
print (sum (filter (> 10) (map (*2) [1..10])))
-- With $
print $ sum $ filter (> 10) $ map (*2) [1..10]
This looks similar to the Unix pipe operator |
, except for the direction of the flow. You can use infixl 0 |>
declares the operator to be left-associative with a precedence level of 0, which is the same as $
. x |> f = f x
defines the operator such that x |> f
is equivalent to f x
.
infixl 0 |>
(|>) :: a -> (a -> b) -> b
x |> f = f x
--
result = [1, 2, 3, 4]
|> filter even
|> map (*2)
|> sum
filter
It takes a predicate and a list, returning a list of elements that satisfy the predicate.
filter :: (a -> Bool) -> [a] -> [a]
---
let evens = filter even [1..10] -- evens is [2,4,6,8,10]
map
It applies a function to each element of a list, returning a new list of the results.
map :: (a -> b) -> [a] -> [b]
let squares = map (^2) [1..5] -- squares is [1,4,9,16,25]
flip
In Haskell, flip
is a higher-order function that takes a function as an input and returns a new function with its first two arguments swapped.
flip :: (a -> b -> c) -> b -> a -> c
-- Without flip
subtract :: Num a => a -> a -> a
subtract x y = x - y
-- With flip
subtract' :: Num a => a -> a -> a
subtract' = flip subtract
-- Example usage
result = subtract 10 5 -- Output: 5
result' = subtract' 10 5 -- Output: -5
foldr
foldr
is a higher-order function that takes a binary function, an initial value, and a list, and applies the function to the elements of the list from right to left, starting with the initial value.
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ e [] = e
foldr f e (x:xs) = f x (foldr f e xs)
--
foldr (@) e [x,y,z] = x @ (y @ (z @ e))
[x,y,z] = x : (y : (z : []))
--
sumList :: [Int] -> Int
sumList = foldr (+) 0
from functools import reduce
# Mimic foldr by reversing the list first
def foldr(func, acc, lst):
return reduce(func, reversed(lst), acc)
# Example usage
result = foldr(lambda x, y: x - y, 0, [1, 2, 3])
print(result) # Output: 2, because it calculates ((1 - (2 - (3 - 0))) = 2
### also works
def foldr(func, acc, lst):
if not lst:
return acc
else:
return func(lst[0], foldr(func, acc, lst[1:]))
# Example usage
result = foldr(lambda x, y: x - y, 0, [1, 2, 3])
print(result) # Output: 2
In Haskell, foldr1
is a variant of foldr
that does not require an explicit starting value (accumulator). Instead, foldr1
uses the last element of the list as the starting value. This function is useful when you can assume the list is non-empty and the operation is associative and has an identity element, but you don’t need to specify it explicitly.
maxInList :: (Ord a) => [a] -> a
maxInList = foldr1 max
scanl
In Haskell, scanl is a function that is similar to foldl, but instead of returning a single final result, it returns an intermediate result at each step, producing a list of progressively reduced values. This can be particularly useful for seeing the evolution of the reduction process over each element of the input list.
scanl :: (b -> a -> b) -> b -> [a] -> [b]
scanl f e = map (foldl f e) . inits
inits :: [a] -> [[a]]
inits [] = [[]]
inits (x:xs) = [] : map (x:) (inits xs)
--
runningMax :: (Ord a, Num a) => [a] -> [a]
runningMax = scanl1 max
calculateDrawdown :: (Fractional a) => a -> a -> a
calculateDrawdown maxSoFar price = (price - maxSoFar) / maxSoFar * 100
drawdownPercentage :: (Ord a, Fractional a) => [a] -> [a]
drawdownPercentage prices = zipWith calculateDrawdown (runningMax prices) prices
Imperative functional programming
The type IO a is an abstract type representing actions that interact with the outside world and produce a value of type a. The main
function is the entry point of a Haskell program and has the type IO ()
.
An action is a function that takes a world and delivers a value of type a and a new world. The new world is then used as the input for the next action. Having changed the world with an input–output action, you can’t go back to the old world. You can’t duplicate the world or inspect its components. All you can do is operate on the world with given primitive actions, and put such actions together in a sequence.
putChar :: Char -> IO () -- write a character to the screen
putStr :: String -> IO () -- write a string to the screen
putStrLn :: String -> IO () -- write a string to the screen followed by a newline
print :: Show a => a -> IO () -- write a value to the screen followed by a newline
getChar :: IO Char -- read a character from the keyboard
getLine :: IO String -- read a line from the keyboard
return :: a -> IO a -- return a value from an action
Bind operator
One simple operation to sequence actions is denoted by (>>)
and has type
(>>) :: IO a -> IO b -> IO b
Given actions p
and q
, the action p >> q
first performs action p
and then performs action q
. The value returned by p >> q
is the value returned by q
.
If you care about the value of the first action, you can use the >>=
operator, which is pronounced “bind” and has the type:
(>>=) :: IO a -> (a -> IO b) -> IO b
-- A safe division function that fails (returns Nothing) if trying to divide by zero.
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
-- A function that adds 10 to the given number but fails if the result exceeds 100.
addTen :: Double -> Maybe Double
addTen x = if x + 10 > 100 then Nothing else Just (x + 10)
main :: IO ()
main = do
-- Successful operation: 100 divided by 2 is 50, then 10 is added, resulting in 60.
print $ Just 100 >>= safeDivide 2 >>= addTen
-- Fails at division: 100 divided by 0 fails, so the entire computation results in Nothing.
print $ Just 100 >>= safeDivide 0 >>= addTen
-- Fails at addition: 100 divided by 1 is 100, then adding 10 would exceed 100, so it fails.
print $ Just 100 >>= safeDivide 1 >>= addTen
Components of the type signature:
IO a
: This represents an action that, when performed, will produce a value of typea
along with some side effects. For example, reading a line from the console (getLine
) is an action that produces aString
(a
isString
in this case), and the side effect is waiting for the user input.(a -> IO b)
: This is a function that takes a value of type a and returns an action of typeIO b
. This function specifies how to take the result of the firstIO
action and create a newIO
action based on that result. The new action can produce a value of typeb
along with its own side effects.IO b
: This represents the action that results from applying the function(a -> IO b)
to the result of theIO
a action. Performing this action produces a value of typeb
along with the combined side effects of the original action and the action produced by the function.
The bind operator (>>=)
sequences two IO actions in such a way that the output of the first action is fed into the function that produces the second action. It’s how Haskell chains operations that have side effects, ensuring that the effects occur in the correct order.
Monads
The type IO a is an abstract type in the sense described in the previous chapter, so we are not told how its values, which are called actions or commands, are represented. But you can think of this type as being
type IO a = World -> (a,World)
where a World is a representation of the state of the world, and an action of type IO a
is a function that takes a world and returns a value of type a
and a new world. The new world is then used as the input for the next action. Having changed the world with an input–output action, you can’t go back to the old world. You can’t duplicate the world or inspect its components. All you can do is operate on the world with given primitive actions, and put such actions together in a sequence.
Monads are a fundamental concept in functional programming, particularly in Haskell, where they play a central role in handling side effects, managing state, dealing with exceptions, and more, in a pure functional way.
At their core, monads are a type of abstract data type that represent computations instead of data in the traditional sense. They can be thought of as “computation wrappers” that provide a context for the values they contain. This context can represent various things, such as the possibility of failure (as with Maybe
), a side effect (as with IO), or a state (as with State).
In Haskell, a monadMonads provide a way to abstract and encapsulate computations and side effects in a pure functional language like Haskell. They allow for: Sequencing operations: Especially useful when each step depends on the outcome of the previous one. Handling failure: By encapsulating operations that might fail in a context that can be easily checked and managed. Managing side effects: Such as IO operations, in a controlled manner without breaking the purity of the functional paradigm. State management: Allowing for state to be threaded through computations in a controlled way. is represented by the Monad type class, which defines two essential operations: >>=
(bind) and return
.
return
: Takes a value and puts it into a minimal default context of a monad. Despite its name,return
in Haskell is not related to returning from a function like in imperative languages; it’s just a way to wrap a value in a monad.>>=
(bind): Takes a monadic value and a function that takes a normal value and returns a monadic value, then chains these operations together. This allows for sequences of computations to be linked, where each step may depend on the result of the previous one.
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
Why
Maybe
monad is needed? The Maybe monad in Haskell serves several important purposes, particularly in handling operations that can fail or result in an absence of value. It encapsulates the concept of computations that might not return a value in a way that is both type-safe and expressive, avoiding some common pitfalls found in other programming paradigms, such as null pointer exceptions. Here’s a deeper look into why theMaybe
monad is needed and beneficial:1. Safe Handling of Absent Values In many programming languages, operations that can fail or produce no value are handled by returning null or a similar sentinel value. However, this approach can lead to errors such as null pointer exceptions if the absence of a value is not properly checked everywhere it’s used. The
Maybe
type makes the possibility of an absent value explicit in the type system, forcing the programmer to handle it. This leads to safer code by design.2. Type Safety The
Maybe
monad wraps a value in either Just (indicating presence of a value) or Nothing (indicating absence), making it clear from the type signature which functions might not return a value. This explicitness integrates with Haskell’s type system, providing compile-time checks for potential runtime errors related to missing values. It’s a way to leverage the type system to ensure that code dealing with potentially absent values is written safely.3. Composability Monads, including
Maybe
, provide a way to compose operations that might fail. Using the »= operator (bind), you can chain together multiple computations that returnMaybe
values without having to manually check for Nothing at each step. This leads to cleaner, more readable code by abstracting away the boilerplate of error checking and handling.4. Functional Approach to Error Handling The
Maybe
monad represents a functional approach to error handling. Instead of using exceptions for control flow (which can be considered side effects and are not purely functional),Maybe
encapsulates the presence or absence of a value as a result of a computation. This aligns with Haskell’s philosophy of pure functional programming, where functions are expected to return a value for every input and side effects are minimized.5. Enhanced Expressiveness Using
Maybe
makes code more expressive by clearly indicating the intent and the possible outcomes of a function. For example, a function signature likefindElement :: (a -> Bool) -> [a] -> Maybe
a clearly communicates thatfindElement
might not find an element that satisfies the predicate, and thus, might not return a value.6. Integration with Other Functional Patterns >
Maybe
integrates well with other functional patterns and utilities in Haskell, such as pattern matching, higher-order functions, and other monads. It can be used with map, fold, and filter operations in a way that is consistent with functional programming principles, further enhancing code safety and readability.Conclusion The
Maybe
monad addresses several issues related to error handling, absent values, and type safety in a functional programming context. By making the potential absence of a value explicit in the type system and providing a structured way to compose operations that might fail,Maybe
helps create more reliable, readable, and maintainable Haskell programs.
It’s just a way to wrap a value in a container.
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
case lookup x alist of
Nothing -> Nothing
Just y -> case lookup y blist of
Nothing -> Nothing
Just z -> lookup z clist
do notation
In Haskell, do notation is a syntactic sugar that simplifies working with monadic operations by allowing you to write sequences of actions in a way that looks imperative, making it easier to read and write monadic code. It’s particularly useful when dealing with monads like IO, Maybe
, Either, and stateful computations, among others.
do {p} = p
do {p;stmts} = p >> do {stmts}
do {x <- p;stmts} = p >>= \x -> do {stmts}
do
a <- Just 10
b <- Just 20
return (a + b)
-- just a syntactic sugar for
Just 10 >>= \a ->
Just 20 >>= \b ->
return (a + b)
Under the hood, do
notation is translated into a series of applications of the bind operator (>>=)
and the >>
operator (which is a variant of >>=
that ignores the result of the first action). This translation process allows you to write code that appears to be executing step-by-step instructions, similar to imperative languages, while still retaining the functional nature and benefits of monads.
do
a <- action1
action2 a
b <- action3
action4 b
-- is equivalent to
action1 >>= \a ->
action2 a >>
action3 >>= \b ->
action4 b
Monad laws
Monads in Haskell must satisfy three main properties or laws: left identity, right identity, and associativity. These properties ensure that monads behave in a predictable manner when chaining operations together.
- Right Identity: Taking a monadic value and chaining it with return using
>>=
does nothing; it’s just the original monadic value.
(p >>= return) = p
--
do {x <- p; return x} = do {p}
--
Just 42 >>= return = Just 42
- Left Identity: Applying a function to a value using return and then chaining that with another function using
>>=
is the same as just applying the second function directly to the value.
(return x >>= f) = f x
--
do {y <- return x; f y} = do {f x}
--
return 3 >>= (\x -> Just (x + 100)) = Just 103
- Associativity: Chaining multiple monadic operations together does not depend on how they are grouped.
((p >>= f) >>= g) = p >>= (\x -> (f x >>= g))
--
do {y <- do {x <- p; f x}; g y}
= do {x <- p; do {y <- f x; g y}}
= do {x <- p; y <- f x; g y}
--
f x = Just (x + 1)
g x = Just (x * 2)
(Just 5 >>= f) >>= g = Just 12
Just 5 >>= (\x -> f x >>= g) = Just 12
The monad laws say nothing much more than that expressions involving return and (>>=)
simplify in just the way one would expect.
Monad: from mathematical perspective
In category theory, a monad is a monoid in the category of endofunctors of some fixed category. Let \(C\) be a category. A monad in \(C\) consists of a functor \(T: C \rightarrow C\) and two natural transformations.
\[\begin{aligned} \eta & : 1_C \rightarrow T \\ \mu & : T^2 \rightarrow T \end{aligned}\]These are required to fulfill the following conditions (sometimes called coherence conditions), such that the following diagrams commute:
\[\begin{aligned} \mu \circ T\mu &= \mu \circ \mu T \\ \mu \circ T\eta &= \mu \circ \eta T = 1_T \end{aligned}\]\(\eta\) is called the unit of the monad, and \(\mu\) is called the multiplication of the monad.
For comparison, the return
function in Haskell corresponds to the unit \(\eta\) in the mathematical definition of a monad, and the >>=
operator corresponds to the multiplication \(\mu\).
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
In category theory, a Kleisli category is a category naturally associated to any monad \(T\). The objects of the Kleisli category are the objects of the original category, and a morphism from \(A\) to \(B\) in the Kleisli category is a morphism from \(A\) to \(TB\) in the original category. Given two morphisms \(f: A \rightarrow TB\) and \(g: B \rightarrow TC\) in the Kleisli category, their composition is given by the following diagram:
\[\begin{aligned} A & \xrightarrow{f} TB \xrightarrow{Tg} T^2C \xrightarrow{\mu} TC \end{aligned}\](>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(f >=> g) x = f x >>= g
--
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
(f <=< g) x = g x >>= f
--
(p >>= f) = (id >=> f) p
-- that is,
(>>=) = flip (id >=>).
State monads
The State monad is a way to encapsulate stateful computations in Haskell in a purely functional manner. It allows you to thread state through a series of computations without explicitly passing the state around as a parameter. This can help simplify code that involves mutable state by making the state transitions explicit and composable.Not an exclusively functional feature – react, e.g., has a similar state management hook.
We start by considering a simpler monad, called State s
, for manipulating an explicit state s. You can think of the type State s a
as being
type State s a = s -> (a,s)
Specifically, as well as the monad operations return
and (>>=)
, five other functions are provided for working with the state monad:
put :: s -> State s ()
get :: State s s
state :: (s -> (a,s)) -> State s a
runState :: State s a -> (s -> (a,s))
evalState :: State s a -> s -> a
import Control.Monad.State
-- Define a type alias for our State monad instance. In this case, our state is just an Int.
type Counter = State Int
-- A simple function to increment the counter.
increment :: Counter ()
increment = modify (+1)
-- A function to get the current value of the counter.
getCounter :: Counter Int
getCounter = get
-- A function that uses the State monad to increment the counter twice and then retrieve its value.
exampleUsage :: Counter Int
exampleUsage = do
increment
increment
getCounter
main :: IO ()
main = print $ runState exampleUsage 0
- We import
Control.Monad.State
which provides theState
monad and related functions. - We define a type alias
Counter
forState Int
, indicating that our state consists of a single integer (the counter). - The
increment
function usesmodify (+1)
to increment the state. The modify function is a utility provided by the State monad that applies a function to the current state. - The
getCounter
function usesget
to fetch the current state. - In
exampleUsage
, we demonstrate a sequence of stateful operations: incrementing the counter twice, then getting its value. This is done in a do-block, which allows us to sequence operations in the State monad. - Finally,
main
demonstrates how to run a stateful computation and print its result.runState
takes a State monad action and an initial state, and returns a tuple containing the result of the action and the final state. In this case, we start with an initial counter value of 0, and after runningexampleUsage
, we get the final counter value of 2 (since we incremented twice).
State-thread monads
State-thread monads are a generalization of the State monad that allows you to thread multiple pieces of state through a computation. This can be useful when you need to manage and update multiple pieces of state independently within a single computation. Like state monads, state-thread monads provide a way to encapsulate stateful computations in a purely functional manner.
type ST s a = s -> (a,s)
The core difference is the type variable s
cannot be instantiated to specific states, such as Seed
or [Int]
. Instead it is there only to name the state. Think of s as a label that identifies one particular state thread. All mutable types are tagged with this thread, so that actions can only affect mutable values in their own state thread.
The “thread” part is where things can get a bit confusing because it doesn’t refer to threads in the sense of concurrent programming. Instead, it refers to the way the ST monad “threads” state changes through a sequence of computations. You can think of it as “threading” a line (the state) through a series of beads (the computations), where each bead might change the shape or color of the thread but everything is done in a single, linear sequence, not in parallel or concurrently.
STRef s a
is a type that represents a mutable reference to a value of type a
, where s
is a type parameter used to ensure that references cannot be used outside the ST monad in which they were created. Neat similarities with react: the main difference between useState()
and useRef()
is that useState()
is used to manage a state that triggers a re-render when it changes while useRef()
is used to store mutable values that do not trigger a re-render.
newSTRef :: a -> ST s (STRef s a)
readSTRef :: STRef s a -> ST s a
writeSTRef :: STRef s a -> a -> ST s ()
Pure functional:
fibonacci :: [Integer]
fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)
fibN :: Int -> Integer
fibN n = fibonacci !! n
With STArray
:
import Control.Monad.ST (ST, runST)
import Data.Array.ST (STArray, newArray, readArray, writeArray)
import Data.Array (elems)
fibST :: Int -> Integer
fibST n
| n <= 0 = 0
| otherwise = runST $ do
arr <- newArray (0, n) 0 :: ST s (STArray s Int Integer)
writeArray arr 0 0
writeArray arr 1 1
mapM_ (fillFib arr) [2..n]
readArray arr n
fillFib :: STArray s Int Integer -> Int -> ST s ()
fillFib arr n = do
a <- readArray arr (n - 1)
b <- readArray arr (n - 2)
writeArray arr n (a + b)
--
main :: IO ()
main = print $ fibST 10
Without STArray
The action repeatFor repeats an action a given number of times:
repeatFor :: Monad m => Int -> m a -> m ()
repeatFor n action = foldr (>>) (return ()) (replicate n action)
This version of repeatFor
is a higher-order function that takes two arguments: an integer n
and a monadic action action. It returns a new action that, when executed, performs the original action n
times in sequence, discarding the results of each action, and ultimately returns a unit value ()
.
Signature breakdown:
Monad m
: This part of the type signature indicates thatrepeatFor
works within any monadm
. The Monad constraint is necessary because the function uses monadic operations (>>
andreturn
).Int
: The first parameter is an integern
, which specifies how many times to repeat the given action.m a
: The second parameter is a monadicaction
of typem a
. This means the action is within the monadm
and produces a result of typea
. The specific type of a doesn’t matter torepeatFor
, as it only cares about the sequencing of actions, not the results produced by them.m ()
: The return type is another action within the same monadm
, but it specifically returns a unitvalue ()
. This indicates that the primary purpose of repeatFor is to execute side effects from the repeated action, not to produce a meaningful result.
Body breakdown:
replicate n action
: This creates a list ofn
copies of the action. Ifn
is 3, for example, and action isprint "Hello"
, you get a list like[print "Hello", print "Hello", print "Hello"]
.foldr (>>) (return ())
: This folds (reduces) the list of actions using the right-associative fold function foldr. The folding operation uses(>>)
as the binary function to combine actions andreturn ()
as the initial value.(>>)
: This is the monadic sequencing operator. Given two actions, action1 » action2 executes action1, discards its result, and then executes action2. This operator is used to sequence the actions in the list.return ()
: This is the base case of the fold. It’s a no-op action that simply returns a unit value()
. It serves as the starting point for the right-associative fold, ensuring that the fold operation has a “neutral” action to start with, which doesn’t alter the behavior of the other actions when combined using(>>)
.
Given an action and a count n
, repeatFor constructs a composite action that, when executed, runs the given action n
times in sequence. It starts from the end of the list of replicated actions, sequencing each action with the next using (>>)
and ultimately sequencing the last action with return ()
, ensuring that the entire composite action returns a unit value ()
. This function is useful for repeating side-effectful actions a specific number of times in a monadic context, such as printing messages, modifying mutable state in a controlled manner, or sending requests in an IO context.
The repeatFor
function can be defined as a simple wrapper around replicateM_
, which repeats an action a given number of times. Here, n
times repetition of the action act is desired:
import Control.Monad (replicateM_)
import Control.Monad.ST (ST, runST)
import Data.STRef (newSTRef, readSTRef, writeSTRef)
repeatFor :: Int -> ST s () -> ST s ()
repeatFor n act = replicateM_ n act
fibST :: Int -> ST s Integer
fibST n = do
a <- newSTRef 0
b <- newSTRef 1
repeatFor n $ do
x <- readSTRef a
y <- readSTRef b
writeSTRef a y
writeSTRef b $! (x + y)
readSTRef a
computeFib :: Int -> Integer
computeFib n = runST (fibST n)
main :: IO ()
main = print $ computeFib 10
- The
ST
monad andSTRef
are used here to maintain mutable state in a purely functional way. This allows for an imperative style of programming (using mutable references) while keeping the benefits of Haskell’s type safety and purity. - The
repeatFor
function is a utility that repeats a given action a specific number of times. It’s defined here as a wrapper aroundreplicateM_
for clarity. - The
fibST
function initializes twoSTRefs
to hold the first two Fibonacci numbers, then iteratively updates these references to compute subsequent Fibonacci numbers up to the nth number. - The use of
$!
forces the evaluation of(x + y)
to ensure that the computation is strict, which helps avoid building up large thunks and potentially causing stack overflow for largen
.