Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Here's a question for the Haskellers out there:

How does Haskell help you write less code? What mechanisms are in the language that allow you to write higher level abstractions (as compared to, say, Ruby or Python)?

How does Haskell help you to express concepts that are very difficult to express in other languages?



One of the things that I find pleasing when writing Haskell is the idea that I don't have to think about the context in which my code might need to run.

For example, a common pattern that I find myself repeating in many languages is downloading a bunch of webpages asynchronously and collecting the results in a consistent order. To do this in Go I might wire up a channel and collect the results that way. In C# I might use the BeginAsync functions and possibly locks to collect all my results. And what if one of them times out? Throws an exception?

In almost every language I have had to specifically think about all these situations, making sure I have handled all of the exceptions and timeouts properly and by the end of the day I usually end up with a mudball procedure that is really good at asynchronously downloading webpages, but it is far from reusable.

In Haskell, once I'm done writing my function "downloadWebPage", I just lift it into the context of the problem I'm trying to solve. If I'm trying to download a page asynchronously, I'll use "asyncDl = forkAsync downloadWebPage" to make it a function that downloads a page asynchronously. If I need to handle timeouts, I'll use "timeout 200000 . asyncDl" to make a function that returns Nothing after a certain amount of time. What if I wanted to do this for a list of web pages? Well I just use mapM run the asynchronous, timed out webpage download function over a list of websites.

What if I want to use any function instead of one that download websites? Well you make a library: https://github.com/jb55/async-util

What I find in Haskell/Lisp that I have not seen in many other languages is the idea of a combinatoric API. One where combining functions is your main tool for building abstractions rather than more rigid computation paths. The amount of code that you do not have to write when programming this way is something that you just don't see in other languages.


Haskell abstracts things at an even higher level than most programming languages. For example, the infamous monads are a way of abstracting out the details of how 'effectful' computations (i.e. computations that do something other than simply taking arguments and returning values) should work. Monads model effects like exceptions, mutable state, input/output, nondeterminism, and so forth, and by abstracting them into the same pattern, it allows you to write functions that work with any effect in the same way. For example, if I have a list of operations which have effects and all have the same result type, I can sequence them together using the sequence function, so for IO:

    sequence [putStrLn "foo", putStrLn "bar", putStrLn "baz"]
will print those three strings on their own lines. However, I can use the same function for Haskell's equivalent of null:

    sequence [Just 3, Just 8, Just 5] -- results in Just [3, 8, 5]
    sequence [Just 3, Nothing, Just 5] -- results in Nothing, because Nothing is
                                       -- propagated if it occurs
or for nondeterminism

    sequence [[1, 2], [3, 4], [5, 6]] -- results in every possible list whose first
                                      -- element is drawn from the first list, whose
                                      -- second element is drawn from the second
                                      -- list, and whose third element is drawn from
                                      -- the third list.
or for any other effect that Haskell has. A lot of learning Haskell has to do with looking at the abstractions Haskell offers (monads are only one; you also get things like arrows, functors, applicative functors, &c) and understand how you can phrase your problems in terms of them and consequently use Haskell's abstractly written functions to your favor. A great practical example here is parsing; parser combinators can be expressed as monads or as applicative functors, both of which make your parsing code very small and quite natural to read. For example, the following is a function to parse strings like "(552,864)" using the Parsec parser combinator library:

    parseDigit = oneOf ['0'..'9']
    parseInt = many parseDigit
    parsePairOfInts = do
        string "("
        p1 <- parseInt
        string ","
        p2 <- parseInt
        string ")"
        return (p1, p2)


Why can't you use monads in ruby? A monad is essentially just an interface, right?

The original question was about the mechanisms, not the style or libraries.

I don't really know haskell, so correct me if I'm wrong.


Type-classes are like interfaces, but not quite the same.

They allow return-type polymorphism, which ordinary OO interfaces do not. And the Monad type-class requires this feature.

Basically, when you call the "return" function in Haskell, (e.g: return 5) -- the code being called depends on the type of the result. The type of the result is determined from the context (by type-inference or rarely, type annotations). In Ruby or Python, there's no easy/direct way to encode something like "return" such that it works with any Monad instance.


Interesting. I see what you are saying, but could you provide an example (or a link to an example) for clarity? Preferably something not easily accomplished in ruby?


I found an article about return-type polymorphism. See http://vpatryshev.blogspot.com/2010/01/dispatch-by-return-ty...

One thing is impossible, though: defining two methods that differ only in return type, something like

Most important point:

Something like

    int i = parse(String source);
    long l = parse(String source);
    boolean b = parse(String source);
is possible in Haskell, but not in Python, and barely imaginable in Java.


Elaboration:

  class Read r where
    read :: String -> Maybe r
Note that it's polymorphic on the type of the return within the Maybe. It's very flexible, the polymorphic type can appear anywhere within the type signature.

Now you can write various functions that use "read" and they all remain return-type polymorphic. For example, you can write one that loops, requesting the user to repeat entry until parse-able data (of the wanted type) is given:

  repeatReadingUntilValid :: Read r => IO r
  repeatReadingUntilValid = do
    line <- getLine
    case read line of
      Nothing -> do
        putStrLn $ "Invalid input: " ++ show line
        repeatReadingUntilValid
      Just result ->
        return result
That's just a silly example, because read isn't that interesting.

Another example is QuickCheck, which uses type-classes to auto-generate fuzz-testers for functions.

For example:

  import Test.QuickCheck

  pretty :: MyType -> String
  pretty = .. pretty print my type here ..

  unpretty :: String -> MyType
  unpretty = .. parse the pretty printing of my type here ..
Now I can test that unpretty is indeed the inverse of pretty:

  quickCheck (\x -> unpretty (pretty x) == x)
(for every x, the unpretty of pretty of x equals x).

I can generalize this property to:

  isInverse f g x = f (g x) == x
And then use:

  quickCheck (isInverse unpretty pretty)
Similarly you can define:

  commutative f x y   = x `f` y == y `f` x
  associative f x y z = (x `f` y) `f` z ==
                        x `f` (y `f` z)
  transitive f x y z = x `f` y && y `f` z ==> x `f` z
Which makes an important type of unit testing a breeze.

The type system is saving us from writing code here.


Parsec looks pretty similar to python's pyparsing or Pysec.Pysec is a "Monadic Combinatoric Parsing" library in python, so i think you could do monads in python.


If you can do continuation passing, you can implement any monad instance. You still may not be able to encode the Monad generalization (which indeed, Python cannot).

As for continuation passing, Python can do explicit continuation passing, but it has caveats:

* No TCO means it doesn't work well in larger cases

* Explicit CPS is ugly. yield-based continuation passing is restricted. You can't implement non-determinism via generators.

So using yield, you can implement most monad instances in Python (excluding non-determinism).

But Haskell can goes beyond a particular/useful monad instance (e.g: Parsing) and has a Monad generalization (basically a type-class that allows lots of libraries to be written such that they work with any monad, not just this parsing monad).

Additionally, monad transformers are a technique to compose monad types together to form a new monad that can do the things each of the monads can.

For example, I can compose the non-determinism monad with the parsing monad to get a non-determinism parser that yields various possible parse results.


Because it's pure, you can know what comes out at the other end when you put something in. (That sounds more disturbing than it's meant to be). You can build pieces that you can reuse within many different structures, like different types of pipelines and filters. All without having to worry about leaks or the wrong stuff ending up in the wrong place.


Haskell has some expresiveness advantages over Python/Ruby, but it also has some expressiveness deficiencies compared with these languages. I can list some of these if you're interested.

But the end result is that in my experience, Haskell code is not much shorter than Python code (I don't have much Ruby experience, but I suspect it's probably on par with Python).

IMO, the main advantage of Haskell over Ruby/Python, is that you don't pay the enormous prices for the high expressiveness that you do for roughly the same expressiveness in Ruby/Python. I find it much easier to maintain Haskell code, it has virtually no runtime failures, it has good performance, and so forth.

I cannot possibly list all of the useful things that are nice about Haskell, the rabbit hole is very deep. But I'll enumerate just a few.

Type-classes:

As a simple illustrative example, let's look at the Show class:

  class Show s where
    show :: s -> String
This means: Any type "s" is an instance of Show, iff it is declared an instance, and has an implementation of the "show" function.

Example instance:

  data Bool = False | True

  instance Show Bool where
    show True = "True"
    show False = "False"
This definition also illustrates Pattern-Matching, which is a pretty useful benefit on its own.

Note, Haskell has a "deriving" mechanism that allows replacing the above boilerplate with:

  data Bool = False | True
    deriving (Show)
