This is a port of an older twitter thread, I thought it was worth having on my blog as well.
Sometimes people say that learning Haskell changes the way you think, but they don't always say how or why. So here are 7 concrete things I learned from Haskell.
1. Referential transparency is freeing
Referential transparency means that when we bind an expression to a name
y = f x), the two are interchangeable, and whenever we use
we could just as well use
f x and vice versa, and the behaviour will stay the same.
Enforcing referential transparency in a programming language means that:
- We need to have more control over effects
- We can use substitution and equational reasoning
The value of that for me is that I can trust code to not surprise me with unexpected effects, I can use substitution to understand what a piece of code is doing, and I can always refactor by converting a piece of code to a new value or function because referential transparency guarantees that they are equivalent.
Because of that I feel like I can always understand how something works because I have a simple process to figure things out that doesn't require me keeping a lot of context in my head. I never feel like something is "magic" that I cannot understand. And I can easily change code to understand it better without changing its meaning.
Referential transparency is freeing, and makes me worry less when working with code!
2. Building bigger parts from smaller parts
Building bigger parts from smaller parts like lego or a pipeline is how we normally go about writing functional programs.
Smaller pieces are easier to write, verify and test. They are easier to combine with other parts, and they can be swapped in favor of something else if requirements change. This approach makes it easier to separate the responsibilities of different ideas, and in Haskell this is the idiomatic way to write code.
3. The 'functional core, imperative shell' pattern
The 'functional core, imperative shell' pattern, an approach to building programs by pushing effectful code to the outer layer of the application and provide a thin layer that calls pure logic, and decoupling IO operations from logic, helps us write code that can be used in many contexts, and keep the other benefits of referential transparency.
Combining parsing with reading from a file means that if I need to read from the network I need to rewrite or duplicate my code. This pattern helps make code more robust, easier to extend and change, and easier to test.
4. (First class) functions are really flexible
This one simple concept can be used to model so many ideas and approaches. From mapping inputs to outputs, to iteration, control flow, composition, state, strategies, dependency injection, abstractions and even data!
5. Modeling data precisely and succinctly is awesome
Understanding the data helps to guide the development process, focus on the relevant details, and ignore the irrelevant.
Worry about nulls? No need! Empty list is invalid state?
NonEmpty. Using a magic number to encode failure? Nah - use
Did I remember to handle all the cases in my CSV parser?
Fortunately the spec translates directly to ADTs and we got exhaustiveness checking.
Also types, and distinguishing the shape of data from its meaning, really helps with understanding the intention and context of code and what kind of operations should be used in context.
Being able to easily declare a temperature, distance, height, age or port instead of just a number makes it easier to understand context, and makes it harder to make mistakes and use irrelevant operations or mix irrelevant data! Haskell makes this easy and idiomatic. And it's even sweeter when you can derive behaviours for your new types.
Embedded Domain Specific Languages are a really useful technique to create abstractions and focus on the essence of the relevant domain for the task. They also help to avoid writing code that combines the "how" with the "what".
Haskell with its lightweight syntax, controlled effects, non-strict semantics, and honestly a bunch of other features, makes writing little languages as Haskell libraries easy and delightful. Without having to reach out to macros, we can implement EDSLs while keeping the same understanding of normal Haskell code and evaluation model, and being able to express domains, such as HTML, software transactional memory, and argument parsing, in a precise and composable way.
And if you are interested in compilers and proramming language design, this is quite the gateway drug.
7. Common APIs with laws
Common APIs with laws really help to avoid being surprised by new code, and make it easy to learn new abstractions.
Is this new type is a monoid? Means it has these operations and
it behaves this way when used. Is that a functor?
Means I can combine these two
maps into one map that chains the inner operations
and it will give me the same result, saving the overhead of one
These kind of scenarios are quite common. Ask your fellow Haskellers!
I can often skip reading a lot of text just by learning that a certain type implements certain interfaces. After reading that, I already know how to work with this type in a lot of cases, how to combine values of that type, how to iterate over it, and how it is expected to behave.
It's quite nice when your database library of choice, your web framework and your testing framework share API. Less things to learn, easier to get started with. It's a huge boon to discoverability.
These are lessons, techniques and features that I use daily, and they help me design solutions, fix bugs, and complete projects.
I think some of these things are hard, or even impossible to learn from other languages, and is why I think Haskell has a lot to offer to the working developer.
If you are interested in learning what Haskell might be useful for, check out my other post, Consider Haskell.
If you are interested in learning some Haskell and see these things for yourself, maybe check out my free and online book, Learn Haskell by building a blog generator.