Why is Haskell (sometimes) referred to as "Best Imperative Language"?
Asked Answered
S

4

89

(I hope this question is on-topic -- I tried searching for an answer but didn't find a definitive answer. If this happens to be off-topic or already answered, please moderate/remove it.)

I remember having heard/read the half-joking comment about Haskell being the best imperative language a few times, which of course sounds weird as Haskell is usually best known for its functional features.

So my question is, what qualities/features (if any) of Haskell give reason to justify Haskell being deemed the best imperative language -- or is it actually more of a joke?

Smalltime answered 8/7, 2011 at 9:31 Comment(4)
donsbot.wordpress.com/2007/03/10/… <- Programmable semicolon.Lail
This quote probably originates from the end of the Introduction of Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell which says "In short, Haskell is the world’s finest imperative programming language."Delphine
@Russel: thanks for pointing out the most probable origin (being SPJ himself as it seems) of that saying!Smalltime
you can do strict imperative OO with Haskell: ekmett/structsHirsh
T
98

I consider it a half-truth. Haskell has an amazing ability to abstract, and that includes abstraction over imperative ideas. For example, Haskell has no built-in imperative while loop, but we can just write it and now it does:

while :: (Monad m) => m Bool -> m () -> m ()
while cond action = do
    c <- cond
    if c 
        then action >> while cond action
        else return ()

This level of abstraction is difficult for many imperative languages. This can be done in imperative languages that have closures; eg. Python and C#.

But Haskell also has the (highly unique) ability to characterize allowed side-effects, using the Monad classes. For example, if we have a function:

foo :: (MonadWriter [String] m) => m Int

This can be an "imperative" function, but we know that it can only do two things:

  • "Output" a stream of strings
  • return an Int

It can't print to the console or establish network connections, etc. Combined with the abstraction ability, you can write functions which act on "any computation that produces a stream", etc.

It's really all about Haskell's abstraction abilities that makes it a very fine imperative language.

However, the false half is the syntax. I find Haskell pretty verbose and awkward to use in an imperative style. Here is an example imperative-style computation using the above while loop, which finds the last element of a linked list:

lastElt :: [a] -> IO a
lastElt [] = fail "Empty list!!"
lastElt xs = do
    lst <- newIORef xs
    ret <- newIORef (head xs)
    while (not . null <$> readIORef lst) $ do
        (x:xs) <- readIORef lst
        writeIORef lst xs
        writeIORef ret x
    readIORef ret

All that IORef garbage, the double read, having to bind the result of a read, fmapping (<$>) to operate on the result of an inline computation... it's all just very complicated looking. It makes a whole lot of sense from a functional perspective, but imperative languages tend to sweep most of these details under the rug to make them easier to use.

Admittedly, perhaps if we used a different while-style combinator it would be cleaner. But if you take that philosophy far enough (using a rich set of combinators to express yourself clearly), then you arrive at functional programming again. Imperative-style Haskell just doesn't "flow" like a well-designed imperative language, e.g. python.

In conclusion, with a syntactic face-lift, Haskell might well be the best imperative language. But, by the nature of face lifts, it would be replacing something internally beautiful and real with something externally beautiful and fake.

EDIT: Contrast lastElt with this python transliteration:

def last_elt(xs):
    assert xs, "Empty list!!"
    lst = xs
    ret = xs.head
    while lst:
        ret = lst.head
        lst = lst.tail
    return ret 

Same number of lines, but each line has quite a bit less noise.


EDIT 2

For what it's worth, this is how a pure replacement in Haskell looks like:

lastElt = return . last

That's it. Or, if you forbid me from using Prelude.last:

lastElt [] = fail "Unsafe lastElt called on empty list"
lastElt [x] = return x
lastElt (_:xs) = lastElt xs

Or, if you want it to work on any Foldable data structure and recognize that you don't actually need IO to handle errors:

import Data.Foldable (Foldable, foldMap)
import Data.Monoid (Monoid(..), Last(..))

lastElt :: (Foldable t) => t a -> Maybe a
lastElt = getLast . foldMap (Last . Just)

with Map, for example:

λ➔ let example = fromList [(10, "spam"), (50, "eggs"), (20, "ham")] :: Map Int String
λ➔ lastElt example
Just "eggs"

The (.) operator is function composition.

Trachoma answered 8/7, 2011 at 9:59 Comment(18)
You can make the IORef noise much less annoying by making more abstractions.Mesomorph
This is - so far - the only answer that IMHO answers the OP's question. But don't be to hard - you can write lastElt in an imperative fashion whithout having to use IO - how about STM?Josiejosler
@FUZxxl, you mean ST? Yeah, you can, but the code looks almost identical (roughly s/IO/ST/g). STM is the same story actually: s/IORef/TVar/g.Trachoma
@augustss, hmm, I'm curious about that. Do you mean more domain-level abstractions, or just by building up a richer imperative sublanguage". For the former, I agree -- but my mind associates imperative programming with low-abstraction (my working hypothesis is that as abstraction increases, the style converges on functional). For the latter, I'd be really interested to see what you mean, because I can't think of how off the top of my head.Trachoma
Your lastElt example seems more complicated than one in a modern imperative language; Wouldn't you rather use a syntax-sugared iterator-foreach loop, instead of manually traversing your list with a while-construct (requiring you to manually manage the iteration variable)? -- and btw, wouldn't a literal Java-syntax translation using a while construct look complicated as well?Smalltime
@hvr, I've added a contrasting example in python to demonstrate my point. It's not perfectly idiomatic python; I'm trying to boil down the function to what I consider the essence of imperative programming: looping and assignment.Trachoma
@Trachoma Using ST would be a good example for characterizing allowed side effects. As a bonus it is possible to jump back into a pure computation from ST.Josiejosler
Using Python as the comparison isn't entirely fair--as you say, it's well-designed, one of the syntactically cleanest imperative languages I'm familiar with. The same comparison would argue that most imperative languages are awkward to use in an imperative style... though, perhaps that's exactly what you meant. ;]Asmara
@camccann, the question asked why Haskell is called the best imperative language; comparing Haskell to a crappy imperative language does not give us any information about the answer to that question :-)Trachoma
Ah, true. I guess I think of the better abstractions as being more than enough to make up for a merely adequate imperative syntax... but yeah, point. I suspect I agree with augustss about better abstractions helping, and I have a few idioms I like, but I don't do enough truly imperative programming in Haskell to justify spending time on making it prettier.Asmara
@Trachoma I mean that your code could look more like the Python code. You don't need all the readIORef and writeIORef, they can be done implicitly.Mesomorph
@augustss, I can see defining an operator for writeIORef. How do you suggest getting rid of the readIORef and the associated need for fmap (I consider the latter a larger annoyance for imperative style)?Trachoma
A footnote to the conversation, for posterity: @Mesomorph using ad-hoc polymorphism to make IORefs implicit, or at least attempting to and being thwarted by changes to GHC. :[Asmara
I see your point, but one of the other reasons Haskell is a good language for imperative code is that you're not forced to write everything in a monad; you can slip in something pure painlessly. Newcomers to Haskell should know your implementation of lastElt is extrordinarily bad Haskell, and we'd write lastElt = return.last (that's all) for the same functionality, instead of this terrifying demonstration of why pure code is nicer to write than imperative code. I know your point was the syntax, but it does highlight one of the reasons you might want to not program in an imperative style.Thornburg
The python comparison is unfair. It is a lossy transformation. You lose all of the static typing when going to python, including the disciplined effects. Not only that, the Haskell version could probably be cleaned up to equal the Python version with static typing and effect tracking...Ayacucho
@nightski, yeah? You want to give this alleged cleanup a try? Make sure lastElt can still have its polymorphic type.Trachoma
Is "actio8n" a typo, or is this spelling intentional?Gustafsson
@AndersonGreen haha definitely a typo. FixedTrachoma
F
24

It's not a joke, and I believe it. I'll try to keep this accessible for those who don't know any Haskell. Haskell uses do-notation (among other things) to allow you to write imperative code (yes, it uses monads, but don't worry about that). Here's some of the advantages that Haskell gives you:

  • Easy creation of subroutines. Let's say that I want a function to print a value to stdout and stderr. I can write the following, defining the subroutine with one short line:

    do let printBoth s = putStrLn s >> hPutStrLn stderr s
       printBoth "Hello"
       -- Some other code
       printBoth "Goodbye"
    
  • Easy to pass code around. Given that I've written the above, if I now want to use the printBoth function to print out all of a list of strings, that's easily done by passing my subroutine to the mapM_ function:

    mapM_ printBoth ["Hello", "World!"]
    

    Another example, although not imperative, is sorting. Let's say you want to sort strings solely by length. You can write:

    sortBy (\a b -> compare (length a) (length b)) ["aaaa", "b", "cc"]
    

    Which will give you ["b", "cc", "aaaa"]. (You can write it shorter than that, too, but never mind for now.)

  • Easy to re-use code. That mapM_ function is used a lot, and replaces for-each loops in other languages. There's also forever which acts like a while (true), and various other functions that can be passed code and execute it in different ways. So loops in other languages are replaced by these control functions in Haskell (which are not special -- you can define them yourself very easily). In general this makes it hard to get the loop condition wrong, just like for-each loops are harder to get wrong than the long-hand iterator equivalents (e.g. in Java), or array-indexing loops (e.g. in C).

  • Binding not assignment. Basically, you can only assign to a variable once (rather like single static assignment). This removes a lot of confusion about the possible values of a variable at any given point (its value is only set on one line).
  • Contained side effects. Let's say that I want to read a line from stdin, and write it on stdout after applying some function to it (we'll call it foo). You can write:

    do line <- getLine
       putStrLn (foo line)
    

    I know immediately that foo doesn't have any unexpected side effects (like updating a global variable, or deallocating memory, or whatever), because it's type must be String -> String, which means it is a pure function; whatever value I pass, it must return the same result every time, without side effects. Haskell nicely separates the side-effecting code from the pure code. In something like C, or even Java, this is not obvious (does that getFoo() method change state? You'd hope not, but it might do...).

  • Garbage collection. A lot of languages are garbage collected these days, but worth mentioning: no hassles of allocating and deallocating memory.

There's probably a few more advantages besides, but those are the ones that come to mind.

Frugivorous answered 8/7, 2011 at 10:1 Comment(10)
I would add in the strong type safety. Haskell allows to the compiler to eliminate a large class of errors. After working on some Java code recently, I was reminded how horrible null pointers are and how much OOP is missing without sum types.Palinode
Thanks for your elaboration! Your mentioned advantages seem to boil down to Haskell treating ''imperative'' effects as a first-class objects (which thus are combinable) together with the ability to ''contain'' those effects to a delimited scope. Is this an adequate compressed summary?Smalltime
@hvr: I can't speak for Neil, but that's an excellent compressed summary of the argument I would have made if the existing answers hadn't already covered things well enough.Asmara
@Neil: Jfyi, I would have flagged both yours and luqui's as answers to my question, but alas the SO-system requires one answer to be the very one...Smalltime
@Michael Snoyman: But sum types are easy in OOP! Just define an abstract class that represents the Church encoding of your data type, subclasses for the cases, interfaces for classes that can process each case, and then pass objects that support each interface to a sum object, using subtype polymophism for control flow (as you should). Couldn't be simpler. Why do you hate design patterns?Asmara
@camcann, Also, null pointers really do combine benefits of undefined and Nothing without any of their disadvantages (no way to detect the first and inconvenient to work with the second). :DIntentional
@camccann I know you're joking, but that's essentially what I implemented in my project.Palinode
@Michael Snoyman: Good choice, then! The real joke is that I was describing what's pretty much the best encoding in a way that sounded like a joke. Ha, ha! Laughing all the way to the gallows...Asmara
Data.Ord comparing (for those wondering about the writing the sortBy shorter)Merchandise
Using Data.Ord.comparing the example would become sortBy (comparing length) ["aaaa", "b", "cc"] which has the additional advantage of reading like normal English. There's also the slightly newer Data.List.sortOn which makes it even more compact: sortOn length ["aaaa", "b", "cc"].Sync
O
17

In addition to what other's have already mentioned, having side-effecting actions be first-class is sometimes useful. Here's a silly example to show the idea:

f = sequence_ (reverse [print 1, print 2, print 3])

This example shows how you can build up computations with side-effects (in this example print) and then put the in data structures or manipulate them in other ways, before actually executing them.

Obsequious answered 8/7, 2011 at 11:20 Comment(1)
I think the javascript code that corresponds to this would be: call = x => x(); sequence_ = xs => xs.forEach(call) ;print = console.log; f = () => sequence_([()=> print(1), () => print(2), () => print(3)].reverse()). The main difference I see is that we need a few extra () =>.Sync
C
0

Using the same example as @Chi in this answer, you can use the State monad to simulate an imperative loop with recursion:

C code:

// sum 0..100
i = s = 0;
while (i <= 100) {
   s = s+i;
   i++;
}
return s;

Haskell code:

import Control.Monad.State
final_s :: Int
final_s = evalState sum_loop (0, 0)  -- evaluate loop with initial state (0, 0)
sum_loop :: State (Int, Int) Int
sum_loop = do
  (i, s) <- get           -- retrieve current state
  if i <= 100             -- check loop condition
    then do               -- if condition is true:
      let new_s = s + i
      let new_i = i + 1   
      put (new_i, new_s)  -- update state with new tuple
      sum_loop            -- recursively call loop with new state, simulate iteration with recursion
    else
      return s            -- if condition is false, return s as final result

main = print final_s

As you can see this is quite similar to the C code, we just have 3 more lines:

  • (i, s) <- get to get the current state.
  • put (new_i, new_s) to update the current state with the new state
  • sum_loop to recursively call loop with new state, simulating iteration with recursion

You can add debug only printing with put $ traceShowId (new_i, new_s) instead of put (new_i, new_s), but you should only use this for debugging because it cheats the type system.

So a few things more things have to handled "manually" but it is possible to write reasonably readable imperative code in Haskell.

Centaurus answered 17/3, 2023 at 0:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.