Hacker News new | past | comments | ask | show | jobs | submit login

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.




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

Search: