Check out a free preview of the full Hardcore Functional Architecture Patterns in JavaScript course

The "Free Monads" Lesson is part of the full, Hardcore Functional Architecture Patterns in JavaScript course featured in this preview video. Here's what you'd learn in this lesson:

Brian continues to refactor the CLI blog app using Free monads to test the different app tasks.


Transcript from the "Free Monads" Lesson

>> Before we go too much further into this whole thing, this is all well and good. But how do you test this? [LAUGH] I mean, like it's just a bunch of tasks, right? And they're literally reading the standard out, and that's not great. And so, for this, I feel like we just learned free monads.

And I felt like that was a great solution to this particular app. So that we can treat these interactions as data structures and then interpret them with whatever interpreter we want. Via either a test interpreter or a live console interpreter. So that'll gain us testability and portability in environments.

Okay, so to do this, we have to make some changes. These changes are, we have to think about the effects. Now we're not just throwing everything in task, right? Everything here is just task task task task task. And what would be better is if we had these specific actions that we're capturing, just like we did with HTTP GET.

Those could just be task, but what we're gonna do is turn those into data types that represent the actions. So what we'll do here with print. I wanna change the word print, but let's first bring in liftF, we need that. So we'll do that from free, liftF comes from free, are you there?

Are you there free? It's me, Brian. Somewhere in here, we'll figure it out. I probably have to jump out of a folder to get to it. Are you there? I probably ruined my suggestions, we'll find out. Okay, might be in lib, I'm assuming it's in lib. But so we have liftF.

Note, turn things into free monads. Let's bring in our daggy or taggedSum. Great, so we'll make type for reading and writing to the console. We'll call it Console, right? And this will be interpreted in different ways. And we'll do another one for the database. Those are two primary effects in this program.

And instead of having to worry about that in our code and just throw everything tasks, we're gonna treat it like a data structure. So taggedSum and we'll call this Console. taggedSum takes it's name here instead of trying to infer it, so as the first argument. And then we'll say we have a question, perhaps.

That takes the string or how about a q. Turns out these don't really matter, these names. I mean, it's good for inspection, but when you do your catamorphism, they just destructure the type and you can name this whatever you want. So don't lose too much sleep over that.

And then we'll do print and print will take a string, terrific. q and s, true functional programmers write impossible variables, just so you know. [LAUGH] If I was doing this at work, I would write it all out. [LAUGH] So Db, we'll do Db and where we get a Save, right?

And that takes a table and a record. And All, get out of here, All takes the table and a query. I made it optional to pass the where clause in so we didn't see that earlier, but it does. Okay, cool, so we have these two. One thing I didn't mention about a free monad is that it actually expects functors [LAUGH] to be passed to it.

We should be making these functors. But you can actually make anything into a functor with whole thing, don't worry about it. I just want to call it out. [LAUGH] All right, so, Console, Db, two data types gonna represent our functions. So now we have to go into our functions where we have print.

And instead of actually printing, let's do realPrint here, right? And we'll make a print function that simply does that liftF nonsense, right? liftF and we'll construct our Console.Print and we'll pass the string to it. Likewise with our question function. I think we called it getInput here. So we're just gonna call print and question here to make the data types.

And then when we interpret it, we'll call the real functions. So we could put those together with realPrint and getInput. Maybe we call it write output, I don't know, writeOutput [LAUGH] looks great. Does anybody have any questions at this point? We're just going through the motions of putting stuff in free.

But now we have these two. We've got to do the exact same thing for All, we'll call it dbAll and dbSave. This will be Db.All, this is rather boring. Most of this stuff just follows from the definitions and you're just filling in the blanks, right? Table, query, table, query.

Then you have Db.Save(table, record). And we take our table and our record. And again, I could have automated all of this. It's just composition of the type, this is literally just composed dbAll and liftF, right? And even then I don't have to write that out every time. But I'm doing this for completeness' sake and so you can see cuz a lot of this is kinda confusing.

All right, great, now what happens? [LAUGH] Well we gotta switch this to dbAll and dbSave in these places. I probably should have just renamed the originals, but you live, you learn. And this is now going to be question, question, question. If I miss any, I'm going real fast, [LAUGH] so let's see.

