Hacker News new | ask | show | jobs
by sytelus 4218 days ago
I'd been enough in to these battles so my 2 cents. I'm not saying that declarative mode is bad, in fact I love it, but the problem is that people tend to over do it in undesirable way. I've seen "architects" designing declarative language on top XML and asking programmers to code in it. There are also examples in likes of WPF which is perhaps the ugliest fattiest hairiest thing out there that lot of people have to fight with to get their job done.

1. Declarative languages or constructs are much harder to debug when things are not working as expected. There are no breakpoints to put or no debug statements to write or no watch to put. It was supposed to do that and you just can't tell why it's doing this.

2. Performance issues are much harder to resolve with declarative constructs. When you get in hotspot, there is no way to run. You would be fortunate if your language/platform allows you to fall back to imperative mode but there are platforms/languages out there which insist in 100% declarative styles.

3. There is lot of bad declarative syntax that is not designed to be composable. Lot of time, it's just is not extensible or allows to take advantage of modern programming language constructs such as inheritance, functional patterns, etc.

7 comments

All good points. However...

1. Declarative expressions are much easier to spot for defects (and much easier to not introduce defects). So do you want easier interaction w/ your debugger or do you want less bugs?

2. "There is no way to run" is an overstatement, I think. You have to run toward understanding the abstractions you're using. (E.g., if I have GC issues in the JVM, it's not that I have no where to run, it's just that now I have to go understand the GC abstraction.)

3. There is a lot of bad imperative code out there that is just as inflexible to work with.

I can really only cede point one if you qualify it with a "per line of code" or some such. I have seen plenty of "declarative" solutions that are no easier to spot errors in then the much more compact imperative version.

This is the difference between closed form versus summation problems. Sure, short closed form problems are much easier to understand than infinite sums. However, sometimes the closed form is borderline alien compared to the sum.

There are concise declarative solutions in languages such as Ocaml and Haskell.
Depends entirely on what solutions we are talking about.

Show me a nice concise solution to the N-Queens problem in those languages.

Now add performance as a requirement. :)

Here you go!

C (http://rosettacode.org/wiki/N-queens_problem#C)

Program 1 (compiled with gcc -O3 c1.c -o bin/c1)[0]

    real	0m0.003s
    user	0m0.000s
    sys	        0m0.002s
Program 2: (compiled with gcc -O3 c2.c -o bin/c2)[1]

Same as above, second code listing (faster while still readable,excluded unreadable fastest c version)

    real	0m0.001s
    user	0m0.000s
    sys	        0m0.001s
Haskell (http://rosettacode.org/wiki/N-queens_problem#Haskell)

Program 1 (modified to do 8 queens like the c versions, compiled with: ghc -O2 -threaded haskell2.hs -o bin/haskell2)[2]

    real	0m0.010s
    user	0m0.005s
    sys	        0m0.005s
Program 2 (modified to do 8 queens like the c version, compiled with: ghc -O2 -threaded haskell2.hs -o bin/haskell2)[3] (word of warning, this program is only 52% efficient according to ghc and has a lot of optimization that could be done)

    real	0m0.006s
    user	0m0.006s
    sys	        0m0.000s

Note that the fastest Haskell solution was also the shortest by quite a bit. Are you convinced yet?

EDIT: Found an ATS solution, but I'm not setup to compile it: http://www.ats-lang.org/SERVER/MYCODE/Patsoptaas_serve.php?m...

0: C, solution 1

    #include <stdio.h>
    #include <stdlib.h>
 
    int count = 0;
    void solve(int n, int col, int *hist)
    {
    	if (col == n) {
    		printf("\nNo. %d\n-----\n", ++count);
    		for (int i = 0; i < n; i++, putchar('\n'))
    			for (int j = 0; j < n; j++)
    				putchar(j == hist[i] ? 'Q' : ((i + j) & 1) ? ' ' : '.');
     
    		return;
    	}
     
    #	define attack(i, j) (hist[j] == i || abs(hist[j] - i) == col - j)
    	for (int i = 0, j = 0; i < n; i++) {
    		for (j = 0; j < col && !attack(i, j); j++);
    		if (j < col) continue;
     
    		hist[col] = i;
    		solve(n, col + 1, hist);
    	}
    }
     
    int main(int n, char **argv)
    {
    	if (n <= 1 || (n = atoi(argv[1])) <= 0) n = 8;
    	int hist[n];
    	solve(n, 0, hist);
    }
1: c solution 2

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
     
    typedef uint32_t uint;
    uint full, *qs, count = 0, nn;
     
    void solve(uint d, uint c, uint l, uint r)
    {
    	uint b, a, *s;
    	if (!d) {
    		count++;
    #if 0
    		printf("\nNo. %d\n===========\n", count);
    		for (a = 0; a < nn; a++, putchar('\n'))
    			for (b = 0; b < nn; b++, putchar(' '))
    				putchar(" -QQ"[((b == qs[a])<<1)|((a + b)&1)]);
    #endif
    		return;
    	}
     
    	a = (c | (l <<= 1) | (r >>= 1)) & full;
    	if (a != full)
    		for (*(s = qs + --d) = 0, b = 1; b <= full; (*s)++, b <<= 1)
    			if (!(b & a)) solve(d, b|c, b|l, b|r);
    }
     
    int main(int n, char **argv)
    {
    	if (n <= 1 || (nn = atoi(argv[1])) <= 0) nn = 8;
     
    	qs = calloc(nn, sizeof(int));
    	full = (1U << nn) - 1;
     
    	solve(nn, 0, 0, 0);
    	printf("\nSolutions: %d\n", count);
    	return 0;
    }
2: Haskell solution 1 (comments stripped out for comparison)

    import           Control.Monad
    import           Data.List
    
    queens :: Int -> [[Int]]
    queens n = map fst $ foldM oneMoreQueen ([],[1..n]) [1..n]  where
      oneMoreQueen (y,d) _ = [(x:y, delete x d) | x <- d, safe x]  where
        safe x = and [x /= c + n && x /= c - n | (n,c) <- zip [1..] y]
    printSolution y = do
         let n = length y
         mapM_ (\x -> putStrLn [if z == x then 'Q' else '.' | z <- [1..n]]) y
         putStrLn ""
    
    main = mapM_ printSolution $ queens 8
3: Haskell solution 2

    import           Control.Monad (foldM)
    import           Data.List     ((\\))
    
    main :: IO ()
    main = mapM_ print $ queens 8
    
    queens :: Int -> [[Int]]
    queens n = foldM f [] [1..n]
        where
          f qs _ = [q:qs | q <- [1..n] \\ qs, q `notDiag` qs]
          q `notDiag` qs = and [abs (q - qi) /= i | (qi,i) <- qs `zip` [1..]]
What is the speed of this on N of 16 to 20? At 8, any implementation should suffice nowdays.

Edit: Also, apologies for the possibly ninja edit. This is ultimately somewhat silly. I am not going to lie and say that the these are easily readable. I fully cede that this could just be a training thing. I picked this particular example because I had fun with it using the DLX algorithm. Which is admittedly far above my understanding in many ways. :) Just the naive recursive solution was much easier to read, but much much slower to execute. To the point that it was laughable. (If interested, I can post my code.)

Thinking about SQL which I use quite a lot (and the only real declarative programming I do):

1. I would not say that SQL is harder to debug than imperative code. I have also noticed that I get less bugs and they take less time to solve when I push as much as possible to SQL. Usually something either works properly or it doesn't. I personally find SQL is one of the few languages where reading the code is easier than writing it.

2: Database optimizations must be some of the most well known ways to improve performance in the tech field.

3: SQL syntax isn't great (I wouldn't say it is terrible either).

