My Go Resolutions for 2017
research.swtch.com
research.swtch.com
> Part of the intended contract for error reporting in Go
> is that functions include relevant available context,
> including the operation being attempted (such as the
> function name and its arguments).
I know the Go folks don't like exceptions, but this is an example of them learning the hard way that about one useful thing they lost by deciding to not do exceptions.Exceptions give you stack traces automatically. All of that context (and more) is there without library authors having to manually weave it in at every level of calls.
> Today, there are newer attempts to learn from as well,
> including Dart, Midori, Rust, and Swift.
For what it's worth, we are making significant changes to Dart's generics story and type system in general [1]. We added generic methods, which probably should have been there the entire time.Generics are still always covariant, which has some plusses but also some real minuses. It's not clear if the current behavior is sufficient.
Our ahead-of-time compilation story for generics is still not fully proven either. We don't do any specialization, so we may be sacrificing more performance than we'd like, though we don't have a lot of benchmark numbers yet to measure it. This also interacts with a lot of other language features in deep ways, like nullability, whether primitive types are objects, how lists are implemented, etc.
[1]: https://github.com/dart-lang/dev_compiler/blob/master/STRONG...
How is this even possible? Does the compiler just fail if you try to put a type parameter in contravariant position? Or does it allow it and then blow up at run time?
So, for example, List<T> is covariant—you can assign a List<int> to List<Object>—even though add() takes a T. This isn't statically safe, so the language inserts runtime checks to ensure you don't break soundness.
It's a bit easier to think about it as ">=T" vs "<=T" though.
In one direction, you allow T and any subclass. This is probably the most common - all T subclasses should act like T, e.g. have method x() on them. You can assign a T subclass to a T variable or return it from a T-returning method. When you add something to a List<T>, it could be a T subclass, because it's "at least a T". (covariant)
In the other direction, you allow T and any superclass. My main way to think of this is for a callback, e.g. for declaring a `map` operation. If you're mapping over List<T>, you can't declare your callback as accepting a subclass of T because the list only contains "at least T". But you can accept a superclass (e.g. Object), because any T (or subclass) has that superclass. (contravariant)
---
If you don't have support for contravariance, you either give up flexibility (you can't make a reusable Object mapper) or safety (you can't guarantee the supplied callback is safe to call).
With functions and a type hierarchy of some kind (or implicit conversions, or whatever) you have the same kind of issues. When you declare the type of your map function (explicitly or implicitly) you're still bounded by the type you're mapping over. If you make a "(float x, returns float) x + 1e10" function, you can't use it to map over a list of doubles, because they can't be safely reduced in precision. The reverse works though, because floats can be promoted to doubles safely. You essentially have "double" as a subclass of "float". Whether it's OO or not has nothing to do with type hierarchies, OO just embraces them with reckless abandon.
Good type inference systems can hide a lot of this from you, allowing you to drop types most of the time and let the compiler specialize it / make sure it's safe to do this particular thing. But they can fail. When they do, how do you ensure safety?
I wrote Scala for the last 2 years and was the resident type system "expert", so I had to explain variance to people pretty often. Luckily, it's a pretty quick, mathy definition:
Some notation first:
A <: B means "A is a subtype of B"
A >: B means "A is a supertype of B"
F[_] refers to a unary type constructor.
Now for the definitions: If F[_] is "covariant", it means that if A <: B, then F[A] <: F[B]
If F[_] is "contravariant", it means that if A >: B, then F[B] <: F[A]
Examples of covariant type constructors are List and Functions in their output. An example of a contravariant type constructor is Functions in their input type.^ This is all that needs to be said! I can write it on a whiteboard in ~5 minutes. I'd say that's a reasonable thing to be expected to learn.
[1] https://www.reddit.com/r/golang/comments/3gtg3i/passing_slic...
if B >: A, then F[A] <: F[B]
and B >: A means the same as A <: B, so this means: if A <: B, then F[A] <: F[B]
which is the same as your covariant definition.Perhaps you meant:
If F[_] is "contravariant", it means that if A <: B, then F[B] <: F[A]
?If you are a language where it matters, it comes in handy, even though you usually aren't aware of it. You probably already have a correct intuition of it without realizing it.
Say you have a class like:
class Enclosure<T> {
void cage(T item) { ... }
}
And assume some sort of class hierarchy like "Mammal is a subclass of Animal". If you have a method like: putKittyInCage(Enclosure<Mammal> enclosure, Mammal kitty) {
enclosure.cage(kitty);
}
Is this OK? putKittyInCage(new Enclosure<Animal>(), kitty);
The method expects an Enclosure<Mammal> and we're giving it an Enclosure<Animal>. Is that allowed? You probably intuitively see that it should be — an Enclosure<Animal> can hold any kind of animal and mammals are all animals. Your intuition is right."Contravariance" is the term to describe precisely what that intuition represents.
I couldn't imagine myself returning to languages with exceptions and ternary operators, for example.
Why ? Is it because no language with those feature is good (let say you wouldn't like to return to Java for instance), or is it because you genuinely prefer this over ternary operator :
if expr {
n = trueVal
} else {
n = falseVal
}I know that the typical answer is "it's just bad programmer", but that's where Go chooses different view. Many design decisions in Go were taken with awareness of the social context of programming.
If the feature incentivizes using it in a wrong way - it's not a bad programmer, it's a bad feature. Programming language is a language between humans in a first place and it should be readable as much as possible, it should deincentivize using obscure and easy-to-use-improperly things.
It so much better when you go to practically any Go repository and find that code is readable and clear. I've never had this experience with any other language before.
PS. Found one of the real examples of abusing ternary operators. That was one of the coolest I've seen. And I guess it's easy to understand that the same programmer wouldn't write the same with "if .. else .." blocks in Go. He would realize that it's too verbose and hard, hence probably wrong design, so he need to find better solution within the space of language features. http://imgur.com/a/hjKOe
What languages did you have to endure before?
> Found one of the real examples of abusing ternary operators.
Yeah, the original code is silly, the binary search trees are not always balanced. They should be more like:
if (type <= 10) ? (if (type <= 5) ? ... : ...) : if (type <= 7) ? ... : ...So there's nothing good about alcohol (it may cause liver damage), cars (they may cause accidents), prescribed drugs (they may have unpleasant collateral effects), ...
Hey, it's also true that programming incentivizes bugs, so programming is bad. Let's stop programming!
var n int
if expr {
n = trueVal
} else {
n = falseVal
} n := map[bool]func() int{
false: func() int { return falseExpr },
true: func() int { return trueExpr },
}[expr]()
https://play.golang.org/p/TXIse32WD9Now, let's see how to abstract it with "go generate"...
n := defaultVal
if expr {
n := otherVal
}
Which I think is easier to understand, and not that much more typing. YMMV of course. n = if expr trueVal else falseVal
(which is what e.g. Scala does) over meaningless-unless-memorized '?' and ':'Reminds me of Rob Pike's post: https://commandcenter.blogspot.fr/2011/12/esmereldas-imagina...
Reading, maintaining and improving legacy codebases are some of the hardest.
If that extra typing makes it much easier to do the above tasks, I don't mind doing this.
Go runtime panics include stack traces, complete with the offending statement's line number in the source code.
The first thing I do in a Go project is reimplement exceptions it seems. It isn't even so much the stack trace, but that the _cause_ can be chained on. Often times one error causes another and the error interface in Go is too weak to capture it.
Rob Pike has his blog post about errors being values, but it's basically useless because the standard library hardly uses the more advanced error types. Pretty much every library returns error instead of MoreAdvancedError, which means you are doomed to speaking the lower common denominator.
(for the record, promising in the documentation an error will always be some type is pretty weak).
I find exceptions really useful but also, I think that with exceptions people tend to loose very useful panic/error dichotomy. I saw projects without a single "panic" in the code. All those projects gravitated towards dumb error handling mechanisms aka "log all errors and continue".
It's a library for More Expressive Error Patterns. You can declare error types like this:
type ErrFrobnozMalformed struct {
Frob *Frobnoz
meep.TraitTraceable
}
... and any type you compose with a meep trait like that gets superpowers, like automatically attached stacks.I'm the author. I don't think it's perfect -- in particular you really can't avoid a certain amount of boilerplate :( -- but with meep you get stacks, and you get custom error types, and that's worth a lot to me.
Whether or not you use this code, the idea that might be a useful takeaway is the fact that the stack-capturing behaviors (and others) are a trait that you can "mix in". Whether a stack is appropriate depends on situation. Some errors are fairly regular (e.g. certain kinds of IO halt) and putting a stack on them is not useful (and is CPU-costly). This doesn't necessarily follow any sort of direct inheritance tree. (Java has started doing something similar with adding Even More parameters to exception constructor for e.g. 'capturestack=false'.) I think this is an important point: errors often should have stacks, but not always.
I'm still hugely looking forward to seeing what the Go authors do in the future to make errors smarter. Doing the right thing should be easy, and it's almost impossible to strap something this essential on with a library: special syntax and compiler support for informative errors is warranted.
---
They wrote that they considered it to be redundant with interface programming, but really i don't understand why. Interface is about behavior, not data. An int doesn't "behave" like one, it is one. And something that's either an int or an array of string, doesn't "behave" like anything you'd want to describe with an interface...
As an example, one should see how protobuf "one of" messages are dealt with in go : switch on arbitrary types followed by manual typecasting. That's just gross...
Switch on arbitrary types, followed by typecasting? That's the go way. No surprises. Explicit instead if implicit behavior.
I see no reason for go not to adopt it, in all honesty. it's nothing like generics, because it doesn't seem to add complexity to the rest of the language ( imho ).
This is also true in Go with type switches:
switch y := x.(type)
case MyStruct:
And with type assertions: y, ok := x.(MyInterface)
In both cases, y is of the correct type. The ok is optional, if omitted and the assertion fails, you'll get a runtime panic.Which is exactly what pattern matching does, which is what I was responding about.
Edit ( since i can't reply) : just look at https://developer.apple.com/library/content/documentation/Sw... with something like the barcode example.
It's all compile time checked.
Sooo just accessing fields on a struct? I don't understand the distinction you are trying to make.
The complexity is inherent and you can either choose to make it implicit and play the losing game of ensuring it doesn't fail with tests or make it explicit from the outset.
Great, now my enums can panic because I have to play "be the typechecker", instead of focusing on something actually important like business logic.
How is this not seen as universally a bad thing?
1) Abstract data types encourages closed systems. You may view this as a positive: It enables exhaustiveness checks. But I view it as a way to make your program more fragile. Go's type asserts let you convert to interface types, so you can add new interface implementers later and not have to go fix-up old type-assertions.
2) First-to-match pattern matching complects order with dispatch. Each match clause has an implicit dependency on _all_ of the clauses before it, since you can match Foo{x, 1} and if that fails match Foo{x, y} where you know now that y != 1. This is sometimes useful, but as your patterns grow larger, it's simpler to just match Foo{x, y} and then branch on y == 1. A series of type-asserts with if statements has a little bit of order dependency on it: interface clauses and named wrapper types are first-to-match, but type-switch on struct clauses are completely order independent because there can only be one concrete representation underlying an interface{}.
3) Relying on positional fields causes two classes of problems: A) it's harder to grow your system later, since every pattern match needs to mention the new field you added and B) you can't _not_ mention a field by name (or at least give it a placeholder name) at every use as well. This is the same issue as the Foo{x, y} vs Foo{X: x, Y: y} notation. It's considered good practice in Go to use the later, since it's more future proof, ie Foo may grow a Z field and it will be initialized to zero.
Or, rephrased: the compiler doesn't help you find the old type-switches that need new branches.
As you say, being closed gives exhaustiveness checks, and this is often a good thing: it's harder to accidentally not handle new cases in existing code (or not handle existing cases, when writing new code), and it makes refactoring much easier. For some code the flexibility of being open isn't the right choice, just like for some code, it is.
> First-to-match pattern matching complects order with dispatch
This seems orthogonal to ADTs: one can retain retain behaviour like Go's current switch, and only allow switching on the variant, not doing more detailed pattern matching. This retains order independence.
> Relying on positional fields causes two classes of problems
Again, totally irrelevant: one can pattern-match using the names of fields.
Presumably you're adding to a "closed" set of types, so it's quite easy to grep for the type names in that type set. I just don't find this to be that much of a problem in practice. Never mind the fact that wildcard clauses exist and so this problem comes up in ML/Haskell/etc as well, since the compiler will consider those patterns exhaustive.
> only allow switching on the variant
Go already has this. It's called type-switch:
https://golang.org/doc/effective_go.html#type_switch
> one can pattern-match using the names of fields
Of course they can, but traditional algebraic data types impose order on to fields, which is an undesirable feature IMHO. However, field dispatch loses the nice order-independence attribute.
Yes, that's an option, but it's not nearly as nice as the compiler automatically telling you what's up, nor does it help downstream users of a library that adds a new possibility. They just have to hope the change is communicated to them.
> Never mind the fact that wildcard clauses exist and so this problem comes up in ML/Haskell/etc as well, since the compiler will consider those patterns exhaustive.
This is an explicit vs. implicit situation: by using a wildcard, the programmer has explicitly chosen to give up some compile-time assistance, whereas Go implicitly makes this choice for the programmer.
> Go already has this. It's called type-switch:
... I don't see how a facetious response like this is at all helpful. The downsides of type-switch are exactly what we're already discussing.
> traditional algebraic data types impose order on to fields, which is an undesirable feature IMHO
Go is its own language, and can choose to adopt/adapt features to suit its idioms. Referring everything back to how "traditional ADTs" work seems silly when there's trivial tweaks that fit Go better.
> However, field dispatch loses the nice order-independence attribute.
I don't know what you mean by this (I can guess, but I don't see how it relates to naming fields in patterns), could you rephrase?
If you add a new type to a sum type in a statically typed language, that's a breaking change for all downstream consumers. Again, you may view this as a benefit (clients get compiler errors about new cases to handle!), but I view it as a drawback (clients can't upgrade until they handle all new cases!).
> Go implicitly makes this choice for the programmer
And I believe that Go (and Clojure, which omits both types and pattern matching for reasons including those I'm discussing) makes the right decision for the programmer, as openness to future changes without breakage is better > 90% of the time.
> could you rephrase?
You said "one can pattern-match using the names of fields", I took that to mean matching on something like Foo{x: 5}. If you do that and also have a clause Foo{y: 10}, now you have ambiguity and the order you compare x==5 or y==10 first matters, since you might have an object Foo{x: 5, y: 10} which would match both clauses.
If you just meant that you could do Foo{x: x} with blanks, that's fine, we're in agreement then. Clojure offers {:keys [x]} in destructuring for this purpose, and it's great.
One argument against adding the new feature is it forces people to think about which to use, and they may choose an inappropriate one. (This choice argument is far stronger than vague concerns about the feature possibly resulting in breaking changes.)
In any case, libraries can already make breaking changes, and presumably quite a few want to/do (so it's not like having a feature that can also result in breaking changes is anything new), and, adding a new type to existing open set can easily be a semantic breaking change that stops code from running correctly, even if it doesn't stop code compiling.
> You said "one can pattern-match using the names of fields", I took that to mean matching on something like Foo{x: 5}. If you do that and also have a clause Foo{y: 10}, now you have ambiguity and the order you compare x==5 or y==10 first matters, since you might have an object Foo{x: 5, y: 10} which would match both clauses.
No, that is orthogonal, as I also discussed in my original reply.
I was visualising something like `Foo { x: binding }` in the pattern, which would make the value of the x field available under the name `binding` (and, for convenience, `Foo { x: x }` case could be allowed to be abbreviated to `Foo { x }`). Whether the binding on the right-hand side of a field allows pattern matching or not is its own discussion.
[0]: ADTs aren't forced to allocate, and possibly use a more efficient switching scheme (it's just an integer for an ADT; I don't know the implementation details of Go's type-switch so it might be that fast).
> I was visualising something like
OK then. We're in agreement on that point. This is what Clojure has in it's destructuring syntax, and, as I said, it's great.
And all I meant to do was point out that lack of exhaustiveness checks is not without its tradeoffs. :)
How is it a tangent? One of the biggest reasons to want exhaustiveness checking is to avoid the even uglier problem of "compiling but semantic changing" changes.
> I greatly dislike _breaking_ changes and want languages that make it possible to avoid them
It seems you are only talking about disliking breaking changes that cause your program not to compile whole totally disregarding the danger of changes that compile but change semantics.
As respectfully as possible, that seems backwards to me.
Not true, consumers that have an explicit "catch-all" pattern at the end will work normally
Yes, but that's true regardless of whether the compiler tells you or not. The question is whether adding the value to the set of types that can be returned breaks at compile time or at run time.
The problem is that we're not talking about the same things. You're talking about making new things behave the way the an existing interface wanted, and i'm talking about a data structure updating.
Once again, interface is not data..
Except this is simply not true if you have any wildcard patterns in any of your matches. Besides, it's easy to grep for one or each of the names of the types in your closed sum. Which is what you're going to do anyway if you have any wild card patterns!
> interface is not data
I don't know what you intend to communicate by saying this.
My point about interface vs data is that those are two different things, and that i don't think you can't try to solve an issue on type compositions with a feature working on functions only.
Is the idea here that all types should be open? Should I be able to e.g. add 1.5 as a possible instance for the Int and String types?
Interfaces should be open, but one also needs the ability to represent closed datatypes.
type Foo = { x : int }
type Bar = { y : string }
interface DoStuff { doStuff() -> void }
function Foo.doStuff() {
...do stuff with Foo.x
}
function Bar.doStuff() {
... do stuff with Bar.y
}
function main() {
value := getSomethingThatImplementsDoStuff()
value.doStuff()
}
Compare this to ML: type DoStuff = Foo of int | Bar of string
let main =
let value = getSomethingThatReturnsDoStuff ()
match value with
| Foo of x -> ...do stuff with x
| Bar of y -> ...do stuff with yHere's another way to do this in Go:
func f(obj interface{}) {
switch obj := obj.(type) {
case Foo:
// Do stuff with obj.x
case Bar:
// Do stuff with obj.y
default:
return fmt.Errorf("obj is of unexpected type: %T", obj)
}
}I'm not addressing your other points, just your first sentence, since that is a matter of fact I can concretely address.
Not sure what you mean here. Struct embedding is composition. It enables delegation of methods, which allows the outer struct to satisfy the same interfaces as the inner struct. This necessarily means you can't create closed interfaces. See for yourself: https://play.golang.org/p/akn9rir5of
This holds even if the outer and inner structs are defined in different packages.
src/b/b.go:16: cannot use b (type B) as type
a.private in argument to a.CallPrivate:
B does not implement a.private (missing a.privatemethod method)
have privatemethod()
want a.privatemethod()
A public interface with a private method does behave a bit interestingly. You can create new structs that compose in the implementation, and if you have a function (not method, function) in the private package that tries to type match on them, it can indeed end up with a type unknown to your package. However, you still have not necessarily "opened the interface" because it remains impossible to ever call a private method on that interface that was not defined in the original package. I haven't tested what public methods do.I was able to construct a struct that in some sense had two different methods which could both be called under different circumstances with the same short method name, which is a bit weird.
So, in Go, you can completely close an interface such that you'll never get a type you don't expect, or you can close an interface in a way that means you may get an underlying type you didn't expect but the method will still be guaranteed to come from your package.
So you can create a "sealed interface" only in the sense that you can't use it at all outside of its defining package, and it won't be sealed inside of its defining package. I can't see a way to build an enum from this in the way the OP described.
The expression problem is motivated by the fact that the OO style (which Go favors via interfaces) makes adding new cases easy (just add another subclass) but adding new methods hard (you have to modify every subclass), while the pattern matching style makes adding new methods easy (just add another match statement) but adding new cases hard (you potentially have to modify every match statement in the program). Depending on the task at hand, either one may be more advantageous. The solutions to the expression problem are attempts to get the best of both worlds.
A lot of programming language designs deny that this is a tradeoff, which I think is misguided. Sometimes it's more convenient to be able to add methods easily, and sometimes it's more convenient to be able to add cases easily.
Sometimes one wishes people from the go time would have spent some time hacking ML-derived languages.
Dependency management is a big pain point for me. I'm really glad to see several of my pain points on the list for this year, including another look at generics.
Generics are genuinely tricky: They allow you to write many kinds of useful functions in a type-safe manner, but every known approach for implementing them adds complexity to the language. C#, Java and Rust all bit the bullet and accepted (some) of this complexity. Maybe Go will find a sweet spot, preserving its simplicity but adding a bit of expressiveness?
Anyway, it pleases me to see that the Go team is thinking hard about this stuff. At the bare minimum, I'm going to be contributing code to other people's open source Go projects for the foreseeable future. :-)
I don't buy that. There is no proof Go programming at large scales better than Java programming at large.
Go didn't reach the scale of Java programs yet. And no, Kub or Docker, while fairly large, are nothing compared to 15 y.o. multi-million line Java codebases. Go certainly needs less bureaucracy due to the ease of deployment, but it doesn't mean Go projects scale better in large teams.
> For single person projects that I think you are doing, many people want intellectually stimulating language where Go may fall short.
I really hate this kind of arguments. Features like generics aren't intellectually stimulating, they are here because people want to write type safe code. Context.Value(interface{})interface{} isn't type safe code.
Now tell me, what is more intellectually challenging? writing concurrent programs free of race conditions or generics?
I didn't claim Java projects do not scale. They do. I use Java all the time at my day job. I would use whatever makes sense. Maybe someday Rust is absolutely essential I would use it then.
> Features like generics aren't intellectually stimulating, they are here because people want to write type safe code.
When people need generics they can use languages with generics facility then. I am not arguing otherwise.
This is the point where Go's advocates disagree with its detractors. The latter tend to feel that you need generics (Just like you need to program in something higher level then assembly) far more frequently then they are told.
I want intellectually appealing code, not intellectually torturous.
That's exactly my hope. When i switched from Rust back to Go, i had a sigh of relief, i was able to prototype quickly and easily and my cognitive load felt much lower.
Strangely enough, this felt very similar to switching from NodeJS to Golang. In Node, i was constantly worried about what is async or sync and the dynamic nature of it made my code feel like the wild wild west. Both Rust and NodeJS put a lot of mental burden on me, in different ways - Go was definitely a sweet spot in both correctness and ease of use, and i hope they achieve that with Generics as well.
NOTE: My Rust programs felt vastly more secure than in go, and i miss that - that part was less cognitive load in favor of Rust. The struggle was mainly at the design phase, and i just wanted to mock up some code and types & borrowing posed many refactoring issues. I hope in the future strong Rust tooling will make refactoring a breeze.
I seriously doubt Go and the people who maintain it are going to do groundbreaking work in this area. It's and extremely developed area of language design (and still developing way ahead of where Go would ever go)
What "complexity" does simple parametric polymorphism (i.e. forall a) bring? The only thing I can think of is some extra syntax, which is far less complex than a codebase built upon a lack of parametricity. Hell, don't even allow user-defined parametric types and force everyone to stay with Go's parametric builtins but allow programmers to abstract over them. Seems like a no-brainer to me.
I very much wish Go to succeed, it's built on a few nice ideas, but where it currently is it has a number of usability impairments that stop me from wanting to work with it.
But I see that these impairments are seen as problems by key developers, and work is underway to eventually fix these problems. (And this is besides the "routine", incremental but very important improvements, such as GC or stdlib.)
What will inevitably happen is that Pike et al will argue that such things are merely problems because "you're doing it wrong" or "there's no way to do this without any tradeoffs of any kind" (generics), and ultimately very little will change.
Well, the error interface is { Error()string } and gophers were told to use errors as values, not errors as type because supposedly "exceptions are bad". By providing context you are just re-inventing your own mediocre exception system. Why use errors as value at first place if you need context? just put exceptions in Go, therefore people don't need to use a third party library to wrap errors in order to trace the execution context.
The value is that your code's happy path is clean and not encumbered with error checks every ten lines, like we see in Go sources all the time.
Separating the happy path and the error handling in different sections of the code contributes to clean code, especially if exceptions are checked and cannot be ignored by the developer.
No, the separation has nothing to do whether errors are handled or not.
If error handling is mandated by the compiler (e.g. checked exceptions), there will be no unhandled errors since the compiler will simply refuse to compile your code until you handle these errors.
Whether you handle these errors near the happy path or in a different section of the code is an orthogonal concern.
I don't see the value in a clean happy path and a hidden error path.
Go already has unchecked exceptions. It's called "panic".
So either developers are already abusing it, in which case go's current error-handling approach is no better than the one proposed according to your metric. Or developers have access to it but aren't abusing it, in which case there's no reason to expect they'd abuse it in a system where doing it the right way is even easier than it is today.
In my experience, users already do experience stack traces as it's embarrassingly easy to accidentally operate on a nil pointer.
There's a whole lot of space between "include useful context in errors" and "exceptions".
(And FWIW, Go does have exceptions, it just calls them panics, and has a culture not using them for "known knowns" error conditions.)
As opposed to panics? Checked exceptions don't blow up in your face, you have to handle them. Nil errors and type errors might yet these also happen to Go. I see no difference with Java here. Go isn't better when it comes to error handling, in fact Go is extremely tedious when it comes to error handling.
> (And FWIW, Go does have exceptions, it just calls them panics, and has a culture not using them for "known knowns" error conditions.)
So Go has both(unchecked exceptions and errors as "value"), how does it make things better? it doesn't. If it did, the blog wouldn't be talking about people "handling errors the wrong way".
I think it depends somewhat on the kind of software you're writing.
Being very careful with errors is quite handy for a long-running server processes; maybe (maybe!) not so worth it for a program that starts and stops within the attention span of a single user.
That also happens to be the intended distinction between Java's checked and unchecked exceptions.
the definition of "handle" widely varies, to the point of making the exercise near meaningless.
You mean outside of accidentally running into a nil, and having your program spontaneously abort?
Even worse, reliably testing for nil (`if (foo == nil)`) doesn't even save you because go's nils are typed. So `foo == nil` can actually return false even when foo is nil. Utter madness.
It is true that Go doesn't do as much as other languages to avoid nils. It's a small, unambitious language in some ways. (Like, it adds more typing than Python, not as much as rust or swift.)
foo == nil
will always tell you the truth. The edge case that trips people up at first that I think you're thinking of is that storing a nil-pointer into an interface will not give you a nil interface: package main
import "fmt"
type Foo struct{}
func (f *Foo) Do() { fmt.Println("do the foo", f) }
type Doer interface {
Do()
}
func main() {
var foo *Foo = nil
fmt.Println(foo == nil) // true
var doer Doer = foo
fmt.Println(doer == nil) // false, because doer is (nil,Foo)
doer.Do() // prints "do the foo <nil>"
}
https://play.golang.org/p/Ul9cX34qfFThat's because an interface is a (pointer,type) pair, so even if the pointer is nil, the type being non-nil will make the pair non-nil.
var foo *Foo = nil
var bar Bar = foo
foo == nil # true
bar == nil # false
is, to be completely honest, completely ridiculous. It means you can perform a sanity check for nil values and still accidentally operate on a nil value.Null references have been referred to by Tony Hoare as his billion-dollar mistake. We should not be reintroducing mistakes of this level of magnitude, and worse, compounding on them, in new languages invented with fifty years of hindsight.
func (o *Object) DoSomething() {
if o == nil {
fmt.Println("Nil implementation")
}
fmt.Println("Something with the members here")
}
Whether or not this is a good idea would be a different discussion, but typed nil struct pointers don't automatically crash the way they do in C. Therefore, it is perfectly valid to have some interface implemented by what is a nil pointer to some object type. I've even used this a few times. nil only crashes when you try to write into a nil map and a few other operations. Even some operations you'd expect to crash are implemented in a way that they will not. For instance, you can append to a nil slice, and a new slice and underlying array will be allocated for you rather than crashing.Go does not by any means solve the "billion-dollar mistake", and I'd still like to be able to put "non-nillable" on things, but it is less affected by it than C. (Which is damning with faint praise, certainly.)
But the fact that `foo == nil` can return false when foo is actually nil is indefensible.
You keep saying this, but it's not true. …
foo = nil
bar = foo
// this check may or may not evaluate as true
if (bar == nil) {
…
}
Saying that `bar` here isn't actually equal to nil if it's an interface is tautological. It's not, but only because the language has defined this to be the case. Go could likewise define `1 == 2` to evaluate to true, and you could reuse the same semantic reasoning to defend it.The point being argued is that it doesn't matter that go defines this to be the case, the point being argued is that it's surprising, frustrating, and can lead to bugs. Particularly when `bar` starts off as a pointer to a struct, but later is refactored to be an interface type. Code that used to work still compiles, but now encounters a runtime nil panic.
In Go (as in most programming languages) it's not true that the assignment "a = b" implies that "b == foo => a == foo", if a and b are different types.
For example, this C code will print "nope":
float b = 3.5;
int a = b;
printf(a==3.5 ? "yup" : "nope\n");
So back to Go, sure, this is initially surprising, and most people get bit by it at first. Once you know about it, it's fine.In practice I haven't encountered any refactoring bugs as you describe, though it is theoretically possible.
I personally don't see it as a big issue.
(In retrospect, Go could've help guide intuition better by using a new keyword like "unset" or something to test for the zero-value of interfaces, instead of overloading nil.)
While I haven't counted and compared, I suspect that in most programming languages, "a=b" being a non-error implies that either a and b are the same type and value, or that a and b are values that, if they are of different and comparable types, will compare equal.
There are certainly popular languages that do it the way you describe, but I don't think most languages do.
While this is true, in all of those other cases you have the benefit of compile-time type checking so cannot call a function on the wrong type.
With nil, you get runtime errors.
I'll agree with the opinion that it can be confusing that an interface consists of two elements, the type and the value itself, and that the "== nil" check may not do what you initially expect, because the interface values are hidden below the surface of the abstraction the language provides. But it is not true to say that "the interface value is nil", because it factually isn't. To use psuedo-Go, since neither "Interface" nor the types are first-class values, Interface{ConcretePointerType, nil} does not and should not compare as equal to nil. There are values in memory corresponding to this interface value, and those values do not correspond to any interpretation of "nil" Go uses. We're talking about real numbers in real RAM here and a real specification of nil that corresponds to those real numbers in RAM; this is not a matter of opinion.
I don't know of any language that doesn't have a few dozen quirks of this sort buried in it. It can be pointed out as a legitimate criticism of the language, but it's not particularly a flaw relative to other languages. Either it's not possible to build a truly clean programming language that lacks this sort of quirk, or we're not very good at it.
Before replying to me with the language you believe lacks these quirks, please do me a favor and search for "$LANGUAGE quirk" and "$LANGUAGE gotcha" first, because that's the first thing I'm going to do. And it won't be a defense to me to explain how what you found is not really a quirk because you just have to properly understand the language, because that defense is true for this quirk of Go's too.
a = nil
b = a
// => b == nil, right? but no.
one of the devs on /r/golang posted he wished they had used a different keyword like "unset" to check for an empty interface, which would be much less intuitively confusing.i agree all languages end up with quirks like this (can't get everything right without really using the language, and by then it's too late!), and in fact go is low on the quirk level in my opinion.
the other one i really wish i could change is that
for i, x := range foo
doesn't give x a new binding each time through the loop. Confusing, and almost never the behavior you want. if err != nil {
return err
}
Is anyone else surprised that forcing programmers to do the tedious, repetitive, and boring work of being a manual exception handler overwhelmingly results in people doing the least amount of effort to make it work?I feel like so many of the headaches of go could have been avoided had the developers spent any time whatsoever thinking about the programmers using it.
I also feel like there has been a lot of emphasis put on keeping the APIs consistent, which is something that a lot of developers will tell you makes PHP a nightmare sometimes.
People keep saying this, but the alternative doesn't have to be exceptions.
Rust strikes a great balance here. There's no nil, you must handle the Err case of a Result enum, or the None case of an Option enum (this is much nicer than go; in go after `val, err := failingThing()`, it's entirely possible to accidentally use `val` as a meaningful value, which is strictly impossible in Rust), and you can use functions like `map` and `and_then` on these types which has the benefits of explicit error handling without the absurd loss of readability caused by dozens of identical error handling clauses.
> I also feel like there has been a lot of emphasis put on keeping the APIs consistent
This isn't something special about go. This is a minimum requirement for a language nowadays. Rust, Ruby, Python, Swift, and others all do this.
I would love for Go to include something like this:
type Error struct {
Description string
Cause error
}
func NewError(cause error, descriptionFmt string, args ...interface{}) error {
return Error{
Description: fmt.Sprintf(descriptionFmt, args...),
Cause: cause,
}
}
func (me Error) Error() string {
if me.Cause == nil {
return me.Description
}
return fmt.Sprintf("%v: %v", me.Description, me.Cause.Error())
}
func RootCause(err error) error {
if err, ok := err.(Error); ok && err.Cause != nil {
return RootCause(err.Cause)
}
return err
}Unless I'm mistaken, this is an impossible dream as long as shared memory exists. It's the core tradeoff that distinguishes the Erlang runtime from the Go runtime (there are others, but they all stem from this).
Your goals are either memory isolation for better distribution/concurrency/clustering/fault tolerance/garbage collection or shared memory for ability to work with large datasets more efficiently.
It's one of those details that changing it would essentially create a new language. You'd have code, packages and libraries that either worked that way or they wouldn't.
IMO, this is an area where Go gets into dangerous territory of trying to be all things to people. Be great at what you're good at which is the "good enough, fast enough, portable enough, concurrent enough, stable enough" solution for backend services in most standard web architecture.
If people need distributed, fault tolerant, isolated, race proof, immutable run times that aren't quite as top end fast and aren't ideal for giant in RAM data structures...there's already a well established solution there by the name of Erlang (and Elixir). They made the tradeoffs already so you don't have to reinvent them.
The isolation is not physical, but logical. Implementations are free to use zero-copying and make everything just as efficient. Theoretically compiler could even optimize message passing overhead away for systems with shared memory in some cases. The opposite is also true, shared memory is also logical and there is a lot of room for a lot of clever things, like eliminating races.
https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.h... is interesting reading.
Another example: I can take a value and mutate it locally, then pass it immutably to a number of threads which use it to compute some results, and once they are all complete, I can mutate the data structure again. If I try to write mutation code before all the threads are re-joined, the compiler will tell me that I can't mutate while there are outstanding immutable borrows. At compile time, at no runtime overhead or locking.
It's simple. With exceptions, we got used to "errors" that are, by default, debugable. But Go got rid of default debugable errors, and programmers are lazy.
main.go:13 main.main(): highest level error;
Details: foo.go:8 foo.ExportedMethod(): mid level error;
Details: foo.go:42 foo.innerMethod(): low level error!
It provides handy helpers like GetRootErr and PanicToError too. I hope we can opensource it in the next month or two.The only downside to this is you can no longer treat errors as values. Eg, you can't do:
err := Func()
if err == ErrBadThing {
// do stuff
}
So you have to implement some type of `Cause()` method. In my lib, i have `errors.Cause()` which returns the root error, as well as a shorthand func `errors.Equals(err, ErrBadThing`)`It is API-compatible with the errors package, and you get errors.Wrap(err, message), errors.Wrapf(err, message, args) and errors.Cause(err).
The problem is they shot themselves in the foot and may have to incorporate a form of "default" method to get around it like Java ended up having to. You can't have a "ContextualError" type because what does "Cause()" return? If it returns "errors.Error" then you require users to use a type assertion. If it returns "ContextualError" then it can't chain existing errors without wrapping. They also can't add anything to the "errors.Error" interface because they would invalidate all existing impls of that interface.
Python 2 has nothing, and libraries haven't agreed on anything. Python 3 has "raise ... from ...", which largely resolves it, but I'm not sure what adoption is like. Anyone know?
I know there probably won't be immediate fixes, but it gives me confidence in Go's future.
I think that's true, but I do think its been said by a number of Go users and advocates, which is where the perception comes from.
The builtin generics are a mess. At this point I don't trust the go team to implement any more complicated generic system.
Could you elaborate on this? I must not have used arrays, slices, maps, or channels enough to find that there's some common issue with all of them, other than that the user can't write her own functions generic over them.
You cannot pass a []string to something that expects a []interface{} for instance, even though you can put every item in the first in an instance of the second.
[1] https://github.com/surullabs/lint
Disclaimer: I'm the author of the above library.
Also what to put and not put in context objects is really important to document as it could easily snowball into a catch-all construct and be totally misused after a while.
I'll be curious to see how this pans out, because it sounds like a very deep rabbit hole. Is there any precedent for this in other language toolchains? I've seen some mondo test suites in Java that could desperately use it.
As an optional external tool it may work quite nicely, however.
Well, with some caveats. Firstly, tasks have to be written to support this mechanism, and although the built-in tasks are, not all third-party ones are, and those will always be re-run. Secondly, if a test task fails, it will be re-run. That makes it easy to re-run tests which failed for extraneous reasons.
In your mondo case, to take advantage of this, you'd want to break your test suite up into multiple tasks. You already get a task per subproject, but you could easily define multiple tasks per subproject. I've often had separate tasks for unit tests, integration tests, and browser tests.
Make also does this if you describe tests as a rule to create a test report.
Perhaps an array is better than a slice, so `immutable [...]byte` Also, the for-range loop would have to behave differently so I guess it's a version 2 change. And if semantics are changing anyway, I'd prefer a `mutable` keyword to an `immutable` one.
Slice is the right choice, I think. Were they to choose array as the alias, the language would need to "bubble up" the "array length" type parameter to the string type, which would make strings of different byte lengths incompatible types. But hey, Go is already pretty clearly inspired by Pascal, so maybe we'll see that after all :-)
Glide takes our team 90% of the way, but is a bit glitchy (need to wipe ~/.glide sometimes) & lacks a command for `npm link` type functionality.
To expand on this a bit, in ocap theory there is a concept of "vat", an isolated memory space for objects which has its own concurrent actions isolated from all other vats. In a vat model, data races are nearly nonexistent; in order to race, one would have to choose a shared variable in a single vat, and then deliberately race on it. But this is not common because ocap theory enforces heavy object modularity, and so shared variables are uncommon.
Additionally, a "deliberate race" is quite tricky. Vats prepare work with a FIFO queue. In the Monte language:
# A flag. We must start it as `false` because Monte doesn't allow uninitialized names.
var flag :Bool := false
def setFlag(value :Bool) :Void:
flag := value
# Set the flag to `true` immediately.
setFlag(true)
# Set the flag to `false` on the next turn.
def first := setFlag<-(false)
# Set the flag to `true` on the next turn.
def second := setFlag<-(true)
# And finally, when those two actions are done, use the flag to make a choice.
when (first, second) ->
if (flag) { "the flag was true" } else { "the flag was false" }
You might think that this is non-deterministic, but in fact the delayed actions will each execute in order, and so the flag will be set first to `false` and then to `true`.