I couldn't imagine myself returning to languages with exceptions and ternary operators, for example.
Reminds me of Rob Pike's post: https://commandcenter.blogspot.fr/2011/12/esmereldas-imagina...
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
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!
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) ? ... : ... 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 = if expr trueVal else falseVal
(which is what e.g. Scala does) over meaningless-unless-memorized '?' and ':' n := defaultVal
if expr {
n := otherVal
}
Which I think is easier to understand, and not that much more typing. YMMV of course.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).
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.
---
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?
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.
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.
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".
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.
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...