The Kleisli Category captures the idea of sequencing computations that involve monadic effects.
Milewsky gives a good example using logging functions:
https://bartoszmilewski.com/2014/12/23/kleisli-categories/
https://www.youtube.com/watch?v=i9CU4CuHADQ
Let’s rewrite the logger example using golang. The main goal here is to define a logging functionality that will provide an output of the concatenation of all the logs from all functions.
This is our basic logging struct/function:
// Logger represents a simple logger that writes messages to the console.
type Logger struct{}
func (l *Logger) Log(message string) {
fmt.Println(message)
}And now we want to use this function inside other functions. But we don’t want to hurt the “single responsability principle”, so we definitely don’t want to use any approaches involving the usage of global variables or dependency injection.
The category theory approach will be the usage of a **Kleisli Arrow, that is used to compose monadic functions. **
In this logger example we don’t have a traditional embedded context like we usually have when doing monads stuff in FP (we are not dealing Maybe, Optional or anything like this). But we are carrying a context with our “raw value”, because for each function output, we are also returning a log string.
So we want to be able to compose all the functions that carry this same context. And this is what Kleisli category does:
// Kleisli is a function type that represents a Kleisli arrow.
type Kleisli func(input interface{}) (interface{}, error)
// Compose composes two Kleisli arrows into a new arrow.
func Compose(f Kleisli, g Kleisli) Kleisli {
return func(input interface{}) (interface{}, error) {
result, err := f(input)
if err != nil {
return nil, err
}
return g(result)
}
}
So let’s compose a dummy chain of operations to se how this is really helping us:
func main() {
logger := &Logger{}
// Example Kleisli arrows
logArrow := func(message string) Kleisli {
return func(_ interface{}) (interface{}, error) {
logger.Log(message)
return nil, nil
}
}
errorArrow := func(errMsg string) Kleisli {
return func(_ interface{}) (interface{}, error) {
return nil, errors.New(errMsg)
}
}
// Compose Kleisli arrows
arrowChain := Compose(logArrow("Step 1"), Compose(logArrow("Step 2"), errorArrow("Error in Step 3")))
// Run the composed arrow
result, err := arrowChain(nil)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success:", result)
}
}And here is another example using a “realistic” scenario:
package main
import (
"errors"
"fmt"
)
// Logger represents a simple logger that writes messages to the console.
type Logger struct{}
func (l *Logger) Log(message string) {
fmt.Println(message)
}
// Kleisli is a function type that represents a Kleisli arrow.
type Kleisli func(input interface{}) (interface{}, error)
// Compose composes two Kleisli arrows into a new arrow.
func Compose(f Kleisli, g Kleisli) Kleisli {
return func(input interface{}) (interface{}, error) {
result, err := f(input)
if err != nil {
return nil, err
}
return g(result)
}
}
// Real function: Simulate a bank account deposit
func deposit(amount float64) Kleisli {
return func(_ interface{}) (interface{}, error) {
// Simulate deposit operation
return nil, nil
}
}
// Real function: Simulate a bank account withdrawal
func withdraw(amount float64) Kleisli {
return func(_ interface{}) (interface{}, error) {
// Simulate withdrawal operation
return nil, nil
}
}
func main() {
logger := &Logger{}
// Example Kleisli arrows for logging
logArrow := func(message string) Kleisli {
return func(_ interface{}) (interface{}, error) {
logger.Log(message)
return nil, nil
}
}
errorArrow := func(errMsg string) Kleisli {
return func(_ interface{}) (interface{}, error) {
return nil, errors.New(errMsg)
}
}
// Compose Kleisli arrows to log deposit and withdrawal
bankingOperations := Compose(
logArrow("Starting banking operations"),
Compose(
Compose(deposit(100), logArrow("Depositing $100")),
Compose(withdraw(50), logArrow("Withdrawing $50")),
),
logArrow("Ending banking operations"),
)
// Run the composed arrow
result, err := bankingOperations(nil)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success:", result)
}
}https://blog.ssanj.net/posts/2017-06-07-composing-monadic-functions-with-kleisli-arrows.html
https://blog.ploeh.dk/2022/04/04/kleisli-composition/
https://ncatlab.org/nlab/show/Kleisli+category
https://blog.ssanj.net/posts/2017-06-07-composing-monadic-functions-with-kleisli-arrows.html
https://dev.to/mikesol/how-monads-encapsulate-side-effects-5cnj