Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> it's true that writing generic containers in Go isn't so great

That's one hell of an understatement.

> is it really sufficiently horrible to warrant that?

A number of people obviously and repeatedly say that yes, a statically typed language without parametric polymorphism is sufficiently painful to use something else instead.



I'm curious what programs people are writing where parametric polymorphism is so crucial. I've written hundreds of thousands of lines of Go code (servers, web apps, data processing, etc.), and there's probably been maybe O(hundreds) of lines of code that were annoying or tedious because of a lack of parametric polymorphism.

My suspicion is that what's more likely is that lots of people don't want to consider writing their programs a little differently, and see that writing code with parametric polymorphism is the only acceptable solution to their problem. But maybe there's a whole class of problem (actual problem, not a sub-problem like "I want to implement a container") that I don't come in contact with.


"But maybe there's a whole class of problem (actual problem, not a sub-problem like "I want to implement a container") that I don't come in contact with."

i want to apply a transformation to every element in a collection

i want to select the elements from a collection for which some predicate holds true


Use a for loop. Stop thinking in LISP/Haskell/LINQ, start thinking in Go. Go is an imperative programming language. When in Rome do as the Romans do. Look at the code of the standard library to see how it's done.


It's not because it's unfamiliar that people are objecting; it's because it's suboptimal. Loops are harder to get right (try writing the loop to count down from 100 to 0 inclusive on unsigned ints), and they involve more boilerplate, obscuring the meaning of the code.

I don't think generics are an imperative/functional thing so much as an important feature of any statically typed system.


Those are both sub-problems.


of every software problem in history


I think a big part of the issue is that in Java and C++, you really need generics a lot more than in Go. Without templates, you would not have any easy way of doing maps and lists in C++. There are no builtin types for those like in Go. The way the type system works in those languages also makes things very difficult if you don't have generics.

Think about the sorting example I wrote earlier: https://news.ycombinator.com/item?id=7080562 If you were writing it in Java, is sort.Sort an interface or an abstract base class? Well, you can only "extend" one class (single inheritance only), so you would probably want Sort to be a Java interface. That means that you would always have to implement all three functions, not just one as I did, since Java interfaces cannot have default implementations. The comments indicate that most posters didn't even consider the idea that you could reuse the StringSlice functions. That method of easy composition simply doesn't exist in Java.

In general, generics get used a lot as a band-aid to avoid multiple inheritance in C++ and Java. You can't (or shouldn't, in C++) have your Foo inherit from both a (non-abstract) Bar and Baz. But you can certainly template on them. In C++, this kind of thing is called "traits" and Alexandrescu wrote a whole book about it. It's also why std::string is actually std::basic_string<char, std::char_traits<char>, std::allocator<char> >. In Go, you don't need all this... you just implement as many interfaces as you like and you're done.

So the Java and C++ programmers need time and an open mind to get up to speed. There's also another contingent of programmers that really just wants Go to be like their favorite strongly-typed functional programming language (Haskell, Scheme, SML, etc.) I like to think that these people will eventually give up. After all, nobody comes into every Lisp thread demanding that Lisp grow a type-checker. People have sort of accepted that that is not what Lisp is about and that there are other languages that will give you that if you want. Hopefully much the same will happen for Golang.


> It's also why std::string is actually std::basic_string<char, std::char_traits<char>, std::allocator<char> >

Actually, that's not a band-aid. It allows strings to be generic over allocators without suffering the performance penalty of a virtual call on every malloc and free. Go's solution would be to just take the penalty of a virtual call (as interfaces require virtual calls everywhere) and try to make it up with devirtualization optimizations and/or inlining.

> After all, nobody comes into every Lisp thread demanding that Lisp grow a type-checker.

That's because making a workable type system for Lisp is a very hard problem. By contrast, generics are well known at this point.


It's certainly true that Go doesn't currently support compile-time polymorhpism, and C++ does. In that sense, Go interfaces are not a replacement for C++ templates, as you correctly note.

However, compile-time polymorphism is a mixed bag because of how it tends to blow out the icache by generating many copies of similar code. Java does away with it completely, and nobody seems to be protesting. The Linux kernel is written in C, and nobody seems to be protesting that the lack of compile-time polymorphism makes his/her job impossible. Clearly, the decision as to whether to include compile-time polymorphism in a language is something reasonable people can disagree about.

With regard to a type system for Lisp: it's been done before. Check out Qi.


It is common in both C++ and Java for a class to implement several interfaces. So, one can accomplish the same kind of genericity in C++ and Java as one can in Go. Both languages still eventually saw enough of a need for a kind of generics to develop one.

There is a difference in Go, and that is you don't need to explicitly implement the interface. That is, you don't need to declare that you do. But I am unconvinced that is so different from implementing interfaces in C++ and Java that C++ and Java programmers "need time and an open mind" to understand Go idioms.

Don't get me wrong: I think Go is a neat language and culture. I like many aspects of it. And I know the stance the designers of Go have on generics: they would like to do them, but they can't think of a design that they like. And they don't consider it that important, so they've bunted on it. They may do them in the future. But I find it strange when people who are not the designers of the language argue against having them, in any form.


Java 8 will have interfaces with default implementations.

There are a few of optional type checkers for Lisp to choose from.


Parametric polymorphism is crucial in an API like jOOQ's:

http://www.jooq.org

In fact, it is one of the fundamental cornerstones of internal DSLs


I think the biggest advantage of parametric polymorphism comes from program analysis, not just increased capability. The notion of parametricity depends crucially on parametric polymorphism and is super useful. Parametricity is the property that says greater polymorphism means lesser variation in implementation. Highly parametric functions can be almost completely described by their types along meaning it provides incredibly high value as documentation.

The most common simple example is reasoning about the following pair of functions

    f :: a -> a           g :: Int -> Int
While (f > g) obviously, we can completely characterize the behavior of `f` while we know almost nothing at all about `g`. Up to non-termination, `f` must be `id` while `g` can be any recursive function on integers.

                g a = 3
    f a = a     g a = a * 2
                g a = if (a == 0) then 1 else a * g (a - 1)
If you hold in advance the triviality of this example, then we can already notice how much better documentation `f`'s type provides than `g`'s. We also can produce a list of properties and laws about `f`'s behavior that we cannot easily do with `g`. This means that I'm able to reason about `f` better and more confidently.

---

So we can take it up a notch. Consider Functors in Haskell. In order to avoid the full generality, let's just say Functor is the container interface and types like [], Set, and Map instantiate it. This gives them the function

    fmap :: (a -> b) -> f a -> f b
where `f` is specialized to the particular container of interest

    fmap :: (a -> b) -> [a]     -> [b]    
    fmap :: (a -> b) -> Set a   -> Set b  
    fmap :: (a -> b) -> Map k a -> Map k b      -- for some key type k
Now what can we reason about `fmap`s knowing only their type? Well, since the type `a` contained inside each container is unknown (or, to be more clear, any implementation of fmap must work for any type `a` and thus it's "unknown" in that we can't take advantage of information about it) then the only thing `fmap` is capable of doing is applying the first function to those types.

Furthermore, since every mention of `a` vanishes in the result of the function we know that each value in the input container must either be transformed by the input function or dropped.

    -- a specific example, not a general function
    fmap f [a, b, c] = [f a, f b, f b]
In this way, parametricity has pruned down the possibilities of functions that can implement `fmap` a great deal. If `a` or `b` were specialized to specific types then this would not be the case as we'd have a large variety of specific functions that could apply to each one of them.

    notFmap :: (Int -> Char) -> [Int] -> [Char]
    notFmap f [n1, n2, n3] = [f (n1 + 2), head (show n2), chr n3] ++ "hello world!"
Indeed, in order to ensure that type-checking `fmap`s behave correctly we say they must follow a few laws. First, they respect function composition and second they respect identity

    fmap g (fmap f x) == fmap (g . f) x
    fmap id x         == id x
And we only ever need to check the first one because the second one is already guaranteed by a more sophisticated parametricity argument.

    fmap f []     = []
    fmap f (x:xs) = f x : fmap f xs
---

The take away is that while you can definitely write something that "maps over containers" without using parametric polymorphism, you have a harder time taking advantage of parametricity to have the types provide guarantees about the safe behavior of functions. Types (and in particular type inference) can sometimes make things more expressive, but their primary power lies in making things easier to understand and reason about.


I'm going to note that even if Go added some kind of parametric polymorphism at this point, it still wouldn't have the same advantages as parametric polymorphism in Haskell and related languages. In Go, you can branch on types at runtime, so in a hypothetical generic Go, you could write something which appears to only ever return its argument, but does something special if the argument is of a particular type:

    func<T> id(t T) T {
      switch t := T.(type) {
        case int: return t + 1;
        default:  return t;
      }
    }
The ability to examine on types at runtime can enable some interesting programs, but it also means that you don't get guarantees like you do in languages like Haskell. An example of this biting someone in practice is this blog post[1], where the author reasoned that because a library function expected an io.Reader, all it would do is use the Read method as exposed by Reader. In reality, though, the function was able to branch on the type and determine whether it also exposed a Close method, and called that if it could. That kind of feature destroys the useful static guarantees provided by parametric polymorphism in other languages like ML or Haskell.

[1]: http://how-bazaar.blogspot.co.nz/2013/07/stunned-by-go.html


As a second addendum, while andolanra's comment is completely true, you can actually do type dispatch in Haskell using the Typeable machinery; however, the type system requires that you mark your types when this occurs.

    what :: Typeable a => a -> a
Here `what` is no longer guaranteed to be `id`. Instead, we can translate it like this

    what :: (a -> TypeRep) -> a -> a
where `TypeRep` is, as it sounds, a representation of the type of a returned dynamically. This means that we can do careful coercions to, say, be `id` unless `a` is `Int` in which case we add `1`.

Suffice to say that the degree of guarantee breaking that this kind of code invites makes Typeable a very specialized library that's almost never recommended without a "unless you really know what you're doing" kind of warning.


> In Go, you can branch on types at runtime, so in a hypothetical generic Go, you could write something which appears to only ever return its argument, but does something special if the argument is of a particular type

I'm not sure why you bring this up - nearly every language that enables reflection lets you do this, including Haskell[0].

No decidable static type system can ensure correct semantics for all use cases, including even badly written code[1].

[0] And you don't even need reflection to do this, per se; it just makes it more convenient.

[1] One could design a static type system that did, but it would be undecidable.


Parametricity is definitely not impossible to violate, but it's generally morally correct to assume it mostly holds in Haskell. Parametric polymorphism helps make that workable. Further, type classes help to tag code that does use reflection.

You can always get around things, but languages that make a strong commitment to keeping reasoning as a priority are qualitatively different from those that do not.


The last part is backwards -- when F is a functor, the only law we need to check is the identity law, but checking the composition law isn't enough. For example,

  fmap _ _ = []
satisfies fmap f . fmap g = fmap (f . g), but not fmap id = id.


Whoops, that's correct.


heh, interesting. as somebody mucking about with containers in Go, i also recognize it to be a sub-problem, and not one really worth having a language-level debate over.

perhaps odd, because i come from a crappily-typed PHP background. but if PHP teaches you anything, it's that you can accomplish just about anything with arrays (maps/slices).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: