Having fun with Go's nil, interfaces and errors
katcipis.github.io
katcipis.github.io
A man orders coffee in a restaurant.
"Would you like it with or without cream?"
"Without"
A few minutes later the waiter returns.
"I'm sorry sir, we've run out of cream. Would you like it without milk instead?"
Just because a language supports exceptions does not mean you always have to use them. You can always use a helper function to catch any error and return it as a value.
You could argue that Go can use Exceptions in the same manner as X lang too, by using panic and recover... but most people seem to not like that.
With Go and Rust, return value error handling is just simply nice. There's no question about if a function might error out, just look at the signature.
This would not be "replicating" anything, exceptions already are values in most languages. The only thing to do is wrap your call with a function, like "err = catch-all(expr)".
> ou could argue that Go can use Exceptions in the same manner as X lang too, by using panic and recover...
Except that it would be cumbersome ;-) because panic/recover works only in combination with defer[0].
> With Go and Rust, return value error handling is just simply nice.
Using non-exceptional control flow for errors can be useful depending on the circumstances. Using exceptions is simply nice in many other cases.
> There's no question about if a function might error out, just look at the signature.
Just look at the documentation. Code defensively.
err = catch-all(expr)
If your language does not provide a similar tool, you can't just use a function because "expr" is evaluated before the function is called. I had Lisp macros in mind when writing this. A poor man's approach that is still generic is to wrap the expression in a closure: err = catch-all(() -> expr)
Not too cumbersome, but YMMV.When a nil value is converted to an interface, it carries type information. When an interface carries type information, it isn't nil.
[1] github.com/joeshaw/multierror
var m map[T]T
you're basically writing var m *runtime.hmap
which is a nil pointer. Go has sane zero values, so it makes sense that reading from a nil map returns the zero value for whatever type its values are.However, there's a trade-off: either Go's maps allow nil assignment (in a similar fashion to slice's `append') _or_ it retains its "reference" (forgive me for using that term) semantics.
Go's designers decided that maps should differ from slices in that regard, and I tend to agree. I think it'd be a mistake to have to always return a map _just_ in it's newly allocated.
Why on Earth should a user of a high-level language have to care about language implementation details?
> What the actual fuck ???
Exactly.
Okay, but what was the rationale for designing the language like that?
In this case, it's an implicit conversion from "nil" to the "nil value" of a given type. This is convenient in many cases, but causes confusion in other cases.
Upvoted because of this, because it clarified my earlier confusion. That being said, I don't really agree with the rest.
It's also not an implicit conversion from nil to something. Nil isn't a value the way 0 is a value of an integer or "foo" is a value of string.
Per Go spec (https://golang.org/ref/spec) nil is "predeclared identifier which has no type". That implies it doesn't have a value and therefore cannot be converted to a value.
Nil is zero value of pointer types (which in practice end up being a pointer whose value interpreted as integer is 0) and also a zero value of interface type (which is interface without a type and without a value) and a few other types.
Also, sometimes nil is a perfectly valid value of something and can implement interfaces. You can call methods on nil receivers because method calls are just curried function applications over the value. It would be odd if the interface compared equal to nil just because I happened to implement it with a value that's valid with nil.
func (o *Object) ReturnSomething() int {
if o == nil {
return 0
}
return o.SomeOtherValue
}
It is also therefore legal to return in an interface value a nil pointer to a struct of a particular kind, and therefore "an interface containing a nil pointer of a particular type" and "an interface containing nothing at all" are fundamentally different things that can not be collapsed together.So as Vendan says, it is just part of the language. All languages have this sort of wart in them, where two or more perfectly sensible decisions interact to create something that isn't sensible at first glance.
(I'm still in favor of going back in time and changing Go to have non-nillable values, but that ship has certainly sailed.)
The languages I'm used to don't even have `nil`.
> "nil" is a perfectly valid value for a pointer to have.
So, just like C and Java. I don't see the difference.
> You can write methods like this: (snippet)
Oh, now I get it: `nil` itself isn't a value. `nil` is syntactic sugar that expands into a nil value. (In other words, confusingly enough, `nil` and “nil value” are different things!) This is unlike Java, where `null` itself is a value.
It would be very helpful if people described things precisely.
> All languages have this sort of wart in them, where two or more perfectly sensible decisions interact to create something that isn't sensible at first glance.
This means that at least one of the two (or more) decisions was less sensible than it originally seemed. Good design decisions don't introduce warts into a system.
func (o *Object) ReturnSomething() int
to func ReturnSomething(o *Object) int
In the first case, it may "seem" that a nil Object would be invalid, but in the second case it's obviously valid, and makes complete sense. It may be described as a "wart", but it makes complete sense to me. The issue is, in my opinion, that people drag their own expectations into programming languages as they learn them.But dynamic dispatch on "what type of object do I not have?" is still a damn weird thing to want.
It the types don't match, the operands are not considered equal. Type and value have to match for two interface values to be equal. How does that not make sense?
For mismatched dynamic types you can't meaningfully compare the values in the general case, so of course you need to consider types.
Because people expect == to compare values, not types. Nobody has ever questioned this behavior in Java and C#.
Maybe for you. For me, non-leaky abstractions have enormous value. And, while designing non-leaky abstractions requires more effort upfront, in the long run, it requires less effort than plugging leaks in carelessly designed (non)abstractions.
> That's not the goal anyway: The goal is to leverage abstractions to build things.
The goal is to do it efficiently, and creating as few problems as possible for the future.
> The implementation details frequently do matter.
Of course they do matter - for the implementor.
> When possible, it's preferable to design abstractions that have a clear implementation in terms of the lower level's abstractions.
No disagreement here.
> This way, when something goes wrong - and it will - you can understand and fix it.
That's the job of the abstraction's implementor. If that happens to be me, I will fix it. Otherwise I'll just submit a bug report. If that doesn't work, then I'll reimplement the abstraction myself, but by no means will I work around an existing broken implementation.