Skip to content

Instantly share code, notes, and snippets.

@graninas
Last active April 25, 2024 20:49
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save graninas/1b7961ccaedf7b5cb92417a1599fdc99 to your computer and use it in GitHub Desktop.
Save graninas/1b7961ccaedf7b5cb92417a1599fdc99 to your computer and use it in GitHub Desktop.
Haskell Approaches Comparison Table

An Opinionated Comparison of Software Design Approaches in Haskell

Raw IO Free Monads; Church Encoded Free Monads Final Tagless / mtl Effect Systems ReaderT Service Handle
Core Principle Bare IO monad, imperative code Interpretable, instrospectable, custom pure monads Effects are explicit in BL; a list of constraints Effects are explicit in BL; either a list of constraints, or a list of types Interface to subsystems is described as a control structure and placed to ReaderT environment A Handle structure as an interface to subsystems
- Interfaces None ADTs Type classes; Type classes + type families Different; ADTs; type classes; type families Monadic methods placed into ReaderT env (usually IO methods, but not necessary) Monadic methods placed into a structure (usually IO methods, but not necessary)
- Business Logic Impure monadic Pure monadic Impure monadic; effects are constraints Depends; pure or impure monadic BL in a custom monadic stack with ReaderT Any kind of BL; service handle can be passed either as a parameter or as a ReaderT env
- Implementation Mixed with BL Interpreters Type class instances Different; interpreters; type class instances; type family instances Implementation is filling the control structure Implementation is filling the Handle structure
Separation of Concerns None 5/5 3/5 Depends; 3-5/5 3/5 3/5
Layering - Good, essential, unavoidable Weak; Implementation details tend to leak Depends; 3-5/5; Implementation details tend to leak; Lists of effects prevent dividing a BL into own layers 3/5 3/5
Mechanisms Simplicity 3/5 5/5 1-4/5 1/5 5/5 5/5
Complexity Reduction 1/5 5/5 3/5 2/5 3/5 3/5
Robustness 2/5; No restrictions on effects 5/5 4-5/5; Sometimes IO is allowed 5/5 3/5; No restriction on effects 3/5; No restriction on effects
Maintainability 1/5 5/5 3/5 2/5 3/5; Details are in the BL 3/5; Details are in the BL
Testability 1/5 5/5 2/5 Depends; 3-5/5 Depends; 3-4/5 Depends; 3-4/5
Extensibility 3/5 4/5 4/5 3/5 3/5
- Effects 5/5 2/5 5/5 3/5 3/5 3/5
- Domain Features 4/5 3/5 3/5 5/5 5/5
Rare design cases Will be a mess Approachable, relatively simple Hard, mind blowing, complicated Hard, mind blowing, sometimes extremely complicated 2-3/5; Approachable but often complicated 2-3/5; Approachable but often complicated
Expressiveness Not limited; not convenient Very expressive; Different semantics and syntax is possible; Very limited syntax; limited semantics Very limited syntax; limited semantics 4/5; Limited syntax; limited semantics 4/5; Limited syntax; limited semantics
Boilerplate 3/5 3/5 3/5 3/5 3/5
- Interfaces None 3/5 5/5 Depends; 3-5/5 3/5 3/5
- Business Logic 1/5 5/5 2/5 2/5 4/5 2/5
- Implementation 1/5 4/5 4/5 3/5 4/5 4/5
Error Handling When you're lucky Simple; Error domains Complicated Depends; usually complicated Approachable Approachable
Problems & Limitations Bare IO; Lazy IO; Bad testability; Bad separation of concerns; Bad in general FM: O(n^2) monadic binding; No exceptions in BL All layers are mixed; IO is not separated; No real separation of concerns; Bad testability; Very complicated error handling; Implementation details tend to leak into BL; On rare special cases, design becomes very hard; Too many special hacks for incorporating of some external libraries Explicit type lists of effects are really hard to maintain; A lot of difficult high-level concepts involved; Rigidness and inconvenience to use; Overall overengineering; Often a bad testability; Very complicated error handling; Implementation details tend to leak into BL; On rare special cases, design becomes very hard; Too many special hacks for incorporating of some external libraries All the layers are mixed (usually); Effects are not limited; Too hard to hide implementation details; Hard to maintain; Rare design cases can be hard to implement; All the layers are mixed (usually); Too hard to hide implementation details; Hard to maintain; Rare design cases can be hard to implement;
Performance 4~5/5 FM: 2/5; CEFM: 5/5 5/5 Depends; 4-5/5 5/5 5/5
Docs & Showcases
Market Share
Overall Bad approach; suitable for small apps; Not suitable for big real-world apps Very powerful. Best testability. Best expressiveness. Best complexity reduction. Best layering. Introspection is very useful. An approach for lazy developer. Doesn't satisfy the requirements. Suitable when there is no time for design. Not really suitable for big codebases. An approach for those who wants to control everything in the BL. Lists of effects prevent dividing a BL into own layers. Relatively simple. Can be used for small and middle apps. Code tends to become polluted by implementation details. No explicit guarantees Relatively simple.Can be used for small and middle apps.Code tends to become polluted by implementation details.No explicit guarantees
@goertzenator
Copy link

