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

Did you perhaps jump into the water too quickly? I'm currently learning a couple of functional languages (including Haskell) and using it in production environments but my current use is restricted to "I have an input that will always produce a certain output. There are no database or environmental dependencies, this is straight computation. I want to never have to worry about this function ever again". And so far, knock on wood, haskell has been killer for that scenario. I'll probably eventually transition a lot more of my code to functional languages, but will do so slowly (using Go otherwise).


Haskell is beautiful, I love it, but I can easily see his points. There are a few traps one can easily fall in:

- Reach a point in a complex application where it becomes hard to reason what laziness will do to performance.

- End up in type-hell. E.g. some libraries extensively use existential quantification of type variables. Before you know it, you are chasing "type variable x would escape its scope"-type or error messages, in perfectly fine looking code.

- Pattern matching is nice, but if you extensively use it, adding a constructor argument is a lot of work.

- No-one uses the same data type for common things. For instance, for textual data, there is String, Data.Text, Data.Text.Lazy, Data.ByteSting, and Data.ByteString.Lazy. These days there is more or less consensus on when to use which type, but you are often converting things a lot. There are also types of data for which no consensus is yet reached (e.g. lenses).

- Artificially pure packages. There are some packages that link to C libraries, but (forcefully) provide a pure interface. (Or in other words: purity is just convention).

- For a lot of code you end up using monads plus 'do' notation, making your programs look practically imperative, but an oddball variation of it.

- Using functions with worse time or space complexity, to maintain purity.

- I/O looks simple, but for predictable and safe I/O you'd usually end up using a library for enumerators. Writing enumerators, enumeratees, and iteratees is unintuitive and weird, especially compared to (less powerful) iterators/generators in other languages.

Learning Haskell is something I'd certainly recommend. It provides a glimpse of how beautifully mathematical programs could be in a perfect world. Unfortunately, the world is not perfect, and even Haskell needs a lot of patchwork to deal with it.


> Artificially pure packages. There are some packages that link to C libraries, but (forcefully) provide a pure interface. (Or in other words: purity is just convention)

Explain? What would the alternative be?

> Using functions with worse time or space complexity, to maintain purity.

This seems like the opposite of your previous complaint.

> For a lot of code you end up using monads plus 'do' notation, making your programs look practically imperative, but an oddball variation of it.

This seems to be a "psychological problem" with Haskell: the idea that because Haskell supports declarative, it's not OK to be imperative. It makes beginners tear their hair out looking for 'do'-free solutions when they could just use 'do'. C.f., "Lambda: the Ultimate Imperative" (and the rest of that series of LtU papers) http://dspace.mit.edu/handle/1721.1/5790


Explain? What would the alternative be?

Box the value that is the result of evaluation an expression that calls impure code in IO?

This is what I'd expect for calling impure code in third-party libraries.


If the library developer can prove that a C operation is pure, why shouldn't he tell Haskell about that?


And if you don't trust the developer, it's easy to fix his mistake:

    unUnsafePerformIO :: a -> IO a
    unUnsafePerformIO = return


Sorry, that doesn't actually work.

    ezyang@ezyang:~$ cat Test.hs
    import System.IO.Unsafe
    unUnsafePerformIO = return
    main = do
      let a = unUnsafePerformIO (unsafePerformIO (putStrLn "boom"))
      a
      a
    ezyang@ezyang:~$ runghc Test.hs
    ezyang@ezyang:~$


Yes, true. Patch the library if the annotation is wrong :)


I would say that after a little more experience, space leaks are the only thing that really worries me in Haskell. It's one of those things that I have to think about a little too much to really feel "safe" about. (The other worry is expressions that evaluate to ⊥ at runtime, but it's been shown that static analysis can solve that problem. I don't actually use those tools, though, so I guess I'm a tiny bit afraid of those cases. Like with other languages, write tests.)

Your other concerns don't seem too worrisome to me. Type hell doesn't happen very much, though there are some libraries that really like their typeclass-based APIs (hello, Text.Regex.Base) which can be nearly impossible to decipher without some documentation with respect to what the author was thinking (``I wanted to be able to write let (foo, bar) = "foobar" =~ "(foo)(bar)" in ...'').

