Please keep in mind that there are differences at scale. What is "easy to work with" for 1 programmer over a month might not be so for 20 programmers over years.
The argument can't be that paucity is good as a general condition, its that there are forms of abstraction and programming language features that Gophers find unhelpful or difficult to understand
The argument is that simpler is better at scale. Airplanes can move freely in 3 dimensions, but airliners are constrained to fly in particular ways around busy airports and cross country.
I also have a hard time believing that the ability to define parametric types and functions costs you anything. It's almost always self-evident when to use parametric types or functions, things that are "wrappers" or "collections" probably account for 80% of their use.
I could see an argument for parametric collections and parametric sorting in Go. Not, however, for wrappers.
The frustration of using Go is actually that I now have to consider the later as a possibility or trade off type safety by using unsafe casting.
In your experience, what kind of "cost" has there been in unsafe casting to use collections? Even in environments like Smalltalk, where all use of collections amounts to "unsafe casting," I've rarely seen situations where a mistake of this type wasn't found trivially. Does your frustration come from having to abandon the "assured safety" the type system would give you, or does it come from an experience of the costs?
For me, it's entirely about expressiveness. This:
reverse: 'a list -> 'a list
where the type encodes that `reverse` is a function from a list of some type of elements to another list of the same type of elements, is more informative than this: reverse : list -> list
where it's obvious that this list has elements of some type, yet it's not clear what the element type is, and it's not clear that the resulting list has elements of the same type as the input list.Beyond being able to express and communicate intent, there's the added benefit that the type system can statically check that the input elements are the same type as the resulting elements. There's also no worry about information loss associated with subsumption (the rule of subtyping that allows a value of a subclass to "become" a value of one of its superclasses, losing specificity in the process which may only be regained with a type cast (this is one reason I tend to favor row polymorphism as well -- no subsumption means no information loss and no need to cast)) because no subtyping is involved in this case of parametric polymorphism.
Parametric polymorphism is simple and well understood. And not exactly new either: it has been understood for some 40 years already.
> I could see an argument for parametric collections and parametric sorting in Go.
C++'s <algorithm> header is proof that there are lots of algorithms that benefit from being expressed generically, not just sorting.
> In your experience, what kind of "cost" has there been in unsafe casting to use collections?
Without type safety, there's a disincentive for decomposing things into smaller parts, because the cost of manually verifying that the parts are compatible is greater than the benefits of decoupling them. Would a Go programmer even dream of bootstrapping fancy data structures from simpler ones?
> Even in environments like Smalltalk, where all use of collections amounts to "unsafe casting," I've rarely seen situations where a mistake of this type wasn't found trivially.
At scale, the law of large numbers says that even improbable events will occur every now and then. Unfortunately, a program with even one bug is still incorrect.
What use is there for a fancy data structure? In practice, these occasions aren't that common. Many "fancy" data structures tend to exhibit bad cache behaviors if implemented naively.
At scale, the law of large numbers says that even improbable events will occur every now and then. Unfortunately, a program with even one bug is still incorrect.
Are you an undergraduate? Depending on how you interpret the spec (which isn't cut and dried when business requirements meet the real world) almost every page of production code has some kind of bug in it. Also, the law of large numbers isn't that relevant for most codebases and developer populations -- the numbers aren't that large. The effect of hubris is much larger in practice.
Improving asymptotic bounds. Providing functionality typically not supported by common data structures. (e.g., I want a key-value container that's a priority queue on keys, but concatenates multiple values associated to the same key)
> Are you an undergraduate?
No.
> Also, the law of large numbers isn't that relevant for most codebases and developer populations
The law of large numbers certainly applies to >100 KLOC codebases, unless your bug rates are somehow magically two or three orders of magnitude lower than the average.
> The argument is that simpler is better at scale. Airplanes can move freely in 3 dimensions, but airliners are constrained to fly in particular ways around busy airports and cross country.
For one, I just debate the premise the simplicity has anything to do with cardinality of features/concepts. But let's take that argument at face value: then why _not_ assembly if this is the case? Why not a language with the absolute minimum number of concepts? I think if you interrogate this premise you'll find it doesn't hold a lot of water and that Go doesn't really aspire to this goal anyways. I think we have some amount of working memory for being able to intuit programming with a certain number of concepts. There's a valid argument that some languages suffer by breaking that barrier (though I personally think Go underestimates where that barrier is), but it seems incorrect that language designers should be optimizing for a minimal number of features.
I think complexity at scale has more to do with features that interact poorly (or cause poor interactions more frequently with a larger number of people). Specifically its about composition. For instance, there's a valid argument to be made that asynchronous exceptions (i.e. the ability to interrupt another thread with an exception) and locks poorly compose. Mutable state is a common example of a feature that's a detriment to composition. But parametric polymorphism, if anything, gives us a much greater ability to compose. It allows us to define functions that work on data arbitrarily parameterized by other types, which makes them conducive to composition. And likewise, we don't suffer ability to reason about composition at scale with parametric types. A parametric function does not gain complexity as more team members are added, more code is written, more deadcode accumulates, etc. Parametricity changes nothing at scale.
> In your experience, what kind of "cost" has there been in unsafe casting to use collections? Even in environments like Smalltalk, where all use of collections amounts to "unsafe casting," I've rarely seen situations where a mistake of this type wasn't found trivially.
That's an argument for Go to not have types. But Go does have types, and type safety is often espoused as a benefit of Go. If you're going to have types, it makes zero sense to me why you should not have parametric polymorphism, since this is the only way to have things like typed collections without opening yourself up to the possibility of casting errors. Frankly I find it bizarre that people claim that they have found type errors to be trivially fixable, because the scope of where a type error can be introduced is enormous in an untyped language... its literally every location that potentially calls into the code where the error occurs.
> Does your frustration come from having to abandon the "assured safety" the type system would give you, or does it come from an experience of the costs?
Yes, type safety is an enormous advantage to writing correct code in my opinion. It's one of the best mechanisms a programming language can give you for enforcing invariants about data. The curry-howard correspondence is a huge advantage to writing correct code. Every place a type checker isn't being used to delimit acceptable data is a potential source of a huge number of bugs. It's also a frustration because casting introduces conversation and type checking boilerplate that a type checker could ultimately take care of for you.
Okay, then you can throw away the rest of your post and stop right here. The overwhelming historical evidence is that assembly doesn't scale.
> That's an argument for Go to not have types.
Sorry, that doesn't follow. Is the logic here just because I mention Smalltalk, that I'm advocating late binding and the only type being Object for Go? Sorry, but that doesn't follow. The argument is that Go doesn't need a more complicated type system to avoid problems with heterogeneous collections -- because practice shows that even a simpler one can suffice.
> Frankly I find it bizarre that people claim that they have found type errors to be trivially fixable, because the scope of where a type error can be introduced is enormous in an untyped language...
Sounds like you're invoking freshman level false "common knowledge." Have you ever worked in an "untyped" language in a real project? What if a project simply used runtime asserts? Then a type error in a heterogeneous collection would be caught in unit testing. If it got out to production, it could be easily caught and logged. In 15 years of Smalltalk industry work I never encountered the kind of heterogeneous collection type error you're referring to in production. The closest thing I can recall involved the heterogeneous typed reuse of a local variable. (Which is simply bad coding style in Smalltalk.) In Go, you have a type system that provides much more feedback at compile time, and workable mechanisms for detecting the problem at runtime. So at least in this one instance (heterogeneous collections) there is arguably almost no practical benefit to parametric polymorphism.
(P.S. Technically speaking, Smalltalk is strongly typed with message passing semantics for methods implemented through late binding. It's not "untyped.")
Huh? I'm not actually arguing that assembly is a scalable language. I'm invoking a counter-example to the idea that a smaller cardinality of concepts is inherently a good thing. Assembly has a smaller number of concepts than Go, so by the espoused benefits of having a language with less features, assembly should be favored. But obviously this is not true, so I debate that Gophers actually ascribe to this version of "simplicity".
My point here is that Gophers need to examine their rhetoric a little more and get better at honing their definition of "simplicity", since its clearly not just having less "stuff" as Rob Pike seems to claim in every Go presentation.
> Sorry, that doesn't follow. Is the logic here just because I mention Smalltalk, that I'm advocating late binding and the only type being Object for Go? Sorry, but that doesn't follow. The argument is that Go doesn't need a more complicated type system to avoid problems with heterogeneous collections -- because practice shows that even a simpler one can suffice.
Your original question was how does unsafe casting introduce cost. It adds cost in exactly the same way that every other means of circumventing a type system or not having a type system introduces cost: it allows runtime errors to occur at points where data is illegally used.
Type systems are effectively proof solvers. Just like making an improper assumption in a logical proof can lead to a faulty conclusion, forcing a type system to assume a type for a value that it cannot prove can lead to a buggy program. This is why programmers who strong believers in static type checking take issue with casting: its a way of circumventing the protection that a type checker gives you, when instead you can add power to the type system for expressing your constraints or add means of showing the equivalency of different types.
> Sounds like you're invoking freshman level false "common knowledge."
There's no need to get personal here. I'm making a factual point: it's true that any code path calling into the point where a type bug occurs is potentially responsible. Nothing in my comment is invoking "common knowledge". Also you should keep in mind that invoking your "personal experience working on X large scale system in industry" is not a compelling argument. It's not even a comparative argument about an untyped language vs a statically typed language.
> Have you ever worked in an "untyped" language in a real project? What if a project simply used runtime asserts? Then a type error in a heterogeneous collection would be caught in unit testing. If it got out to production, it could be easily caught and logged. In 15 years of Smalltalk industry work I never encountered the kind of heterogeneous collection type error you're referring to in production. The closest thing I can recall involved the heterogeneous typed reuse of a local variable.
Yes, I have. I've worked in Python and Javascript for a couple large projects. I'm not going to get into my feelings about this, because I don't think it forms the basis of a compelling argument.
However, I take issue with the claim that these kinds of bugs are always trivially caught in unit tests. One thing to note about untyped languages is that they allow an infinite number of values to be passed to a function by virtue of being untyped, so there's no way to write an exhaustive unit test. This isn't unique to untyped languages (for instance, I can't write an exhaustive unit test in haskell for a function that accepts strings), but a sufficiently expressive typed language always gives me the ability to reduce the scope of my tests by writing more constrained types (for instance, with sized collection types using Data Kinds in haskell). Similarly, languages that disallow parametric types cannot express constraints about contained values in a type, which allows exactly the same sorts of bugs that an untyped language can have.
Unit tests are great, but they are better suited for probing the edges of acceptable inputs based on assumptions about the code under test, and are generally poorly matched to providing the guarantees of a type system. They are not perfect: they can suffer from laziness, code rot, faulty assumptions, etc. I've seen bugs in test code far more frequently than I've seen bugs in a type checker (in fact I don't ever think I've seen a bug in a type checker).
My argument here boils down to the fact that you can trivially show there's potential for human error here that a type system can protect against. The point of contention you have is that these kinds of bugs don't manifest in practice. In my experience they do, and they occur more frequently in larger scale systems where there's more invariants to juggle that a type system doesn't ensure for you. I'd also argue that this largely explains the resurgence of typed languages with more expressive type systems (like scala, rust, swift, idris, hack, etc.). Ultimately I think we just have to agree to disagree here.
> (P.S. Technically speaking, Smalltalk is strongly typed with message passing semantics for methods implemented through late binding. It's not "untyped.")
Untyped is commonly used in academic literature to refer to "dynamically typed" languages[1]. The strong/weak typing distinction is arguably imprecise or a useless distinction, especially for dynamically typed languages. For example, how does smalltalk prevent "type punning" when functions do not declare the types of values they may be called on? Perhaps you can make the argument that dynamic languages like these can justify their claim to "strong typing" by having builtin operators that do not make implicit conversions of the values they work on, but this guarantee doesn't hold in general in user defined code, so it seems like a useless distinction.
[1]: http://stackoverflow.com/questions/9154388/does-untyped-also...