Hacker News new | comments | show | ask | jobs | submit login
Why learning Haskell makes you a better programmer (dubhrosa.blogspot.co.uk)
205 points by dons 1760 days ago | hide | past | web | 98 comments | favorite



This is a great article.

To me, the most powerful example is this: "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. "

That's a higher level of thinking that is faster and yet at the same time more precise than thinking in terms of imperative loops.

Haskell has taught him to think more powerfully.


I recommend people study APL / J / K for the same reason: It has the right primitives that make everything expressible as maps, folds, filters, scans, ranges, and stuff like that.

Even more so than Haskell.

It doesn't have monads and the kind of abstractions that lets you modify control flow semantics. It doesn't even have the facilities to build abstract data types - which makes you work with less-abstract data, and realize that although some abstraction is useful, most of what is practiced today is useless.

APL / J / K promote, at the same time, container abstraction, and concrete down-to-the-metal real work.


I've always wanted to dive into these guys. Is there a good introduction for fluent Haskell programmers somewhere?


I haven't done anything in Haskell, but I doubt that makes much of a difference.

Maybe more inspiration than introduction, but the "Game of Life in APL" video is a must see (http://uk.youtube.com/watch?v=a9xAKttWgP4&fmt=18)

The description on YouTube points to a "Game of Life" tutorial at http://tryapl.org. I haven't tried it, but it looks nice.

For a more academic (as in "read about, don't play with") introduction, I found Iverson's "Notation as a Tool of Thought" (http://awards.acm.org/images/awards/140/articles/9147499.pdf) a good introduction.


I don't know how i feel about APL now, but I have some funny APL stories. One of my friends was the Jimi Hendrix of IBM APL2, he was amazing, unfortunately i can't tell the stories but when he learns haskell he'll be top notch. Another was the Dweezil Zappa of APL, he learned the language as a high school sophomore or junior from Iverson himself in Yorktown Heights.


Learning other languages can broaden your repertoire in a similar way. Why is it more important to decompose problems into map/fold/filter/scan than other sets of operations?


Well, when you really boil it down, these are the fundamental operations.

At least, I believe that is true. Someone stronger in CS theory could come along and verify/correct that statement.

This paper [1], for example, appears to argue that FOLD (foldr in particular) is universal, meaning that there is only one way to move through a list and resolve it to a single value. In other words, you may have some other algorithm that you think is not a FOLD, but if it moves through a list and resolves it to one value, then your algorithm is FOLD. (I say the paper "appears" to claim that, because in all honesty there's about 10-15% of the paper that I didn't fully grok).

I suspect that applies to map/filter/etc. After all, if you look at their definitions, they contain nothing but the essence of the operation (map/filter/fold/etc) itself.

So, map/filter/fold/etc are THE operations on lists.

[1] http://www.cs.nott.ac.uk/~gmh/fold.pdf


Interesting. Going through the 4clojure problems gave me a taste of this.. And the ->/->> operators make it even simpler.


..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...

You mean these functions - http://karma-engineering.com/lab/wiki/Tutorial5

Why Haskell, then?)


Type safe pattern matching would be my reason, followed by separation of effects from pure functions, and allowance for creating real data structure types so we don't have to do things like "(define make-tree cons)" as in the last example of your page.

We can instead give tree a proper type, give operations over the type proper signatures which help to explain what the operations do, and get verification from the compiler at the same time that we aren't mixing things up.

It's hard to beat data Tree a = Node a [Tree a]


(define make-tree cons) - is very natural thing - it is called synonym - just another word for the same thing in a new context. Moreover, this is a better candidate for a proper way.)

separation of effects from pure functions - this I cannot understand. Aren't these function pure?

And another part of "properness" - each node of a tree is a tree itself. Sometimes less types is more.)

Well, I'm old-fashioned, but I think that being able to put any kind of data into a list is the strength, not weakness of a language, and that user-defined ADTs and compound data-structures needs no explicit typing, they are nothing but conventions.


Think again.