So far, we've used only type-classes that Python interfaces can encode. This is basically Python's __repr__ (except it's auto-generated to a sensible implementation that is usable as Haskell syntax).

Here's something Python/Ruby cannot directly do:

  class Read r where
    read :: String -> Maybe r
Note that "r" appears in the result type of "read".

Now, you can compose "read" with other functions to build interesting values, that are still polymorphic on the result type. For example:

  readLine :: Read r => IO (Maybe r)
This type means: Given that the "r" type is an instance of Read (its values can be parsed from String), readLn is an effectful procedure that when executed, yields a "Maybe r" value (which may be Nothing if input is not parseable).

With Python/Ruby you may be able to encode this via classmethods, but then all the relevant classes have to be modified to inherit from this. You can try returning factories, but then these must return any particular type they're requested. There's no easy/direct way to encode this as in Haskell.

This is basically a safe __unrepr__, which is not implementable in this way in Python not just because of the lack of return-type polymorphism, but also because of mutability/identity concerns. Immutability-by-default is another nice benefit, gets rid of a whole slew of object-identity/aliasing issues.

On top of the extremely versatile type-class mechanism, many useful abstractions are built, which do not exist in Python. Monads are just one of these useful abstractions.

For example, using lists and their instance of the Monad typeclass, I can write the "powerset" function as:

  powerset xs = filterM (\x -> [True,False]) xs
filterM is much like filter, except it allows a monadic "effect" to be applied at the predicate function. In this case, the effect is non-determinism (or multiple results). We ignore the value of the element, and accept AND reject every element in the list -- thus we get the powerset.

Concurrency:

One of Haskell's models of concurrency is very similar to Erlang, and uses immutable-shared-state threads with explicit-mutable-state. This is great for both performance and simple semantics (the rarity of mutable state makes reasoning about data easy. The preemption makes reasoning about starvation easy). Haskell's threads are also user-level cheap threads, and you can literally create millions of them.

Say we want to do latency-hiding on some expensive procedure. I'll present the Haskell code to do so by creating a thread-pool that preemptively executes the given operation and passes the result to callers that need it:

  preemptively :: Int -> IO a -> IO (IO a)
  preemptively poolSize act = do
    requestMVar <- newEmptyMVar
    replicateM_ poolSize . forkIO . forever $ do
      res <- try act
      responseMVar <- takeMVar requestMVar
      putMVar responseMVar res
    return $ do
      responseMVar <- newEmptyMVar
      putMVar requestMVar responseMVar
      either ioError return =<< takeMVar responseMVar
preemptively takes a number of threads to run, and an action to preemptively execute in all of those threads. It creates an empty MVar (basically an inter-thread channel with a 1-sized buffer) for placing requests.

Then it uses "replicateM_ poolSize . forkIO . forever $ do ...". This means the same as: replicateM_ poolSize (forkIO (forever (do ...))). forever (do ...) executes the given do block in a loop, forever. forkIO executes the forever-loop in its own thread. replicateM_ poolSize executes the given forkIO action poolSize times. So this single line is enough to create our thread pool.

Then comes the code for the thread's single iteration. In each iteration, the thread executes "act" via "try". So any IO exceptions are caught and converted to a value, and placed in "res". Then the thread waits for someone to request a result by waiting on the mvar (takeMVar). Note the requests put on the requestMVar consist of a response MVar to reply to. The thread then places the result of the execution (which may be a value or an exception) into the response mvar.

Then we return an action that asks a thread in the pool for a result, and re-raises the IO exception if there was one, or just returns the threads' result.

Now, if you want to use preemptively, all you have to do is:

  getConnection <- preepmtively 20 (sshConnect remoteHost)
And you have a thread pool of 20 threads that preemptively make an ssh connection to your remote host. getConnection will then be immediate if it happens after the threads are done.

This example is meant to illustrate that real-world, concurrent, effectful programming is nice in Haskell, and not just the theoretical pure stuff.


Thank you for your detailed answer.

If I gather from the other answers here correctly, the Haskell money feature (for abstraction) is type-classes.

FYI, I'm keying off of abstractions/features here because I see that as a clear route towards writing less code (less bugs).


> If I gather from the other answers here correctly, the Haskell money feature (for abstraction) is type-classes.

On a more elementary level, lots of languages could benefit from algebraic data types. Most languages only support the `product' part of them well, but the `sum' is equally important.

Where product means in terms of C putting things together in a struct, and a sum is more like union. Only that C's unions are dangerous.


I'll also add that type-classes are the main feature in Haskell that Python/Ruby cannot easily encode.

There are many interesting features that Python/Ruby/Lisp-variants can encode, but Haskell does with static guarantees, whereas Python/Ruby use dynamism (e.g: general polymorphism, "duck typing", various strong type system features that Python/Ruby do not "need" because they don't attempt to give guarantees, etc).


You also get far less bugs because of controlled effects and the strong polymorphic types (i.e: Haskell's strong type system).


http://news.ycombinator.com/item?id=2722732

Dons does a good job showing how my python code is significantly shorter when he writes the equivalent haskell.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: