 So, hello, my name is Alexander Grannin, and today I'll be talking about hierarchical frame monads and software design and functional programming. Actually, not in functional programming in the world, but maybe in Haskell. Do you agree with me that Haskell is the best programming language? Who's agree? Okay, a half of the room. Okay, I guess it's okay. Fine. Yeah. So, what we will be talking about. Firstly, I'll describe what is the state of software design discipline in Haskell. I'll present you some model of how it was been developed for time, for decades maybe. And after that, we'll discuss some design principles whether we need them, and maybe how we can do our functional programming in Haskell more popular and how it can be applied to industrial work. After that, we will go directly to the hierarchical frame monads idea. And we'll see whether it's helpful, whether it's good, or maybe we can do something else. So, let's me introduce myself. I'm a Haskell developer for not that much, two, three years maybe. I used to be C++ developer for many years. And actually, I am giving talks about functional programming in C++. I'm driving some ideas from Haskell and porting them into C++. And it's very interesting because C++ has its own way to functional programming. But probably my biggest interest is functional design and architecture. And I'm writing a book. And all the ideas I'm describing here are also described in that book. So, it's online, it's published, it's incomplete, I'm still working on it. But you can probably read it if you wish. Okay, so what is the state of Haskell in terms of software design? Yeah, I think we have a situation when the software design is underrated in our community. Meaning that, okay, we have some concepts, we can combine these concepts and we are fine. We are building our applications. Nothing, yeah, we built our applications, nothing can stop us from using monos, applicatives, functors. Is it really a software design? Let's start from the beginning, from the big bang when Haskell was born and on that time we could not do any I.O. So we have a black square here. No I.O., no interaction with the other world. Okay, that was a babyhood. And then a childhood came. We now can do I.O. We realized that we can even separate our business logic that can be pure in some sense from the real world in which we interact with, with databases, with file system, and other stuff. So we have two layers on this childhood stage, on this childhood phase. And okay, we think it's okay to divide our application into pure and pure layers, but is it enough? We started thinking about it and it came to this situation when we realized then that we need more effects to be described in our monos. And we created monos techs, we invented monotransformers, and now we can compose in some sense different effects like you see here we can compose state effect like storing and getting release from our context and I.O. effect like interacting with our outer world. Still we realized it's not enough to obey some requirements the software should obey, like stability maybe, simplicity maybe, and maintainability because when you have a pure, when you don't have a pure world, when all the things are in the I.O. like here maybe not directly in the I.O. but in the state T environment you can do everything. It's not really easy to understand is your code reliable? Is it testable? So next step will be to separate these effects from our I.O. logic and to declare some interfaces to these effects. And we ended up with the service handle pattern or service pattern or maybe handle pattern as you wish. We can define some interface to a subsystem, in here it's logger subsystem and we can place this interface into a handle and then we can pass this handle across our logic. Once we need some login possibilities we go to the handle, we call this logger. We don't know what the logger does. It can write to file system, it can push a log to a log server, whatever, because we just have its interface in form of type. And okay, still we have our logic in the I.O. Then we went further and we created an idea of reader T pattern. When we have the same handle but we can write it and put this handle into an environment of reader T. Why we should pass a handle by our hands? We can just wrap it and let it be in our reader T environment, okay? It was a boyhood because still we are dealing with I.O. And it's not really convenient for some cases and Haskellers started thinking about how to specify our effects in some other way to control them better, maybe to describe them better and to clearly understand where we have such effects, where we can do these effects and these tools should not interact. These tools should be distant. You cannot do any effects anywhere. And this is how the story with effect system started. Okay, we have a ridiculous situation. We have 10 maybe libraries for effect systems in Haskell. And probably before we consider some of them, let's talk about Final Tagless and MTL style. And here we should define an interface to our subsystem in form of type class, Haskell type class. Or okay, logger, maybe random, maybe something else like database. Then in the business logic, we need to compose these type classes in form of constraints. We place them into a list of constraints like here, logger and random. And then we have M1 in which we can do all these effects. Okay, this is the most spread way to define effects in Haskell world nowadays. Interesting that PureScript treat to implement own effect system in own manner like using records on the type level. You have to define several effects as lists of this kind. And then you can only compose these functions because in PureScript effect system is built using open records and the E here can contain any effect. You can compose some functions with these effects. It's okay, except it doesn't work properly because when you write a lot of business logic, you have to consider, you have to track your effects here and there and the PureScript system can occasionally say that it doesn't work. You duplicated your effect or you lacks of some effect or even this effect is not corresponding to that effect like this effect contradicts to that effect. And writing business logic with this system was not really a pleasant experience. And hopefully, fortunately, they removed this system from the next versions. Okay, so I think it was not a good, that good invention actually. What about Haskell libraries for effects? As I said, we have a lot of them in our ecosystem. Some of them are based on the free monads. Some of them are made using type-level magic. But the idea is always the same. You need to define an interface to your effect like file system and methods to work with it. After that, you have a possibility to combine these effects into type-level lists like here. It's a type-level list containing one effect. Currently, still you can add more. And when you write your business logic, you have to track your effects again. So this was a very simple, extensible effects looks pretty much same except the effect list can be described like this. Or you have another possibility to use MTL style and describing the effects in form of constraints. And free effects. Again, you need to define an interface. You have some way to compose these effects. I don't show here how to run these effects against a real environment because we probably don't need those details. And free library. I had experience with this library in production. We were writing a lot of business logic regarding invoices, some other stuff from the mine. And it seems like when you miss some effect or when you place it in one wrong order, you will get a very complicated type here from compiler. And it's really hard to understand what's happening because you need to firstly check your business logic whether you miss some effect. And then you need to understand what the order is. And then you can realize that other functions that depend on this your wrongly written function should be updated as well. And the changes are propagating all the way down to the call stack. And that's a huge obstacle for refactoring, to my opinion. Okay. You can be disagree. You can love this cool type level stuff. I understand this. Still, we probably need to talk about what makes it difficult to use. Okay. One more effect about polysemi. A very recent invention and capability. I don't have samples. Sorry, it was very tedious to write these samples into slides. In my opinion, this old stuff. Okay. It's cool. Yes. It's cool because you want to define all the cases. You want to understand what exactly every function does. Still, defining all the cases is not how the real world works. Because in Mafia, you have to consider all the cases. Otherwise you will end up, you have wrong theories with something that is broken. But real world requires from us as a developers to do some hacks in our code sometimes. We deal with the requirements that are not quite defined. They can be regular. And all this stuff, we cannot encode it incomplete. And to my opinion, it's an intention to have all the stuff described in types is not really a good way to build software because it makes difficult to develop your applications in the proper way. So yeah, this is my thinking about how we can consider our Mafia approaches that we need to understand that it's not an answer. It is an answer for many cases for sure, but not an answer for all the cases. We still need to have a possibility to use a simple approaches. And what are the options we have? We'll see in further. And to my opinion, the only way to get the Haskell community and the Haskell as a language into a real world and to give it more popularity, to have an interest from the industry is to provide the data to those industries a way to build applications and do not bother about all this cool stuff. When you have a framework, you can use all the possibilities out of the box and you do not to invent a particular ways to solve a particular problems because if you will be solving the problems again and again with cool stuff, particular approaches like type layout magic, you cannot really move fast. You cannot really solve all the problems, meaning that you need to constantly invent something which is not really how the industry works. Industry works in such a way that you take ready solutions and you write business logic and it's okay. And you get money. You probably, yeah, you can disagree with me for sure. Still I think we need more frameworks and we need more considering respect from Haskell community towards the frameworks and framework writers, I guess. And what can be the last phase of our development in Haskell? It could be dependent types, except I'm not sure whether we will be writing our code because using dependent types because to understand dependent types, you probably need to understand what is written here. Okay, this carries me a little, maybe not a little. And I'm not sure how it will look like in the code. If it gives, if it will give a real advantage, okay. Seems it not likely to be happen because type level stuff in Haskell very complicated. Okay, let's move to the next part. I'm constantly hearing from Haskell developers, why do we need all these principles? Is this for all P developers? We have math and we're okay. Why should we both are at all? Well, I think we should because design principles are not about the concepts we use in Haskell, but rather in the way we structure our applications. You can have a lot of concepts, still composing them and reading the code and provide a clear idea what the code does the code does is not only about finding essential properties of this code like monos or applicative applicatives, but also it's about how we compose it in a higher level. So design principles help us to achieve that. And first design principle I want to talk about is inversion of control. It's a well-known principle from a mainstream development. And why we need it? Because it helps to reduce complexity. We as software designers, we as architects, we are not writing the code. We are making our code less complex. We work with complexity. There are two types of essential and accidental. We should both are about reducing the dental complexity. Okay, we also need to separate our concerns because we need to work on the parts of the application differently, separately, independently. And we want to have our business logic testable, maintainable and all other buzzwords, you know. How we can do it? There are several approaches to do it. Final tag list. Yes, this is an approach to inversion of control. It has own benefits, it has own advantages, and it has own flaws. Fremont approach is another way to do it. It may be service handle pattern and reader tip pattern. You are, as a software designer, you are free to choose any solution, but you need to understand how much complexity each solution brings into your code. And when we can do this, for sure we cannot build big applications without design principles. We can, but we will throw these applications after a while because they are not maintainable completely. When we have complex domain with different subsystems interacting with each other, when we have long life cycle of our subsystems, this all makes us think about how we can define subsystems in terms of interfaces, how we can implement them in terms of some mechanism and what, this is what we are talking about constantly in our functional design books, in our design books. And we probably need to talk about it in our Haskell community more. What about solid principles? Are the solid principles only applicable to OOP? Even Robert Martin says they are applicable to any paradigm because they are paradigm agnostic. We want to have our code simple, right? Simplicity, simplicity matters. We want our code to be somewhat extensible in some forms. We want to have different parts. And so he says, Functional programmers want to spread the code to avoid cross talks between responsibilities and the users. Okay, they want to minimize the number of modulus affected by a change. That's also a value. They want to establish a conform to reliable interfaces contracts. Okay, yes, we need to interface contracts. They want to avoid hanging dependencies on modulus and resources they don't need. Seems true. And they certainly want high-level policy to be independent on low-level details. This one last sentence says that we need to abstract from our details. Our business logic should not depend on such details that are not really matter in our business logic. They make our business logic polluted, very fragile and less readable. So how we can abstract things? There are different approaches. And one of them is to layer your application in certain ways. For example, we can talk about onion architecture or maybe three layered cake as well. They all, these are all two approaches are have some in common. You have interfaces to subsystems. You have business logic that is aimed only to use these interfaces. And you have implementation can be impure, should be impure. And these three layers are independent. Actually, it's nothing new for a mainstream developers. It might be something new for Haskell developers because we don't really want to read what's happening in mainstream. Still, it's nothing new. Okay, let's probably go to the next theme and consider hierarchical free-monets as a solution to all these problems. Okay, all, do you agree? Is it okay? Is it okay? So what exactly I'm proposing? This approach is used in practice, in JustPay, for example. Not only, but in JustPay in particular. We built several frameworks based on hierarchical free-monets. And this approach allowed us to write a lot of business logic separately from the impure world. After that, we realized we can easily test it. We can even test it automatically because of so-called system automatic irrigation system. And we realized that we can even put some developers who is not familiar with Haskell or PureSkip. And they will be fine writing business logic using this framework. Because the framework states you should easily write your business logic. You should not buffer about type-level magic. Why should you? You need to focus on your domain, not on the cool stuff. Because only business logic has real value. Nothing else. In this talk, how much time I have? In this talk, I will present you an upper source project that is called Nidra Hydra. I built this project for my book. And it has comparison between free-monets, final tagless, and churchy-coated free-monets. You can find several applications built using those three approaches. And you can compare how they look like, how they behave, what is the performance. So it's a kind of showcase. The application we will be building is about tracking meters, like those meters which are falling to the earth. And we need to take them because it's some kind of danger for us. So astronomers do this kind of stuff in their work, okay? The application will be simple. We have some domain model in which we define our stuff that is important to us, like meter, coordinates maybe, maybe some properties of this meter. And it will be a part of our application. We might want to store our data into the DB, so we have to define some way to map our domain logic to DB logic. In this particular case, I'll be using BIM. It's a library for abstracting over the different databases. And okay, you probably don't need to get into the details of what's happening there still. I want to show you how different parts should be organized in terms of layering, okay? For example, we can define a meter table and we can define extra DB with some tables inside. And what about business logic itself? Using these types, we can write some logic in the app L monad. This app L monad is a free monad from the framework. It provides several possibilities out of the box. What possibilities? For example, working with databases. And here I can call a scenario function that should get run DB function, which is a function from another free monad language, like language L. And then I can pass, for example, BIM there. There are different ways to organize this hierarchy. In this particular way, I have at least three layers of free monadic languages, like app L uses the possibilities of language L, which uses the possibilities of SQL DB L. And the BIM itself, the BIM library itself is built using church accord free monad. So we can consider it's another level of abstraction here. Okay, what can we else do with our business logic? For example, we can log something. Yeah, we need to log something. App L language provides you some possibilities to load this. Do you see something about implementation? What logger will be called here? Who knows? It can be a library like coloc, it can be catheap, it can be HS logger, it can be tiny logger, it can be even just a simple path as a sterl ln. We don't bother here. We can only, we will specify this real logger when we run this logic against real environment because we need to focus on the interfaces, not on the implementation. And running this scenario requires some additional bits, like runtime in which I want to store my operational data that should not be visible for business logic, that should be somehow hidden. It's implementation details. I don't bother about it in my business logic. I can create this runtime and can run my app L here within this runtime. There is possibility to use different runtimes because all you need is to write some interpreter for this free monadic language. And let me shortly describe the top concepts of this free monadic approach. Firstly, you need to define an effect. This is similar to the effect systems, except it's just a type that you won't be composing in sense of type-level lists. It will be a type that provides you the possibilities out of the box. Okay, I defined this, only the two methods here, like eval-lang and init-sql-db. There are many other methods in this app L language. Still, it's enough for demonstration. Yeah, it looks like JDTs, but it's not necessarily. Yeah, the JDTs syntax a bit simpler to me to define these methods. But yeah, it can be, actually. Okay, another free monadic language, and here we can see the nesting, because this free monadic language, like logger, is nested into another free monadic language, like langel. On the previous slide, you can see that we call here another nested language. So you are free from composing these effects on the type-level. They are composed for you out of the box. And what about the real stuff that should happen when you run your business logic? You need to define interpreters that will be connecting your methods to real environment, like a log method to real logger facilities. And here the main idea is that interpreters from free monadic languages can repeat the same hierarchical structure as the languages themselves. For example, we have a nested free monadic interpreter for langel, and then we can define the interpreter for logger, and we nest these interpreters inside others, and we have all those possibilities that we need. We should not think how to compose funkers, for example. We don't need to think how these interpreters are built, how to connect our effects together, like in type-level lists, or whatever. The idea is simple. Are you following this idea? And probably the last thing I want to show you is how to use this free monadic approach within a real application. Let's define an API for HTTP server to deal with Astro events like Meteos, like maybe other stuff from the Astro field. We have two methods here for our server-based application, and then we need to define an environment for this server-like environment. This environment will be an impure environment, and we put our application runtime to that environment. When we get a call from the outside, we can take this environment from the CervantN type, like from the reader team environment. You see this pattern is used here. And we call our business logic from this handler, like Meteos. And so one layer of our business logic is like free monadic languages. Another layer of our application is like CervantHandlers. Another layer of our application is real runtime and real interpreters. And thus we made a layer of application in which all the parts are separated from each other. Okay. Let's conclude something. What we learned. We have a significantly reduced complexity here because when you write business logic, it's very simple. It doesn't require you to know type-level stuff like type families or type lists or whatever we have, complex type classes, type classes which have many arguments and maybe functional dependencies. You don't need to know about this. Just focus on your business logic. Simplicity. The code looks simpler. The code looks simpler and it's simpler to refactor because you don't need to mangle with effects. The application is layered. One concern regarding effects systems is that we have all the layers mixed together. When you work in the MTALE style, in the final TACDA style, the monotube express contains bits from your business logic, bits from real-round time. When you need to define something that does not fit in this system, you have to hack this somehow. This is not about layering. It's convenient to use. There is no magic inside. All the scenarios, all the interpreters are just some values. You can just pick them up and compose them in a functional way. There is a little boilerplate on the interpreter and the interpreters and on the layer of free monadic languages themselves. We can deal with it because the boilerplate on that side is not that critical. We write it once and after that we don't need to touch it. We write business logic more often than we change our framework. Performance. Sometimes high-skillers complain about performance of free monads because composing them internally looks like composing the lists when you need to path through the list and add another list to the end of these. When you need to compose even more lists, you need to pass it again and again and again. This makes the free monads slow. The short-circuit free monads do not work like this. They are much faster. The most viable thing about free monads is that you can easily test your business logic. In our just-play work, we implemented a system that I mentioned earlier, IRT. We take our application, our business logic. We track all the steps, all the effectful steps that are happening there. We write these steps into a file in form of JSON entities and after that we can play these steps against the changed business logic. We can see the problems if the business logic changes wrongly. How we can do it? Writers of the business logic do not know about the internal mechanism. They don't see it. Still, it works on the interpreting level and tracks all the effects. So you get unit testing for free and you can even tweak this, not only for unit testing but for integration testing and other stuff. Okay, I think that's almost all I wanted to tell you and here are some links to provide you more info. You can follow these links and see us there. And this is all and thank you for your attention. It was tough. Can we just go back to the code slides where there's two free monads are there? Yeah, that's this? Yes, there's a... So, if I say want to deal with the database effect separately and say the red is all the key value pair, there are two kinds of databases and I want to sort of track them separately so I will write a free monad interpreter for the database I will write a free monad interpreter for the for red is. Now if I want to write a function where at the type level I want to say so I want to write say three functions, one which can deal only with the database, one which can deal only with red is and one which can deal with both. How you can do this? How I can? Okay, essentially we have different QVDBs in our frameworks in Hydra I implemented two of them Redis and RocksDB and the idea here is that you write that you can have two different interpreters and two calls, one of them from the business logic. You have a notion of connection which is parameterized by a type I will rephrase my question I think you misunderstood it so I can see app L over there and my understanding up till now is that once some function is in app L, then it can do any of those effects like app L supports without changing the type signature like if you are reading a function type signature and that says app L it can access the state it can access SQL it can access logger it can do all of those effects now if I want to restrict a function I want it to do some effects but I want one function to do only SQL and one function to do only logging. Yeah, you can do it with this particular effects like you can write business logic inside logger L and then you just pass this logger L to a function like this it will be logger L here there is a function for this they are nested not just by declaring them nested they are nested by calling functions which take scenarios or the underline languages so you can easily write this function separately in this. So logger L is a separate free monad and SQL DB L is a separate free monad and app L is composing them now if I want to do ad hoc compositions of these effects is that possible using this particular library so app L it seems to me combines all of these free monads but suppose I want to do it in an ad hoc manner I have one function wherein I want to make sure that it can access it can do only two things SQL and logging will that be a combinatorial explosion of effects will I have to write yeah that's probably possible still you need to write some additional stuff for this but I don't like this approach because why do you need to specify your effects so granular I understand that we want to control these effects in particular still it comes to the situation when you don't need such behavior in your application you need some domains of the behavior not the effects scattered all over the functions yeah when we were doing this it was like pure script role-level effects you just end up having these types which are polluting your names your all your code and your types and it gives you nothing like you don't really gain something by isolating say console and log like a network over here and only db over here we didn't actually see any gains from that granularity of splitting things up in practice at least maybe other people have had different experiences and I actually would love to hear about that if that's the case but we haven't seen it and it looks like the pure script community itself did not see the value in that because they dropped those role-level effects in 0.12 and just moved to something that's like IO yeah so it's only effect now there's no f of anything it's there's just one yeah it's called effect it's the same as IO yeah still many has to do will not agree with us because you know it's it's a common practice to specify exactly a set of effects for these function and exactly another set of effects for those functions still it makes reflecting really hard and when you write a lot of business logic you don't need to control all these functions in their own particular way actually you need to understand areas of your code not a particular parts of this areas like this is my kvdb subsystem and all those functions will be work with kvdb one motivating example for that is when you're building caching layers so you would want to write a bunch of functions which can talk only to redis and not to the db so that you make sure that they're just picking up stuff from the cache I have not completely thought about this idea but the core thing was that in the MTL style it sort of encourages you to have slightly more granular effects so you can write a free monadic code in an MTL style there are type classes defined for all these languages and you can write a business logic with those type classes and they will be using a free monadic inside still I think it's not worth it so we actually ran into something like this and it was actually the opposite you want the calling code to care about whether it's going to the cache or not because usually what you'd want to do is if you fail at the cache you hit the db so this approach helps you not care about that so you basically want to say look up this thing and that look up might hit your cache first and then fall back to the db so it's actually worse if you have that level of that kind of split does anyone else want to ask questions otherwise I'll just keep going so how does the performance compare versus MDN? it's an interesting question because I gave another talk Final Tags versus Free Monad for which I used to get the performance and there are several things to consider here first when you write some business logic you can write this business logic inside those monadic languages and then you can compose those functions inside APL and they will have own own interpreting processes if that was just a regular free monadic free monad then those separate languages should have own content continuation list and then you can cut them in the app so in this sense the the performance should be considered like a sum of the performances in internal languages but if we are talking about church and coded free monad it shows the same performance as Final Tags and MTL I have a graph in which a free monad blows up right to the sky and Final Tags and Church and Coded Free Monads go together linearly so yeah Church and Coded Free Monads are like Final Tags and MTL in the sense of performance which is good you know BIM is using Church and Coded Free Monad inside I would also add that I built STM library using Church and Coded Free Monad and Just Free Monad both in Haskell and C++ and I saw significant improvement in performance in Church and Coded part thank you