>>5982Free monads are very scary but they are actually more simple than you'd think if you ignore all the irrelevant stuff.
First step is to create an instruction set or an API, or whatever you want to call it.
I'll be using a pseudo-code fake OOP language to make it easier to understand.
class API; // the parent class
class AddUser(u: User) extends API
class GetUser(username: String) extends API
…
etc.
Think of it like an enum (or an ADT), but with stuff. Here's another alternative implementation in another different but also fake language:
type API =
AddUser(u: User) |
GetUser(username: String)
…
The purpose of this is to use it in your program.
In an imperative language, it would look like this:
class API; // the parent class
class AddUser(u: User, next: API) extends API
class GetUser(username: String, next: API) extends API
class End extends API
function API logIn(username: String, password: String){
new GetUser(username, new ComparePasswords(password, new RegisterLogin(user, new SignCookie(cookie, new End))));
}
Ok that's cool but what the fuck is this anyways?
It's a data structure that tell you instructions. As you can see, it uses variables that aren't even available, namely, "user" and "cookie". It's not even a complete implementation.
In more functional programming languages we can do something like this instead (now shamelessly using scala-like language):
def logIn(username: String, password: String) return API[Cookie]{
for {
user <- GetUser(username)
isValidPassword <- ComparePasswords(password)
cookie <- RegisterLogin(user)
signedCookie <- SignCookie(cookie)
} return signedCookie
The interesting part here is that this is still just a nested data structure, even if it doesn't look like one. Reducing the complexity a bit, the data structure is around the same as the above, namely:
new GetUser(username, new ComparePasswords(password, new RegisterLogin(user, new SignCookie(cookie, new End))))
This is useful because you can then iterate through this data structure and replace GetUser for a real get user call.
def interpret(api: API){
switch(api):{
case GetUser(username):
return databaseCall(username)
….
}
}
This means that your program can be purely descriptive, with no concrete implementation, and then supply the implementation somewhere else. It also means you can swap out implementations at any time without changing the description of your program.
eg.
switch(api):{
case GetUser(username):
verifyUsernameIsValid(username) // this was added, without changing the logIn function
return databaseCall(username)
….
}
And these compose, so you can do something like:
def signIn(username, password): API[Boolean]:
for {
user <- logIn(username, password)
isSuccess <- sendEmailNotification(user.email)
} return isSuccess
Then, where you want to use this, you need to supply the interpreter.
def signInRequestHandler(username, password){
val programDescription: API[Boolean] = signIn(username, password)
intepreter(programDescription) // runs the function with the switch statement
}
Everything else related to this is just the machinery to make this possible.
The Free monad gives your API the "nested" structure.
Inject/liftF, etc just make it easy to work with this shit.
FunctionK, ~>, natural transformation: It's the function called interpreter. Fancy names for a mundane purpose.
So yeah, at the end you'll get somewhat incomprehensible code that makes all of this come together.
Bonus:
If you have several APIs, say Api1, Api2, and Api3, you need to supply extra machinery to compose these.
Namely the Inject stuff, and EitherK/Coproduct stuff. You seriously don't need to understand how this works.
Api1and2 = EitherK[Api1, Api2, a]
Api1and2and3 = EitherK[Api3, Api1and2, a]
Then when you use this shit, you need to supplant more "switch functions" ie "interpreters".
def signInRequestHandler(username, password){
val programDescription: Api1and2and3[Boolean] = signIn(username, password)
val interpreter1or2or3: Function = (interpreter1 or interpreter2 or interpreter3) // or is a magic operator that joins these functions
interpreter1or2or3(programDescription)
}
All you need to do to use this is to understand how to define the machinery. But these are the basic concepts without any type level bullshit.