Types provide one important thing: static guarantees. Compile-time type checking can only work with strong, expressive data types, and ADTs provide a very good way of enforcing that. (Together with Haskell's `newtype` keyword, which really is nothing more than a convention, if you will, a different way of expressing a type synonym, but with the static guarantee that when you make up a `newtype Name = Name Text`, you cannot use `Name` wherever you can use `Text`, but you can access the `Text` `Name` is a wrapper for easily.

This makes it much easier for your intent to be expressed directly in the code, namely in the data types. Just compare:

    makePerson :: Name -> Age -> Gender -> Address -> Person
    makePerson' :: Text -> Int -> Text -> Text -> Person


I really like your executive summary there, these functional concepts really help with solution composition and abstraction. I really am pleased the penny has dropped with myself too. I am trying daily to encourage my colleagues to spend some time learning these functional fundamentals!


> Haskell makes your functions pure by default. You have to change the type signature if you want to do IO

The author seems to be conflating IO and side effects. IO is just one of the many ways that side effects can occur.

> Purity also means you can't read or write global variables, that's just another type of IO.

Er... what? No, not at all.

Reading global variables is not side effects, much less IO.

Writing global variables is side effects, but has nothing to do with IO.

> Functions that do IO are difficult or impossible to test.

Absolutely not, IO functions are not harder to test than regular ones. Is your function creating a file? Start your test, assert the file doesn't exist, call your function, assert your file exists. Done.

> I now put all my IO in top-level functions that are called directly from main

This is great for homework, but in the real life, IO needs to be performed throughout your entire application, and unsafePerformIO is just not an option. You have to use iteratees for this kind of operation, which adds a pretty thick layer of complexity to your code base.

> boo :: Map Integer String -> String -> Integer

This syntax is clean but it doesn't tell me anything that

def boo(map: Map[Integer, String], key: String) : Integer

doesn't tell me.

> The point is, you can't confidently reason about a function from its signature if IO is involved.

Sure, you can: the mention of IO tells me this function operates within the IO monad, so I know a lot of operations and laws that apply to it.

Haskell is a fantastic language but this article is doing a very poor job at explaining why. I suspect its author has only recently started using Haskell and would have been better advised to wait a bit before sharing his enthusiasm.


I'm not sure if it's due to a misunderstanding, but many of your comments seem to be incorrect or nonsequiturs.

> > Haskell makes your functions pure by default. You have to change the type signature if you want to do IO

> The author seems to be conflating IO and side effects. IO is just one of the many ways that side effects can occur.

Indeed, but that does not refute the OP's statement.

> > Functions that do IO are difficult or impossible to test.

> Is your function creating a file? Start your test, assert the file doesn't exist, call your function, assert your file exists. Done.

Right, and (in my opinion) that is more difficult than testing something that doesn't create a file.

> > boo :: Map Integer String -> String -> Integer

> This syntax is clean but it doesn't tell me anything that

> def boo(map: Map[Integer, String], key: String) : Integer

> doesn't tell me.

Indeed, but again that does not refute the OP's statement which was simply that the syntax is clean.

> > The point is, you can't confidently reason about a function from its signature if IO is involved.

> Sure, you can: the mention of IO tells me this function operates within the IO monad, so I know a lot of operations and laws that apply to it.

You do? Please tell me a law that applies to IO.


> > boo :: Map Integer String -> String -> Integer

> This syntax is clean but it doesn't tell me anything that

> def boo(map: Map[Integer, String], key: String) : Integer

> doesn't tell me.

OP's point was not so much that the Haskell syntax is clean, but more that there is a huge difference between

Map Integer String -> String -> Integer

and

Map Integer String -> String -> IO Integer

The latter is allowed to do IO, while the former is not. There are very few languages that allow you to express this difference and have the compiler enforce it (Haskell being one of them). So "Map Integer String -> String -> Integer" tells you a lot more than "def boo(map: Map[Integer, String], key: String) : Integer" does.

For more information about what exactly types tell you, look up parametricity or read Philip Wadler's "Theorems for free".


"Haskell being one of them"

Also Fortran being one of them (you can mark functions and subroutines 'pure').


> You do? Please tell me a law that applies to IO.

How about three? Right identity, left identity and associativity.

That's for the laws. For the properties, IO is a monad so I can compose any value in the IO monad with other monads (Maybe, ST, etc...).


There are certainly laws as to what goes on when you do various things with IO values. The relevant point, though, was that access to IO means there are few (any?) laws restricting what is actually going on when that integer is generated.

Actually, I kind of wish IO was split up a bit... I have been meaning to play with rolling some typeclasses (with an IO instance and a testing instance) that wrap up IO of various types, so I define my functions in terms of these and spot what kind of IO various functions might do - I'm not yet sure how much of a win it will be, but it seems like something to explore.


I've seen them created a few times. I'm pretty sure there are at least a few implementations in Hackage of such divided IO monads. More generally, the haute method for solving this would be to use Operational or Free monads to create your restricted monad type and then interpret it into IO.

http://www.haskellforall.com/2012/07/purify-code-using-free-...

http://www.haskellforall.com/2012/06/you-could-have-invented...


I think optimally, with a set of typeclasses, one could define instances for IO and just use things with no interpretation or build a datastructure with free monads to perform whatever other inspections or manipulations you want. There could be some incompatability between the two notions that I'm missing, though...


I think I've seen the typeclass route as well before. It may have just been a blog post and not a library implementation, though. The two methods have different epistemological ideas, but could be combined. For instance:

    {-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving, FlexibleInstances #-}
    module CrWr where
    
    import Control.Monad.Free
    import Data.IORef
    
    data RWRefF t a = NewRef t (IORef t -> a) | PeekRef (IORef t) (t -> a) | PutRef (IORef t) t a
                    deriving Functor
    newtype RWRef t a = RWRef (Free (RWRefF t) a) deriving Monad
    
    newRef :: t -> RWRef t (IORef t)
    newRef t = RWRef $ liftF $ NewRef t id
    peekRef :: IORef t -> RWRef t t
    peekRef r = RWRef $  liftF $ PeekRef r id
    putRef :: IORef t -> t -> RWRef t ()
    putRef r t = RWRef $ liftF $ PutRef r t ()
    
    interpret :: RWRef t a -> IO a
    interpret (RWRef (Pure a)) = return a
    interpret (RWRef (Free (NewRef t f))) = newIORef t >>= interpret . RWRef . f
    interpret (RWRef (Free (PeekRef r f))) = readIORef r >>= interpret . RWRef . f
    interpret (RWRef (Free (PutRef r t next))) = writeIORef r t >> interpret (RWRef next)
    
    -- Highly specialized class of IO monads
    class Monad m => RWIntRefMonad m where
      new  :: Int -> m (IORef Int)
      peek :: IORef Int -> m Int
      put  :: IORef Int -> Int -> m ()
      toIO :: m a -> IO a
    
    instance RWIntRefMonad (RWRef Int) where
      new = newRef
      peek = peekRef
      put = putRef
      toIO = interpret
    
    incr :: RWIntRefMonad m => IORef Int -> m ()
    incr r = peek r >>= put r . (+1)
    
    main = do r <- newIORef 0
              -- We need to specialize before the m gets erased
              toIO (incr r :: RWRef Int ())
              readIORef r >>= print


> Actually, I kind of wish IO was split up a bit... I have been meaning to play with rolling some typeclasses (with an IO instance and a testing instance) that wrap up IO of various types, so I define my functions in terms of these and spot what kind of IO various functions might do - I'm not yet sure how much of a win it will be, but it seems like something to explore.

There's a section on this practice in Real World Haskell: http://book.realworldhaskell.org/read/programming-with-monad...


Right, that's very much what I had in mind - I might very well have got it from there and forgotten; I've read much if not all of RWH at one point or another.

I would point out that in addition to the mentioned testing use (itself not to be underestimated - QuickCheck is amazing and making more things tractable to QuickCheck is great), there's also some other big potential wins. If I have a function that deals with local files in a monad that's an instance of MonadHandle, I can write an instance for a RemoteIO type that performs file operations on a remote system through a network connection, and suddenly the function that was local also works remotely.


OH! That's where I saw it. I couldn't remember, but I know the idea has been floating around for a long while.


> > Functions that do IO are difficult or impossible to test.

> Absolutely not, IO functions are not harder to test than regular ones. Is your function creating a file? Start your test, assert the file doesn't exist, call your function, assert your file exists. Done.

They are harder to test (although definitely not impossible). What if your function does a network request, and the result is highly dependent on latency etc, sure you can mock an endpoint that responds with the various properties desired, but it's much harder than just using Quickcheck or HUnit or one of the other testing libraries.


Global variables certainly create opportunities for side-effects. The author may be throwing everything into one big pot and labeling it I/O, but they're right that you can't sneak a global variable access into a function without changing the function's type. That's a valid point even if it's poorly worded.

> This is great for homework, but in the real life, IO needs to be performed throughout your entire application

My experience has been that I could get away with pushing I/O out to the fringes of the app and make 80-90% of the code pure. But everybody lives in a different "real life" and it's not unlikely that the kinds of things I do on the side for fun just don't require it.

I fully concur with the rest of your remarks.


> Absolutely not, IO functions are not harder to test than regular ones. Is your function creating a file? Start your test, assert the file doesn't exist, call your function, assert your file exists. Done.

There are a lot of things you're not testing for. Permission issues, etc. Besides, you have -plenty of functions doing IO on databases, or doing parallel operations. That's definitely a pain to test.

> > boo :: Map Integer String -> String -> Integer

> This syntax is clean but it doesn't tell me anything that

> def boo(map: Map[Integer, String], key: String) : Integer

> doesn't tell me

It tells you it's not doing IO. Besides, what about this:

  def boo (foo: Some Obj): void
What does this do? Write to a file? Change a field? I don't know.


About the difference between the signatures. Well, one is in Haskell and one is in an imperative language. I think there's a significant software-engineering-philosophical difference, though. Concatenative programming languages are kinda based on this same philosophy.

When you call boo function in Haskell, you have the power to create the "universe" which is both necessary and sufficient for the function to perform some self-contained computation. When enough of these computations are composed and nested together, you can really see how data flows through some logical procedure, possibly one with lots of essential complexity. On the other hand, when I look at the same signature translated into Java, for all I know it could be the 4th function call in a stateful 8-function-long set of functions you always have to call in order, except you don't have to call the 6th function if you pass true to the 2nd function. Easy enough, right?

The fewer combinations of arbitrary state are allowed to determine the behavior of your program, the fewer unanticipated scenarios arise. I think this is a good software engineering principle. The corollary that I think Haskell really brings home when it comes to designing programs is that it's better to change the way data flows in one small section than to add a little more state somewhere and handwave away state combinations which you think are impossible. This idea applies in both Haskell and Java.


> This is great for homework, but in the real life, IO needs to be performed throughout your entire application, and unsafePerformIO is just not an option.

Large Haskell applications benefit from different overall design than the typical C++ or Java app. Our very large Haskell codebase doesn't use IO much nowadays, and as we go back through to refactor older code it diminishes even more.


> Absolutely not, IO functions are not harder to test than regular ones

Testing IO involves systems and context that are external to both your code and your tests. This makes them more complex to set up and more error prone.


I've felt the power of Haskell before, through tidbits I've learned and books like Learn You A Haskell. However, Haskell still presents itself as a giant rock in my mind, somewhat impenetrable and obtuse. It's so different from my normal thinking processes that my brain has an incredibly difficult time adjusting. It's like my thinking process turns to goo after the first few layout steps.

Basically, I wish I could understand Haskell so I could harness its power.


For what it's worth, I would suggest just writing code as a means of building your intuition. Don't worry about understanding every last detail; just hack away at it and intuition will come as a natural consequence of your persistence. At least that has been my experience. (Honestly, this is probably true of most things that are challenging to learn, not just unusual programming languages.)

I felt much the same way as you when I first decided to dig into Haskell and went through a period of some frustration in which I felt that, although I was more or less understanding the literal text of the books and documentation I was reading, I was lacking a feel for the "big picture" of how it all fit together. I could explain specific details of how the language worked but I couldn't really visualize how to put together a complete, useful program in it.

For whatever reason, something about Haskell really intrigued me despite my frustration, and I kept at it. There was never any major epiphany where everything just clicked, though there were a few minor moments of enlightenment, as when I discovered that the neat pattern I had started using in some of my code actually had a name and a well known type class: "Applicative". I also recall reading a particular article about monads (and manually working through its examples) that made me suddenly realize how utterly simple they were, despite the ridiculous hype.

After writing enough Haskell code, it eventually dawned on me that I "got it". I wasn't struggling anymore. Haskell had, without my noticing it, become my default, natural way of thinking and reasoning about code, and it just made sense in a deeply fundamental way. I could no longer even see why I'd been confused in the first place.

Professionally, I've used mostly Java and Ruby, and have never written a single line of Haskell for pay. But like the author of this article, I've found that it shapes the way I write code in other languages, even those far removed from pure FP, in significant ways. My mental design process when writing new code at work usually takes place in terms of Haskell's data types and abstractions, and then gets translated into whatever language I'm actually using once I feel like I have a good handle on the direction that I'm heading. This usually results in nice, clean designs, consisting mostly of pure, composable functions and immutable data with as little shared state as I can get away with, arranged, when necessary, around an imperative, IO-driven core. Basically the same way I'd structure actual Haskell code.

If you're intrigued enough to keep at it, then patience is all you need, and you'll get there. It's pretty fun when you do.


> It's like my thinking process turns to goo after the first few layout steps.

If you've gone beyond a few layout steps, consider factoring out more small functions from your code. I've found that even very large Haskell codebases tend to end up with mostly two or three types of functions: short single-purpose functions of a few highly expressive lines, long but conceptually simple pattern matches covering a data type with many constructors, and long but conceptually simple monadic functions sequencing a flat series of steps gluing together other functions.

You'll spend most of your time reading and writing the short functions, writing types and names that make them self-explanatory, finding ways to factor out more of them, and occasionally staring at them and thinking "it feels like I've missed some simpler way to write that"; the long functions will fly off your keyboard almost as fast as you can think and type them.


The best way to do that would be to sit down and just write something fun in Haskell. I think the Write Yourself a Scheme in 48 Hours tutorial[1] is a good place to start. While I'm not sure it has the best code style, it's probably the easiest way to get yourself into the Haskell mindset.

[1]: http://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_H...


Go on stackoverflow or or haskell beginner list and ask questions! The crew on SO won't instantly close your question as "duplicate" or "shows no effort" theyre really understanding.

i had a list of lesser known haskell tutorials/cheats/ search engines here

https://news.ycombinator.com/item?id=4927532

but i forgot the Stanford course notes

http://www.scs.stanford.edu/11au-cs240h/notes/basics-slides....

____________

for all those squiggles like (>>=) use hoogle, index of RWH hardcopy or:

http://rigaux.org/language-study/syntax-across-languages-per...

http://symbolhound.com/?q=haskell+%3C%24%3E

and bookmark these 3 pages

http://www.haskell.org/haskellwiki/Keywords

http://hackage.haskell.org/packages/archive/base/latest/doc/...

http://hackage.haskell.org/packages/archive/base/latest/doc/...

______________

also, I think more time reading code, esp type signatures, and less time reading language spec and descriptions of syntax vs. O-O pantheon:

https://news.ycombinator.com/item?id=4863667


It's funny, because I used to feel that way about Lisp before I learned Haskell, and I still feel that way about Smalltalk even though I do know Haskell.

At some point I decided that there are things I like and am passionate about that I'm simply not going to be able to master for one reason or another and I'll have to live with admiration from afar. The last time I used Lisp I came away feeling like I had successfully tricked it into doing what I wanted, and I looked back at what I had written and thought, this is no way for me to live. I suspect what you're describing is the same thing.


> It's so different from my normal thinking processes that my brain has an incredibly difficult time adjusting.

This is precisely why learning Haskell is important...at least for those interested in self improvement. When I started Haskell, I picked it because it seemed like the hardest language I knew of at the time. It was one of the best things I've done for my career.


Build a project in it. Then all of a sudden tricky concepts like the ReaderT monad transformer become "oh, I'm just carrying around a ProjectConfig object inside my IO monad."


Don't give up. I kept myself at it for many days, thinking while walking/eating and even before I slept. Eventually it clicks. Probably the most memorable experience I have had as a programmer, so far (still young).


I'm sure the author is right about IO, but these sorts of articles always include such trivial examples that they do the opposite of the intended job. The one code snippet is from a dictionary look-up function. Here it is in Haskell:

    boo :: Map Integer String -> String -> Integer
And here it is in C++ as a standalone function:

    template <class Key, class Value> Key boo (const Map<Key, Value> &, const Value &);
or, more idiomatically, as part of a class:

    template <class Key, class Value> class Map {
       […]
       Key boo(Value &val);
    };
The author's point is that the Haskell code can't do much other than use its parameters, because there is no IO involved, whereas the C++ version could do anything. Well, sure, it could, but let's be reasonable (which is all we can be without reading the code): it's probably gonna map a string to an int. It's just as likely that the Haskell code could do weird stuff (how does it behave if it finds multiple candidates for Value?) as the C++ code, practically speaking.

So this argument actually turns out to be an argument for good containers, or, more generally, for good built-in types. The author could make all those assumptions about his function from the type signature because he/she knew what "Map" meant, not because of anything inherent in Haskell's type system. I mean, I grant that Haskell has a great type system, and great built-in types. It's just that this example doesn't make the argument it claims to make.

Let's look at some code I'm writing now. Here is strstr:

    boo :: String -> String -> Int
Not too helpful really. Here is a function which searches ComplexDataStructure, which contains file and directory names as well as other data, for partial matches of "string", returning a "goodness" value indicating the closeness of the match for the highest-matching candidate:

    boo :: ComplexDataStructure -> String -> Int
I challenge anyone to work out what the function does from that. Sure, you can make more assumptions about the Haskell version, but, practically speaking, IO cleanliness has little to do with it. What matters here is knowing what the data structures do, and using decent types.


As others have pointed out, the example is a less defensible choice since the type has already been specialized. Polymorphic types (quantified theorems) begin to greatly restrict the number of programs (proofs) down to the point where you sometimes get "free theorems" such as those discovered by Djinn. For instance

    foo :: forall a b. a -> b -> a
is inhabited by precisely one program

    foo a b = a
Furthermore, you have the opposite effect such as the nonexistence of functions with types like

    freeCoerce :: forall a b. a -> b
The best you can do is write

    safeCoerce :: forall a b. a -> Maybe b
which sometimes works but is allowed to fail if the coercion isn't possible.

---

Taking this to a logical extreme you can extend a Hindley-Milner type system to include dependent types and then construct meaningful types (theorems) which are only inhabited by correct and obvious programs. For instance, this Agda type

    sort : List a -> Sorted a
maps lists not just to other lists but instead the type of sorted lists guaranteeing the meaning of the function 'sort'.

---

Obviously any static typing system inherits a bit of this power. There's no doubt that reading the signature of a C++ function will help you to make educated guesses at what's going on. Furthermore, a malicious Haskell library implementor could use the "unsafe" family of functions to create something like 'coerce' or a function that has impure side-effects but doesn't end up in the IO monad. There is some amount of trust that libraries do not abuse such unsafe commands and the IO monad can be guaranteed to isolate impure effects---but so long as that isn't violated, the types can be incredibly informative.

---

It's also worth noting that Haskell does have "bottom" inhabiting every type, similar to null but denoting infinite loops instead of null values. So you actually can write coerce without using 'unsafe' commands like

    coerce :: a -> b
    coerce _ = let bot = bot in bot


Actually no. There is a lot you could do to make it easier to reason about that function. Just because you picked a poor type signature does not mean that Haskell is fundamentally limited. By making the signature a.) More generic and b.) restricting behavior through the use of better types you could achieve exactly the expressiveness you are looking for.

