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)".
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.
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.
> 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.
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...
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.
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.
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?
> 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.
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.
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.
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.
For example, the comparison about maps at the bottom would benefit from seeing the way Go maps work.
There's the single assignment form:
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:
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).