Podcast
Monad Architecture
You can also follow our feed. Listen to more episodes in the archives.
This week Cameron and Taylor answer our first listener question: How should you structure large applications? We compare ReaderT with MTL along with other approaches.
Episode 56 was published on 2021-11-15.
Links
Transcript
>> Hey there Haskell Weekly listeners. Welcome to another episode of the Haskell Weekly podcast. I'm your host Taylor Fausak. I'm the Director of Software Engineering at ACI Learning. And with me today is Cam. Thanks for joining me today, Cam.
>> Hey Taylor. Nice to, nice to be here. It's always good to catch up. I know we've seen each other a couple times this week for lunch. So, you know. What, what's another day. It's three days this week we've seen each other. Whether that be virtual ...
>> Yeah, unfortunately no TexMex today, unless that's what you had for lunch, but.
>> Nope. I went to a place in town that, uh, just like kind of teriyaki bowls. Fantastic. Good good stuff. Uh, but yeah.
>> Sounds good. Cam, I didn't give you an intro. You want to intro yourself?
>> Uh, sure. My name's Cam. Just kidding. My name's Cameron Gera. Uh, I am a senior software engineer at, uh, Caribou now. Our name was previously MotoRefi and we just relaunched on Monday a new brand. So, uh, yup, that's been really most of my week anyways, but yeah, it's been good. How's uh, how's things in the ITPro world.
>> It's going good. Uh, I actually am off today and I was off yesterday for veteran's days. So it's been a short week for me and we got Thanksgiving coming up, coming up in a couple weeks and then Christmas. So getting into that holiday vibe, um, but it's been good, as I mentioned last week, still working on that merger with practice labs, which is going well. And, uh, and in terms of tech stuff, as a team, we are working on moving a lot of. Models our database models into persistent from the previous ORM that we were using and that's going really well. And we're looking forward to being done with that migration, but we've been hitting very interesting little tricky bits throughout the process. Nothing that's persistence fault, just like, oh, this thing does it that way. And persistent does it this other way. And now we have to make those things agree with each other.
>> Yup. I mean, that's really moving. Major system over, right? I mean, yes, it's just a database layer and interaction library, but it does do things differently. And it does a lot of things for you. So when you change those, it's 90% of the time not going to be one for one replacement and you'll have to do some mix and matching to get it, uh, to a nice, good place.
>> Yeah. And I was actually talking about this with the team and I remarked. Earlier in my career, when I was working on a Ruby code base of similar size and age, it would have been unthinkable to change our core database, like the core library that talks to the database that would, there's no way you could confidently do that. And the fact that it is even a possible refactoring we can do in our Haskell code base, I think speaks to the overall maintainability of Haskell. In my opinion, one of its biggest strengths.
>> yeah. Yeah. We're using Postgres simple right now because we don't really have a ton of database interactions at the moment with microservice architecture, we can kind of keep it isolated and keep it pretty small. Um, and when we do do queries we're using, uh, the SQL quasi quoter from Postgres simple quasi quoter or whatever it's called, uh, I like it. Yeah. It's more pretty well. So far haven't had. Production issues or anything like that. So, and we've, I guess this is the fifth day of our being in production state. Um, so now we've, we've got like for our new product that we're doing, which is the insurance product, uh, that I'm a part of, we already have close to 300 leads just in this week and it's not been marketed or anything. So it's a pretty cool given some. Yeah, it's nice to see that everything works. You know, you can say yes, I want to make this thing and I want to see that it works, but the fact that we hadn't out of any of the teams, we had really no fires. Um, that's coming from our code, so, and it's worked well. We've already had some insurance request transferred over to the insurance agency and all that stuff's been moved through. So, I mean, it's, it's nice to see that the power of your. Programming language work and yeah, your plans actually show up to do something that you want it to do.
>> Agreed. I was going to ask what's up with you. What's new at Caribou, but it sounds like you just told me, so thanks.
>> Look at you, rhyming. What's new at Caribou.
>> And I feel like that also ties into what we were planning on talking about today. It was a bit of a slow news week in the Haskell world. So rather than doing what we normally do, which is kind of review a blog post from the past week, um, we're going to answer our first listener question, which came to us through Slack. So thank you very much to drew on the Haskell Foundation slack for sending me a question and he asked. He would like to know how to structure real large applications. Do you use tagless final or MTL or reader T or free monads. So specifically he seems to be asking about. The monad stack that is behind the scenes powering everything. So not like which, you know, web framework are you using or, or how are you deploying it or how do you test it? None of those things. So, um, I think cam you, and I obviously have some shared experience at ACI Learning, working on this, and you have some new experience at caribou. So. Um, we have some answers to this and hopefully they'll be satisfactory, but you want to get us going here. Cam, what are you guys doing at caribou in this regard?
>> Yeah, so, um, with our microservices, um, we have really kind of a core monoid. We use more monoid stack. That's just really a wrapper around reader T with an environment. So, um, for us, we don't necessarily. We just need to make sure we have an environment that we can access anywhere in the pro each of our services. So, uh, that way we can call out to third-party services or a call out to the database, those kinds of things. So, uh, you know, we, we found that reader cheese. Good enough for us. Um, another little thing we have on top of that is, or another, some of the classes that we're deriving, uh, for that would be like, we're actually using UnliftIO. So we don't have to like. Run something in IO in the monad, uh, which has been kind of cool. And, um, uh, there's a couple other things. I mean, my reader, uh, in the basics, obviously applicative, monad functor, um, I don't have the whole shebang in front of me, but overall it's not overly complicated. It's yeah, but it's a, it's a reader T How about you?
>> So this reader T. Oh, I'll answer your question in a second, but I got some for you. Your reader t is it wrapped around IO or is it wrapped around something else? Uh, you know, what's the base monad for the reader T
>> yeah, we originally, I was kind of leaving that to, up to the we're we're allowing that to be polymorphic, but more recently we've been more just IO because there's not really a lot that we're doing. It has to be a different type. Um, we don't have to be in a different monad to execute that. So, uh, yeah, IO's really the, the core monad underneath.
>> Okay. And then when you write functions that need to, or that ultimately get executed in this environment, do you write them with concrete constraints that say this particular reader T with this environment in this base monad or do you say something like. Has database access as a constraint on the whole thing. And then that's just a polymorphic M
>> um, we've right now, since our services are so small and we're kind of trying to make sure we can like easily explain what's going on in our Haskell code to other others in the organization who don't know Haskell yet we are being pretty explicit in that. So, you know, most monadic functions are our app. Um, that's our monad name. It's fine. It's weird for me because I know where I'm, I'm used to what we're doing, ACI, where we need to have something fun for our monoid name, you know, something different, so, uh, it, it's not bad. It's just different for me, but it's, um, I totally lost my train of thought there, but, uh, w what was your question again, on that piece?
>> You were talking about, um, constraints versus concrete type signatures.
>> Yeah. So we're concrete.
>> Yeah. Nice. Uh, so you were asking me about what we do at ACI. Obviously you kind of know already, so this is more for the benefit of the listener. Uh, for context, uh, we currently have essentially one monad stack. We have multiple executables that we end up delivering, but they all use the same on ad stack. Now that didn't used to be the case. And it's really similar to what you're describing, where we have a new type wrapper that has a reader T inside of it. And then that's wrapped around. Um, usually IO, but sometimes it's wrapped around servant handler because we use the same monad stack for both our servant handlers and for non web related stuff, stuff that runs like on our job queue or locally, if we need to run a script or something like that, they all use the same. Monoid with a different choice of an inner monad. And as a result, many of our T uh, Yeah, type signatures for functions are polymorphic over at least that inner, um, monad choice. Usually it will say you can sub in whatever M you want here, but it has to be monoid IO or something like that. Right. And what we've been trying to do recently is move more towards. A capability as a constraint, um, which is usually in the community called MTL style after the library that implements all these things where instead of saying, you have to be this specific monoid, uh, rather we're trying to say you can be any monad that has, let's say, access to Postgres, our database or access to Recurly our payment provider. And these are very coarse-grained, uh, Constraints. We're not saying, you know, read access to these things or write. Access or a particular model within the table or anything like that. Just can talk to the database is a big
>> brush. Yeah. W D is that a type class for you guys or is that just a conglomerate of, uh, other, you know, derived instances that are just kind of tight to this one?
>> It's actually a bit of both. So we have defined our own custom type classes to represent new constraints that we want to abstract over in the system. For instance, I mentioned Recurly our payment processor. That's one where the capability to communicate with the Recurly API is. Behind this constraint that we create a new type class. Uh, but like I mentioned earlier, we're moving stuff over to persistent and persistent provides a lot of type classes for doing this stuff already. They have one for reading or performing database queries that are read only, or read-write. So we're trying to use those when we can and, um, Our motivation for doing this change was that we're trying also to write more tests and it can be convenient to have a function that is polymorphic over these things, not necessarily so that you can pick a different implementation. Like we try to use a real Postgres database in. For example, but we don't want to talk to Recurly actually in our test suite, because maybe you don't have an internet connection when you're running your tests or maybe you just don't want to hit Recurly 30 times over and over again, while you're running the test suite trying to fix something else. So having it be polymorphic, lets you sub those things out. Or even if you are using the real thing, like with the Postgres database, having it be polymorphic makes it so that you don't have. Um, set up the rest of the config to fulfill that environment that you may not care about. So it's just a little bit easier. Nice. Yeah.
>> I, uh, just this week, actually I was pulling out a, we use message DB, which is kind of an event sourcing, um, I guess set up for Postgres. So they have kind of their own sets of functions and things like that that you can use. And, um, and so we're using that and we have it in all of our applications. And so I was really getting to the. These are this, none of this is really that different. So I've actually pulled it out into a library that I'm hoping maybe open source here soon. Um, that is, you know, I've currently namespace it behind like database Postgres message GB. So it's clear. Okay. You, it's got to really be the Postgres implementation of message. GB. Right. And then, um, I created a, for all the functions within that, um, instead of binding it to a specific monad. You know, uh, message DB or something like that. I opted to go with the type class route and say like, yes, if it has this type class, then it can work. So now each of our app monads in our services can just derive their own instance for getting the configuration and getting the connection and pool and those kinds of things. Uh, and that way they're separated out. So I felt, I thought that was a pretty good win this week. So that's, uh, it just sounds very familiar to what you were just speaking of.
>> Yeah. I think there's a lot of commonality between these things. Um, but for you, if you were to give advice to somebody who, you know, drew asking this question, how should I structure an application? What would be your answer?
>> I mean, I hate this, but it depends. So I know that's not a good answer, but that's the honest answer here because. To me. I think it depends on first of all, how large your application is, what the purpose of your application is. And really overlarge like overarching, like direction. Your company's trying to go. So for us, we're going with a microservice architecture and we really care about just being consistent because you have a lot of microservices that are really not all that different, but you don't want to have. Five different styles of code. And so right now we're, we're creating, you know, concrete, um, dark, concrete, monad selection and things like that. So we're not abstracting any of that away because we're not really concerned. Like we do have some Servant stuff, but we're actually running that in IO rather than in the servant handler. And so we don't have to really worry about handling two different monad, you know, 200 Lyme on ads. Our command handle that did. And that's what I kind of like, which was our first service. And I was using, I was really using my knowledge base from what were you doing? ACI. And, but through some of the work we've done and you kind of simplifying it, we realized, okay. I O is really the only thing we need to be in. Um, so for me, I would say, keep it as simple as you can. And, um, I mean, I definitely did like the parameterized monoid, you know, rather. Saying, it's always this one, but, uh, with these small services, it makes sense with larger services, I would definitely be on the boat of, yeah, parameterizing it. And, you know, being dynamic or polymorphic in that way. How about you?
>> I fully agree with that. I think it definitely depends. But as a general rule, my suggestion is going to be stick with reader T until you start to feel the pain of that. And you may even be able to address some of the pain by using a library that gives you some niceties with this approach. So like the Rio library or Rio, I can never remember which way it's meant to be pronounced that library. Um, codifies this reader T. Idea and provides you a lot of common helpers with it. So it can be really useful to use that rather than slowly building up all those things yourself. Um, yeah, go ahead.
>> Oh, sorry. Yeah. I was just going to say, um, in aggreance there reader T until you can't, um, and then also like deriving monad reader in that reader T so you can just kind of call asks whenever it's pretty. It's pretty nice. It's convenient. I
>> would say. Yes. Make liberal use of the deriving mechanism to get all the hand handy instances that you need. Um, I was going to mention that, like I said, we have been moving a little bit toward using more MTL style constraints on our functions, and that's a natural extension of this application architecture, where you have something concrete and you want to make it polymorphic. And this is the way that you. But the kind of other approach in the community is free monads. And I haven't talked much about those because I haven't used them in anger. I am aware of them and I know kind of what they do, but I haven't used them enough to really feel confident, having an opinion about them. But it seems to me like taking an application that's written as a reader T um, app monad. That could be a challenge to pull that into the world of Fremont ads. And maybe that's not true again, I don't really have experience doing this, but that's why I'm not talking much about this at least. So Fremont ads could be good. Could be bad. I really don't know.
>> Yeah. And I also am in that boat of not really, I mean, I've read some posts on free monads and got the general idea. Never have put it into practice in production, so I can't speak to it. Um, but I do say, you know, if the monads just want to be free, let them be free. You know, like that's all I got to say about that one. I know that was obviously terrible. Not the answer to free monads. So, uh, tell me that I'm wrong on the internet. It's okay.
>> And then the last note I have on this is that both you and I are coming at this from the viewpoint. Generally speaking a web application and we have associated services that run like a job queue, or like I mentioned at the top of the show, someone off scripts or something like that. But typically the apps you and I have built are web apps. So if you were not building a web app, this advice may not apply quite as much. Um, and really the only other thing that I have a big amount of experience building in Haskell is a command line application where. It is something that takes in an input file and processes it and spits out an output file, which describes many, many things. Uh, it's a rocket league replay parser, but the details aren't super important. And the way I've structured, that application is not using this reader T concept. And instead, what I do is the, uh, Pure core imperative, shell architecture that Gary Bernhardt talks about that isn't specific to Haskell. And what that means is I do all of the IO at the boundaries of my system and I say, okay, I, you know, read my input, file, read the config, figure out what I'm going to do, set everything up and then hand it off to a pure thing that computes the answer, or, you know, does the computation, the analysis, whatever. And then produces a pure value as its output. And then again, hand it hand that back off to the IO world and say, okay, pretty print this, or print it out to a file or do whatever it is you need to do. You know, I'm done with my part. And I really like structuring that thing, structuring things that way, because it lets me test things much easier so that I can assume the IO part is pretty small and it'll probably do what it's supposed to do and I can test the pure part. in the middle. And obviously there are many different types of applications. These are just the two that I've worked on most commonly. So that's what my advice is going to be biased toward.
>> Yeah, I don't, yeah. I mean, um, web application all the way at this point. So maybe, maybe one day I'll get into the realm of a CLI with Haskell or something along those lines, but, uh, yeah, I'm learning all about tiny micro web services. Um, aggregators and event sourcing and CQRS and all kinds of fun jazz. And it's been actually really cool to see and understand.
>> So, yeah, there's a lot of great variety in web apps and lots of fun problems they can solve. I just wanted to acknowledge that there are other types of apps and our advice may not apply too much to them. So if you're working on, you know, a real time trading system or something like. You know, maybe use reader T, maybe it works or maybe do something completely different. I don't know.
>> Yeah. Yeah. And if anyone from this has more specific questions that they'd like us to dive in and research on. We're also happy to do that. So,
>> yeah. And this was our first reader question, so thank you so much drew for sending this in over slack. If you're listening to this show and you've got something you'd like to hear us talk about, please reach out. We can be found all the usual places, um, Slack, Twitter, carrier pigeon, whatever you want to do.
>> Nice. And, uh, yeah, I was just gonna, uh, thank you. Thank all of our listeners. And you know, if, uh, do you want to do the part about your company or should I do the part about your company?
>> I'll do the part about my company. All right. Um,
>> should I tell them where to find us, you didn't tell them HaskellWeekly.News. So if you want to check out this week's, News that's out there. Go to HaskellWeekly.News, and you can find latest podcasts as well as the latest newsletters and yeah. Stay in the know with the Haskell world, yo, over to you Taylor.
>> Sign up for that newsletter. And this week, like every week we are brought to you by my employer, ACI Learning, uh, specifically ITProTV. The learning platform for IT professionals. If you'd like to get 30% off the lifetime of your subscription, head over to ITPro.TV and put in offer code HaskellWeekly30 at checkout that's HaskellWeekly30 at ITPro.TV.
>> Nice. Uh, yeah. And another quick announcement before we break off here is that you have a couple more days, I guess, by the time this is published, you won't have any more time to do the Haskell survey. So you may just want to cut this bit out.
>> If you're listening to this, when it's published, fill out the Haskell survey, it may be open for another few hours and if it's after, then look for the results soon.
>> Boom. Awesome. Well, thank you guys for listening. We'll see you guys next week. Bye.