Also, while we may all choose to "trust" the C++ standard library, where it really comes in handy is reasoning about code written by your team on a day to day basis and being able to quickly re-factor code you did not even write.


Don't forget about type synonyms. ComplexDataStructure might be better called FilesystemAffinityMap. In general, if you have a bunch of functions which operate on ComplexDataStructure, you serve your future-self and others if you give your type signatures stronger semantic meaning.

That said, the choice of a C++ map as your example is unfortunate but illustrative. :)

The bog-standard mapping type in C++ is std::map, which has a method like this:

    T& operator[]( const Key& key );
One might think, based on the type and how this works in other programing languages and even from reading other people's code, writing `mymap[foo]` would just be an access. But it's not! If the `key` is not in the map, it inserts an item T, presumably calling the default constructor.

It's a good example of how, in general, the semantics of a lookup for a non-existent key varies by language. You have to use std::map::find() in C++, which returns an iterator which will equal std::map::end(). Python generates a KeyError exception (!). In Ruby and Clojure, you get nil, but note that you can have nil values in a map. :) Java returns null.

The Haskell signature for lookup is this:

   k -> Map k a -> Maybe a
What's going to happen is encoded in the type system. In general Haskell is good about this; Maybe indicates possibility of failure, lists suggest nondeterminism, etc.

