4 ms·
For me, everything made sense when I read that generator functions in python can be used to implement algebraic effects, and algebraic effects and monads expres
by bbminner 2y ago
For me, everything made sense when I read that generator functions in python can be used to implement algebraic effects, and algebraic effects and monads express the same ideas/logic using somewhat different language (and for some, including me, the algebraic effect lagngauge is easier). So I like an explanation that goes like "langaues without either but with exceptions and async io > generators/coroutines > algebraic effects > monads".
On a fundamental level, all these describe abstraction over what happens "between functions calls" and how values are passed around.
1. Some languages provide special syntax for exceptions and async io - in both cases "something" might happen between function calls - either an exception handler is called under certain constitutions (eg prints logs) or a kernel level callback is created and the program is suspended.
2. One generalization of this can be implemented using python generator functions / coroutines - you can build a call graph by invoking all functions in you callstack as "yield from func()", and use "yield Exception" to propagate errors and "yield Timer(1)" to ask an outer caller (event loop) to wait, or "yield Print(mag)" if we want to keep our functions pure and make the C event loop handle all the IO. You can also pass values down the stack via corp.send(x).
3. But it is a little clunky. Some langaues like Koka give you an ability to define arbitrary "effects and effect handlers" which is essential special syntax for how we were abusing coroutines in the previous step that makes differentiating and handling different "kinds things that we pass up and down the callstack" (exceptions vs timers vs prints) easier.
4. But some langaues do not have effect handlers but still want to do "custom arbitary custom things between function calls". That's where monads come in - they define a type for defining chains/trees of function calls and rules for how these threes must be iterativley unwrapped and wrapped back depending on how the wrap looks like. Eg instead of doing a(b(c)) you say unit(c) | b | a and describe how piping must be done it terms of types of values that these steps process. I hope it makes it clear how one could implement side effect IO, or exceptions, or async io that pauses by defining how to "unwap such piping". In principle, you abstract away control flow by introducing your own syntax for building function call trees and then rules for writing piping that works with such trees.
Monad guru please correct me if I am wrong.