Cuz this is not interesting, I'm just updating the functions that I should not have renamed in the first place. dbAll, cool, think we did it. Did we do it, dbSave, dbAll, great. So, now that we're doing this, what's gonna happen? Well f returns returns a free monad, not a task.

So we have interpret, we have to foldMap(interpret) on the free, and we're gonna interpret it to a task. Interpet, interpet, interpret, how do we interpret it? Well just like we did before, we'll write a little interpreter. And this is where if you ever do free monad stuff like looking this up online, you're trying to get really into free monads, you start to deal with composable interpreters.

Like I have this interpreter and that interpreter and I want to kind of compose them together. And as I'm composing all my effects into free, which I've just normalized all my effects into this one monad so they just all chain. Now my problem becomes, how do I break down and pass in different interpreters?

For the time being right now, I have two effects. And I'm not gonna bring in co-products and all this stuff to try to compose interpreters. So what we're gonna do is I'm gonna say, I take this, take this thing. I don't know if this thing's a console or a DB right now, I'm not sure.

And instead of trying to formalize and make this a little nicer, I'm just gonna go ahead and say, hey, do you have a table? Because I know that both of these have table. So this is, I'm just cheating, because I don't want to go through all that right now.

And honestly, I think that stuff is much nicer when you have types. And there's probably better solutions in an untyped language to compose interpreters. All right, so if you've got a table then we'll interpret, here call dbToTask, otherwise, we'll call consoleToTask. So these are the two. And we'll pass in our x.

Does everybody see? Wow, does everybody see all those x's? [LAUGH] So obviously, why I'm doing this and what that's for, it's like I've got an x, I don't know which one it is. I have to turn this data type that's just raw data into an actual running task.

And instead of doing anything fancier, I'll just do that. So dbToTask takes our x, and we'll do that nice little catamorphism where if we have a save, we take a table. And literally it's just dbSave [LAUGH] right, or it's just Save. Where we have an All, it's just All.

We went through and renamed our types but Save is holding the same arguments. So it's just gonna pass its arguments into the save function. So normally you'd say like, it takes a table and a record. And then we'll go ahead and call save with the table and record and that returns as a task.

So I could just pass it save. See how that works? Same thing with All, I get the table and a query and then it just calls all(table, query). Let's do the same thing with the consoleToTask. And what's gonna happen here is if I have a question, I'm just gonna call getInput.

And if we have a print, we'll call writeOutput, excellent. So we're just mapping our datatypes holding our arguments into actual functions that will run with this. So that's cool, let's see what happens. I'm highly doubtful this is gonna work. All right, I'm Brian. [LAUGH] All right, what happens here?

I got the first one. Blah blah blah, .foldMap is not a function, okay. So what's happening here? So we turned it into a task and we ran it. We saw that happen with start. We ran f, it did nothing but return us a free monad. Then we interpreted that free monad which actually ran the question, which is awesome.

And then we forked that so it actually ran the task and what it returned should be passed in to runApp. So the question is, what did getInput return? Or sorry, not what did getInput return, it's what did start return? We'll return menu or getAuthor. And I have a suspicion that menu, these are functions, so they have to be called and then they're free monads.

But here we're calling it, let's just see what f is. We're real close though, and we're almost out of time for this one. We spent a little bit longer debugging this so we can figure out what's going on. Maybe we can take a second to write our other interpreter.

So it says the first f is this thing, okay? It's a function, okay, and we call that function and we get this free, cuz it calls dbAll. The second one is a function, it's interesting it already called this twice, right? What happened, is it called start, it didn't have any authors.

So then it went over and called createAuthor, and then it's ending up calling, does dbSave and then goes to menu. Is dbSave the thing that we wanted it to do? Is that what we called it, dbSave? Yeah, liftF, Save, Db. All right, let's just try this again, now what do we get, what is f in here?

f is getInput, we screwed up. We left the getInput in here. Where did that come from? I forgot, it was way up there, there you go. So that's what's going on. So what happened here is that we didn't return the right type. We don't have a type checker, I didn't update that, there we go.

All right, so let's see what happens. I can clear all these logs now. All right, so run that, I am Brian. Where do you want to go today? I want to write. Whew, go to Title: Hi. So that's it, kick ass. All right, so we're doing free monads.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now