Declarative language on top of XML... Are you talking about this one? http://thedailywtf.com/articles/We-Use-BobX
Oh god, this one hits way too close to home for me.
Or maybe XSLT?
Good points.

To someone not familiar with the practice, I'd say that imperative programming is like manufacturing a car step by step, attaching parts to the engine, building the frame and so on. There are a plethora of tasks involved but it's usually clear how to finish them if you know the principles.

Declarative programming is like finding a magic incantation that makes a car pop into existence. It feels great when the default incantation works and you deliver a finished car in five minutes, but things tend to get confusing quickly if you decide that the car needs a customized transmission, a new row of seats, or (heaven forbid) a different number of wheels. Soon you're poring over arcane texts on GearChangeCommands and CabinPresenters and WheelLocators and potentially spending longer than you would have by just building the car in the slow, predictable way.

That said, I can't help but like the declarative style too, and would still prefer it much of the time to build user interfaces.

Isn't the hallmark of good API design essentially moving from the imperative to the declarative?

We abstract the "how" into declarative statements of "what" and "how many".

Can't say it's surprising. The von-Neumann architecture computers and their assembly languages are imperative. Functional programming language structs are just unnatural. Being computationally universal, one can simulate one using the other. But to me, it feels like an unwanted abstraction layer.

Analogy #1: if I were going to write a game for PC, I would directly write a game for PC, not a GBA ROM + GBA emulator for PC and make-believe that it's a PC game.

Analogy #2: if I want to write a novel in Spanish, it will not be possible to achieve the quality of a text written in Spanish from the beginning, by say, writing it in Japanese and using a translator (no matter how much you may like Japanese). Some idioms and culture-dependent things will be lost in translation. (Italian or Portuguese might be better, however)

The von-Neumann level nature of the machine really doesn't enter into it, though. I mean, ultimately, yes, somehow your solution has to be run in an imperative way. At the high level "looking at the declarative solution," though, it is irrelevant.

So, to your analogy #1. If such an emulator already existed and was in wide use, you would not be doing any harm to your system to target it.

To extend your analogy to the absurd, it doesn't make sense to write your program out symbolically in a programming language, because at the end of the day it is electrical values in a processor.

> The von-Neumann level nature of the machine really doesn't enter into it, though.

It does enter when you try to execute the code; which is a translation from declarative to imperative (assembly).

> To extend your analogy to the absurd, it doesn't make sense to write your program out symbolically in a programming language, because at the end of the day it is electrical values in a processor.

Care to tell me one single example of a declarative computer?

I'm more of a hardware engineer than a programmer, and I can't think of any universal logical circuit with memory that is not imperative. Even at the transistor layer, everything is imperative. This includes gate-model quantum computers and biological computers. There are some analog models that work for specific problems, but there certainly are nowhere near being declarative.

And the point was, at the high level, declarative -> imperative translation (which is done by a compiler) will typically be worse that an imperative -> imperative translation.

This is why a very written compute kernels in C outperforms its, say, Haskell equivalent, typically by orders. It's also the reason why scientific computation, signal processing, transcoding software etc. (when raw CPU performance outweighs everything) is typically written in Fortran/C/C++ with a mixture of assembly.

The reason is simple; they map to the underlying hardware well. It is also the reason why assembly languages are imperative.

Apologies, I'm not sure what it is you are trying to say. :(

The fact that the underlying system is von-Neumann seems just completely non sequitor. That is my only point. Every point you make afterwards is just as true of the "Harvard architecture" as it is of the von-Neumann one.

In the rest of what you are saying, it honestly sounds like we agree.

Exercise: are the type systems of functional languages in the vein of Haskell declarative in the sense that is associated with the disadvantages you mention?