The stillness of Haskell code

As I learn more Haskell it's hard not to internalize the view that other languages are quite noisy. Even Python could be characterized as noisy if you compare it with the simplicity of Haskell.

I want to demonstrate the stillness and elegance of Haskell code in general by contrasting some simple tasks done with both TypeScript and Haskell.

One might argue that Haskell is esoteric and that the elegance of Haskell solutions stands in proportion to its unreadability. But if one contrasts the solutions, I argue most programmers would agree that at least simple tasks are expressed with greater clarity and elegance in Haskell than in most other languages.

Haskell takes full advantage of whitespace as a separator and views most characters and character patterns we're used to from other languages as redundant. By working with whitespace only, Haskell manages to remove distractions, keeping only essentials to make the abstractions operate properly.

I choose to contrast with TypeScript mainly because it's a language most programmers (even if they don't know TypeScript) can read, and it has a type system just like Haskell does.

What does Haskell code look like?

When defining a function it is curried by default in Haskell even though we can’t see it. If we make a function that adds two numbers we could write (add’ since it is included in the Prelude, the appended ‘ is valid in Haskell names)

add’ x y = x + y

Haskells type inference would make a type signature superfluous in this case but if we added it we would have,

add’ :: Num a => a -> a -> a

The type signature reads, for all members (Int, Integer, Float and Double) of the Num class, we take number of type a, then we take another number of type a and in the end we return a number of type a. A generic not needed in TypeScript since JavaScript only have numbers in general as a primitive.

This would at a first glance look like a simple thing to translate,

const add = (x: number, y: number) => x + y;

But it would really be,

const add = (x: number) => (y: number) => x + y;

We would use add like so add(1)(1). Since it is curried, we could write things like,

const add1 = add(1);
const thenAdd1More = add1(1); // 2

However, this is not very beautiful, is it?

Using function overloading we would arguably prefer,

function add(a: number): (b: number) => number;
function add(a: number, b: number): number;
function add(a: number, b?: number) {
  return b === undefined
    ? (b: number) => a + b
    : a + b;
}

With this implementation, we could write add(1,1), but we could also use only one argument which we would return a curried function expecting another number before resolving and returning the sum.

Now, say we had a list of integers [ 1, 2, 3, 4, 5 ] and we want to add 1 to each value in the list.

We could make a function add1,

const add1 = (x: number) => x + 1;

And add 1 to each value in list, writing

const list = [1, 2, 3, 4, 5];
const list2: number[] = list.map(add1);

We could instead have used an anonymous function, writing

const list2: number[] = list.map(
  (x: number) => x + 1
);

In Haskell we would provide ‘the same’ solution like so:

add1 x = x + 1
list2 = map add1 list

We could also have used an anonymous function,

list2 = map (\x -> x + 1) list

It's a simple task to add 1 to every list of integers in a list. But when one compares the TypeScript way with the Haskell solution the noise in the TypeScript solution is apparent.

Now, let’s make a simple implementation of FizzBuzz in TypeScript,

function fizzBuzzRange(n: number): String[] {
  return Array.from(
    {length: n},
    (_, i) => fizzBuzz(i + 1)
  );
}

function fizzBuzz (x: number): String {
  let v: String;
  if(x % 15 === 0) {
    v = 'fizz buzz';
  } else if (x % 3 === 0) {
    v = 'fizz';
  } else if (x % 5 === 0) {
    v = 'buzz';
  } else {
    v = x.toString();
  }
  return v;
}

Using a Haskell list comprehension we create a list stretching from 1 to n. Every x is taken from the range provided in fizzBuzzRange and is expressed through fizzBuzz, returning the number as a String or given the fizz buzz rules as 'fizz', 'buzz' or 'fizz buzz'. (The procedure is called pattern matching and will be added to JavaScript and presumably still later TypeScript.)

Note the modulo operator. In Haskell, we can always inject and infix a function. We could also have written i.e. mod v 3 making a more Lisp-like solution. However, infixing the modulo operator makes the code more readable, also closer to the TypeScript version.

fizzBuzz :: Integer -> String
fizzBuzz v
  | v `mod` 15 == 0 = "fizz buzz"
  | v `mod` 3  == 0 = "fizz"
  | v `mod` 5  == 0 = "buzz"
  | otherwise       = show v

fizzBuzzRange :: Integer -> [String]
fizzBuzzRange n = [fizzBuzz x | x <- [1..n]]

If you cast a quick eye on my TypeScript solution for FizzBuzz it's harder for your eyes to focus on names and values as they are surrounded by noisy characters. At least my eyes must focus (if just for a moment) to see what I need.

Contrast this with the Haskell solution. In the Haskell implementation you instantly see all relevant parts since they're separated only by whitespace.

When I look at the Haskell code I see names and values and relations between. But what I don't see is more important, I don't see unwarranted parentheses, I don't see curly braces, semicolons (we need them in JavaScript to avoid hazards and free cognitive energy) and so on.

This is from a demo, Monster wants cookies, I've made.

As a step in learning Haskell, I’ve attempted to make ‘the same’ application in both Haskell and TypeScript.

When comparing two or more shapes, what’s unique for each shape emerges with greater clarity. Therefore, by comparing a feature of a programming language we already know with a feature of a language we are learning, we accentuate differences and similarities and learn from them.

My ambition with the project was to learn the basics of Haskell through TypeScript, an attempt to use an example-driven approach. I don’t know how pedagogical my attempt would be perceived, but I learned a lot from doing the comparison.

I've tried to stay close without exaggerating and if you look at the source code, you will see that the main logic is similar, still respecting the individual attributes of each language, I think. It would be absurd to bend TypeScript too much, just for the sake of staying close.

In this context it is beside the point, but I think that the elegance of Haskell shines even in a simple demo made by a Haskell beginner. Also, it is a more interesting example as it involves IO and side-effects.

I believe that the visual elements - the shapes - of programming languages attract attention in different ways and indirectly affect how we relate to code, but also to the abstractions behind code.

Aesthetics in code is important, I believe. Aesthetics matters. And so far, I find Haskell a very concise, distract-free experience that lets me focus on how to solve problems.

In Haskell form and content coincide with the type system. Even as a beginner your sense is that the type system helps you as its syntax in itself guides you rather than hinders you, something which sometimes is the case in TypeScript and presumably other languages similar in syntax.

A type signature is needed in Haskell only when type inference fails and you need to guide the compiler about a certain construct, or you believe that types would make the code more communicative. You get all the benefits you should from having a strongly statically typed language but at the same time, you seldom have the feeling that the type system works against you, a not uncommon sensation when working with TypeScript.

Take a simple sum function implemented in TypeScript,

function sum(ns: number[]): number {
  return ns.reduce(
    (acc: number, v: number) => acc + v,
    0
  );
}

At first glance, it looks 'heavy' even though it is simple to understand. But we have to concentrate to see what's happening. We have to extract what is relevant from the surrounding noise to see what's essential. Compare with a simple sum function written in Haskell (once more named with a ' appended since sum is included in the Prelude).

First we make a very explicit version,

add' x y = x + y

sum' ns = foldl add' 0 ns

But with Haskell we can still remove some noise,

sum'' = foldl add' 0

Even,

sum''' = foldl (+) 0

As long as we know that we can take any operator (we could easily recreate every or some using boolean operators, for instance) or really any operator-like function and infix them, also this version is clear to us. The Haskell solution would just not be better for reasoning with other programmers, it would also be communicative, I think, to non-programmers since all the traditional noise of programming is removed and only the logic and a few names are left.

To be fair we could have simplified the TypeScript implementation, but it would still have been noisy,

function add(x: number, y: number): number {
  return x + y;
}

function sum(ns: number[]): number {
  return ns.reduce(add, 0);
}

Of course, in the end, any type system has the purpose to aid but it's a difference in degrees in how pleasant (or not) a type system is. And to be objective this comparison is not very fair. TypeScript is an ad-hoc-solution for a dynamically typed language. But it's a bit sad that the type system of Haskell is stronger than that of TypeScript, while at the same time easier to use.

I appreciate TypeScript and I am very fond of JavaScript, but I will miss the stillness of Haskell code when writing code in other languages.

Let's continue with some examples from Scheme. In Scheme (I know far less Scheme than Haskell though; here I use Chicken Scheme) we would write 'the same' code as above,

(define add-one (lambda (x) (+ x 1)))

(map add-one '(1 2 3))

This solution is Scheme can also be written straightforwardly with a lambda.

(map (lambda (x) (+ x 1)) '(1 2 3))

I think Scheme is a very clear and elegant language. We have a symbol, map. map is a procedure that receives a function for mapping - usually a transformation - a set of values to another set.

In the example we have associated an expression with the symbol add-one, a mapping procedure which the procedure will apply to every value of the provided list (1 2 3) treated as data rather than an expression (designated by the ', syntactic sugar for quote).

It is not much noise here. But too me, all parentheses of Scheme obscures what's happening. Scheme is elegant, but at the same time obscure.

In general, I think that functional programming languages (or almost functional such as Scheme) often seems to have a more thought-through syntax. And I don't think it's only about the fact of using another programming paradigm. There may be deep reasons behind, it may be pure coincidence but I believe that imperative languages should take inspiration from this.

Isn't this why we think Python is easy to read and write? …because Python has less noise than most other imperative languages?

The use of whitespace (in visual terms an absence of characters that can be seen) helps to create a space in which what's relevant is accentuated rather than drowns in arbitrary noise of characters needed for the parser to understand what's going on. Characters that make sense to the computer, not characters that make sense to us.

The stillness of Haskell provides us with the means to focus. But the purity also has an aesthetical level that elevates this feeling even more. It's easy to fall in love with such simplicity and therefore care more for your code, attempt to make it even simpler - more elegant.

On the other hand, this beauty also can be dangerous. That code can be simplified doesn't always mean that it should. If programming is a social activity and code is not private, we must also consider the readability of code. And sometimes Haskell perhaps is too elegant?