That said, as others have pointed out, Haskell functions often end up being really generic. The language sort of pushes you in the direction of not specializing on types but rather putting looser restrictions, like typeclasses, on them. Type synonyms can help, too, as mentioned above.


The C++ version can (and often will) use mutable fields behind the scenes. The Haskell version can't. So it is thread safe by default, while the C++ version might break some internal invariant if used from multiple threads. So even on such simple functions, purity can give some advantage.


I think your C++ example corresponds more to the following haskell type.

    boo :: Map a b -> b -> a
since it nowhere mentions int.


Actually,

    boo :: Map a b -> b -> Maybe a
since Haskell doesn't have null inhabiting every non-primitive type.


I think the Haskell example should've used this type signature instead. Compared to the OP's example, this has a much stronger implication. There's very few implementations of this type that would make sense, whereas with the other one - it could be 'count' of the number of times 'string' shows up in the map; Or it could be number of letters in the provided string (ignoring the map entirely); Or it could be (key - 1); or any number of other things.

Here, you can't just magic into existence or modify an unknown 'a' value.


I think it's still pretty unclear what this function might do, since the inverse of a map isn't well-defined. I think this is a type signature that makes sense and has a fairly obvious most-likely meaning:

    boo :: Map a b -> b -> Set a
I'm not sure how you would conjure up just an 'a' in general.


