As I've been working my way through this project of mine, I've come to respect exactly why Haskell is so mind-bendingly different from other programming languages. It has to do with the level of abstraction you're expected to track in your head.
In the classic "Object Oriented Programming Language" tutorial, when inheritence is usually introduced, the abstraction is flat: you create an Mammal
class, which says it has four limbs and fur, and then add specific subclasses such as Dog
and Cat
, with different function bodies for the abstract method speak()
, defining what sounds the animal makes.
The impression you're given is that object orientation is often just fiddling around the edges; subclasses define different kinds of business logic. This impression is reinforced in languages like Java and C++ in realms as wildly different as office application development and game programming. Even in game programming, subclasses are often just business logic in a pretty dress: this class of Object
moves four pixels per frame, that class has a weapon that does 20 points of damage.
Haskell throws all that out the window and asks you a simple question: "What is addition?"
Like, we think we know what "addition" is. It's the sum of two numbers to create a new number. "Well, what's a number?" And so forth.
In my project, the discovery that regular expressions both are semirings and that they can produce semirings was mind-boggling and promised a very neat and pluggable way to adapt regex to a variety of purposes, and then when I learned what semirings really were, the embogglement went much higher.
Because a semiring is often described as this: (0, 1, ⨯, +, ℙ)
, where ℙ is some set. Not a set "of numbers," just some set. If we say ℙ is "the set of natural numbers" then we get all the operations you'd expect. But this is just a concrete implementation! The underlying abstraction is still there. The exact same behavior holds for (F, T, &, |, 𝔹)
, the set of booleans values; F&x
is always false, just as 0⋅x
is always zero. F|x=x
, T&x=x
, etc.
And then the mind-boggling part I got from Might & Co, which I later discovered were called Viterbi Parse Forests: ([], [""], ⊗, ∪, [𝕊])
, where the brackets represent "set of..." and 𝕊 represents "strings" (as programmers understand them) has the exact same behavior The ⊗
operator is the cartesian product, that is, the concatenation of every string from the left set with every string from the right set. With this, what you get back is a record of the exact strings that were recognized and matched.
If you replace "set" in the above paragraph with "list", you get prioritized unions— which is the basis of disambiguation in Parsing Expression Grammar theory. If you define the semiring rather strangely but still within the rules, you get "longest match" disambiguation— which is the basis of μ-regex parsing under the POSIX rules. And so forth.
This is a much deeper level of abstraction. The user has to be able to not just internalize the "obvious" uses, but to understand the deep connective tissue linking these different uses and then come up with new ones. Haskell developers are a mixture of very quick and very slow— the slow part comes while their brain is tickling out a new understanding of the abstraction before them, and the quick is implementing it with good IDE support. It separates the clever from the skilled. Ideally, it would be good to have both.