What Is Good About Haskell?(doisinkidney.com)
doisinkidney.com
What Is Good About Haskell?
https://doisinkidney.com/posts/2019-10-02-what-is-good-about-haskell.html
421 comments
I've been using Haskell for quite a bit in production. My personal take, as an engineer who is generally skeptical of fancy language features:
Plus:
- The type system. It can make your life a huge pain, but in 99% of the cases, if the code compiles, it works. I find writing tests in Haskell somewhat pointless - the only places where it still has value is in gnarly business logic. But the vast majority of production code is just gluing stuff together
- Building DSLs is extremely quick and efficient. This makes it easy to define the business problem as a language and work with that. If you get it right, the code will be WAY more readable than most other languages, and safer as well
- It's pretty efficient
Minus
- The tooling is extremely bad. Compile times are horrendous. Don't even get me started on Stack/Cabal or whatever the new hotness might be
- Sometimes people get overly excited about avoiding do notation and the code looks very messy as a result
- There are so many ways of doing something that a lot of the time it becomes unclear how the code should look. But this true in a lot of languages
Plus:
- The type system. It can make your life a huge pain, but in 99% of the cases, if the code compiles, it works. I find writing tests in Haskell somewhat pointless - the only places where it still has value is in gnarly business logic. But the vast majority of production code is just gluing stuff together
- Building DSLs is extremely quick and efficient. This makes it easy to define the business problem as a language and work with that. If you get it right, the code will be WAY more readable than most other languages, and safer as well
- It's pretty efficient
Minus
- The tooling is extremely bad. Compile times are horrendous. Don't even get me started on Stack/Cabal or whatever the new hotness might be
- Sometimes people get overly excited about avoiding do notation and the code looks very messy as a result
- There are so many ways of doing something that a lot of the time it becomes unclear how the code should look. But this true in a lot of languages
This is a good discussion of a problem that suits Haskell well, but it's unfair to Python in some respects.
For example: I haven't read or written a lot of Python in a while, but would Python programmers really want to implement mutation in such a class by copying dicts around? The hand-wringing about "oh no, I wrote the condition as not is_node" is silly since one could just define an is_leaf method that can be used without negation. And "changing heapToList to return a lazy sequence makes it no longer return a list, oh no!" is just as silly, since one would of course not do that but define a separate heapToSequence (and probably base heapToList on that).
Also: "pattern matching and algebraic data types which have massive, clear benefits and few (if any) downsides". They have downsides whenever your data is not tree-shaped. Yes, a lot of data is tree, but then a lot isn't. I work in compilers, which is often touted as a prime application of ML-family languages, and this is very true, but not 100%. If you can have sharing in your abstract syntax tree (like a goto node that might refer to a target node somewhere else in the tree), you start to have difficulties. And you get even more difficulties when you try to model control flow graphs and the like. Nothing insurmountable, but still things where it's suddenly good to have other tools than only algebraic datatypes. OCaml is good in this regard.
For example: I haven't read or written a lot of Python in a while, but would Python programmers really want to implement mutation in such a class by copying dicts around? The hand-wringing about "oh no, I wrote the condition as not is_node" is silly since one could just define an is_leaf method that can be used without negation. And "changing heapToList to return a lazy sequence makes it no longer return a list, oh no!" is just as silly, since one would of course not do that but define a separate heapToSequence (and probably base heapToList on that).
Also: "pattern matching and algebraic data types which have massive, clear benefits and few (if any) downsides". They have downsides whenever your data is not tree-shaped. Yes, a lot of data is tree, but then a lot isn't. I work in compilers, which is often touted as a prime application of ML-family languages, and this is very true, but not 100%. If you can have sharing in your abstract syntax tree (like a goto node that might refer to a target node somewhere else in the tree), you start to have difficulties. And you get even more difficulties when you try to model control flow graphs and the like. Nothing insurmountable, but still things where it's suddenly good to have other tools than only algebraic datatypes. OCaml is good in this regard.
Haskell is a beautiful research language. Its usecase is to supply subjects for PhDs and MScs, which it fulfills perfectly. Also it's extremely fun to learn and play with.
I would never bring it to the production though, reasons being:
1) Production code should be understandable by an on-call person at 4 am. If business logic is buried under layers of lenses, monad transformers and arrows, good luck troubleshooting it under stress. And real systems do break, no matter type safety.
2) It's a research language, and a big part of the research is about control flow. And therefore haskell has _way too many_ ways to combine things: monad transformers of different flavors, applicative functors, arrows, iteratees, you name it. And libraries you find on hackage choose _different_ ways to combine things. In the business code you probably want to combine multiple libraries, and you inevitably end up with unholy mess of all these approaches. Dealing with it takes more time than writing business logic.
3) Developers look at these fancy research papers and try to reproduce them. As a result, very basic things become extremely hard and brittle. I saw a real-live example when applying a transform to all fields of a record took a team 2 days of discussion in slack, because "writing this manually?! it won't scale to record with 100 fields".
4) Architecture is extremely sensitive to the initial choices due to isolation of side effects. Because if you suddenly need to do something as simple as logging or reading a config in a place where it wasn't originally anticipated, you're in for a bumpy ride.
I would never bring it to the production though, reasons being:
1) Production code should be understandable by an on-call person at 4 am. If business logic is buried under layers of lenses, monad transformers and arrows, good luck troubleshooting it under stress. And real systems do break, no matter type safety.
2) It's a research language, and a big part of the research is about control flow. And therefore haskell has _way too many_ ways to combine things: monad transformers of different flavors, applicative functors, arrows, iteratees, you name it. And libraries you find on hackage choose _different_ ways to combine things. In the business code you probably want to combine multiple libraries, and you inevitably end up with unholy mess of all these approaches. Dealing with it takes more time than writing business logic.
3) Developers look at these fancy research papers and try to reproduce them. As a result, very basic things become extremely hard and brittle. I saw a real-live example when applying a transform to all fields of a record took a team 2 days of discussion in slack, because "writing this manually?! it won't scale to record with 100 fields".
4) Architecture is extremely sensitive to the initial choices due to isolation of side effects. Because if you suddenly need to do something as simple as logging or reading a config in a place where it wasn't originally anticipated, you're in for a bumpy ride.
I've worked at multiple Haskell shops. They've all had problems. None of the problems were actually attributable to Haskell..but Haskell was an easy target for blame by management. I'd say that's Haskell's biggest problem in a professional setting.
I know Haskell and Go about equally. Meaning I know all the language features & common libs & have a grip on their runtimes. Go on day 1 was way easier to write and understand than Haskell on day 1. Now that I've normalized their learning curves, Go didn't get much easier to work with. Haskell did - I use my brain way less when writing Haskell than when writing Go.
But even then, Haskell or Go. It's all just the same stuff.
I've pretty much given up talking on the Internet to people about Haskell. Arguing against some of the pts in this thread about how Haskell isn't good for production.
I'll continue to write Haskell for pay for the foreseeable future. If I'm lucky, I'll do it the rest of my career. I don't see any reason why not.
I know Haskell and Go about equally. Meaning I know all the language features & common libs & have a grip on their runtimes. Go on day 1 was way easier to write and understand than Haskell on day 1. Now that I've normalized their learning curves, Go didn't get much easier to work with. Haskell did - I use my brain way less when writing Haskell than when writing Go.
But even then, Haskell or Go. It's all just the same stuff.
I've pretty much given up talking on the Internet to people about Haskell. Arguing against some of the pts in this thread about how Haskell isn't good for production.
I'll continue to write Haskell for pay for the foreseeable future. If I'm lucky, I'll do it the rest of my career. I don't see any reason why not.
Python may be terrible, but this poster doesn't know Python at all. Here's how to fix the first snippet:
Edit: I kept reading and was immediately sick. __dict__ abuse is a real problem in our society, folks. It's not okay.
Haskell's an alright language. Python's an alright language. They're about the same age. If one is going to write good Haskell, one might as well write good Python, too.
[0] https://hypothesis.works/
leaf = object()
Huh, that's funny, it's shorter than the Haskell? Why is that? Let's keep going. def merge(lhs, rhs):
if lhs is leaf: return rhs
if rhs is leaf: return lhs
if lhs[0] <= rhs[0]: return lhs[0], merge(rhs, lhs[2]), lhs[1]
return rhs[0], merge(lhs, rhs[2]), rhs[1]
Ugh. My mouth tastes funny. Livecoding on this site is always disorienting. I need to sit down for a bit. Exercise for the reader: Continue on in this style and figure out whether the Haskell really deserves its reputation for terseness and directness.Edit: I kept reading and was immediately sick. __dict__ abuse is a real problem in our society, folks. It's not okay.
def insert(tree, elt): return merge(tree, (elt, leaf, leaf))
The Haskell memes are growing stronger as I delve deeper into this jungle. The pop-minimum function here is a true abomination, breaking all SOLID principles at once. I can only imagine what it might look like in a less eldritch setting: def popMin(tree):
if tree is leaf: raise IndexError("popMin from leaf")
return tree[0], merge(tree[1], tree[2])
We continue to clean up the monstrous camp. def listToHeap(elts):
rv = leaf
for elt in elts:
rv = insert(rv, elt)
return rv
The monster...they knew! They could have done something better and chose not to. They left notes suggesting an alternative implementation: def listToHeap(elts): return reduce(insert, elts, leaf)
Similarly, if we look before we leap: def heapToList(tree):
rv = []
while tree is not leaf:
datum, tree = popMin(tree)
rv.append(datum)
return rv
And again, the monster left plans, using one of the forbidden tools. We will shun the forbidden tools even here and now. We will instead remind folks that Hypothesis [0] is a thing.Haskell's an alright language. Python's an alright language. They're about the same age. If one is going to write good Haskell, one might as well write good Python, too.
[0] https://hypothesis.works/
I got curious about Haskell some years ago because of a complete career disappointed with other languages. Ive put out some semi big apps in this language, that have been working well in production. I code for fun and Haskell is the most fun. Sure, there has been a couple of problems with tooling and its hard to find best practices, but I seem to be able to live with. I get a little depressed when having to work with other languages. For me its the end game.
I think python was a poor comparison here. The claim "python doesn't have algebraic data types" doesn't really make sense. Algebraic data types let something be an X or a Y. In python, everything can be an X, or a Y, or a Z, or anything else. In the tree example, an ideomatic tree structure in python would just be a 3-tuple (left, right, value). There's no need to define anything.
Don't get me wrong, I'm all for algebraic data types and think every statically typed language should have them, but it's nonsensical to talk about them in the context of a dynamically typed language.
The author commented elsewhere on this page that the article was mostly a response to missing these features in golang - I think that would've been a much clearer comparison that showed what you were really missing.
Don't get me wrong, I'm all for algebraic data types and think every statically typed language should have them, but it's nonsensical to talk about them in the context of a dynamically typed language.
The author commented elsewhere on this page that the article was mostly a response to missing these features in golang - I think that would've been a much clearer comparison that showed what you were really missing.
One thing that makes programming difficult is how much context needs to be held in the live part of the brain. Past N items (let's say 4), the brain has to swap those values and it makes programming much much slower.
Most of the article is spent explaining this sideways; with Haskell you need to hold less aspects in the head because they are either eliminated entirely (purity) or can be deferred to the compiler (types), ... What we learn is that Haskell is easier to implement tree-like structures.
I think this is what most language proselytes are trying to convey in their articles. But don't talk about explicitly. Inevitably they will pick a task that is easy to achieve in the language because the developer environment aligns well for that use-case, and then let the reader infer that this applies to all programming tasks.
Basically we are trying to benchmark humans, without building a proper model of how humans work. The next evolution in programming will be done by properly understanding how we work and interact with the computer.
Most of the article is spent explaining this sideways; with Haskell you need to hold less aspects in the head because they are either eliminated entirely (purity) or can be deferred to the compiler (types), ... What we learn is that Haskell is easier to implement tree-like structures.
I think this is what most language proselytes are trying to convey in their articles. But don't talk about explicitly. Inevitably they will pick a task that is easy to achieve in the language because the developer environment aligns well for that use-case, and then let the reader infer that this applies to all programming tasks.
Basically we are trying to benchmark humans, without building a proper model of how humans work. The next evolution in programming will be done by properly understanding how we work and interact with the computer.
My Haskell isn't good enough to translate, but I'd love to see these examples in Rust. I believe Rust has all of the Haskell features mentioned in this article, but with a much more familiar syntax.
The tree data type in Rust could be:
The tree data type in Rust could be:
enum Tree<A> {
Leaf(A),
Node(A, Box<Tree<A>>, Box<Tree<A>>),
}Wait, why do the leaves contain no data? What's the point of even adding them then?
With data in the leaves you could easily do something like:
The pattern matching will need to be done manually though, following python's philosophy of duck-typing.
Edit: An alternative involves abusing the pattern matching that python does have to write things like:
With data in the leaves you could easily do something like:
@dataclass
def Node:
data: "Any"
@dataclass
def Tree(Node):
left: Node
right: Node
using the new dataclasses module. You could also add a type parameter to the above if you really wanted to.The pattern matching will need to be done manually though, following python's philosophy of duck-typing.
Edit: An alternative involves abusing the pattern matching that python does have to write things like:
data,*subnodes = myTree
for node in subnodes:
# etc
but whether that's really a good idea is debatable.To play the devil's advocate here: no post speaks about any serious disadvantages of Haskell.
So, for a programming language that has so much to offer, why isn't it adopted more?
Could it be that it doesn't actually increase productivity to such a degree to justify the cost of change?
Legit question, I am not trying to be a troll.
So, for a programming language that has so much to offer, why isn't it adopted more?
Could it be that it doesn't actually increase productivity to such a degree to justify the cost of change?
Legit question, I am not trying to be a troll.
It’s hilarious to read all kinds of rationalizations for why haskell is not useful.
Servant + Aeson beats any api backend in any language, period. If you combine that with Elm on the frontend you’ve got a great onboarding path for new devs to learn enough haskell to work on the backend.
Of course, for production, ignore lenses, monad transformers, free monads, effect systems, etc. They’re awesome, but the complexity is not worth it in practice at this time.
Servant + Aeson beats any api backend in any language, period. If you combine that with Elm on the frontend you’ve got a great onboarding path for new devs to learn enough haskell to work on the backend.
Of course, for production, ignore lenses, monad transformers, free monads, effect systems, etc. They’re awesome, but the complexity is not worth it in practice at this time.
This article reminds of my coworker, touts Haskell all the time, ends up writing shitty Java code which is difficult to understand and performs like molasses.
Real world is not perfect, it is immutable with plenty of side effects. Using Haskell for day to day messy work is not trivial and should not be considered IMO, u nless you have Haskell gurus all around.
I would rather take a dumb language like Go or Java over Haskell for work code.
Real world is not perfect, it is immutable with plenty of side effects. Using Haskell for day to day messy work is not trivial and should not be considered IMO, u nless you have Haskell gurus all around.
I would rather take a dumb language like Go or Java over Haskell for work code.
offtopic: if anyone's interested -- Standard Chartered Poland is looking for Haskell hackers: https://twitter.com/MikolajKonarski/status/11782723158152192...
tldr: I am NEVER nervous about refactoring some Haskell code.
Good:
After working in a variety of organizations using, typed but also dynamic languages I'm now writing all my back-end code in Haskell. I'm becoming more and more convinced that for multi-year, multi-programmer applications (a language like) Haskell is the only way to make it sustainable, while still being able to add features.
Stephen Diehl has a great writeup on "what he wish he knew when he was learning haskell" http://dev.stephendiehl.com/hask/
It's difficult to say to someone "Just go read books for a couple of months because you need to understand purity, laziness, cross compilation, monad transformers (go read The Book of Monads), 20+ language pragmas. etc etc"
It does however feel like I'm learning useful stuff, and it's a lot of fun to get an executable that runs FAST.
Good:
After working in a variety of organizations using, typed but also dynamic languages I'm now writing all my back-end code in Haskell. I'm becoming more and more convinced that for multi-year, multi-programmer applications (a language like) Haskell is the only way to make it sustainable, while still being able to add features.
Stephen Diehl has a great writeup on "what he wish he knew when he was learning haskell" http://dev.stephendiehl.com/hask/
It's difficult to say to someone "Just go read books for a couple of months because you need to understand purity, laziness, cross compilation, monad transformers (go read The Book of Monads), 20+ language pragmas. etc etc"
It does however feel like I'm learning useful stuff, and it's a lot of fun to get an executable that runs FAST.
This is about as far as I got with this...
"While it solves the problem of methods, and the mutation problem, it has a serious bug. We can’t have None as an element in the tree!"
Uhm... what?! Why not? You check that your left and right are None and if they are it is a leaf. And if they are not it is not. What your data value is doesn't matter and you can have as many None values in the tree as you like. Your tree doesn't need leaves to define where it ends, it ends when there are no more branches.
"While it solves the problem of methods, and the mutation problem, it has a serious bug. We can’t have None as an element in the tree!"
Uhm... what?! Why not? You check that your left and right are None and if they are it is a leaf. And if they are not it is not. What your data value is doesn't matter and you can have as many None values in the tree as you like. Your tree doesn't need leaves to define where it ends, it ends when there are no more branches.
I think Haskell is conceptually cool, but I just can't stand its symbols and grammars. To me, "=>" means comparison and "|" means an OR, and I'm cringed to see $s and \s in a program code that makes it look like LaTeX. I also prefer the boundary of terms and expressions to be consistently marked up with parenthesis. I know this is just a matter of taste, but sometimes it affects your motivation a lot.
For those who were wondering about seeing this in Swift. You probably weren't, and I'm aware I could've leaned a bit harder on things that were built-in, but I was basically trying to meet in the middle between representing the Haskell code as written in the interview and something vaguely Swifty.
https://gitlab.com/snippets/1900852
https://gitlab.com/snippets/1900852
I think we can code ADT in Python: https://github.com/lion137/Functional---Python/blob/master/t...
Haskell seems cool, but if I’m going to invest time in a different programming paradigm, I’m more curious about APL/K/J. They seem more useful as well (to me).
I have to disagree with the thesis here in its entirety. ADTs and pattern matching are not what makes Haskell good. Every article like this one just serves to show people the non-compelling bits of Haskell. The parts you can use in every other language, if you want to.
The parts of Haskell that make it a good language aren't the things that you can just write a tutorial for. They're about software engineering, not code snippets.
Purity and immutability remove entire classes of bugs caused by spooky action at a distance. When you assert this, people claim "I don't have those bugs", forgetting about the time someone else changed a function they wrote to mutate one of its arguments, breaking code three steps up the call chain.
Parametric polymorphism documents what information a function cannot use within its definition. If you point this out, people ask what good that serves. There's no way to explain how much easier it is to get things done when you can write a function and know that no matter what values are passed in, there cannot be special cases that trip you up.
I see people try to explain why the `Maybe` type is better than null values, and have their explanations rejected with "You still have to check for it. All you're doing is changing the syntax of the check." I've seen variations on that theme in maybe 10 different HN threads over the last 6 years. When all you talk about is ADTs and pattern matching, why would anyone ever look at the bigger impact of the type system? The relevant detail here is that an `Integer` can never be null, not that you use `Maybe Integer` to talk about potentially missing values.
Further in that same direction, I see people say that the `IO` type just complicated things because your program has to do I/O anyway, so it always needs to be in `IO`. This is exactly the same as the `Maybe` problem, but that similarity is even further from being addressed by articles about pattern matching and ADTs. No, the similarity isn't "monads". Anyone who talks about them here has missed the point entirely. The point is that parametric polymorphism completely prevents distinguishing IO values from non-IO values, so code that's not written to work with IO values cannot do IO accidentally.
There are a lot more cases, especially when you get into more sophisticated things possible in the type system using ghc extensions like generalized algebraic data types or higher-rank types.
But all of the reasons you should be using Haskell in reality come down to practical large-scale software design concerns. The language lacks features that make several common classes of bugs possible. It makes several other common classes of bugs take a lot more work to implement than the non-buggy way to solve the same problem. These aren't things you can just write a short article about. They're things that require years of experience and introspection to see are even problems, and a willingness to accept that a lot of the problem is the ecosystem, not an individual failure to execute. None of that fits in an article.
I think articles about how great pattern matching and ADTs are make the language look worse, because anyone with some experience can say look at what's actually happening and say "I can do that in <other-language>, Haskell clearly doesn't have anything to offer." In other words - stop writing these articles. They drive people away from Haskell, not encourage them to look at the good parts.
The parts of Haskell that make it a good language aren't the things that you can just write a tutorial for. They're about software engineering, not code snippets.
Purity and immutability remove entire classes of bugs caused by spooky action at a distance. When you assert this, people claim "I don't have those bugs", forgetting about the time someone else changed a function they wrote to mutate one of its arguments, breaking code three steps up the call chain.
Parametric polymorphism documents what information a function cannot use within its definition. If you point this out, people ask what good that serves. There's no way to explain how much easier it is to get things done when you can write a function and know that no matter what values are passed in, there cannot be special cases that trip you up.
I see people try to explain why the `Maybe` type is better than null values, and have their explanations rejected with "You still have to check for it. All you're doing is changing the syntax of the check." I've seen variations on that theme in maybe 10 different HN threads over the last 6 years. When all you talk about is ADTs and pattern matching, why would anyone ever look at the bigger impact of the type system? The relevant detail here is that an `Integer` can never be null, not that you use `Maybe Integer` to talk about potentially missing values.
Further in that same direction, I see people say that the `IO` type just complicated things because your program has to do I/O anyway, so it always needs to be in `IO`. This is exactly the same as the `Maybe` problem, but that similarity is even further from being addressed by articles about pattern matching and ADTs. No, the similarity isn't "monads". Anyone who talks about them here has missed the point entirely. The point is that parametric polymorphism completely prevents distinguishing IO values from non-IO values, so code that's not written to work with IO values cannot do IO accidentally.
There are a lot more cases, especially when you get into more sophisticated things possible in the type system using ghc extensions like generalized algebraic data types or higher-rank types.
But all of the reasons you should be using Haskell in reality come down to practical large-scale software design concerns. The language lacks features that make several common classes of bugs possible. It makes several other common classes of bugs take a lot more work to implement than the non-buggy way to solve the same problem. These aren't things you can just write a short article about. They're things that require years of experience and introspection to see are even problems, and a willingness to accept that a lot of the problem is the ecosystem, not an individual failure to execute. None of that fits in an article.
I think articles about how great pattern matching and ADTs are make the language look worse, because anyone with some experience can say look at what's actually happening and say "I can do that in <other-language>, Haskell clearly doesn't have anything to offer." In other words - stop writing these articles. They drive people away from Haskell, not encourage them to look at the good parts.
I wouldn't know. It was impossible to learn.
[deleted]
The problem with Haskell is that it's smarter than most developers. You can study really hard and maybe you'll be good at Haskell. And then you'll find that there are no common libraries for the things you want to do because all the other developers were writing them in Python.
Such is life.
Such is life.
Being a good person is a goal in itself.
In the same way, Haskell is a good language, it's a moral language. Of course this causes some pain, but it's worth it.
In the same way, Haskell is a good language, it's a moral language. Of course this causes some pain, but it's worth it.
Most of my daily job goes into gluing services (API endpoints to databases or other services, some business logic in the middle). I don't need to see yet another exposition of how to do algorithmic tasks. Haven't seen one of those since doing my BSc. Show me the tools available to write a daemon, an http server, API endpoints, ORM-type things and you will have provided me with tools to tackle what I do. I'll never write a binary tree or search or a linked list at work.
If you want to convince me, show me what I need to know to do what I do.