You also need an Ord or at least an Eq constraint on the b's.


I find author to be right in he's statement, but wrong in type signature that can't do anything other from what he described. Actually, your type signature is better (in C++) since it doesn't contain concrete types, which means you cannot use any functions with those types.

Here's what you can do in Haskell:

     boo :: (Eq b) => Map a b -> b -> a
This would describe that all your function could possibly do inside is just using eq operation with values of map items.


Rant: I needed an hour to get pointers and an afternoon to grasp what function pointer is in C. 10 minutes to grok recursion, and OOP (some parts of it) came as a just a sane way to do stuff ... I am hitting my head in the monad brick wall close to a month now.

Aside from that it is an amazing language that really kicks you to think differently.


Copied from a comment of mine from a couple of years ago:

"I've recently finished reading Learn You A Haskell For Great Good!, and I think the author's approach to monads works well. I'd read various metaphorical explanations that only served to further cloud the issue, but thanks to LYAH I finally got it.

In short, he has you using monads long before you understand them (Maybe and IO in particular), then slowly introduces monads by first explaining functors and applicative functors.

In retrospect, the mystique seems crazy. Monads are just not that confusing: they're simply values with added context, along with functions that let you interact with those values without losing the context. It's a shame that this powerful idea is so obscured by its supposed difficulty."