What is "BL"?

@graninas
Copy link
Author

What is "BL"?

@goertzenator, BL == Business Logic.

Yes, I need to create a legend for this table. Will do when I'm free.

@edwinhere
Copy link

FM == Free Monads
CEFM == Church Encoded Free Monads

@dzhus
Copy link

dzhus commented May 1, 2020

I love the empty market share row

@alaendle
Copy link

Maybe its a little off topic, but I really wonder how the different approaches behave in a multi-threaded environment. Especially, if I want to abstract a asynchronous callback. E.g. consider an effect like

data Broker m a where
  Subscribe :: Int -> (String -> m Bool) -> Broker m ()

I'm really struggling to implement a meaningful interpretation with polysemy (polysemy-research/polysemy#373); so is my approach simply wrong? As mentioned in the issue I started from a simple ReaderT-pattern and everything worked just fine; the problems started while I tried to replace ReaderT by an effect system (polysemy) in order to remove a lot of boilerplate code. So I ask myself, if this is a limitation of all effect systems (which seems to be reasonable because effects may maintain state and this might always be a problem in multithreaded environments)? But what about the other approaches? Should I use one of them? Or reconsider my design? And please excuse if I hijack this gist to ask these questions - but I really find it hard to find well-founded statements to these topics.

@graninas
Copy link
Author

Hey @alaendle, thank you for asking this! Please give me a short time, and I'll answer you. I have a lot info about this!

@graninas
Copy link
Author

@alaendle, so it seems you've asked about several different things. I see these questions in your message:

  1. How do different approaches behave in a multi-threaded environment?
  2. How the async code can be organized in those approaches?
  3. Is there any inherent limitation of all effect systems?
  4. Should you use an effect system for your tasks?

Let me start from the last question.

4. Should you use an effect system for your tasks?

Although I'm in the opposition to the effect systems like polysemy or fused-effects (or tons of them, really), and although I prefer my own approach of Hierarchical Free Monads, I'd say that you are free to use any approach you want. This is because you are the only person who is responsible for your choices. But I'd argue that effect systems is a huge mistake from a design standpoint. It's probably okay to use them for small apps, but once you plan something really big, working on such a codebase becomes a nightmare. I had this experience with the freer library. The main problem here is that you have to fight with the effect system library instead of working on your code. Tracking effects in for or a type level list of effects or a set of constraints makes your code really hard to work with. It's a huge obstacle for refactoring because the annotations of your functions tend to be broken each time you do something small. As a result you're getting a big unreadable compile error about some complex stuff from the type level, and you have to fix it which distracts you from working on the business logic. So I agree with John De Goes that effect tracking is commercially worthless. You can also get familiar with my other resources in which I'm expressing my opinion more detailed. Hierarchical Free Monads: The Most Developed Approach in Haskell, my talk Hierarchical Free Monads & Software Design in Functional Programming, my bookFunctional Design and Architecture, and others.

1. How do different approaches behave in a multi-threaded environment?
I really like this question. I can say that Free monads are very good for everything. I personally built many concurrent applications (even frameworks) using Free monads. I think the most interesting for you will be the Hydra framework, which is a showcase project for my book, but the main idea is to provide a playground for comparing different approaches. Hydra has 3 engines (more or less developed): Free monad based, Church-encoded Free monad based, and Final Tagless. You can find several sample applications with a concurrent state there: labyrinth, astro, MeteorCounter. All of them use an STM-like concurrency that is provided by the underlying framework.
Also, I could recommend you to get familiar with a commercial technology I created on top of Free monads: the Node framework, which is exactly about multi-threaded and massively concurrent applications. There are sample apps in it, very complicated ones.
And the simplest way to get into the theme of concurrency and multi-threading with Free monads will be my recent talk from the Haskell Love conference: Concurrent applications with Free Monads and STM

2. How the async code can be organized in those approaches?

This is amazing how in time you are! I’m preparing a talk for Lambda Conf. The main theme will be - how to create an asynchronous environment using Free monads. And I already researched this theme. There is a showcase project with the design allowing for sync and async calculations: zio-free.

3. Is there any inherent limitation of all effect systems?

I’d say that I’m an expert in Free monadic solutions. I have a chapter on multi-threading and concurrency in my book. And I worked on the different multi-threaded apps, including commercial ones (for example, at Juspay). All those apps are based on my approach “Hierarchical Free Monads”. So you can freely ask me about this.
But I don’t have all the info effects systems. Maybe there are limitations. Maybe it’s that the effect system libraries are not investigated in the real environment enough. So it would be really nice if someone researched this topic. I’m providing some shallow comparison to other approaches, and my current feel is that the effect systems don’t have any specific design-space limitations, but they bring too much complexity into the work.

Please, let me know what you think about this stuff.

@alaendle
Copy link

@graninas, first 👍 and ❤️ .

Thank you so much for theses detailed descriptions.

So to really tell you what I think I first have to gain some more experience - especially with the free monad stuff. As a Haskell beginner, I only have experimented final tagless, readerT, raw IO and now effect systems.

Maybe its because I'm having a OOP background - but the effect systems stuffs seemed very compelling to me (at least at a first glance); I think because of its principal simplicity - when you consider the interpreters as a natural transformation (step) from your DSL to the actual program. I also think ReaderT just reminded me to simple dependency injection and therefore just felt wrong (because it couldn't really hide the "lower aspects" - and needs lot of boilerplate code). Also the limited (read: hard) composability of mtl stuff doesn't make it as a concept useful to me.

So maybe I just have to try to realize/refactor my project using Free Monads - to really get a feeling for that approach. But first I have to read trough the referenced posts and watch some videos, so that I become aware of what I am getting myself into.

I will post an update once I made some progress (in whatever direction). And please don't forget that these are questions and feedback from a beginner - so please perceive my statements against my limited Haskell/FP experience.

And once again many thanks for your effort to answer my questions in such detail.

@graninas
Copy link
Author

graninas commented Aug 11, 2020

@alaendle, sure, you're welcome!

Just to note: some effect systems have Free monads under the hood. The very fact of this gives you the interpretation possibility. All my Free monadic solutions are interpretable in the same sense (yes, the natural transformation). The difference is how we track the effects. The effect systems ask you to compose effects as type level list of types, but I don't like this very much. For me it goes out of control quickly. My approach is to nest the Free monadic languages one into another.

I find your experience with ReaderT and mtl very interesting.

And please don't forget that these are questions and feedback from a beginner

Sure. I think many of my materials are pretty much advanced though. Don't hesitate to ask questions :)

@alaendle
Copy link

alaendle commented Feb 1, 2024

@graninas, just wanted to let you know that I just pre-ordered the second edition of your book to continue my journey to sound haskell application architecture 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment