A Rubyist learns Haskell, part 1

Not so different after all?

November 21, 2023 Ā· Felipe Vogel Ā·

Lately a question has been on my mind, a classic for programmers:

ā€œWhat language should I learn next?ā€

More specifically: ā€œA language that has jobs, or one that I would actually enjoy?ā€

I struck a good balance a few years ago when I taught myself Ruby along with front-end basics, which I enjoy a lot and got me hired as an entry-level engineer. In fact, Iā€™d be happy spending the next ten years writing Ruby for fun and profit.

So this time I threw employability out the window completely and chose Haskell.

How?

Iā€™m starting with the Haskell Wikibook, supplemented (of course) byā€¦

ā€¦ StackOverflow answers that I mostly donā€™t understandā€¦

ā€¦ found via my mostly-incoherent Google searchesā€¦

ā€¦ on Haskell topics where I donā€™t even have the vocabulary to express my lack of understanding.

Yep, this is going to be great.

Despite the embarrassment that Iā€™ll probably feel later when I look back on this amateur and uninformed post, I feel itā€™s important to write down my first impressions as I begin my Haskell adventure.

But why?

Because Haskell is different. Ruby is quintessentially object-oriented, whereas Haskell is quintessentially functional.

(I know, thatā€™s an oversimplification: Rubyā€™s diverse influences include functional languages of yore, and apparently OOP is discussed without disdain in some corners of the Haskell world.)

Why learn something so different? Itā€™s not to forget about Ruby and OOP, but to write better Ruby by using a functional approach wherever it makes sense, and to use functional-friendly tools like dry-rb and Hanami more effectively.

It wonā€™t always be easy to bridge the conceptual gap between these two paradigms, so I braced myself for the initial shock of diving into Haskell.

But Iā€™ve been pleasantly surprised in these early days at how often something in Haskell felt familiar to me, coming from Ruby.

Not so different from Ruby after all?

Consistency of language design

The snobbishness of its devotees is not Haskellā€™s only similarity with Ruby. In fact, Iā€™ve sensed an odd kinship between these languages that are in many ways on opposite ends of the spectrum. Maybe itā€™s because they take their opposite approaches with a similar purity or single-mindedness. Hereā€™s an example.

In Ruby, everything is an object. So 2 == 2 is the syntactic sugar for 2.==(2). The number 2, as any Rubyist knows, is an object that has the method ==.

In Haskell, functions are everywhere. So 2 == 2 is syntactic sugar for (==) 2 2. Here, == is just a function.

In both cases, == is not a language keyword but a natural outgrowth of the languageā€™s foundational principle, be it objects or functions.

As Victor Shepelev noted in a recent article,

A lot of things that in other languages are represented by separate grammar elements, in Ruby, are just methods calls on objects.

Iā€™m wondering if something similar could be said of Haskell and functions.

Elegant syntax

Of course, just as in Ruby, some of Haskellā€™s syntactic sugar really is syntax rather than functions disguised as operators. But its effect is to make Haskell surprisingly elegant, not at all what I feared, which was something like Lispā€™s infamous parentheses, memorialized in xkcd and elsewhere.

For example, we can take this function:

mySignum x =
    if x < 0
        then -1
        else if x > 0
            then 1
            else 0

And rewrite it using guards:

mySignum x
    | x < 0     = -1
    | x > 0     = 1
    | otherwise = 0

You can even write it on one line, if you want to:

mySignum x | x < 0 = -1 | x > 0 = 1 | otherwise = 0


So Haskell is really, deep down, similar to Ruby. Right?

Yeah no, Haskell is really different

Rigors of a type system

Haskell, being statically typed, is less flexible than Ruby, at least in the ā€œSure, go ahead and juggle five knives if you want toā€ sort of way. For example, letā€™s take the last example above, and try replacing one of the functionā€™s return values with a string:

mySignum x | x < 0 = -1 | x > 0 = 1 | otherwise = "zero"

An error comes up if you try to define this function, because the compiler wants all the possible return values to be of the same type.

The solution (as far as I understand it) would involve defining an explicit type signature. I say ā€œexplicitā€ because all the above examples of the mySignum function have an inferred type signature. If you open up the Haskell REPL and enter one of the valid definitions of mySingum from the previous section, you can then enter :t mySignum and the inferred type is displayed:

mySignum :: (Ord a1, Num a1, Num a2) => a1 -> a2

So I pasted that inferred type signature above my function to make it explicit, and then I tried tweaking it to include a String return type, but I wasnā€™t successful with my limited knowledge. The closest I got was more errors while trying to use ad-hoc polymorphism or Either.

I know, I know, this is a ridiculous function that no one would write in real life. Even in a more realistic scenario it would probably a bad idea to write a function that returns either a number or a string.

For comparison, hereā€™s how our ridiculous function would translate to Ruby:

def my_signum(number)
  if number.negative?
    -1
  elsif number.positive?
    1
  else
    "zero"
  end
end

Or if you like code golf:

def my_signum(x) = x < 0 ? -1 : (x > 0 ? 1 : "zero")

A new kind of elegance

Another difference is that Haskellā€™s elegance can look quite different from Rubyā€™s. A basic example is function composition:

(unwords . reverse . words) "Mary had a little lamb"

This produces "lamb little a had Mary".

In Ruby, it would be:

"Mary had a little lamb".split.reverse.join(' ')

Ruby does have function composition, or rather proc composition, but youā€™ll be lucky to see it actually used anywhere. Hereā€™s the previous example, now with composed procs:

unwords = :split.to_proc
reverse = :reverse.to_proc
words = -> { _1.join(' ') }

(unwords >> reverse >> words).call "Mary had a little lamb"

An entirely different paradigm

And then there are the ā€œWhaaat?? No way!ā€ šŸ¤Æ kind of moments. Hereā€™s an example, from the ā€œVariables and Functionsā€ page of the Haskell Wikibook:

Because their values do not change within a program, variables can be defined in any order. For example, the following fragments of code do exactly the same thing:

y = x * 2
x = 3
x = 3
y = x * 2

Now weā€™re in a completely different world from Ruby, or any other imperative language. I still donā€™t know what possibilities this opens up, but I get the feeling they are significant.

Coming next

In future posts in this series, Iā€™ll cover several chapters of the Wikibook at once, so as not to bore you with too many details. I only covered the first chapter here because I didnā€™t want to rush past my first impressions.

I find it funny, looking over the Wikibook contents, that the chapter ā€œWelcome to Haskellā€ is found at the beginning of the Advanced Track, halfway through the book šŸ˜‚ It covers topics such as monoids, applicative functors, and comonads. Thanks to the pleasant first impressions Iā€™ve had so far, Iā€™m not dreading that chapter nearly as much as before!


ā€¦ but Iā€™m still dreading it.

šŸ‘‰ Newer: Coming to grips with JS šŸ‘ˆ Older: Server-sent events for asynchronous API calls in a Roda app šŸš€ Back to top