---

I'm not going to give you any more metaphors (a monad is not a burrito). Monads are objects (in the category theory sense) that in some sense "contain" another object, and let you work on both the level of the contained object and the box around it. This can be terribly useful (and necessary in a pure language like Haskell).

It turns out that monads obey the so-called functor laws (which is a just another scary category theory word for a really simple idea) and thus have the function `map` defined on them. What's really cool is that we can map (transform) the boxed data inside the monad without caring whether there's actually anything inside.

Why is this useful? Consider the Maybe monad in Haskell (or Option in Scala). Lets say you want to get data out of a HashMap. You look up a specific key, but there might not be any data there. In most languages, this would be handled by returning a null or throwing an exception. Both solutions break type safety, so we want something better. Using Maybe we don't have to care whether or not the data is present to operate on it:

  // This is Scala because my Haskell is a bit rusty, but the
  // ideas are the same
  val map = new HashMap[String, String]("hello" -> "goodbye")

  // HashMap$get returns, in this case, an Option[String]
  val opt1 = map.get("hello") // => Some("hello")
  val opt2 = map.get("bonjour") // => None

  // What happens when we process these two options?
  opt1.map((x) => x.toUpperCase) // => Some("HELLO")
  opt2.map((x) => x.toUpperCase) // > None

  // Note that `map` here is the ">>=" operator in Haskell
  // It's different than a normal map, which takes in something
  // of type A, a function from (A -> B) and produces something
  // of type B. ">>=" instead takes in something of type M[A],
  // a function from (A -> B) and produces something of type
  // M[B]. The powerful thing about this construction is that
  // it lets us concatenate processing steps:

  opt1.map(_.toUpperCase).map(_ + "!!!").map("!!!" + _)
     //=> Some("!!!HELLO!!!")
  opt2.map(_.toUpperCase).map(_ + "!!!").map("!!!" + _)
     //=> Nothing

  // If we start out with Nothing, we always have Nothing, but
  // we don't need to check for it at every step.

No conditional code checking for nulls, just type-safe processing on possible unreliable inputs.

And Maybe/Option is possibly the simplest monad. You're probably more familiar calling map on lists, and it turns out list is also a monad, with the empty list representing the Nothing value. Things get trickier when we move into the work-horse monads in Haskell: IO and State, but the same principles apply. The IO monad lets us process the results of running IO operations without having to care (until we need to) about whether the options have succeeded or not. It also, crucially, allows us to run IO operations in a determined order by chaining the operations together. The details of all this are too long for a HN comment, so I'll just direct you to Learn You a Haskell [0] if you haven't read it, or Real World Haskell [1] if you have. Both have pretty good chapters on monadic thinking which hopefully will get you over the hump.

My best advice is: don't be scared by the category theory. Contrary to popular opinion, you don't need a PhD to program in Haskell :).

Good luck!

[0] http://learnyouahaskell.com/chapters [1] http://book.realworldhaskell.org/read/


Nitpicking, but:

  // of type B. ">>=" instead takes in something of type M[A],
  // a function from (A -> B) and produces something of type
  // M[B].
The type of ">>=" is M a -> (a -> M b) -> M b, which lets you 'chain' the result of one monadic function (a -> M b) to another monadic function. So, for example:

  return :: String -> IO String
  putStrLn :: String -> IO ()
  getLine :: IO String
  return "foo" >>= putStrLn :: IO () -- prints "foo"
  getLine >>= putStrLn :: IO () -- gets a line of input, then prints it


Oops, yeah, the map function above composes its argument with return, giving it the type M a -> (a -> b) -> M b, making it equivalent to the Haskell `liftm` function.


Want to understand monads? write some.


What for?


I'm also fairly interested in Haskell but I think that studying data structures and algorithms extensively will make me a better programmer. Not learning a whole other way to program... That will not work. I am already very comfortable in C++/Java/D and other imperative languages. I can do the same work faster because I can use global state to get easier solutions to problems. If I want pure functions, I'll write a pure function. If I want shorter, more readable code I'll write a macro. Or maybe not.


I'm genuinely curious how you'd prepare for a multicore and cloud computing future.

Using global state is like blowing Gabriel's horn -- everything dies!

Ok, so not everything, but the upshot is that global state plays very badly in the future of computing.


I feel very comfortable actually, since I'll program in the same language that the OS uses. I'll feel completely at home. The Haskell language uses the operating system to switch threads and send datagrams. And isn't the OS the biggest user of global state? It's like doing Yoga in a Karate class.


I recommend some other functional language like Scheme or OCaml. All I got from my time with Haskell was frustration. I read RWH and worked through tutorials. I never got to a point where it didn't seem like the simplest tasks weren't painful.


Real World Haskell isn't a very good introductory text. Some of the early chapters are riddled with errors and confusing, contradictory instructions.

Learn You a Haskell For Great Good! is one of the better introductory programming books I've read, and the author writes in a ridiculously fun and easy-to-read style.

http://learnyouahaskell.com/


"ridiculously fun and easy-to-read style"

But if you find 'fun' and large amounts of text distracting, then Graham Hutton's 'Programming in Haskell'[1] is a better introduction to what Haskell is about.

[1] http://www.cs.nott.ac.uk/~gmh/book.html


You have to change the type signature if you want to do IO... Purity also means you can't read or write global variables, that's just another type of IO.

If you've written so much Haskell that you think of global variables (!) as a form of IO, then perhaps you would benefit from writing in C for a while? Beware internalizing abstractions so completely that you confuse them with the reality.


This came up a few times already and it really isn't as wrong as it sounds. In Haskell all "side effects", which include memory stores, are in the IO monad (with a few exceptions).

Writing a global variable is a side effect, because you change something other than the return value of a function. Reading would still break functional extensionality: Calling a function with equal arguments might yield different results in different contexts.


Being "in the IO monad" doesn't actually mean "doing IO". It's a bit of a badly punned name. It's more idiomatic to think of it as the RealWorld monad.


Haskell helps you develop your imagination:

> I have no idea what the word "boo" is supposed to mean when used as a function name. But I can be almost certain what this "boo" does. It takes a map that has Integer keys and String values, and a second argument that is just a String, and it returns an Integer. So I'm fairly sure that this function does a reverse mapping


Actually its just deductive reasoning. Because that example is a pure function, you know there is little else it could be doing. The only question in fact would be whether or not it is doing a transformation on the value before returning it.


Things it could be doing:

* Counts the values that have the second argument as a substring

* Returns key that, using some fuzzy string algorithm, matches the second argument the closest

* Computes a score from matching the second argument against the map, which tracks document occurrences in a search engine type gizmo

I don't think the meaning is obvious at all. If anything it's a lesson in naming stuff: Don't call your functions "boo".


The actual function would be written more like

    invMap :: forall k v. (Eq v, Eq k) =>
              Map k v -> v -> Set k
The parametricity means that while this function may get specialized to have v ~ String and k ~ Int, we can be sure that invMap isn't using that information. Instead, with the typeclass restriction, we know that the only thing the function can be doing internally to the ks and vs is comparing them for equality.

Intuitively, invMap can either be computing the Set of ks which hold a particular v or, maybe, the set of ks which don't hold that value and not much else.


As long as people don't abuse that power and don't write C++ code, full of statefull functors (intellegent fools they are, the worst kind), I'm happy.


"Functor" in Haskell has a very different meaning from "functor" in C++. In Haskell, a functor (loosely speaking) is any type you can map a function over--lists, trees, options, functions... If you squint, they're similar (but simpler and more general) to types that can be iterated over.


This.

A lot of Haskell's power comes from it being able to enforce the validity of this style of programming. C++ does not - and when working with C++, you have to reason at both the C++ level like you always do, and at the Haskell level (but without the compiler verifying your work).

That's extra burden without extra benefits.


It is just that author in the "Container Operations" says that using functors in C++ is such a good practice.

No. While concept is nice, in practice, with C++ it can result in time waste and unreadable/unmodifiable code with no clear benefits. Cleaning up some statefull functors overdesigned by some intellegent fool can be a gruesome job.


> Cleaning up some statefull functors

This makes no sense at all. A functor is a type class, it doesn't contain any state.


In Haskell, it makes no sense, agreed.

In C++, it makes sense: http://www.sgi.com/tech/stl/functors.html. Functors are function objects, which is to say they're a class/struct with one method, operator().

If this makes you sigh, know that you're not alone.


> full of statefull functors (intellegent fools they are, the worst kind), I'm happy.

If you are going to be judgmental and calling people fools, I suggest improving your spelling first, or you'll just appear to be one yourself.


Yes, I think you are right. My language was not precise enough.

I wanted to describe a coder with little experience; tendency to overdesign; tendency to use 'design patterns' with no reason; tendency to build very elaborate designs; tendency to use the same pattern everywhere.

I think that "intelligent fool" and maybe "baby with a hammer" fit that description.

