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
I never really understand why I would want a type system until I learned Rust and was forced to learn it. Now I don't understand what I was thinking before..
I wouldn't. Why would you want to take errors that happen at compile time and create runtime errors? In dynamic languages you still have type invariants, it's just now they are invisible and can break your code at runtime.
For a dynamically typed language, a REPL ends up being essential. In a lot of ways, REPLs can be a superior form of programming. Those are often much harder to get to work with static type languages (often too much ceremony around the types).
The other thing that comes up is sometimes those compile time errors are somewhat pointless. For example, in many cases the difference between an int and a long are completely inconsequential. But further, whether or not your type is a Foo with a name field or a Bar with a name field or an Named interface simply does not matter, you just want something with a name field. While static typing would catch the case of passing in something without the name field, it unnecessarily complicates things when you want to talk about "all things with a name field" (Think, in the case of wanting to move Foo, rename Bar, etc).
Then there is the new concepts you need to learn. With dynamic typing, you can write "add(a, b) { return a + b; }". But how do you do that with Static typing? Well, now you need to talk about generics. But what if you want to catch instances where things are strictly "addable?" now you are looking at constrained generics. But what if you want a specialized implementation? Now you are potentially talking about doing method overloading. What if you want a different style of adding? Now you might be talking about plugging in traits. Typing and type theory have a tendency to add a requirement that you learn a whole bunch of concepts, but also that you learn how to correctly use those concepts.
It is no wonder dynamic typing has it's appeal. Dynamic languages are generally low on ceremony and cognitive burden.
I say all this being someone that likes static typing. Just want to point out that dynamic typing has it's appeal. Obviously, the big drawback is when you come back to a dynamically typed language and you want to fix things. It can be insidiously hard to figure out how things are tied together and you get no aid from the language.
> For a dynamically typed language, a REPL ends up being essential. In a lot of ways, REPLs can be a superior form of programming. Those are often much harder to get to work with static type languages (often too much ceremony around the types).
Haskell has probably one of the best and most useful REPLs around.
> But further, whether or not your type is a Foo with a name field or a Bar with a name field or an Named interface simply does not matter, you just want something with a name field.
This is perhaps an argument for a structural type system, IMO. Though I completely disagree with it.
> Well, now you need to talk about generics. But what if you want to catch instances where things are strictly "addable?" now you are looking at constrained generics. But what if you want a specialized implementation? Now you are potentially talking about doing method overloading
Those same invariants are still in your dynamic code, it's just now they are invisible to everyone and will crash at runtime if broken.
> It is no wonder dynamic typing has it's appeal. Dynamic languages are generally low on ceremony and cognitive burden.
There is a low cognitive burden on the writer, but for every refactor afterwards, and anyone who wants to change your code later, the cognitive burden is higher.
Dynamic typing does have an appeal, but it seems to be shrinking these days while people wake up to the benefits of static types. And it's no wonder, they just make sense from a pragmatic perspective.
Thanks for your post. I mean, I disagree with almost everything you said, but it's interesting to hear the perspective.
> Then there is the new concepts you need to learn. With dynamic typing, you can write "add(a, b) { return a + b; }". But how do you do that with Static typing? Well, now you need to talk about generics. But what if you want to catch instances where things are strictly "addable?" now you are looking at constrained generics...
You lost me here. All of those "what if's" seem to apply equally to dynamically typed languages. If I want "a + b" to work with two Python classes that I just wrote, I'm probably going to have to implement __add__ methods on both classes, and possibly with non-trivial implementations. It's not like dynamic typing makes everything magically addable, with no burden on the developer. Wouldn't you agree?
Not to mention that languages such as Haskell and OCaml have REPLs too. They are not at robust as, say, Common Lisp's -- but REPL-driven development is hardly a stranger in the statically typed camp.
I agree that both camps have their appeal, though!
In typeless languages like forth, asdembly etc the debugging is more confusing than in Python, because you often see the situation blow multiple layers after the error happened. Or you may simply get silent wrong anwers. And in C, just the pointer vs value distinction in the type system makes code dramatically clearer and catches lots of errors.
This is how limited type systems are still very much better than no types.
> This is how limited type systems are still very much better than no types.
This is not what I was arguing against. I was arguing that I would be surprised if people were happy to go from strong typing to dynamic typing, not that 'limited' type systems are better than 'no types'.
Ah. My original comment was about how limited type systems are still useful, in response to someone not seeing their point when using them. Communication is hard :)
Yep, GHC is an incredible compiler and Haskell is an incredible language. If only Haskell had a package manager as slick as Cargo, I'd use it for just about everything.
The great news is that tooling is rapidly improving these days. VSCode with the new ghcide is simply amazing. Ghcide is still new and thus has a few rough edges (eg. TH) but overall it lifts the Haskell IDE experience into the 21st century. Give it a try.
What about runtime stuff? I've found Haskell is very good at abstracting away your concerns about the runtime considerations and sometimes it will come back to bite you. I mean all that stuff you want to see into when you're running your app in production, like caching, function invocation times, threads etc.
Can't really comment on that - most of the code I use is built on the awesome Haxl[0] so we never have to worry about those things. I'm curious what other people think though
I guess it's comparable to Maven, but people usually use Maven with an IDE, and Stack on the command line, so that's not very encouraging. Anyway, compared to something like NPM (Node.js) or Cargo (Rust), its user experience is extremely lacking.
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