 Welcome to using monad transformers without understanding them. My name is Jordan Martinez. I've been involved in the Peerscript community for the past couple of years, contributing to the compiler and also doing some documentation work. I currently work at Arista Networks. Everything that will be talked about in today's talk will be covered in this repo. I encourage you to check it out afterwards, so there's some additional goodies you might find helpful. Now, in today's talk, my entire goal is to make it possible for you to use the accept-t, reader-t, and state-t monad transformers in your code immediately after this talk finishes. First, we're going to cover why monad transformers exist and what problem they solve, walk you through kind of the thought process behind them, and then show you how to use them and avoid some of the common mistakes. So here's a few assumptions I'm making about my audience. One, you understand how to use monads. And two, you understand denotation. If you don't understand that, I encourage you to look through the monotype class hierarchy a bit more because we're going to be using this a lot. Okay, so what problem do monads actually solve? We have this in JavaScript, but in Peerscript, we write this. And the difference is between one being a procedural statement, another one being an expression that we can evaluate. Now, when I talk about monads, I want to introduce this analogy of the foreground and the background. For example, we have this picture of a cat. The foreground is the actual cat. It's the thing we care about and it draws our focus. However, the background still has some sort of context here that's important. We're not really focusing on it. We don't really care about it, but it's still part of the picture. Similarly, with monads, foreground is basically the denotation syntax. It's what we see, it's what we read and write, and what we care about. The background, however, is the bind implementation, the stuff we don't really care about, but it's still very important. In the foreground, we have the appearance of a sequential computation. And in the background, we have a whole bunch of boxing and unboxing boilerplate going on that we don't have to do ourselves. So if we look at a few common monadic types, we can kind of see this in action. Let's look at identity as an example first. So here's identity. We have a box that has an A value. We take that value out of the box, pass it into this function F, where this arrow represents what we would see in our denotation. And that produces back another box value B. We can then unwrap the identity box again, take that value out, pass it into this arrow, which is again what we see in our denotation, and that returns back a final box that stores a value C. Here we can kind of see the exact same thing, but now it's actual code. We have identity right here. This left arrow is the bind implementation that handles that unboxing for us and we get the value one. We do the same thing here to get the value two, and we take them, produce a new value and wrap it in a new box. With the next example, we have the either monad. We see something similar here. In this example, we see that the right functions the exact same way as identity. If we have a right A, we get to here, we have a right B, we get to here and move on and so on and so forth. However, it's different in that this either monad allows you to short circuit. So what happens if we don't get a right? Well, if we get a left, that means we never continue on with the rest of our bind calls here because we stop immediately. It's basically a short circadian computation. Similarly, if we have a right the first time and we get to this value and then this produces a left, we stop here and we never actually run this value here. So again, as an example in code, it looks something like this. In this example, we see very similar to the identity. There's a box, there's a box, there's another box. Nothing really new there. In this example, we have something else different. We have an initial box of right, which means this bind call does occur successfully. But then we have a left here. And that means everything beyond this point doesn't actually get run at all. So everything here and down here, these computations never actually occur because we short circuit once we get that left. Moving on to another example, we have the function monad, which basically takes an input argument. And the monad's output is whatever the output of that computation or that function is. For example, we see something like this. We have an input argument. Here's the initial function that produces back a b value. That b value then is passed into this function f, which is what we see in our do notation, our foreground, and that produces back a new function. The function again takes that argument arg, passes it into it, produces a new value c. We then take that c value, pass it into the function g, which is what we see in the foreground's do notation syntax that produces a final function from arg all the way down to d. In code, it looks something like this. Here we can see the input argument, which I'm going to say has a value of one. And we produce a value of two. Here's that same exact argument now, but now we're producing a value four. And finally, we take these two previous values and we produce some new result with them. So what problem do monad solve? They allow us to use do notation to focus on the stuff that actually matters and let bind basically handle everything else for us. And what we look at seems to be sequential in its order, even though there might be some other things going on behind the scenes where it's not. As an example, we can see the function monad doesn't really seem to be sequential at all, but that's what it looks like on our side. But what are some problems that monads don't solve? How would we write this using monads? We can see here that in most cases this would be printed here, but in this one case we have an error being thrown, which means we short circuit. And we have a second part here which is handling that error and does something else if that error does occur. Similarly, we have a function f that has a read-only configuration value that's inside of its body, but that value is never actually defined inside the function's list of arguments. It just suddenly shows up and we can use it somewhere through this computation. And again, how would you do something like this, where you have a value x that refers to the value one at some point in time. Later on in the point of time it now refers to two and later on it now refers to five. How do you express these things using monads? And the answer, of course, is we use monad transformers. So why do we use monad transformers? First, we're adding these effects to pre-existing monadic computations. That allows us to decouple our code and as a result, we start getting something like algebraic effects. An example of this is tagless final, where you have basically an interface that you write and you implement it using monad transformers. So we've covered the why and what of monad transformers. Now let's talk about the thought process behind them. How do we actually produce these things? Where do they come from? What's their design? So first of all, monad transformers work at a macro level. They actually just simulate these effects. They're not an actual runtime system that provides exception handling a throwing, but we can simulate it through various means. On a micro level, however, they work by making that denotations bind part just hide even more boilerplate. And that's it. So if we were going to simulate this effect of this try catch block, what does this actually mean? Well, the try part can return a value if it succeeds. But in the situation where it throws an error, it returns a different value. This means there's one of two possible values it could return, which we can simulate by using the either type. Now what happens if we have the read only value effect? Well, we don't really have a way of just making a value magically appear. But the next best thing is we can say if you pass that into me as a function argument, then I can always use you at some point in the rest of the computation. The state manipulation effect is a bit trickier because we have the monads preexisting output that we need to keep. But on top of that, we now also need to return back the state at that point in the computation. The state manipulation effect is a bit trickier because we have the monads outputs. But on top of that, we also need to return the state at the end of that computation. So we return back the outputs and the states. However, that state can change over time. So we also need to take that old state and pass it into the next monad of computations output that then returns the output plus that new state. As an example, basically, this state here will be passed in as the argument to the next bind call right here, which can then change that state, producing new value here, which is then passed into the next one, and so on and so forth, creating a sort of cycle. And again, this may not make sense immediately, but I will show a diagram later that kind of visualizes this much better. So simulating effects in general sound really good. In principle, at least, there's only one issue, though. Have you seen that boilerplate? It gets pretty bad. So if we see this example here with either, what does it actually look like? It ends up looking something like this, where beforehand we would just have a value of one, but now we have to wrap every single value in a right in order for this to work. Similarly, if we wanted to do this sort of short circuiting and errors thrown thing, we need to wrap the value in a left. And this just means that everything from this point forward never actually occurs. If we try to simulate this read-only value with a function argument, it looks something like this. Here we see that there's a function argument that we are ignoring, and just providing back the value two. Here it is again, here it is again, and here it is again. Again, it's this additional stuff we have to write just to get the value two over here on the left side of this left arrow thing. What about this effect? How do we simulate this? What does it look like? As we can see here, here's the initial state. We pass it into this slot right here, which doesn't show up on the left hand side of our do notation. It's this slot right here that shows up instead. So this is just the same thing as basically saying peer three, but with a lot more boilerplate involved. In this computation, we have the value that we're sticking it now in the output slot. So now it shows up in our do notation. Here we are changing the value of the state so that this is now no longer one on the next bind call. Instead, it gets passed through here as four since it's one plus three. The examples we've seen so far aren't really too bad. But what happens if we compose these effects with one another? Here's an example in JavaScript. We have this value x that equals five. At some point in the future, it's now not five. It's five plus whatever this read only value is. We also throw a new error right here and then handle it separately down here if that error is thrown. So what would be the type of this if we're using our simulating effects? That type would be something like this. We have a read only value which is accessible to the rest of the computation. We have our state that will change over time. And the output is either the output or an error. What does that actually look like in peer scripts? Here's what it's going to look like. We have our read only configuration value. We've got a state which is represented by integer and output and error, which are just strings. Here I have my example where I'm passing in what the configuration value would be in the initial state. And in my examples, I've been using this identity A to represent things. But in this example, I'm going to actually use effect because I want to be able to run this computation. And since effect can't be used like identity in this way, I'm going to use a lower case effect to kind of show you the idea of boxing this value in an effect. So we have a try block, which is the computation we want to run. And if it proves is an error, the catch block will handle that error and do what it needs to do. I'm not going to explain all this code, but essentially you can see that this boilerplate gets really boilerplatey for something that's arguably very, very simple in JavaScript. In principle, this idea of simulating effects with boilerplate sounds really good, but there is no way I'm going to want to write this by hand every single time. This is just a pain. And so perhaps this idea is dead, maybe we just shouldn't go with it. And you might think at this point, well, wait a minute, like the real issue here is that we just have all this boxing going on, right? Like if only there was a way to not deal with that boxing and just focusing on all the stuff that we actually care about. And you then might remember, but wait a minute, don't monads kind of do that. They've got the foreground do notation, which allows us to focus on the things that actually matter. And the background syntax, the bind implementation is handling that boxing and the unboxing boilerplate for us. Well, then is it possible for us to take a monad and sort of transform it in such a way that it adds these effects and still allows us to use do notation? And the answer, of course, is yes. So what happens if we stop simulating these effects with boilerplate and start using something like monad transformer to do the same thing? What does it look like? So first, we had this idea of the tri catch block. We know that we have a monad that produces some output, but now there's a possibility where it can produce either an output or some sort of error. And in order for this to work in do notation, that means it needs a bind instance. Well, to implement that, we need to wrap it in a new type. And since we're wrapping it in a new type at some point in the future, we also want to unwrap that new type. So we also have this function called run except T. Now, what does the bind instance look like? Here's what except T looks like. We have a box identity that stores a value that's right or left. If it's a right, we can unwrap both the identity and the right box and get the value A. We pass it into our function, which we see in our do notation syntax, and that produces back another identity, either error or output. And from there, if it's a left, that means we short circuit. It just stops immediately. But if it's a right, that means we can keep going on to the next computation. And so on and so forth. Now, we may have this idea here, which allows us to do this computation where there's an error now involved or an either that's now involved. But we need some sort of way to say, you know what, I got an error. I need to use that short circuiting capability immediately. We'll call this function throw error and put visually it looks like this. If I would call bind, it just says, take that value, stick inside of the lefts. I have now triggered that short circuiting computation. So anything beyond this point just won't happen. But on top of that, we also want a way to then actually handle that error. So if we do get an error here, we can say if it's a left, we can unwrap that left and get back the error, pass it into a function that will hopefully give us back a right and we can keep on going from here. We call that catch error. And it looks like this. I have a computation here, MA. I run that computation and I see is that either that it returns a left. And if it is, then I can handle it. And if it's not, then it produced a successful output. I don't need to do anything. We can just continue on and be happy. So what does this look like before and after? So here's what we would have to write with boilerplate to get the same effect. And here's what we get to write now. Now that we have a bind implementation for except T, this allows us to focus on the stuff that actually matters. Similarly, this is what we would have to write with except T using all boilerplate. And this is what we now get to use. Let's move on to the next example, read only value. We have a monad that produces some sort of output. Now we need to be able to have access to this read only value at any point in that computation. In order to use the bind or in order to use the do notation, we need to bind instance, which means we need to wrap in a new type. And because we want to get rid of that new type at some point, we're going to have a special function called run reader T that unwraps that new type and passes the argument to the resulting function. Its do notation will look something like this. We have some argument that gets passed and produces a identity of B. We unwrap that box, we get back the V value passed into this function, which returns a new function from argument to an identity of something else, and so on and so forth. But we need a special function called ask that says when you give me that input argument, just return it in the monads output so that I can actually access it in do notation. So remember the argument is sort of in the background syntax of the bind implementation, and this exposes it to the foreground syntax of the do notation. But visually it looks something like this. We have the identity that produces back a C. We ignore that C and say ask and ask says, give me back a function that says when you give me the argument initially, just stick it inside a box so that now it's exposed in the next call to bind in the do notation and that's it. So again, what was the before and after this idea? Here's what reader T looks like with all of its boilerplate. And here's what it looks like when we use do notation. This is much, much clearer. Let's work on the state T next. So we have a monad that produces back some sort of output. Now we wrap it in that state idea as we talked about beforehand, and in order to use this in do notation, we need to write a bind instance for it. That means we need to wrap it in a new type and have a special function called run state T that unwraps that new type and passes the initial state into that function. And if we look at the do notation and how we visualize it, it looks something like this. Here's our initial state that produces back a box that stores the output of the computation and that state. We then take that box out, pass it into the function F, which is what we see in our do notation, and that produces back a new function. That if I take that previous state and pass it into that function, it will turn back a box with another output and whatever that state is. And so on and so forth until we get to maybe like here. At some point, we're going to want to take the state that's hidden in the background syntax and expose it to the foreground syntax, because we actually want to use what that state value is at that point in time. So we're going to have a special function called get, which all it does is it takes the states that we have here and sticks it in that output slot. Put visually, we have state one here. It still stays in its same slot here, but a copy of it now shows up right here as well. And now we can pass it into our do notation through this call right here. Similarly, we're going to want a function called put that when we've done using whatever the state we want, and we want to change it now, but not worry about what that is anymore. We use put where all it says is I go to my state slot. I stick that state in there. And since I don't have any output in this computation, it's just M units monad unit. There's nothing that's returning. And so I say ignore this output from the previous computation, take this new state. And now this state is going right here. Since I have nothing to actually return in my output. This is just unit. So what's the before and after? Here's what state T looks like with all of its boilerplate. And here's what it looks like. Just using this. So we've talked through the thought process behind how these things work. Now let's actually show you how to use them and some common mistakes to avoid. So when do you use mono transformers? Well, first of all, you don't. If you can just use a regular monadic computation, just use that instead. If you do need to use one, the question is how many. If you just have one transformer, it will typically look something like this. Here is an effect computation where every single effect here can return back an either string or some value. And if you want to say at any point if one of these fails, I want a short circuit, this is where you'd want to use the run except T mono transformer. If you need to start using something like multiple transformers, that's when we start to encode things a bit differently. We will use type classes to encode the business logic, and then mono transformers to actually implement that logic. Let me give you an example of what that looks like. Here's a computation where it's using these type classes. This is basically get input, and this is throw error and catch error that we've already covered before. And it says I'm going to get my initial state. If it's less than three, I throw an error. Otherwise, I'm going to set the state to then be zero, and then I'm going to return the actual output value of true. Now this is one interface because we have this type class working for any monad that can implement these type classes. And we can transform a monad by wrapping it in these monad transformers, state T and accept T to implement this stuff. But notice that we can do either accept T and then state T or state T then accept T, and so we actually have two possible implementations for this interface. So what does it look like if we run both of them? What's the difference between them two? Here's what it looks like. You can see here that in the case of accept T then state T, returns back a value like this and similarly the other way returns a value back like this. There's nothing immediately obvious about why this difference matters. Then when you get to the errors, you start to see something else that's different. In the accept T state T, you'll see here that there's a left and some error message, but there's no state. And in the state T accept T, there's a left, but there is a state. And this is where the stack order matters. By stack, we mean that there are numbers around some monad computation. And the order of that stack matters. In the first one, we have a monad that produces an either error or a tuple that's an output and a state. In the second one, we have a monad that produces a tuple of both a state and either an error or an output. And on the happy path, everything's fine. We have all of our values because they're all there. In the error path, that's when we start to see a difference between the two stack orders. The first one represents the left error, which means we lose our state, whereas the bottom one, we still have our state. And that can mean a huge difference. As far as I know, stack order really only matters with the accept T or maybe T monad transformers because they enable short circuiting. And the unfortunate thing is that for many computations, accept T is one of the first ones you want to have wrapping your base monad. So this issue does show up quite a lot. Since monad transformers work on all monads, they look like in practice. So here's the exact same program. We have one interface, this program, two different implementations, but keep in mind that this works for all monadic types. Well, how many monadic types do we have? If you scroll down, we can see that there's a whole bunch of them. We have identity, we have maybe, we have either, we've got array, we've got effect. And so if I wanted to run this, this is what it would look like. We can see here that we have identity, which is what we've seen previously. We have maybe where everything is wrapped and adjust. We have either where everything is wrapped in a write. We have array where it returns back an array of one value. And we have effect that actually runs effects. So we've covered how to use this and we've shown you the mistake of the stack order affecting things. The question is, where do we go from here? Obviously I didn't cover everything because the point of this talk is not to explain monad transformers. It's to enable you to use these three by the end of this talk. So I would encourage you to check out this repo and look through more the content I have there, which does cover more material here than what I presented. Specifically, you might want to look at the source directory to see how each transformer in all of its different versions looks like and run some of the examples yourself using the scripts I provided. And when you get done with that, have a better understanding of monad transformers in general. It would be helpful to read over the capability design pattern if you're not familiar with it already. So in summary, we use expressions, not statements. We take those expressions and wrap them in monad transformers to add the effects. And finally the stack order matters and you can use these on all monads no matter which one. If you have any questions, you can contact me through discord or open up an issue on that repo and I will respond to