As to spelling errors, I'd suggest to try ignoring these and try to get the gist instead. Quite a few people here (including me) are not native English speakers.


Analogous to this I found category theory gave me a better/different view understanding of modern algebra and set theory.


My god yes! Reading through a book or two on category theory is astoundingly useful for improving your deductive chops. I'm not interested in declaring it a new foundation for mathematics, but it's such a useful language!


Sure. Instead of learning the power of composition of high-order functions, you will learn that particular composition, with a trendy name, and will start to argue on forums, that this particular function composition is somehow superior to all other compositions and you must do everything using that particular composition, which is called monad..)

There is one question, before you press "down-vote" few times - what is the sequence of machine instructions your monad compiles into?


Who is arguing that the monad as a means of composition is superior to all others? Or that Haskell doesn't emphasize the power of composition of high-order functions? Function composition is one of the most important tools in a Haskell programmer's toolbox. Monads are just another one of those.

The thing is, most popular languages these days support higher-order functions. It's nothing to write home about. Monads get a lot of attention specifically because they are a fairly unique idea that is not supported out-of-the-box in other languages. People who are advocating (or evangelizing, if that's your perspective) for a particular thing are going to focus primarily on the specific aspects of that thing that make it stand out from the crowd. For Haskell, that means things like monads, algebraic data types, non-strict evaluation, functional purity, etc.

There is one question, before you press "down-vote" few times - what is the sequence of machine instructions your monad compiles into?

I can't understand what you're getting at here. What difference does it make what code the compiler emits, unless I am trying to aggressively optimize low-level performance? If it does what I intend it to do, and is reasonably fast at doing it, I don't care about the specific instructions. How is that relevant?


The question was why should one have to learn some sophisticated, over-engineered type system along with unreadable notation, when the result will be plain, sequential machine code anyway? What is the point besides "I'm so clever"?


Your claim that it is over-engineered is unsubstantiated, and from a Haskell user's point of view, plainly false.

The claim that the notation is unreadable is similar.

Haskell's notation is certainly unfamiliar, and that can be frustrating (as all unfamiliar notations are), but it is one of the best designed ones out there.

As for the question: Why learn the type system? Because it helps you encode your program's invariants as types -- allowing them to be verified cheaply and repeatedly by an automated prover (the type checker), rather than being documented and checked by expensive tests.

They are also documentation you can rely on, as it is guaranteed to stay in sync with the program. It means you can get up to speed with other's code much quicker.

They also restrict code in useful ways, encouraging code that is more concurrency-friendly, simpler, and generally better.


OK, those are arguments in favor of strong-typing, early-binding and compile-time checks. Java arguments, in other words.

I think, looking at sources of arc.arc or news.arc, that it is, how to say, over-hyped.


Strong typing is not nearly as good when the type system is weaker.

Java's "strong typing" doesn't protect you from null dereference errors, as one example out of many.

Also, strong typing is not as good when you have to specify a lot of the types repeatedly. Without good type inference, it loses a lot of the appeal.

Strong typing is not nearly as good when every function has arbitrary effects not specified by its type.

Strong typing is not as good when it is harder to do parametericity, as discouraging it means you get monomorphic types, which are far less effective at preventing errors.

Haskell gives a whole lot more benefits via static types than Java does.


I'm not clever enough to write that "plain, sequential machine code" in a sufficiently correct fashion.


Why are you building up a straw man? The author didn't even talk about monads. The content had nothing to do with monads.

The article was about pure functions as opposed to stateful functions, clean syntax that gets out of your way, and higher-order compositions over container types. It was about good habits that can be taken to other languages.

> what is the sequence of machine instructions your monad compiles into?

What is the relevance? If you're using Haskell, then machine code should be the last thing on your mind. If you're writing code to produce the most efficient machine code, then you should be writing C. The entire point of higher level languages is that they abstract away the gritty, lower level concerns like 'what register should I place this pointer in?'.


OT: why did you post the url with the "m=1" parameter in the end?


I swear if I see another gray on white web page, I will throw my own S#!7 at you like a 900 pound gorilla in a zoo!


And learning Lisp will make you a better Haskell programmer. Read PG's arc tutorial.


Indeed, extending your repertoire is definitely a good thing.


I'd also like to see an article about how learning Haskell makes you prone to preaching to other people about how better programmer it made you and why you should learn it too, despite not having much in terms of actual production to show for it...


You totally missed the point.


I don't care about the point. I've dabbled in Haskell myself. I just hate the preaching, especially when it's unsubstantiated.


He gave concrete examples for all of his points, unlike you.


Wait, what?

I spoke against a concrete and specific thing, this very article, what more "examples" do you want? Pointing to a particular thing is the very definition of an example.

So, yes, this kind of article is very often seen, and we're very familiar with the genre to need more examples. It even admits so itself, as it starts with: "It's OFTEN CLAIMED that learning Haskell will make you a better programmer in other languages.".

As for the articles' examples: meh. Using Haskell signatures to sketch public APIs in other languages is somehow better that using any other similar notation? Even if it was so, he never proves it better. Just some handwaving. And his other big point is about thinking in terms of maps, sets, filters, reduces and such. How is that unique to Haskell? Lisps beg to differ...


I think he fails to mention the very strong static typing in addition to method signatures. There are no nulls, everything is expected ahead of time, you know what you are going to get.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact

Search: