Monads can be scary, but the code below provides a definition of a “Maybe Monad”, and if you are familiar with Golang, it’s not scary at all!
type Maybe struct {
value interface{}
ok bool
}
func Return(v interface{}) Maybe {
return Maybe{value: v, ok: true}
}
func (m Maybe) Bind(f func(interface{}) Maybe) Maybe {
if !m.ok {
return Maybe{}
}
return f(m.value)
}The “ok” field of the “Maybe” struct represents the context of the monad, in this case it’s indicating whether the value is present or not. So in this example, the “ok” field is the monadic context.
A “monadic value” is a value that is wrapped in a monadic context. The specific meaning of a “monadic value” can vary depending on the context and the monad in question.
/Untitled-382.png)
A “monadic context” is a specific context in which a monadic value is evaluated. It can include various things such as:
- the state of the computation
- the environment in which the computation is executed
- the effects that the computation may have on the outside world (e.g. IO operations)
- the error handling strategy
The specific meaning of a “monadic context” can vary depending on the monad in question. But it can be understanded as a “metadata” tied to the plain value.
Take the following golang example, the “ok” field of the struct is the “monadic context”, and as you might expected, it’s possible to add more fields:
more fields → more metadata → more context
The “monadic context” (that is sometimes is just a compose of the plain value with some metadata) is what makes monads hard to compose
The **Maybe** struct has two fields, the first is a **value** of any type, the second is a **bool** indicating if the value is present or not. The **Return** function takes an interface{} and returns a Maybe with the value and ok set to true. The **Bind** method takes a function **f** that takes an interface{} and returns a Maybe, and applies the function **f** to the value of the current Maybe.
Here’s an example of how you might use the **Maybe** struct:
func addOne(v interface{}) Maybe {
if v.(int) > 2 {
return Return(v.(int) + 1)
}
return Maybe{}
}
func double(v interface{}) Maybe {
return Return(v.(int) * 2)
}
func minusOne(v interface{}) Maybe {
return Return(v.(int) - 1)
}
m := Return(3)
n := m.Bind(addOne).Bind(double).Bind(minusOne)The functions **addOne**, **double** and **minusOne** are defined separately, and then passed as arguments to the **Bind** method. This way you can reuse the functions with different parameters and also it is more readable.
In this example **m** is a **Maybe** struct that holds the value **3** with **ok** set to true. Then, **n** is the result of applying three functions **addOne**, **double** and **minusOne** to **m** using the **Bind** method, the behavior of the functions is the same as the previous example.
The monad laws holds:
- Left identity:
**Return(v).Bind(f)**is equivalent to**f(v)** - Right identity:
**m.Bind(Return)**is equivalent to**m** - Associativity:
**m.Bind(f).Bind(g)**is equivalent to**m.Bind(func(x) { return f(x).Bind(g) })**
https://wiki.haskell.org/Monad_laws
As you can see, the Maybe monad is a way of handling errors, so if any of the steps of the computation fails, the monad **Fail** method is used and the final result will contain the error and not the value.
The Maybe Monad can be used to avoid side-effects. Because the **Bind**** **method applies a function to the value of a Maybe without modifying the original Maybe, it allows for functional composition of computations. This means that the order of the computations doesn’t matter, and the computations can be reused in different parts of the code without causing unintended side-effects.
Errors are often considered side-effects because they are typically not the primary result of a computation, but rather an indication that something went wrong during the computation. They can change the state of the program and cause unexpected behavior, making it harder to reason about the code.