Hacker News new | ask | show | jobs
by alecbenzer 4258 days ago
1. Mistake or not, allowing all pointer types to be `nil` seems like simplification from things like option types. How do you see nil as adding complexity?

2. I've never heard of `if ok` being an idiom _as opposed_ to `if !ok`.

3 comments

Nil adds complexity because it creates a hidden failure condition in every possible value. One that's not necessary most of the time.

Option types, on the other hand, are not a source of complexity, because they just use a more general language feature (ie variants). And variants aren't a source of significant complexity because they're symmetric to records (ie structs) and so emerge naturally. Symmetry inherently simplifies design by organizing and structuring things. They're a very small step beyond enums and a much saner alternative to unions.

Nils are ultimately more complex because they're baked into the language and omnipresent. And they give you very little in return! Variants, on the other hand, are a natural and relatively simple design that also vastly increases the expressive power of the language. And gives you option types for free.

> Nil adds complexity because it creates a hidden failure condition in every possible value.

That's simply not true in Go. `nil` is not an inhabitant of all types. Integers, floats, strings, arrays and structs cannot be `nil`. Slices, maps, functions, channels, interfaces and pointers are nilable.

> Option types, on the other hand, are not a source of complexity, because they just use a more general language feature (ie variants).

Go does not have variant types, so if adding option types requires them, you get an increase in the size of the language. In Go's case, variant types would have a weird interaction with interfaces, which arguably increases complexity.

> That's simply not true in Go. `nil` is not an inhabitant of all types. Integers, floats, strings, arrays and structs cannot be `nil`. Slices, maps, functions, channels, interfaces and pointers are nilable.

True. But beside the point. Yes some things can be nil and some things can't. Problem is that there is nothing stopping me from doing this :

  func (x *X) {
    x.boem()
  }
and that can crash. With ADTs ("option types (assuming a Haskell-like Maybe type) the compiler would complain : "x can be of type Nothing, so you can't just call a method on it".

The point is that Option types mean that you can still have optional values, but you can never have nil pointers.

> Go does not have variant types, so if adding option types requires them, you get an increase in the size of the language. In Go's case, variant types would have a weird interaction with interfaces, which arguably increases complexity.

They would have exactly the same interaction with interfaces as they would have with anything else. ADTs are not of any definite type, so you have to case select them in most cases, and you can forego nil checking for everything else.

You would have the "grouping" behavior anyway, since in Go you don't declare that you satisfy interfaces. So if all possibilities for an ADT implement the same interface, then the ADT should magically implement it too. I'm sure it'd be a change in the compiler, but it wouldn't be a change in the language.

> True. But beside the point.

Uh. No. There is a big difference between "every value can be nil" and "only some values can be nil."

And you don't need to sell me on the benefits of ADTs. All else being equal, I'd much rather have them. But this doesn't mean I want to go around shoving them into every language under the sun. I recognize that, sometimes, it's reasonable to persist without them. I very strongly believe that there is no One Right Language Design.

In Go's case, I've written a lot of it, and experience tells me that `nil` errors just aren't a large source of bugs like they are in a language like C. I suspect it is partially because you can dispatch on `nil` values[1], and also partially because of very very strong idioms like `if err != nil { ... }`. You can also `append` to `nil` slices.[2] I recognize that this is practical experience and that it will always lose against theoretical purity, but Go isn't after theoretical purity. (Please be careful. This is not a claim that the two things are mutually exclusive.)

> You would have the "grouping" behavior anyway, since in Go you don't declare that you satisfy interfaces. So if all possibilities for an ADT implement the same interface, then the ADT should magically implement it too.

You're not thinking through everything. What happens when the discriminants of a sum type are themselves interfaces? What happens when you type assert? Which value do you get?

What is the zero value of a sum type?

How are sum types deconstructed? (Pattern matching! But now you've added another language feature!)

Also, interfaces already provide some of the use cases of ADTs with type switching. You just don't get compile time safety. So now you have a case of non-orthogonal features.

Finally, you should note that I am not claiming "these things cannot be resolved." I am claiming that, "it is hard to resolve these things in obviously simple ways that are consistent with the rest of the language." By the time you're done resolving them, you will have made the language specification more complex.

[1] - http://play.golang.org/p/4_SNEi9YgR

[2] - http://play.golang.org/p/D3WRreGNBb

> Mistake or not, allowing all pointer types to be `nil` seems like simplification from things like option types. How do you see nil as adding complexity?

Whenever you have null in a language, for every function you call that returns a reference, you have to check the documentation to see whether or not it will return null. You cannot know from only the function interface whether or not you have to deal with an optional value.

Option types move this checking to the compiler. The function declaration explicitly shows the presence of an optional value. You don't have to read the documentation or investigate the function implementation - the information exists in the function interface itself.

Given, Go tries to deal with this by using the multiple return idiom. By convention, you should only have to deal with null if you have a function with multiple returns. However, I think that compilers, not conventions, should enforce language rules. Leaving it up to conventions opens the window for human error.

> I've never heard of `if ok` being an idiom _as opposed_ to `if !ok`.

This comes from Effective Go: https://golang.org/doc/effective_go.html#maps

They explicitly call it the “comma ok” idiom.

> This comes from Effective Go: https://golang.org/doc/effective_go.html#maps

> They explicitly call it the “comma ok” idiom

The "comma ok" idiom is the idiom of accessing maps w/ `thing, ok = m["key"]`, not the idiom of what to do with `ok` afterwards. Nothing there says to prefer `if ok {` over `if !ok {`.

---

I guess I can see a case for null-ability being more complex than not. I think the argument for them being simpler is that most of the time values are "actually" nullable (ie, the function can return nil and you should check for an error), in which case the implicit "pointers can be null" rule seems like a simpler mechanism than introducing option types (and, potentially, variant types or whatever other machinery will be needed to implement them).

As you said, the "return an err as well if the function can return nil" rule does a decent job of alleviating the complexity here. Compiler-enforced checks here seem like something that adds complexity (there's now more to the language), even if they do achieve stronger safety.

2. Objective-C uses the `if ok` pattern a lot because you can send messages to nil. Not that it's a great pattern, but it is quite common there.
Sorry, I meant specifically wrt. to Go.