The data type stuff can be confusing for people used to other languages, where the standard library is "good enough" for most everything people want. A good example is Perl, which uses "scalar" for numbers, strings, unicode strings, byte vectors, and so on. This approach simply doesn't work for Haskell, because Haskell programmers want speed and compile-time correctness checks. That means that ByteString and Text and String are three different concepts: ByteString supports efficient immutable byte vectors, Lazy Bytestrings add fast appends, Text adds support for Unicode, and String is a lazy list of Haskell characters.

All of those types have their use cases; for a web application, data is read from the network in terms of ByteStrings (since traditional BSD-style networking stacks only know about bytes) and is then converted to Text, if the data is in fact text and not binary. Your text-processing application then works in terms of Text. At the end of the request cycle, you have some text that you want to write to the network. In order to do that, you need to convert the Unicode character stream to a stream of octets for the network, and you do that with character encoding. The type system makes this explicit, unlike in other languages where you are allowed to write internal strings to the network. (It usually works since whatever's on the other end of the network auto-detects your program's internal representation and displays it correctly. This is why I've argued for representing Unicode as inverse-UTF-8 in-memory; when you dump that to a terminal or browser, it will look like the garbage it is. But I digress.)

I understand that people don't want to think about character encoding issues (since most applications I use are never Unicode-clean), but what's nice about this is that Haskell can force you to do it right. You may not understand character sets and character encodings, but when the compiler says "Expected Data.ByteString, not Data.Text", you find that ByteString -> Text function called "encodeUTF8" and it all works! You have a correct program!

With respect to purity; purity is a guarantee that the compiler tries to make for you. When you load a random C function from a shared library, GHC can't make any assumptions about what it does. As a result, it puts it in IO and then treats those computations as "must not be optimized with respect to evaluation order", because that's the only safe thing it can do. When you are writing an FFI binding, though, you may be able to prove that a certain operation is pure. In that case, you annotate the operation as such ("unsafePerformIO"), and then the compiler and you are back on the same page. Ultimately, our computers are a big block of RAM with an instruction pointer, and the lower you go, the more the computer looks like that. In order to bridge the gap between stuff-that-haskell-knows-about and stuff-that-haskell-deson't-know-about, you have to think logically and teach the runtime as much about that thing as you know. It's hard, but the idea is that libraries should be hard to write if they'll make applications easier to write. If everyone was afraid to make purity annotations, then everything you ever did would be in IO, and all Haskell would be is a very nice C frontend.

For a lot of code you end up using monads plus 'do' notation, making your programs look practically imperative, but an oddball variation of it.

That's really just an opinion, rather than any objective fact about the language. I find that do-notation saves typing from time to time, so I use it. Sometimes it clouds what's going on, so I don't use it. That's what programming is; using the available language constructs to generate a program that's easy for both computers and humans to understand. Haskell isn't going to save you from having to do that.

Using functions with worse time or space complexity, to maintain purity.

ST can largely save you from this. A good example is Data.Vector. Sometimes you want an immutable vector somewhere in your application (for purity), but you can't easily build the vector functionally with good performance. So, you do a ST computation where the vector is mutable inside the ST monad and immutable outside. ST guarantees that all your mutable operations are done before anything that expects an immutable vector sees it, and thus that your program is pure. Purity is important on a big-scale level, but it's not as important in a "one-lexical-scope" level. Haskell let's you be mostly-pure without much effort; other languages punt on this by saying "nothing can ever be pure, so fuck you". I think it's a good compromise.

I/O looks simple, but for predictable and safe I/O you'd usually end up using a library for enumerators. Writing enumerators, enumeratees, and iteratees is unintuitive and weird, especially compared to (less powerful) iterators/generators in other languages.

IO is hard in any language. Consider a construct like Python's "with":

    with open('file') as file:
        return file
That construct is meaningless, since the file is closed before the caller ever sees the descriptor object. But Python lets you write it, and guaranteeing correctness is up to you. In Haskell, that's not acceptable, and so IO works a little differently. Ultimately, some things in Haskell are a compromise before simplicity of concepts and safety guarantees at compile time. You can write lazy-list-based IO in Haskell, but you can run out of file descriptors very quickly. Or, you can use a library like iteratees, and have guarantees about the composability of IO operations and how long file descriptors are used for. It's up to you; you can do it the easy way and not have to learn anything, or you can do some learning and get a safer program. And that's the same as any other programming language.


Haskell is great for pure algorithms like that.

As for jumping in too quickly? I was a pretty heavy haskell user for about 3 years.




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

Search: