CoRecursive: Coding Stories - Tech Talk: Throw Away the Irrelevant with John A De Goes

Episode Date: March 21, 2018

Tech Talks are in-depth technical discussions. Today's interview is with John A De Goes. We talk about performance problems with monad transformer on the jvm, various flavours of IO monads and reasoni...ng about polymorphic type signatures.  On the lighter side of things, we discuss how to write technical articles well, flame wars and Zee vs Zed pronunciation.     Show Notes: John's Website and Twitter Descriptive Variable Names: A Code Smell Data Structures Are Antithetical to Functional Programming A Modern Architecture for FP

Transcript
Discussion (0)
Starting point is 00:00:00 Hey, welcome to Code Recursive, where we share the stories and the people behind the code. I'm Adam Gordon-Bell. This is an FP interview. It's about functional programming, and it's an interview with John Degose. I think I originally stumbled upon his blog trying to answer some questions about Scala Z, and he had a lot of super interesting articles, so I invited him on. And I think it's a very entertaining interview. Turns out John is somewhat of a divisive figure in the Scala community, but I think it's a very entertaining interview. Turns out John is somewhat of a
Starting point is 00:00:25 divisive figure in the Scala community. But I think this interview is very interesting. So I hope you'll enjoy it. John A. Degose is the CTO of Slam Data. John, welcome to the show. I found you had an interesting take on functional programming, and that might be fun to kind of explore that. So what is wrong with using descriptive variable names? Right. Descriptive variable names are a code smell, I argue in one of my posts. Yeah. And the reason I say that is I just say it in that way. It's clickbaity. I say it in that way to grab people's attention. What I actually mean by that is that oftentimes when your variable names are very descriptive, that is, you can say this is like a Java bean proxy factory or something else like that.
Starting point is 00:01:19 You know exactly what it is. And when you know what it is, it necessarily implies that it's not a polymorphic type. So it's a so-called monomorphic type. The type is fixed. It's not a variable. And what that means is your code is specialized to a specific type. And there are lots of drawbacks to writing tons and tons of code with monomorphic types. And one of the principal ones is just that once you write a type signature for a function that's entirely monomorphic, the number of ways you can implement
Starting point is 00:01:52 that function and have the compiler type check it goes way, way, way up. So in polymorphic code, classic example is for all A to A, there's only one way to implement that function whereas in monomorphic code if we have something like string to string then you can imagine that there's an infinite number of ways to implement that function so in general i always encourage developers to write as polymorphic code as humanly possible get rid rid of all the types. Prevent yourself from knowing anything about what structures you're operating on. And if you do that, if you do it pervasively, and then if you use principled type classes to inject the minimum amount of structure necessary for you to actually do your task, then the number of ways you can implement these functions will go
Starting point is 00:02:44 way, way, way, way down. And hopefully, in an ideal case, you'll find there's only a few sensible ways to implement the function, and one of them is the one that you want. And then the other thing that you do is not only do you make it harder to introduce bugs by cutting down on the size of the implementation space, but you also make the code much more reusable because now it's able to solve not just one problem, perhaps, but potentially a whole family of problems with totally different types, totally different data structures. So that's why I say if your code is so monomorphic that you can give everything a richly descriptive variable name then most likely you're
Starting point is 00:03:25 missing some opportunities there for abstraction and for reuse and for polymorphism that that are gonna make your code a lot safer and cut down on the odds of defects and make it easier to maintain and easier to reuse over time so do you think is that still true if reuse doesn't happen? So I, you know, I abstract, I pull out some portion of code and because the logic in it actually doesn't refer to anything except for, you know, kind of the structure, I can make it fully polymorphic. It's just like changing the shape of something or returning it um but it's only used in one place is it still
Starting point is 00:04:09 useful to to have it polymorphic in that case i would say you can't necessarily say it's always useful because i can imagine situations in which it's not useful and just adds a boiler blade and doesn't introduce any additional safety. But I find in most cases, if you can make something polymorphic, then in a language like Haskell or PureScript or any other language with full type inference, it often does make a lot of sense to do that because even if you don't achieve reuse, even if you still use that function in one place and in one way with specific concrete data types, you're still making it harder for that function to go wrong. Because it knows less about the types, it means that first off, your initial implementation is more likely to be correct. But that's relatively trivial savings. Like you can write something once and you can make sure it's right and then it's good to go. And the cost of that is constant relative
Starting point is 00:05:11 to the size of the function. The real savings is as that function is maintained over time, if it's highly polymorphic, that means implementation space is constrained, which means all the other developers who go in there and muck with that function, they're going to have a harder time screwing it up and introducing defects over time. functional programming that has long-term effects on minimizing the cost of maintenance by making it just harder in general for different parts of the code base to go wrong. Also, I think, I don't know if this is true for everyone, but I think that for me and a lot of other people I know who do a lot of functional programming, we find that over time it becomes easier and easier to reason about polymorphic code um we gain better intuition for how we're going to satisfy the type signature what kind of things we need in order to be able to satisfy and and that's a matter of like, it's an interval or whatever it might be that you're replacing with a monoid or a group or whatever you're replacing with
Starting point is 00:06:50 it's suddenly something you you don't need to know about you don't need to keep it in your head and all the different things that you can do with that you don't even need to consider them you can't consider them because the types don't even allow you to consider them. And I find that that way of sort of using polymorphism to get rid of stuff that we don't need to keep in our brains right now actually has a tremendously simplifying effect on the way I reason about the code and construct it. So do you feel like this is a muscle? So this is a muscle that needs to be developed when you move to a you know a system like a programming language with a with a more expressive type system i do feel that because i know in the early days me looking at haskell code that was highly highly polymorphic i'm like what is going on here i have no idea and and the variables are all like a and b and C and D, and they don't mean anything.
Starting point is 00:07:51 It's sort of a way of thinking about your code that I think has to be learned. And it only has to be learned because a lot of the programming languages that we're familiar with and that we grew up coding in are monomorphic. Or at least almost monomorphic. Like Java has generics, but they're very constrained in what you can express. And many, many other programming languages don't even have that. You're stuck in C or a language without types or anything like that. Then you look at code and the variable names
Starting point is 00:08:19 and their types give you a big clue as to what's going on. And the concreteness of it makes it easier to understand. And that's a way of thinking about software that I think we learn. And after we learn, we forget the fact that we learned it. And then when we're exposed to this Haskell style in which everything is highly polymorphic in many cases, our brains are like, what?
Starting point is 00:08:43 There's not enough details for me to understand what's happening. And then eventually, after you work at it for a while, you're like, oh, there's no irrelevant details that I have to consider when I'm trying to prove to myself this is correct, because I've thrown all those details away. They don't need to clutter my brain. So it's a totally different way of looking at the problem that I think has to, at least it did for me. And it has had to be learned for many others who are like, yeah, John, you know, this type signature stuff doesn't make sense. I want concrete types here. I want to give these big, long, descriptive names. And then I tell them, stick with it and concentrate on reading code and trying to become familiar with this style. And then six months later or 12 months later, they're like, you were right. I don't want to go back to writing code the old way. So I think it is something that it's like muscle memory. memory it has to be learned you have to train yourself over time i feel like there's some there's some switch where you go from when you're calling a
Starting point is 00:09:51 function looking at the names of the parameters as as a description of it as it may very well do in in java or c to looking at the the types of the parameters as kind of explaining what it does. Yeah, that's right. Yeah, the names are so important in so much code. But then when we move over into functional programming that's highly polymorphic, the names become less important and the structure, the underlying structure, becomes much more important. The structure of the types or their lack of structure in some
Starting point is 00:10:25 cases and you mentioned in there about having the proper i forget what you said the proper type class instances or something could you explain or yeah so oftentimes we're writing a function and we would like to make it fully polymorphic. So for all A, B, C, D, E, F, G, etc. But usually, that does not permit many implementations. Usually, it doesn't allow us to solve an arbitrary problem inside our code base. So in many cases, having a type totally unbounded, unconstrained is insufficient. It's not enough structure. Our code can't truly be polymorphic in all types. We're going to require a little more structure.
Starting point is 00:11:16 And so how we reintroduce just enough structure to solve the problem, just enough details that allow us to write a problem, is we construct principled type classes. And a principled type class can take that type out there that's arbitrary and it can give it some capabilities, the minimum amount of capabilities necessary to solve a problem. And by principled, I mean that ideally the behavior of this type class is given by algebraic laws that we can reason about all possible instances of the type class by using these laws and when we have a type class that does have these algebraic laws we say that the type class is principled and that's the ideal type of
Starting point is 00:12:01 type class to add additional structure to these polymorphic types in your implementation of the function. So instead of saying we're going to work with all A's, we're just going to work with A's that have the structure of a semigroup or the structure of a monoid or the structure a ring, or one of these other algebraic structures. And we're going to be able to use the laws like associativity and so forth that these things have in order to be able to create an implementation that satisfies our requirements. So polymorphism is how we throw details out of our implementation space to constrain the space of implementations and make our code much more reusable and easier to get correct. And then type classes,
Starting point is 00:12:51 principled type classes, are how we add structure back in, just enough structure to be able to solve the problem at hand. No more, because any more structure than we actually need to solve the problem is going to end up creating, enlarging that space of implementations, creating more ways for the code to go wrong now and in the case of interfaces. Because they always used to say one of the best practices for object-oriented design
Starting point is 00:13:35 is to require the smallest interface that you possibly can to solve the problem. And then also when you're building types, provide the maximal interfaces possible to make that type reasonable in the most different circumstances. So I think that this is not new. It's expressed a bit differently in functional programming, but the overall concept of minimizing
Starting point is 00:13:59 the amount of information that you, minimizing the things that you can do with the inputs to a function is a good idea and has been recognized even in object-oriented programming for probably decades at least a couple decades so i saw a post you had something along the lines of maybe this was another clickbaity article but data structures are bad. And I feel like this ties in a little bit. Could you explain that concept? Yeah, it does tie in.
Starting point is 00:14:32 And again, I have to apologize for clickbait. Hey, no, controversy is good. You should see, I mean, I post on my blog and most of the people who read that blog are functional programmers. So they know, they get it, I mean, I post on my blog, and most of the people who read that blog are functional programmers. So they know, they get it, you know, they like it. But I posted that on LinkedIn, and you would not believe the response I got. Just dozens of comments, and most of them are like, what the heck is this guy talking about? I think there's a problem, too, where people often only read titles.
Starting point is 00:15:03 They do, and you can tell who read the article by whether or not they agree with you if they disagree and think it's crazy then they only read the title they stopped there anyway the data structures are uh antithetical to functional programming i think is the blog post you're referring to and it's a very similar idea. And the idea is as follows. A lot of the goodness in functional programming comes from deferring decisions. And an example of that is a polymorphic function defers the types until much higher in our application. And this deferring also not only constrains implementation, but also enables reuse. So deferring, and that's not the only thing we defer.
Starting point is 00:15:51 We defer effects until the end of the world. In Haskell, we defer evaluation until the last possible minute when a piece of information is required. So there's pervasive deferment. It doesn't matter where you look in functional programming. All the good things about it are deferring the destruction of information because that just makes systems a lot easier to predict and reason about. And I think one of the areas where programming languages have not done as good a job enabling us to defer decisions is data structures.
Starting point is 00:16:27 Because many times you'll look at a function and it will produce a concrete data structure, even though it doesn't necessarily need to do that. And an example is, say we have this function that returns either an error message or some HTTP response. Now, all well and good, but the reality is most likely our larger program is embedded in a more powerful monad than either. It could be IO, or it could be some free structure, or it could be a monad stack with IO at the bottom. Most likely, we're not operating at the level of the either monad for big chunks of our program. So why do we, down here in the little pieces, why do we return an either when we don't actually need to return an either? We just need to return something that has the capability to represent success and failure, but may be able to represent lots of other cases as well as success and failure, or may have more fine-grained notions of success and failure. And I think that one of the good things about functional programming that could,
Starting point is 00:17:47 in theory, be enabled by a sufficiently adept programming language is enable us to defer the decision of what actual concrete type data structure to use in a huge number of cases. It's not easy to do that. It's actually extremely difficult to do that in today's programming languages like Haskell and PeerScript and so forth. But I think you could design a programming language in which it's very easy and natural to say, I need to be able to construct something like this, or I need to be able to deconstruct it in these particular ways and maybe some other set of ways that I don't have to handle. And if we did have such a programming language like that, then we would be able to be much more polymorphic in the types of data structures that we work with,
Starting point is 00:18:36 and we would have to do less sort of mind-numbing conversion from one thing to another, from an either to however we're going to get it into the type that represents our monad stack. And I think that when expressed like that, it's maybe not a controversial idea, but it suggests that people take a hard look at when they're writing code and they're using a concrete data structure structure do they actually need to require that or can they can they give up some of those requirements and return something that's that's more polymorphic and i think the answer is in some cases you can do that you can do that through type classes and other types of things in today's programming language but it's not very easy. So let's try to put an example on that.
Starting point is 00:19:27 So I'm thinking, actually, here's an example from your article. So you have a function that takes a list of employee and then another employee, and then it returns an either, which is either an error message or that employee's manager. So what's the problem here? So most likely this function will be embedded into a context that represents failures differently than an either. So, for example, it could be the IO monad, or it could be some monad error of some error T of state T of something of something of something, and so on and so forth.
Starting point is 00:20:21 And if we want to be able to use this function and have those different types of errors lined up, then we're going to have to take the output of this function, which is neither, and we're going to have to manually convert that data structure into a data structure that's bigger, that can represent everything it can represent, but potentially lots of other things that it can't. And there's no problem with this. It's just boilerplate. And not only is it boilerplate, but in some sense, it's like premature specialization. We decided that we were going to commit to the either data type when,
Starting point is 00:20:58 in fact, all we needed was any type that had a failure and a success constructor, but it could have had a thousand other constructors as well. We honestly, in this example, we didn't care if it had more constructors. We just necessarily wanted a success and failure constructor type. And you see this kind of mind-numbing, boring plumbing a lot when you're using, well, just in any case where you have smaller data structures and you're lifting them up you're constantly lifting them up to be in
Starting point is 00:21:33 bigger data structures whether that's monads or um uh all sorts of other things like if you if you write programs using the free monad style you will encounter this a lot with just co-products and so forth just lots of mindless boilerplate lifting that in theory doesn't have to exist it it is maybe in today's programming languages it's actually probably easier just to pay the cost of that boilerplate because getting away from it is even more painful in many cases but in my opinion that's just an argument for a better programming language. It's not an argument why we should always specialize our return types, even in cases where we don't need to.
Starting point is 00:22:14 So in this specific case, I think existing programming languages can handle it, right? We're saying instead of returning an either, we return some type class like either-ish or something that encapsulates this concept? Right. You can have some either-ish type class that has a success and failure constructor. And then any type which is bigger than that can, of course, provide an instance for that. So it's actually relatively easy, I think, to do that in today's programming language. Now, is it warranted? I'm not sure. It depends on how much pain you're in. But I've seen code bases where there's a lot of lifting smaller data structures
Starting point is 00:22:55 into bigger data structures. And it's not necessary if you make your code sufficiently polymorphic. And also, as a side benefit, you actually increase performance by deferring the data structure until the point where it's final in your application, actually end up increasing performance. How does it increase performance? Because less conversions,
Starting point is 00:23:19 less manual conversions. So for example, if either is converted to an either T of something with an error inside there and something else, and then later on that's converted to IO, I mean, there's two conversions in that case, but in theory, there could be five or six conversions. I've seen code bases where there's 12 conversions from the sort of fine-grained individual function all the way up to the top level of the application and all those conversions they're allocating memory on the heap and they're moving bits around and so forth they don't necessarily need to happen so i could see a
Starting point is 00:23:56 case where maybe the performance goes the other way like i'm thinking i have some function and instead of a list it takes it takes like list like or something right it takes a type class that encapsulates these kind of list like options but then um maybe list isn't a good example i'm thinking of what i'm thinking of is a function wherein it does something like add elements onto the end of it but then the concrete instance that we pass in is actually very expensive to add elements onto the end of it. But then the concrete instance that we pass in is actually very expensive to add elements onto the end of it. Yeah, so I'm actually a big fan. I think that Haskell and some other programming languages have made some mistakes here
Starting point is 00:24:35 because they've not baked into the definition of a type class required performance characteristics. And in many cases, that proves to be fatal. You're trying to do too much abstraction. You shouldn't be abstracting over random access over type. You shouldn't be abstracting random access over types that don't efficiently permit O of 1 random lookups. And in many cases, that's being done.
Starting point is 00:25:06 And so you have type class functions or functions implemented in terms of type class functions, which have pathological performance with some data types and wonderful data performance or wonderful performance with other data types. And in my opinion, that does not make sense. It's one of the things that we need to clean up. We need to go in and say, no, this type class, we're not going to,
Starting point is 00:25:29 we're going to provide performance guarantees on these different methods. And I know testing that is something different. And obviously there are some tools out there that you can verify that something's like big O of n or big O of 1 and so forth. They're not very good, but we could push them further. We could make them as good as QuickCheck or Hedgehog or these other tools out there. We could make them really good if we put some effort into it. And then we would be able to say, okay, I'm going to use this function and I'm going to have a guarantee from the laws of the type class, if you will, that it's going to perform at such a
Starting point is 00:26:06 level based on the size of my collection. I think that's something that we absolutely have to do because too much time, I've just seen too much time be wasted tracking down performance problems that exist solely because an instance of something was created that could satisfy the laws, but only at horrendous performance. And that type of thing is not principled programming, in my opinion. So we would incorporate the performance characteristics in the type class laws somehow. Exactly.
Starting point is 00:26:40 And right now, type class laws are by and large just comments. So it's not like we can't add more comments and develop additional machinery to pull that stuff out of there and verify at runtime in your test suite. It's possible, and we should be doing it. It seems a little bit like circling back around. Like we have something, like I have something that takes in an array, which is great because I have, you know, random access.
Starting point is 00:27:14 And then I'm going to make actually a type class that's like array-like, I guess, to say this is a list type thing that I can, you know, randomly access. And aren't I kind of circling back to being a data structure again? So I don't think so, because, for example, in the case of array, you can actually use an array, but you could use a skip list or you could use a vector or you could use lots of other things. And depending on how that thing is produced, it may exist in one concrete form or another. For example, it may exist as a skip list because it was cheaper to build that than it was to build an actual array. And so by making your code polymorphic over all things which are array-like and permit all of one random access, you're actually facilitating its reuse in situations that you did not intend. Whereas if you hard code that to be an array, then suddenly someone has to actually build an array all at once through pre-allocation or whatever technique they're going to use. And that may actually not be all that
Starting point is 00:28:09 efficient. And the question is, well, why did you need to know it was an array? Why did you need more than random access? Arrays permit all kinds of things. If all you needed was random access, then ideally your function should be able to work with anything that supplies random access. then that's just going to open up so many possibilities for reuse you can't even imagine them right now as well as prevent you from doing silly things like mutating arrays and so forth that you didn't need to do in implementing that function because you locked down that interface to the tightest possible bound that you needed in order to implement the function it's kind of circling back to what you were saying earlier that like the o concept of programming to interfaces yeah yeah it is it's very similar to that so type class laws are not enforced in any way do you do you, like, should that be improved, or do you have thoughts?
Starting point is 00:29:06 Yeah, I wrote a blog post on this topic, I think, years ago, but it's still online, and basically I argue that type classes are not the best that we can do. In fact, we could probably do a whole lot better, and one of the things that we, or one of the ways we can improve them, I think, is by allowing some sort of first-class means of specifying laws inside of type class. I mean, now they're written as comments, and depending on what programming language you use and what tools you use,
Starting point is 00:29:39 those comments can sometimes be extracted and used in tests. But it's not part of the language spec. It's just more convention. And I think that there are very real benefits that could be achieved by moving that into the language itself and saying, okay, here's a first-class facility for defining laws for my type class. And there are two common types of laws that are used right now, algebraic laws, but also operational laws, and both of them are useful.
Starting point is 00:30:16 And then there's, if you want to consider it, there's performance laws. Like what are the laws of this have to be? Big O of n, big o of n squared etc and a language that provided first class support for that would would make the concept of principal type class something formal and something well first class so how could it how do you envision it checking things like is this like property-based testing or how is it enforced? Yeah, so it could be enforced.
Starting point is 00:30:51 I don't know if you could do it automatically in all cases, but it's possible that you could do it in many cases because basically if you bake this machinery into your compiler, then in theory, it can have a notion of a generator and it can have generators for all the default types and generators for the polymorphic ones expressed in terms of the quick check style machinery into the language and require things in a much reduced set of circumstances. I'm not exactly sure what that would look like, but I feel like there's a lot of stuff that you could get for free. And in fact, like if you look at a lot of the arbitrary instances out there, they're just sheer boilerplate. That's why you can automatically derive some of these things, because they're just relatively straightforward. So I think that if you were to bake laws into type classes, you would have a lot of this machinery as well baked into the compiler's notion of being able to create arbitrary instances of some type. And you would require less assistance
Starting point is 00:32:12 from the developer. And then you can run these things in the same way that we run quick check properties today. You could actually run it as part of compilation if you wanted, perhaps. Or maybe a special special mode like verify my laws actually hold yeah i imagine some dark future where my my scala compile times like it's like okay verifying this uh this it's kind of a you know not a baked in concept but approximated and has some differences from maybe the Haskell type class model I'm a huge fan of type classes in Scala. I think that, um, I think it totally changes the way you write software in Scala. And it's, I think, I think objectively superior to the object oriented style
Starting point is 00:33:13 of doing things because with the object oriented style, you need control of the inheritance hierarchy of all the types that you want to be able to support these operations. And with type classes, you don't. So you can define a type class called monoid for types you have no control over because they're not bundled into its inheritance hierarchy. And so I think for that reason, type classes are objectively superior to use of traits and object-oriented style interfaces in order to model similar behavior. And that's why I think you see even type classes in the standard library. A standard library can't avoid them.
Starting point is 00:33:59 Things like ordered and whatnot, they were invented. They were invented because, yeah, they didn't have control over all these different types, and they wanted to be able to make some of the code much more generic than would be possible if you required everyone to implement a certain trait in order to provide this functionality. So I'm a big fan of it. I do think that the standard way of encoding type classes in Scala, it does have some drawbacks.
Starting point is 00:34:29 Scala Z8 is experimenting with a different alternative way of encoding the same thing that should move some of the drawbacks at the cost of maybe a little bit more boilerplate. So we'll have to see. I think that the current default way of encoding type classes is good enough to be considered an asset and will hopefully improve with time and will eliminate some of the ambiguous implicit error messages that come when type classes have complex dependencies on other type classes. So would you use a type class even if you controlled the uh underlying object rather than a trait uh yes i would absolutely do that and there are very few circumstances in which i would not in fact i can't even think of any that
Starting point is 00:35:20 i would not all right well i can think of one but aside from that one, I can't actually see any reason at all to use a standard object-oriented style trait instead of a type class, just because it leads to, first off, cleaner will basically limit the potential for subtyping to screw with type inference. Because if you have a sub-trait that has a bunch of methods and your data types are going to extend that, then that's going to affect type inference in very subtle ways. Most of these ways you don't actually want. You want to get rid of that. You want to make things as non-object oriented as possible in order to help with the type inferencer. And if you represent that functionality as a separate type class, then it opens up the possibility of doing that.
Starting point is 00:36:22 And yeah, it may be a tad more boilerplate, but it's the sort of write one spoiler plate, you do it one time, and then it's done, and you don't have to think about it after that. So the cost is constant for a given number of data types. So it's, in my opinion, acceptable cost for a long-term, long-lived project. So you mentioned the IOMonad and Haskell.
Starting point is 00:36:50 So do you think that people should be writing Scala or other languages that don't enforce purity kind of in this Haskell style of using the IOMonad to kind of mark up things that may have side effects? So I absolutely think that they should be. Now, the reason I say that is as follows. First off, if you mix imperative style with functional style, then your brain has to switch modes. Because in one mode, you can reason about the meaning of your program using
Starting point is 00:37:26 equational reasoning, A equals B. So anytime I see an A, I know what it means by substituting the B. And that is pervasively true in a functional code base. But if you mix imperative style with functional style, then suddenly that becomes true in some places and not true in other times. And so you have to have an extremely high level of diligence and awareness about which section of the code you're in, because you have to change how you think about its correctness, because the imperative style is A, then B, then C, then D, and so forth, making all these mutable changes. And so you track it like simulating a machine in your mind and then the functional style is much more mathematical and you understand
Starting point is 00:38:11 its meaning through substitution and those totally different styles your brain has to switch modes and so i it i find it very difficult to do that um and easy to get wrong because things that hold in one case will not hold in the other. And so like the substitutions and transformations you can make when you're sort of mindlessly refactoring, you have to be super careful when you switch modes because the invariants don't apply. And so just having one uniform reasoning principle that can apply to your entire code base, you never have to switch modes. You can just stay in the functional mindset all the time. It's tremendously beneficial.
Starting point is 00:38:52 But then you get other benefits as well. You get the benefits of knowing what a function does by looking at its type signature. And it's hard to overstate how just life transforming that is. Because once you get used to it, then you stop drilling into functions. And that totally changes your life. Without that capability, without the ability to know what a function does by looking at its type signature, then you have to drill into all the functions it calls. And then after you do that, you have to drill into all the functions they call and so forth until you get to the very edges of the graph. And that takes a lot of time. And that's why it's very hard to go in
Starting point is 00:39:34 and modify someone else's code. Because in order to understand what it does, you need to sort of trace out all these branches until you get to the very end but in a functional program you can look at its type and you can have a very good understanding of what it what it can do for example if a function if a if a pure function returns a bool then you know for a fact it's not going to be doing any console input output any socket input output you can memoize the result that function like that tells you so much whereas if an imperative program a function returns a bool who knows you know you know java's inet address uh constructor actually does network calls to resolve the uh the uh domain in in a canonical
Starting point is 00:40:22 fashion it's it's preposterous like that can happen anywhere it could happen in two string or in hash code or all these sorts of weird random effects that take you tons of time to track down and debug you didn't expect them because you didn't you were lazy and actually didn't go and inspect every function that was called by the function that you called and um that's just it's a common source of bugs and it's a source of complexity. And when you have a purely functional program in front of you, you don't have to do that anymore. So you have the uniform reasoning principle that you can apply to the whole code base. You don't have to dig into things because the types tell you what these functions do. And then you end up, I think, as a result of these properties,
Starting point is 00:41:08 developing code that's much more modular and much easier to understand. And of course, much easier to test because everything is purely functional. All your functions are total, they are deterministic, they don't have side effects, and so forth. And it's just much easier to test and reason about that type of code. And then I would also say, like, you get all these benefits, but you get other benefits in terms of expressiveness. So, and the reason why you get additional expressiveness is because functional programming reifies all the things. It makes them real first-class values. So, like effects, like printing a line to the console, that becomes a value.
Starting point is 00:41:57 And so you can take that value and you can put it in data structures like other values. It basically takes programs and it turns them into, so you can reuse all the machinery you know and love from value manipulation in order to build your functional program. And that is a tremendous increase in expressiveness. And if you look at code using future versus code using a purely functional IOMO net like Scala Z8, you'll see a tremendous difference
Starting point is 00:42:24 in just in terms of how many lines of code are required to express the same idea. It goes way down in purely functional code because all the things are first class. There are no effects there, just values. So you can manipulate values and chain them together and use combinators and do all these wonderful, amazing, magical things that you simply can't do
Starting point is 00:42:47 when all your things are actually effectful and they're mutating state and interacting with the real world. So it seems to me what you're saying in a way is that the benefits of using IO everywhere where you're doing side effects is that the things that aren't in IO, you know, are pure. However, like if you have a function that returns IO int, it's hard to reason about that, right? Like all you know is when this is executed, it will return to me an integer. That's right. So it could do any possible thing
Starting point is 00:43:25 at all, which is a reason to make the code polymorphic and to use type classes that say, I need monad socket IO here. So I know it's going to be doing some sort of socket operation, or I need monad random. I can work with any M that supports monad random. And then if it returns M of int, I know that it uses, it potentially uses the random effect, but doesn't use anything else. So I think, yes, you still run into that problem, but the mere fact that it returns IO int
Starting point is 00:43:58 is still tremendously useful because that should raise a big alarm in your head saying, okay, this function could do anything at all. And every time I call it with different parameters, it might return me a different IO int. So I can't rely on it to be deterministic in that same way. So it still communicates a lot of value. And how we take it to the next step is we use type classes
Starting point is 00:44:23 to throw away all the effects that we didn't need to constrain lock that function down and communicate more accurate information to the developer who ideally doesn't have to dig into the implementation to find out that it called math.random a single time so have you have you used this approach where you kind of split io up into a whole bunch of different capabilities or effects or something? Yeah, that's right. So we use that to some degree and actually quite extensively in Slendit as codebase. And then it's the style that I prefer to use when I'm doing my own personal projects as well.
Starting point is 00:45:04 Interesting. There's a whole bunch of potential IO monads in Scala, I guess. There is Scala Z, there is a future, there is a monics task. Is this good or bad? Well, it's like JavaScript having a thousand different frameworks. The bad part is it's it's like javascript having a thousand different frameworks the bad part is it's sort of painful and um it creates lots of confusion and lots of incompatible code bases incompatible libraries and so forth but the good thing is it's parallel exploration of the landscape of possibilities. And I think as painful as it might be, standardizing prematurely on one type too soon
Starting point is 00:45:51 means you didn't explore the space of all possible solutions. And I think that that, in the end, it hurts the ecosystem. It's like standard libraries with programming languages. Once you ship something in the standard library, no matter how crappy it is, it's going to be there forever and all the libraries are going to use it, which is a reason to not have a standard library or a reason to very carefully add things into there because it's very, very hard to change it. It's very hard to improve it over time in response to new learnings. And I think the situation we have in Scala is we have lots of old monads or monad-like things,
Starting point is 00:46:33 like futures, not strictly speaking a monad, but it's kind of monad-like. It has the method names anyway. And then like those made so many mistakes, just so many terrible, terrible mistakes that have screwed people up and introduced bugs and made systems slow and so forth. But we've learned from mistakes. And we've created other data types that don't have the same set of drawbacks. And then we've learned from those.
Starting point is 00:47:00 Like, we're actually probably three generations in. Yeah, we're probably a good three generations in, you know, future, the future era. And then the task era, the ScalaZ task era, basically. And then the sort of post-task era in which we have things like Monix task and ScalaZ 8.0. And we've learned a lot in each of those generations. And even though the existence of all these data types is confusing and leads incompatible libraries and so forth, ultimately, the community is going to benefit. Because like the very latest cutting-edge generation, it's not only way faster than anything that's ever come before, but it also supports incredibly rich functionality that you just can't get access in the older data type.
Starting point is 00:47:49 So it's, and no doubt, we're not at the end of innovation yet. You know, there's probably going to be a fourth generation. There's a limit to how far you can push these things in the JVM, but there's probably going to be additional evolution, at least incremental, on top of the third generation effect monads that we have.
Starting point is 00:48:06 And that will result in, ultimately, it'll result in more people coming to functional programming for the benefits that these effect monads have, which I think is a good thing for the community in the long run, as painful as it might be in the short term so do you think that it would be good at some point to you know to not have Scala Z and cats or or to is it good to have these two competing functional standard lives or something yeah I think that's an interesting question and I think that prior to Scala Z8, there was really no reason for both of them to exist because Katz replicated and used some of the code from Scala Z7. He used the same type class encoding that used a lot of the same structures. It was a subset of Scala Z7, but still architecturally very, very similar. And there was no compelling reason to use one versus the other. But I think with the difference between cats and Scala Z8 is going to be significant enough that it's possible they end up living alongside each other for a good long while. CATS is aiming to be much smaller, much more minimalist,
Starting point is 00:49:29 and much more sort of more of the same in terms of the Scala Z7 style hierarchies and type class encoding and so forth. And it's also aiming for long-term compatibility. So CATS is at 1.0, which means that by and large, it's not going to change, which is going to make it attractive for certain class of companies who need, you know, a few minimal abstractions. They need a monad and so on and so forth. And they want to be able to stick with that for, you know, two or three or five years, maybe never change. Maybe you never have to upgrade. And Scala Z8 is quite different. It's a rethinking of everything that you can possibly imagine about
Starting point is 00:50:15 functional programming in Scala. For example, monad transformers, in my opinion, do not scale on the JVM. There's no way that you can make efficient monad transformer. It's a mistake, a huge mistake. And all these tutorials telling people to use monad transformers in Scala, they're setting companies up for failure. Because if they have a deeply nested stack, five monad transformers, I promise you that machine will be overloaded doing the simplest of things the massive heap churn and massive megamorphism and all these things
Starting point is 00:50:51 we're setting people up for for failure and and as a result i think what we have to do as we strive to innovate in libraries or library versions, as the case may be, that are free to break backwards compatibility. We have to figure out what it means to do functional programming in Scala because it doesn't look like functional programming in Haskell. And just copying more of the same stuff is just going to result in more people rejecting functional programming because they try it and then they find out this stuff doesn't actually work. It doesn't actually solve the problems in a way that the business needs them to be solved. They can't satisfy performance requirements or memory requirements or this or
Starting point is 00:51:34 that. And we need to get away from that sort of, you know, mindset that functional programming in Scala should be exactly the same as Haskell. And there should be no deviation because that's just not true. In an ideal world, that would be true. In reality, it's not. We need to figure out what FP looks like in Scala in a way that solves the problems that enterprises actually have and also gives the functional programmers the things that they need in order to reason about the software. And to date, no library, whether it's, you know, cats or scholars at seven or whatever it might be, has done that pervasively, especially in the area of effect composition. So we need, we need more innovation in my opinion. And cats is going to probably stick on its current path,
Starting point is 00:52:24 which is going to make it useful for shops that are doing light FP and don't need a whole lot. And then I believe that I see Scouts at eight sort of as being the choice for the shops doing pure FP, who also don't want to compromise on performance or any of the other things that are important to business yeah i feel like you know for any given uh problem like there's a certain correct like having one implementation is probably not enough and having four is probably too many like i'm thinking in terms of an open source project right If you have four, you're distributing the people who are working on things too thinly. And if there's only one, experimentation is sort of lost. But yet Haskell, all the type classopedia stuff
Starting point is 00:53:20 is baked right into the standard library, and they seem to do just fine. So I don't know what that says. Yeah, well, every once in a while, they go through these, you know, flamers, like the monofoldable proposal, and like, should tuple be a functor? And like, they go through all these different things.
Starting point is 00:53:51 And they do have to occasionally break backwards compatibility. And of course, their motto is avoid success at all costs. So maybe that makes sense. But yeah, there's still a lot of legacy cruft in Haskell for that reason, because there was a standard library, you know, a more or less official standard library. And as a result, like in Scala, we have better type class hierarchies than Haskell, more clean. clean and like with uh the scholars at eight i o monad has a vastly cleaner semantic for error handling and interruption than the haskell model so like even though people like to trash talk including myself from time to time we actually do have the capability to innovate faster. And partially it's because there is no default thing. It's just, it's wide open and there's nothing that's standard. So people are free to innovate it at their own pace.
Starting point is 00:54:55 And that results in many more improvements over smaller timescales than we see in Haskell. I have a small question about pronunciation. It's funny because, so I'm Canadian. And so I would say the last letter of the alphabet Z, but I figured, you know, Scala Z because it was, you know, predominantly American, but I hear you saying Z as well. Yeah, you know, I've gone back and forth on it. I think I originally said Scala Z, but then I was hanging around with too many people at conferences who were saying Scala Z. So I ended up adopting Scala Z. I don't think it matters a whole lot. You mentioned that, you know, mana transformers don't scale. How so?
Starting point is 00:55:48 Well, so you have the following problems. First off, monad transformer is basically a stack of data structures. It's a data structure inside a data structure inside a data structure inside a data structure, inside a data structure, inside your outer effect monad, IO, or task, or whatever it might be. And every time you do any operation, for example, the sequencing operation via flat map, you have to construct this entire chain stack of nested data structures. And so you put tremendous pressure on the heap, not just from the data structures and all the nesting, but also from all the things that are allocated on the heap that you didn't even know. For example, when most people write a
Starting point is 00:56:42 function in Scala, they don't know that actually transforms into object allocation and so forth. And then, so not only do you have all this heat pressure from all these closures and all these data structures that are nested inside one another, but you have a problem that's probably just as bad in many ways, and that's megamorphism. Megamorphism is where the JVM goes to call a method, and it doesn't know what concrete data type is associated with that method. It's going through the interface. So because it doesn't know, it ends up running a lot more slowly because it has to do a virtual dispatch. And the problem with monad transformers is that every layer of your monad stack adds an additional level of indirection. So not only did you have one layer of megamorphism up there at the top, which is sort of inevitable, or it's not inevitable. If you're using a concrete type, then you don't even have to pay that. For example, if you call IO.flatmap, then the JVM is going to know which flat map you're talking about.
Starting point is 00:57:56 It's the one on IO because you have that concrete data type. But on the other hand, if it doesn't have that piece of information, then you're going to end up in your implementation. When you call flat map at your monad, that's going to end up delegating to other flat maps, which will delegate to other flat maps and so forth. And monad transformers are polymorphic in the type of monad that they wrap. So what that means is you have megamorphic calls to other megamorphic calls, other megamorphic calls, and so forth for as deep as your monad transformer stack is. And so if you think using an IOMonad or TaskMonad or future can be slow try using a a stack of five monad transformers on top of io and then we'll talk about slow and and not just slow by the way but massive heap churn so just massive pressure on the garbage collector just increased latencies you're going to see spikes You're just increased latencies. You're going to see spikes. Performance is just terrible. And you can actually get away with this, surprisingly, for some applications. Like if you have some sort of business process that only has to do 10 requests per second or
Starting point is 00:59:17 something like that, you're going to be fine. You can use these deeply nested Monad Transformer stacks, which has convinced some people who just do these type of applications that this is a good idea, but it's not a good idea because anytime you try to do a typical business application where you need to handle hundreds or thousands of requests per second, then you're not going to be able to achieve those performance numbers using your deeply nested stack of monad transformers. It's just everything is going to be terrible latency, response rates, everything is going to be terrible about that memory utilization. And this gives functional programming a bad reputation, deservedly,
Starting point is 00:59:58 because we've blindly copied things from Haskell and expected it to work on the JVM, and that's never going to happen. You have to change what it means to do functional programming. You can still do purely functional programming in Scala, and you can make it performant, but you cannot do it the same way that you do it in Haskell. And if you try, you're just setting companies up for failure. So I should just be like double flat mapping everywhere or however deep my stack is?
Starting point is 01:00:27 What's the solution? Well, the solution is no stack. No stack at all. Ideally, your program is using type classes to represent the effects that it requires. And for example, instead of using error T, you should use monad error.
Starting point is 01:00:55 Instead of using state T, you should use monad state and so forth. And all these type classes, they don't say anything about what the data type is that has an instance. So what you're free to do is when it comes time to supply a concrete data type at the top of your program, you're free to supply any monad that has the required instances. So what you can do is you can create a cost-free new type, you know, extending AnyVal for like my IO or whatever it ends up being that supplies
Starting point is 01:01:34 instances for all these different things, monad error, monad state, and so forth. And all it's going to do is express all these operations in terms of the IOMO. So you end up going from, you know, potentially a monad transformer stacked with 10 levels to something that is merely one layer of indirection away from my IO, which is secretly underneath the covers and to the JVM. It's in fact the IO monad, which means it goes from 10 levels of megamorphism and nested data structures to one monomorphic level where there's no additional nesting. So you get the performance of raw IO, but yet you were able to use monad error and monad state and all the other things required by your application. And this just dramatically transforms your performance. It makes functional programming actually practical on the JVM. And it handles, I'd say, 80% of the problems that monad transformers are used for.
Starting point is 01:02:47 And to handle the remaining 20 you need localized effects you need the ability to eliminate effects locally inside your application and you you actually can't use this technique for that at least not directly but there are there are ways that you can work around that issue as well so it seemed i thought makes a lot of sense, assuming I have a single stack, but if I, which often you do, I guess, but if I have a different combinations of transformers all over the place, I need like a concrete instance of each one, right? Yeah, for every, if you want to do a strict translation of monad transformer program into one without it then uh yeah what you need to do is for every concrete fully realized stack
Starting point is 01:03:34 that you had you need to create a new type for that which is a wrapper around io with its own custom instances for all the different type classes that were previously supported by that monad transformer stack. So many applications just use one. I mean, there are generally when you see different monad transformer stacks being used, it's because they need localized effects. They need to add a state T to something and then run it and get rid of the state T and then expose the underlying monad. And like I said before, you use a slightly different technique to handle those cases, but it is possible. And I've never seen any application that actually needed a monad transformer stack. In some cases, you'll need the ability to do localized effects
Starting point is 01:04:26 so you can escape from them. But I don't think there are any applications that actually do need a Monad transformer stack. Now I'm worried about my performance. I'm going to have to do some checks. Well, like I said, for some business process type applications, 10 requests per second, the memory requirements don't even matter. And performance is not an issue. And it's good enough.
Starting point is 01:04:52 But for a lot of applications out there, it's just Monad Transformers just totally destroys any hope at achieving performance that is even within two orders of magnitude of the imperative equivalent that we're talking just dramatic reduction in performance one thing i wanted to uh just ask you about is is your writing style i think that you're a very good communicator so you have good good headings that kind kind of maybe are a little bit controversial. And then you dive into details with code. What's your secret of communicating about technology effectively? The most important thing when you're trying to teach someone something is the ability to see the world from their perspective. So you have to know your audience, but then you have to be able to understand what they know.
Starting point is 01:05:51 And the other thing that I think, I mean, obviously you can structure things, you can work on the structure of things. So make sure there's a logical progression, make sure that there's flow. I think flow actually is one of the most underrated aspects of a good presentation or a good blog post. There should be some sort of arc and you should be moving people along that arc. But then beyond that, I think you can do
Starting point is 01:06:18 a little to make things interesting and relevant because when you write something, you're asking for people's time and you don't want to waste their time. You want to give them a reason to stick around. So you want to make things as interesting as possible. And sometimes that's a clickbait article, clickbait article title, just to make people think a little bit like state something in a different way to make them think, you know, that sounded like it was wrong. But after I think about it, that actually is kind of true.
Starting point is 01:06:47 Like give them little puzzles that can have a reward because their reward for spending their time reading your stuff is hopefully going to be some realization that gives them a bigger picture or that teaches them some concept they didn't understand or that cultivates a skill in them, like reading a polymorphic type signature, whatever it is. And that's the payoff when they have that aha moment, that's the payoff for them. That's the thing that keeps them coming back for more, hopefully. That was the episode. I hope you liked it. You know what?
Starting point is 01:07:26 And if you did like it, can you do me a huge favor and just tell somebody else to try out the show? Maybe you could message somebody and say, hey, I think you would like Co-Recursive. It's a good podcast. Until next time. Thank you so much for listening.

There aren't comments yet for this episode. Click on any sentence in the transcript to leave a comment.