Transcript from the "Composition Introduction" Lesson
>> Kyle: All right, our next discussion point, our next unit to discuss is composition [COUGH]. I think composition really deserves its place as one of the core foundational principles of all of functional programming. And the reason for that, I'll take a step back and say, I eluded to earlier when we were talking about why functional programming, what is the purpose.
[00:00:22] The purpose in my mind for functional programming, beyond just the fact that we wanna improve the readability of our code. The real purpose is to declaratively state in our code, what the path of data transformation is. Any piece of data, as it goes through the different steps, we want to make that path and the steps that are gonna be preformed along that path as obvious as possible.
[00:00:49] And composition is really at the heart of that. Because composition fundamentally is gonna say, how do I take a function, give it some input, whatever it's output is, make that be the input to another function. And whatever that output is, the input to another so you can see there's a flow of data in a function, out of a function, and a function out of a function.
[00:01:09] I wanna give you a running metaphor that we'll used to describe composition [COUGH]. And I'll set it up first with code and then, we'll get in to that metaphor. So here I have a function called sum which takes an x and a y and a mult that takes x and y.
[00:01:25] And they do, their obvious respective mathematic work. But I want you to focus on the bottom lines 9, 10 and 11.
>> Kyle: [COUGH] Specifically, on line 10 we are calling mult with that 3 and 4 value, getting an intermediary result. And then taking that intermediary result with some other input and passing that into the sum function.
[00:01:45] Obviously, this is a foo bar baz kind of an example because who would ever use a sum in a mult function? But what I want you to think in your brain is, not the foo bar baz thing but think to your application, that this would be doing some reasonable mildly complex set of computations.
[00:02:03] Say for example, calculating the international shipping rate for some product. And there might be multiple steps involved in the calculation for that shipping rate. And so we see those multiple steps here, we see that mult is the first step of calculating the international shipping rate. And then we take that and we give it some more input, and then we end up, eventually, calculating 17 represents the international shipping rate for whatever product.
[00:02:34] Takeaway here is that that code, lines 10 and 11, are highly imperative. That code is telling us how to calculate the international shipping rate. It doesn't tell us that the international shipping rate is calculated. So when we look at it we have to figure that port it out.
[00:02:52] And you could put a code comment there. But that's just a poor way of saying, this is not that declarative. It's much more imperative in its nature.
>> Kyle: So metaphorically, I want you to imagine that we are the engineers in a candy factory. And we have created this assembly line of,
>> Kyle: Steps that we go through from pouring in the melted chocolate here on the front end of this which then cools it into these chunks of chocolate. And then those chunks of chocolate go into the next machine in the middle which slices them up into small pieces of chocolate.
[00:03:35] And the small pieces of chocolate go into another machine that wraps them up in candy wrappers to drop into the back. So there's three steps that are being performed and what you should see here visually is that the output of one machine becomes the input to another machine.
[00:03:51] And they just rumble along the conveyor belts to do that.
>> Kyle: So the manager of this candy factory comes to us and says, well that's all well and good. I appreciate the fact that you've got the machines working but it takes up an awful lot of space on the factory floor.
[00:04:10] And we would like to make more candy we'd like to have more machines running so we can spit more candy out quicker. So could you engineer go and figure out another way to orient these thing so that we're not taking up so much space? It seems like those conveyor belts are awfully wasteful in terms of space.
[00:04:29] So you're the engineer of the candy factory. You begin to think, what could I do that could make that take up less space?
>> Kyle: Back to the code, we could observe the same thing. If that intermediary step that we calculated, the multiplication of 3 and 4 Instead of assigning that to a variable and taking up space in our code visually.
[00:04:52] We could just simply take the output of that function all and make it directly one of the inputs to another function call. No need for that intermediary variable. In the same way, the engineer at the candy factory might say, well, I know what we could do. We could just stack those machines on top of each other.
[00:05:13] No need for the conveyor belt. Literally just stack them on top of each other and let the candy pouring at the top, cool as it drops, it drops right in the machine that slices it, which then drops right in the machine that wraps up the candies. No need for the conveyor belt.
[00:05:28] So now we have a bunch more of these on the factory floor, we can get more work done. So you tell your boss, that's our solution. And the boss is like, great, good solution, thanks very much. Now, few months later the boss at the candy factory is like this is all well and good but we are getting some complaints from the factory workers because these machines are all separate.
[00:05:57] And they all have individual on and off buttons and the wires are all hanging out all over the place. It's not a very nice clean way for us to work in our candy factory. Could you just make one machine that does all three of these steps? I don't care what happens on the inside but they just want one machine, one nice clean box where they press the on button, pour in the chocolate out spits the wrapped candies.
[00:06:25] So you could say to yourself, what I'm gonna do, I'm gonna make a calculate international shipping rate function. And I will pass in those inputs, and under the covers, on line 10, we're gonna do all of that work. We're gonna do the multiplication and the summing, and we'll just spit out the output.
[00:06:41] So now down on line 14, that code is a bit more declarative cuz that code is saying, calculate international shipping rate. And on line 14, we don't really care how it does what it does, we just care that it calculates the rate. On line 10, we can see how it does that work if that's something that's interesting to us.
>> Kyle: This is abstraction but I wanna point out back to our earlier discussion at the beginning of the course about abstraction. I wanna point out that the purpose of me putting that in a function is not what most people would think which is, I wanna write dry code.
[00:07:21] We've heard of dry D-R-Y don't repeat yourself. Many people think, well that's why you stick in that function. That way I can call that as many times as I need to. Even if there was only one call to this code, we would still wanna put in this function.
[00:07:36] So our motivation should not be, I wanna write dry code. As a matter of fact, I have often times run across cases where in functional programming you want to repeat yourself. So it's not necessarily the case that reducing repetition is our motivation. The real purpose of the abstraction here is, as I said earlier, to create that semantic boundary.
[00:08:01] Between the what and the how. There's now a boundary between the what and the how and on line 14, all I need to focus on is the what. I don't care how you calculate the shipping array, I just care that I have the shipping array. And then I'm gonna go do something useful business logic wise with that computation.
[00:08:22] On line 10 to be honest with you I don't care what you gonna do with the value. The only thing I'm focused about on line 10 is how to do it. Those are both equally important things but I should be able reason about them separately. Creating a function boundary here and obstruction in such that semantic boundary between the two.
[00:08:47] That is the purpose of abstraction. Now we built that multAndSum function that calculate international shipping rate function if you will. And then somewhere else in the code we had to go build another one of those machines, and then another one, and then another one. So basically we're building all of these bespoke one-off machines.
[00:09:09] Back to the candy factory, the engineer at the candy factory says well, I know what I can do, I can just wrap up the big old box and if you screen closely you'll see the machines are on the inside. They are all actually inside there. But on the outside we just have this nice little clean box, with a single on-off switch control panel, a opening in the top for us to poor in the melted chocolate, an opening in the bottom where out spits the wrapped candy.
[00:09:35] We hide all the wires, and all that other unnecessary. So you make one of those, and you put it on the factory floor and all the factory workers are much happier now. Great, that's awesome, we've got a nice easy interface to work with this box, this function. And then, the next candy that you make a different one of those and you make a different one of those boxes and then a different one and a different one.
[00:10:00] And every time you have to do that, that work is very manual, so it takes you sitting down and wiring all of those things together. And at some point, you've made enough different ones of those that the boss of the candy factory is like, isn't there like some way to automate this?
[00:10:17] Isn't there some way where we could just like put a bunch of machines in and out spits the machine like this is a common pattern. We always wire machines together, why do you have to do this work so manually?
>> Kyle: And so it is with our code. We look at that pattern that we saw in our previous code where we are taking one function's output and making it an input to another function.
[00:10:43] We can codify that into a pattern and that pattern happens to have a name in functional programming called Pipe, this is called Function Composition. When we take function one and we execute it and then that output becomes the input to function two, that's exactly what we see here.
[00:11:02] We see function one being called with arg1 and arg2, its output plus arg3 is the input to function 2 on line 11. So it's the same thing that we were doing manually on the previous code snippet but now we've got a repeatable utility. We have in essence, a machine making machine.
[00:11:25] The input to pipe2 is not candy. The input to pipe2 is candy machines. The function themselves are the things being operator on. There's a fancy term for this in functional programming, it's called a Higher Order Function. A Higher Order Function is a function that either or both takes one or more functions as inputs and/or makes a function as an output.
[00:11:58] If it does either or both of those things, it's a higher order function. It is to carry our metaphor forward, a machine making machine. So now the boss says, hey, can you just make me a machine where I just throw some machines in the top and it does the magic, it wires them all together and out pops my little single box machine ready to do what I need it do.
[00:12:23] That way, any time we come up with some new combination of candy we wanna make, we just throw the machines in and out pops a machine. Can you build that for me?
>> Kyle: Being the creative enterprising engineer you are, you say sure, anything can be accomplished. I can make a machine making machine.
[00:12:42] Are you following where we're at now? Because now the input to this big machine is other machines. And of course, we could keep going as high as we wanted to. We could say, take that machine and have it built by some other machine-making machine and on and on and on.
[00:13:01] Of course, that stretches the ridiculousness of the metaphor. But hopefully, you're starting to see that higher order functions are really at the heart of how we do anything in functional programming. We start to think about stuff, not just as operating on a single piece of data like a number, but operations on functions producing other functions.
[00:13:21] We've already seen several examples of higher order functions. The unary function that we looked at, it took a function in and gave us another function out that restricted its input to a single argument that's a machine making machine. Binary took a machine and made another machine, that's a machine making machine.
[00:13:40] As a matter of fact, virtually everything you do in functional programming is the usage of utility where that utility is a machine making machine. Higher order functions are at a heart of everything we do. In this particular arrangement of those higher order functions, is what we call Composition.
[00:14:00] Taking the output of one and making it the input of another.
>> Kyle: So I made that pipe utility that took the first function, called it, passed in the input to the second one.
>> Kyle: If we look at that code, it looked like this, it looked like line one.
[00:14:23] Conceptually speaking, if we were to talk about the composition of baz, bar and foo. It would be that we call baz first, we passing that input and then its output becomes the input to bar whose output becomes the input to foo. To express that with a typical functional programming utility like compose, we would listed as foo, bar, baz in that order.
[00:14:48] Now, I want you to look at line three and compare that to line one. You'll notice that the foo, bar, baz are listed in the same order visually, left to right. But what is the execution order of foo, bar, baz? Which one runs first?
>> Speaker 2: Baz.
>> Kyle: Baz.
[00:15:08] So it actually executes right to left instead of left to right.
>> Kyle: The reason, one of the reasons the compose utility takes it's items, takes it machines in right to left order is because it matches the order that you would write them in code, left to right. So if you're replacing line one with line three, you list them in the exact same order, and that is convenient.
[00:15:36] But on the other hand, now you have to think to yourself the execution is in the other order. And sometimes it's easier to think about not that order I would list things in, but the order that they're gonna execute in. So if you wanted to list them left to right in order of execution, that would be the pipe utility, where we list baz, bar, and foo.
[00:15:57] Baz runs first, then bar runs, and then foo runs. Compose and pipe do actually the same thing. Just they operate on the list of machines in the reverse order from the other. Does that make sense? Both of those utilities are standard utilities you'll find in any functional programming library.
[00:16:19] And I have found in my own coding that sometimes compose makes more sense for me, and sometimes pipe makes more sense. So it's not that one is right and the other is wrong. It's that we want to use both of them. By the way, those utilities often times will go by different names in other libraries.
[00:16:37] So sometimes compose will be called flow, I'm sorry, flow right and pipe will be called flow. I think that's what they're called in, for example low-fp. So they may go by different names, but they still do exactly the same concept. Was there a question?
>> Speaker 3: Yeah, there's a question online about composition using a map function.
[00:17:01] Using mapping to read the functions.
>> Kyle: We have not talked about map yet, so if we're asking about map, why don't we defer that until we get to list operations later in the workshop?
>> Kyle: Okay.
>> Kyle: So I wanna make a simple utility that just does two functions.
[00:17:27] And I'm gonna call that one composeRight. Takes fn1. You'll notice how I listed the parameter order. That utility will do composition of two specific functions. In just a moment, you're gonna work on an exercise where you get to make a general compose that can work on any number of functions.
[00:17:45] But I just wanna show you if I was hard coding it, this is a very simple implementation, just compose two functions together. That comp function returned on line 2, that's our machine that our machine made. That constructed machine takes any number of inputs and passes them to the first function.
[00:18:07] But the first function will always output only one output. So the second and third and so forth from then on, they'll always only have one input. Do you remember the special term we used for functions that only take one input?
>> Speaker 4: Unarian.
>> Kyle: Unarian. Okay. When I said earlier than you're gonna prefer unary functions this is why, because unary functions are a whole lot easier to compose together.
[00:18:33] That pipe two thing idea which is a lot more complex cuz they have multiple arguments in each, that was piping binary functions together and that's a lot harder to do. It's much easier if we wanna pipe around or compose unary functions. Because functions have a single output. So if their shape of single output matches the shape of single input, they just fit together like really nice Legos.
[00:18:56] Does it make sense? S, wherever possible, you would want to design the functions to be unary. And if your function wasn't unary, you might wanna jump through some hoops to make it or adapt it to be unary using some of the tricks we talked about. Like, for example, that unary utility we talked about.
[00:19:16] There will be other things that we look at like currying in a little bit. Those are all things that we can use to take a function that's not a unary, and make it into a unary one so that it'll be easier to compose.
>> Kyle: So here's an example of using composeRight.
[00:19:39] And I actually make two different functions. I compose these in different orders. F composes double first and then increment. P composes increment first and then double. We get a different end result. On line 12 when we called f, the 3 is gonna get doubled first, and then increment it.
[00:19:58] That's how we go from 3 to 6 plus 1 equals 7. But p is gonna increment first before doubling so we're gonna increment 3 to the value 4 and then double it, and that's why we end up with 8. Two different functions that do those operations in different orders.
>> Kyle: If I were to make, I didn't put this in the slide, but if I were to make a function that did this doubling and incrementing thing. Similar to what we did with the calculate international shipping rate, it would take two or three inputs. And then produce a single output.
[00:20:44] And that would look like a bunch of points, wouldn't it? But if I express that function as simply a composition of two or three functions, now I have point three style again. So, we are reinforcing this idea that using these utilities and these patterns that are well known and well established.
[00:21:05] They let us write more declarative code. That's why we were able to write that calculate international shipping rate as a compose.