Hacker News new | past | comments | ask | show | jobs | submit login
Option and null in dynamic languages (hackerschool.com)
59 points by luu on Dec 8, 2014 | hide | past | favorite | 50 comments



I;m sorry there wasn't more talk of Go, I think the author might like some of what Go has to offer.

For example, the comparison about maps at the bottom would benefit from seeing the way Go maps work.

There's the single assignment form:

    foo := myMap[key]  
This returns the zero value of the value type in the map if the key doesn't exist in the map.

and the two valued assignment:

    foo, ok := myMap[key] 
This returns true or false in ok based on whether the value exists, zero value for foo if it doesn't exist.

This is actually very like an option type, but what go is missing is the compilation-requirement that you check ok before using the value.... however, go does have the compilation requirement that the ok value gets used somehow, or it'll give you an unused variable compilation error. In practice, this works quite well as a more flexible option type for the simple either/or types (either there was a value or an error).


Go is quite possibly the worst possible language for this example. You don't have generics, so the best you can do with an option type is return an interface {}, and any error checking you have to do has to be done manually. The problem is not that there is no way to do it properly in other languages (well, except lua), the problem is that the most natural way to do it is wrong.


Go has effectively built in option types - multiple returns... And almost every function that can fail uses them. That's what I was trying to say. No, you can't implement actual option types in any kind of reasonable way, but you don't need to.


> Go has effectively built in option types - multiple returns...

They're more like a tuple than an Option type. An Option is a sum type, and a tuple type is a product type... and Option types are never represented as a product type (like tuple) in languages like Haskell, ML, etc. for a good reason.

(Aside: why does Go have multiple returns, instead of just using tuples? Or just use structs? They could even call tuples "anonymous structs" if "tuple" is too academic...)

Yes, I guess you meant that they can be used like an Option type, somewhat incidentally and indirectly.


Go doesn't have tuples because they aren't very compatible with compile-time type safety. You could using []interface{} as a tuple, but why would you?

Go does have anonymous structs, they're just only of moderate use in a statically typed language (given that you need to specify the types for function arguments and return types)

You can do something like this:

    // make a list of anonymous structs 
    // often useful for table driven tests
    tests := []struct{ Name string, count int }{
        {
            Name: "First test", Count: 5
        },
        {
            Name: "Second test", Count: 7
        }
    }
    
    for _, test := range tests {
        if test.Count > 5 {
            t.Errorf("%s: expected <= 5, got %d", test.Name, test.Count)
        }
    }


> Go doesn't have tuples because they aren't very compatible with compile-time type safety.

That's...amusing. ML-family languages use tuples extensively (Haskell, because of its preference for currying, somewhat less than others), and yet are light years ahead of Go when it comes to compile-time type safety.


Sorry, you're right. I was thinking of tuples without defined types for the slots, like "this function returns a tuple of two objects", not "this function returns a tuple of (string, int)".


> Go doesn't have tuples because they aren't very compatible with compile-time type safety.

What's the problem? Couldn't you have something like:

    (string, int)
For example in the return type of a function? I.e., this function returns a tuple of string and int.


What's the difference between that and multiple returns?

You can have

func foo() (string, int)

func bar(a string, b int)

And do bar(foo())

The only thing you couldn't do is make a slice of tuples... But you can make a slice of structs that have just a string and an int.

I guess I just don't see a huge amount of utility that isn't otherwise already covered.


> What's the difference between that and multiple returns?

It's a more general concept than multiple returns. Why special-case something, needlessly?


A tuple appears to be a special case of a struct. Not sure why you need (int, bool) when you can have struct{i int, b bool}... the tuple you have to access via index, and the struct you access via name... and by name is much more clear in your code.


> Not sure why you need (int, bool) when you can have struct{i int, b bool}...

You tell me: you for some reason do need (or want) multiple returns. Why, when you can just use a struct? ;)

> the tuple you have to access via index, and the struct you access via name... and by name is much more clear in your code.

I guess Go doesn't have destructing (like - `(x, y) := some_fun()`). In that case, I can see how it could be more annoying than the multiple return thing. I do not see, however, how tuples necessitate indexing - in the languages I've used with tuples, the arity is known at compile time, and you have to use certain functions to access the first, second, etc. But if the choice is between multiple returns (which I guess is just two values?) and tuples, then I don't see why the common case (2-arity) can't be easily supported.


I think this is too harsh. Out of the languages mentioned in the article, at least one (JavaScript) has no builtin lookup-or-throw method, and another (Ruby) requires a method call that's significantly uglier than the normal [] syntax, slightly discouraging its use. I don't know enough about the others (besides Python) to speak for them. In comparison, Go's multiple-return lookup thingy might be less natural/obvious than straight usage of foo[bar], but not by much.


The thing that's always annoyed me about the way go does this, where extra return values are treated somewhat specially to encourage you to actually check them, if your function returns only a success/failure condition you're either forced to return a dummy value for no reason and ignore it, or allow the condition to go unchecked.

Meanwhile, the actual concept of option type has been around for ages and could have been used instead of trying to twist MRV into it.

Thankfully, all the other newer languages are doing the right thing imo.


Not sure what you mean by needing a dummy value if your function only returns success/failure. That's what a boring old error return is for.

    func foo() error


Yes, but unlike having a second result this particular error check need not be touched. To make it so the caller has to check it, I'd have to return a dummy normal return.


You can always call a function and not assign the return calls to anything.

If you have

func foo() (string, error)

You can still call it thusly:

foo()

However there are tools to check for unchecked errors like this one: https://github.com/kisielk/errcheck


Well, Go isn't a dynamic language.


Heh, good point :)


> This returns the zero value of the value type in the map if the key doesn't exist in the map.

Wow, that's quite horrible (for a static language, anyway). So if my map returns `0`, I can't really be sure whether that is the zero-value (no value at this key), or that the value that I queried actually is `0`.

> This returns true or false in ok based on whether the value exists, zero value for foo if it doesn't exist.

Better, but still unnecessarily error-prone.

> however, go does have the compilation requirement that the ok value gets used somehow, or it'll give you an unused variable compilation error.

OK, I can see how that could alleviate the problem in practice: maybe in most contexts, there aren't anything more sensible to use the `ok` variable for than to check its value. In that case, the programmer will be alerted if he tries to use the `foo` variable without checking `ok` first.


> Wow, that's quite horrible (for a static language, anyway). So if my map returns `0`, I can't really be sure whether that is the zero-value (no value at this key), or that the value that I queried actually is `0`.

Replace "0" with null, and it's no different from Java maps.


I think there is a difference, and for the worse: `0` is a perfectly valid integer value which can be used like any other value. `null` is a valid reference value, but is invalid as far as dereferencing goes; refer NullPointerException. `0` has more potential for propagating through the program, and being erroneously used as a valid map value, while it should really be treated like a sentinel value by the program (no-value-in-map). A reference which is assumed to be valid can only propagate through the program to the point where it is dereferenced, in which case the erroneous assumption will be challenged.

It seems like the strong/weak typing distinction: one value can be used freely like any other value, since that is what it is. The other can only be used as a regular value to a degree: it can be passed around, but not actually used.


BTW, there's one more gotcha with java maps:

    public static void main(String[] args) {
        Map<String, String> testHashMap = new HashMap<String, String>();
        Map<String, String> testTreeMap = new TreeMap<String, String>();
        testHashMap.put(null, "foobar");
        testTreeMap.put(null, "foobar");
        System.out.println(testHashMap.get(null));
        System.out.println(testTreeMap.get(null));
    }
This will throw NullPointerException at the last line :) For hashmaps it works, for treemaps not.

Happened on production in my previous company (we used map2.get(map.get(...)) and used null rturned from the inner map to get the default value from the outer. Then we changed to tree maps to keep order when displaying the maps...


This is one nice thing about go maps... They won't panic if you try to access a null map, they just return the zero value (and false).

    var m map[int]int = nil
    a, ok := m[1] // returns 0, false


I don't know go, but this seems very dangerous.

I prefer Python solution - throw Exception, and if programmer want default - there's always defaultdict.


I don't think it's dangerous at all. What's the difference between an empty, non-nil map and a nil map? Why should their behavior differ? The fact that a map can be nil is effectively like an implementation detail. You asked for the value by a key, and the map tells you that key doesn't exist. Why should a map not having a value in it cause an exception? It's not exactly a rare occurrence. go supports this with the two value return, with the second value telling you if the key existed.


> Why should a map not having a value in it cause an exception?

    map controlCenters = database.getControlCenters(); // returns map column->value, nil if no connection
    int controlCentersInExistence = controlCenters["count"]; //FIXME - use the other syntax, I forgot how it looked like
    if (controlCentersInExistence == 0) { // someone nuked USA!
        WW3Manager.launchMissiles(); // we need to retaliate!
    }


See, that's not a problem of the map, that's a problem of poor error handling. If for some reason database.ControlCenters() could ever not return a valid map, it would also return an error value.

    controlCenters, err := database.getControlCenters()
    if err != nil {
        return fmt.Errorf("couldn't get map of control centers: %v", err)
    }
    // now we know controlCenters is valid.
    controlCentersInExistence, ok := controlCenters["count"]
    if !ok {
        return errors.New("no count of control centers!")
    }
    // now we know the column existed
    if controlCentersInExistence == 0 {
        WW3Manager.launchMissiles()
    }
Go's conventions are actually excellent at making this code MUCH more robust.


Yes obviously it's bad code, but it's bad code that's easier to write, than good code. That's dangerous. That's why Python does it better IMHO.

BTW err != nil? I don't know, it seem fragile. Maybe it's years of exception indoctrination speaking, but I am scared that someone will return false for errors and someone else will check err != nil, or the reverse, or some other misunderstanding, and it will fail.


False is not a valid value for an error... Welcome to static typing ;). It's either nil or some valid error.


> False is not a valid value for an error... Welcome to static typing ;).

I'm confused. Previously you gave this example:

    a, ok := m[1] // returns 0, false
Which means that `ok == false`. But how can it be false if that is not a valid value for an error?


Sorry to be confusing and/or confused. Map access in Go returns value, boolean where the boolean indicates if the key existed in the map. This is in contrast to most functions which can fail with a generic "error" value. So, for errors, nil means no errors. For map access, false means it didn't exist in the map.

About your original point about someone returning "the wrong thing" in an error... there's really no misunderstanding possible. Because Go is statically typed, the ONLY things you can return for an error are nil (no error), or something that fulfills the error interface (a type with a method Error() that returns a string). You can't return false, or 0, or "" when the code expects you to return an error... the compiler won't let you.

This is how all error handling works in Go. From the most basic program you write, you learn that err == nil means success, anything else means failure.

Does that help any?


This way the program will fail somewhere else long after the actual failure occurred. Great!


That doesn't seem nice if the map wasn't supposed to be nil (if you assume that failing fast on buggy behaviour is good). But I guess you might want to, on purpose, instantiate and use a map which is nil?


What's the difference between a nil map and a non-nil but empty map? You asked if the map had a value for your key, and it said no.


> What's the difference between a nil map and a non-nil but empty map?

A nil map is the absence of a map. An empty map is an empty map. The distinction is clear.


> If a language makes it easy to conflate nil and absence, to write code that does not reliably distinguish between success and failure, then bugs are sure to follow.

This is why after working on large apps in Ruby and Python, I strongly prefer Python. Accessing a non-existent key in a Ruby dictionary returns nil, whereas in Python a KeyError exception is thrown.

If you store nil in a Ruby dictionary, then you can't test for membership unless you use hash.fetch.


Yup. Python is how I learned to stop worrying and love exceptions.


You can also use Hash#include?.


prefer hash.fetch(:key) over hash[:key]. fetch raises NoKeyError if key not in hash.


We need more approachable introductions to types--they're all over this issue. Good stuff.


I could swear I read the exact same article somewhere else a couple of months ago.

Anyway, I fail to see how either raising an exception or returning None (which the author recognizes as Python's version of null/nil) is anything at all like an Option value (as opposed to "null-like")...


> Anyway, I fail to see how either raising an exception is anything at all like an Option value

The author asserts their similarity in that they completely differentiate lookup success and failure, there is no situation under which success and failure can be confused.


I sense a connection, for example, between Rust's `fn foo<T>() -> Result<T, E>` and Java's `void foo<T, E>() throws E` (this might be incorrect Java syntax). I'm not entirely confident though.


I find Java's checked exception similar to Option type, because they are all part of the function's type. You cannot pass alone without checking the possible failure case.


I guess I would have written the Ruby version as

    def values_in(keys, dict)
      dict.slice(*keys).values
    end
Though I suppose it's not the point of the article.


Just None, like Just Nothing is nonsense.

If you have a special marker for something, just explicitly check for it each time.


OK, lets make it long and explicit.

Asserting that an obscure, optional either-of type is some fundamental concept every single language has been overlooked is, of course, nonsense.

The fundamental concept is notion of "nothingness", "emptiness", what other languages call NIL. Without this a language would be very clumsy, like Math without zero.

The notion of empty vs. non-empty is captured in so-called "either-of" types. The most obvious example is '() - an empty list. Conceptually, it is a List, but it is also indicator of "emptiness", of "nothing inside".

A List is an "either-of type" itself, so it does not require any additional specialize type to capture the notion that it could be empty - it already has it.

Another example is so-called "C-strings". There is notion of end-of-string marker, so a string could be viewed as a list instead of an array of characters. In this case (of a list) we don't have define anything special for "empty string" - the notion of "just end-of-string marker" is good-enough.

Again. This Just/Nothing type in Haskell is nothing special or fundamental. It is an optional, non-essential construct.


The article explains exactly why it is useful to be able to distinguish `Nothing` from `Just Nothing`.


Yeah, instead of having only "nothing" - NIL - they prefer to have "nothing-in-a-box", and "nothing-in-a-bag", and "nothing-in-a-suitcase". Nothing-in-a-container-of-this-kind.

Due to such over-abstraction madness for sake of abstraction they end up with Java or Javaesque Haskell code.




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

Search: