Option and null in dynamic languages
codewords.hackerschool.com
codewords.hackerschool.com
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).
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 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)
}
}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.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.
It's a more general concept than multiple returns. Why special-case something, needlessly?
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.
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.
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.
func foo() errorIf 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
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.
Replace "0" with null, and it's no different from Java maps.
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...
var m map[int]int = nil
a, ok := m[1] // returns 0, falseA nil map is the absence of a map. An empty map is an empty map. The distinction is clear.
I prefer Python solution - throw Exception, and if programmer want default - there's always defaultdict.
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!
} 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.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.
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?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 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.
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")...
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.
If you have a special marker for something, just explicitly check for it each time.
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.
Due to such over-abstraction madness for sake of abstraction they end up with Java or Javaesque Haskell code.
def values_in(keys, dict)
dict.slice(*keys).values
end
Though I suppose it's not the point of the article.