Hacker News new | ask | show | jobs
by jephir 4258 days ago
Go still has some unecessary sources of complexity, for example:

1. It uses null (the billion-dollar mistake) to indicate optional values. A better designed language uses an option type.

2. It has conflicting idioms for conditions. Error types use the `if err != nil` idiom while map access uses the `if ok` idiom. This means that you read the main execution path downwards and the error path to the right, unless you access a map, then the execution path goes to the right and the error path goes downwards.

3. The `:=` operator declares new variables, unless you use it to declare multiple variables where one already exists in the same scope. As a result, you don't know if you have actually declared a new variable using `:=` unless you read upwards to see if a declaration for the same variable name already exists.

6 comments

I don't understand your problem with errors versus maps.... they're actually the exact same code pattern:

    val, ok := m["foo"]
    if !ok {
        // handle not found
    }
    // good path

and

    val, err := m("foo")
    if err != nil {
        // handle err
    }
    // good path
3 very good (if minor) points here - it'd be great if the go team looked at fixing things like this for a go 2. I've heard talk they don't have any ideas of what might be in a go 2, but a few grammar tweaks like these plus pkg dependencies would be a good place to start. There are definitely some rough edges to the language and some decisions which in retrospect they might not have made, but since it's frozen at go 1, there won't be any changes for the foreseeable future. In some ways that's comforting if you're building real apps on the language, so not at all a bad thing.

I'm not even sure if they need a := operator, is the additional complexity and wasted time worth the benefit (compile error on redeclared variables, most of the time)? It feels a chore having to type out := most of the time and, as you point out, confusing where there are several variables assigned at once.

Another area which is unnecessarily complex is allocation: http://golang.org/doc/effective_go.html#allocation_new

You might use:

    func () (t T) {}
    var t T
    t = T{}
    t := &T{}
    t := new(T)
    t := pkg.New()
    t := make([]T,5)
Though the pkg.New() option is really just an optional convention. It all makes sense eventually, but do we really need all of these options? I'd rather have something like:

    t = T{}
    t = &T{}
    t = [5]T
Still, these are pretty minor niggles, which can mostly be overcome with conventions, except perhaps the use of nil everywhere, which is a shame. There's a lot to like, and the emphasis on simplicity (at the cost of features) definitely is a feature of go, even if it does have some rough edges it is significantly simpler than most other languages both to learn and to use.
Rob Pike mentioned variable declaration when asked at GopherCon what he wished he hadn't done in Go

http://youtu.be/u-kkf76TDHE?t=16m10s

The solution to number 2 (if it really needs one) is simple to use `if !ok` instead to push the exceptional case to the right.
#3 has always bothered me. I feel like:

   someString := "hello"
   someString, err := ReturnStringAndError("world")
   if err != nil {
      fmt.Println("there's no way this could happen")
   } else {
      fmt.Println(someString)
   }
Should produce an error.
It's a matter of pragmatism. When you have a sequence of functions that all return (res, err) pairs, it's extremely helpful to be able to use := even though the err is not redefined.

Occasionally, I wonder whether := with several variables on the left hand side should have been to defined to redefine variables (shadowing the earlier definition), but obviously the Go people thought that such shadowing would be worse.

Yup, this is why you don't need err1 err2 err3 err4... (I've had to do similar things in C# before).

Also, := does shadow in a sub-scope. The difference between shadowing and simply assigning in the same scope is negligible (i.e. either way you can't get at the old value).

    err := foo()
    if err != nil {
        err := bar() // err shadowed here
    }
    f, err := baz() // err assigned here
What would be the difference between shadowing and assigning on that last line? You can't unshadow without leaving scope, at which point the value you were shadowing also leaves scope.
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`.

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.
I don't see #1 as a problem. Only pointers can be null, values cannot. Strings are not null terminated so that removes a whole class of problems.