Learning Haskell makes you a better programmer?

bearophile bearophileHUGS at lycos.com
Tue Dec 25 15:36:55 PST 2012


(In the end this answer has become a little article.)

Walter:

Being a "good programmer" means having a quite long list of 
different skills and qualities. I think that learning Haskell 
helps you improve, but only in a small percentage of those 
skills. So I think overall Haskell doesn't make a large 
difference (despite since many months I have started to study 
Haskell).

Regarding D, it's very different from Haskell, and I think it's 
very hard to learn from D what you can learn from Haskell. 
Haskell is single-paradigm and almost forces you to program in 
certain different ways.


 From that little blog post:

>It forced me to think of every such problem as a chain of the 
>primitive list operations, maps, folds, filters and scans. Now I 
>always think in these terms. I "see" the transformation of a 
>container as a simple sequence of these operations.<

Decomposing part of the code in terms of such higher order 
functions is sometimes a good idea that makes the code cleaner 
and frees the mind to think at a higher level.

Python goes one step further with its list comps that make the 
code even more readable, removing map and filters (list comps are 
available in Haskell too, but the syntax is a little worse, and 
probably for cultural reasons it's used less than in Python):

foos = (x * x for x in items if x % 2)

In D you have to use map and filter to do the same thing, and 
it's more noisy, less readable and longer:

auto foos = items.filter!(x => x % 2)().map!(x => x * x)();


In Haskell sometimes you define an inner recursive function 
(sometimes named "go") where in D/Python you just use a loop. The 
loop doesn't risk blowing the stack (especially in Python), is 
often equally shorter to write, and in many situations I keep 
finding loops more readable and cleaner than recursion.


So on one hand you have C code that contains lot of hairy nested 
loops with twiddly pointer arithmetic, that shows you the details 
of how something is done, but it doesn't tell you why. To 
understand such C code you have to make a summary of the code in 
your head, and build a bit higher view of what is done (from an 
older version of http://rosettacode.org/wiki/Topswops ):


#include <stdio.h>
#include <string.h>

typedef struct { char v[16]; } deck;

int n, d, best[16];

void tryswaps(deck *a, int f, int s) {
	int i, j, k;
	deck b = *a;

	if (d > best[n]) best[n] = d;

	for (i = 0, k = 1 << s; k >>= 1, --s; ) {
		if (a->v[s] == -1 || a->v[s] == s) break;
		i |= k;
		if ((i & f) == i && d + best[s] < best[n]) return;
	}
	s++, d++;

	for (i = 1, k = 2; i < s; i++, k <<= 1) {
		if (b.v[i] == -1) {
			if (f & k) continue;
		} else if (b.v[i] != i)
			continue;

		b.v[0] = j = i;
		while (j--) b.v[i-j] = a->v[j];
		tryswaps(&b, f|k, s);
	}
	d--;
}

int main(void) {
	deck x;
	memset(&x, -1, sizeof(x));
	x.v[0] = 0;
	for (n = 1; n <= 12; n++) {
		best[n] = d = 0;
		tryswaps(&x, 1, n);
		printf("%2d: %d\n", n, best[n]);
	}

	return 0;
}



A short Haskell solution is shorter, and much cleaner/nicer if 
you know a little of Haskell (but also much slower, possibly ten 
times slower or more):


import Data.List (permutations)

topswops :: Int -> Int
topswops n = foldl max 0 $ map topswops' $ permutations [1..n]

topswops' :: [Int] -> Int
topswops' xa@(1:_) = 0
topswops' xa@(x:_) = 1 + topswops' reordered
     where
         reordered = reverse (take x xa) ++ drop x xa

main = mapM_
         (\x -> putStrLn $ show x ++ ":\t" ++ show (topswops x))
         [1..10]



On the other hand Haskell code sometimes contains tens of tiny 
functions that requires you a elephant-grade long term memory 
(even if you are using an Haskell IDE) to remember the meaning of 
all those functions and several 3-4-symbol-long operators, plus 
it asks you a large medium-term memory to keep in mind how those 
numerous little functions are plugged together in complex ways to 
produce the results.

So while I've seen both very good C and good Haskell code, in 
both languages it's possible to write code that requires lot of 
brainpower to be understood.

I have found that good D code is a good middle point between 
those kinds of C and Haskell coding: it calls and uses a moderate 
amount of functions, it often uses a very limited amount of 
pointers, its loops usually are clean and at worst use slices 
instead of pointer arithmetic. And micropatterns like map and 
filter are available in D, but also normal loops are available to 
avoid recursion. Immutability is available.

The end result is a kind of D code that for me avoids the 
problems visible in both that C and Haskell code. This is why I 
often use D instead of Haskell or C. (I have not yet studied 
Scala, maybe it offers similar qualities as D or maybe even 
better, despite on the JavaVM it often doesn't allow you to 
specify with enough precision the layout of the data structures 
in memory). This is a modified D version of the C code (it's not 
exactly equal), and for me it's significantly more clean than 
that C code:



import std.algorithm, std.range;

uint topswops(in uint n) nothrow
in {
   assert(n > 0 && n < 32);
} body {
   static uint[32] best;
   alias T = byte;
   alias Deck = T[best.length];

   void trySwaps(in ref Deck deck, in uint f, in uint d)
   nothrow {
     if (d > best[n])
       best[n] = d;

     foreach_reverse (i; 0 .. n) {
       if (deck[i] == -1 || deck[i] == i)
         break;
       if (d + best[i] <= best[n])
         return;
     }

     Deck deck2 = deck;
     foreach (i; 1 .. n) {
       immutable uint k = 1U << i;
       if (deck2[i] == -1) {
         if (f & k)
           continue;
       } else if (deck2[i] != i)
         continue;

       deck2[0] = cast(T)i;
       foreach_reverse (j; 0 .. i)
         deck2[i - j] = deck[j]; // Reverse copy.
       trySwaps(deck2, f | k, d + 1);
     }
   }

   best[n] = 0;
   Deck deck0 = -1;
   deck0[0] = 0;
   trySwaps(deck0, 1, 0);
   return best[n];
}

void main() {
   import std.stdio;
   foreach (i; 1 .. 12)
     writefln("%2d: %d", i, topswops(i));
}



Using templates you can make that D code about twice faster.

That D code is still low-level, it contains a static variable, 
but still that code is much simpler to understand compared to 
that C code.

In that D code this is an alternative way to write the reverse 
copy, but unfortunately here you have to pay some abstraction 
penalty, because using DMD this makes the code significantly 
slower (in Phobos "copyTo" is named "copy" but I prefer "copyTo" 
to remind me what the source and destination are!):

deck[0 .. i].copyTo(deck2[1 .. i + 1].retro());


That D code is quite "algorithmic" and designed for speed. If you 
care less for performance it's easy to write higher level D code 
that's shorter and easy to understand:


import std.stdio, std.algorithm, std.range, permutations2;

uint topswops(in uint n)
in {
   assert (n > 0 && n < 32);
} body {
   static uint flip(uint[] deck) pure nothrow {
     uint[32] temp = void;
     temp[0 .. deck.length] = deck[];
     uint count = 0;
     for (auto t0 = temp[0]; t0; t0 = temp[0]) {
       temp[0 .. t0 + 1].reverse(); // Slow with DMD.
       count++;
     }
     return count;
   }

   return iota(n).array().permutations!0().map!flip().reduce!max();
}

void main() {
   foreach (i; 1 .. 11)
     writefln("%2d: %d", i, topswops(i));
}


There are ways to write this D code in an even a little simpler 
way to understand, producing a little longer (less packed) code.


On the other hand D is not perfect, and there are always bad 
parts or things to improve in everything.

The lack of a "yield" as in Python makes many D coding patterns 
less handy (and the lack of list comps makes the D code a little 
less readable, but this is less important).

The lazy lists of Haskell (also present in some form in Perl6 and 
Scala) offer some handy coding patterns, that are rather harder 
to use in D.

The lack of good subtyping makes D code less strong (I have 
discussed it some times, like here 
http://forum.dlang.org/thread/pxnyniryvdwpdxoymuau@forum.dlang.org 
).

In D sometimes the creation of immutable data structures is not 
handy. I have discussed this in two or more past posts.

If you compare Haskell with D, the use and management of the 
Maybe Haskell type is much better than the Nullable of Phobos, 
because of D lack of both pattern matching and of a bit of 
typestate (that should allow D to know that in the "else" branch 
of a not-null test of a Nullable with isNull, the type is not 
null, it can't throw an exception and it can be used safely).

Some of such things should be improved in D language and Phobos, 
where possible.

--------------------

SomeDude:

>I've seen some Python code using heavily map/filter/etc that was 
>simply unreadable to me.<

Pythonic code does not contain many map/filter (Pythonic code 
uses lazy or eager list comps). So probably that was bad Python 
code, or at least not pythonic.

Bye,
bearophile


More information about the Digitalmars-d mailing list