1. 25
  1.  

  2. 11
    1. 2

      humm, i thought this would be real thang though.

    2. 6

      (Reading the proposal) I am not convinced by type lists as proposed, they seem un-moduloar.

      Summary: The bounds on type parameters are “constraints”, which are an extension of existing interface types (which describe a set of methods that the object should have). This makes it easy to declare that values of a parameter type must have a certain method. However, we may also want to use operators / multimethods on those values, and interface types are not expressive enough to specify this. The solution in the Generics proposal is type lists, basically you can define a constraint with a hardcoded list of allowed “underlying types” for your values (in addition to interface constraints).

      For example to say “x < y should be supported on values of my type parameter”, the proposal suggests to define the following constraint:

      // Ordered is a type constraint that matches any ordered type.
      // An ordered type is one that supports the <, <=, >, and >= operators.
      type Ordered interface {
      	type int, int8, int16, int32, int64,
      		uint, uint8, uint16, uint32, uint64, uintptr,
      		float32, float64,
      		string
      }
      

      I think this is un-modular. This means the standard library will contain a single, authoritative list of all types that can use the comparison operator in generic code. Maybe it’s okay for numeric comparison, but in general this approach sounds like it will add a lot of friction. You want to allow library authors to introduce concepts, and then library users to introduce their own user-defined types satisfying these concepts (as interface types allow for methods as opposed to operators); but this design forces library authors to decide in advance of a fixed list of admitted types.

      If you compare with Haskell type classes or Rust traits or Scala implicits, in those languages it is easy to express that comparison operators for a type should be available. In Haskell for example Ord a is a constraint that basically requires that an operator (<) :: a -> a -> Bool is available, and usage of x < y under an explicit Ord a constraint will correctly infer that this is the operator we mean. In other words, a constraint on a type a come with operations that mention a, in more flexible ways than just “I want values at a to have this method”. Ord a is an example of constraint allowing to use a binary operators, but for example Default a is a constraint with that provides a default value of type a (default :: a), so here there is no value of a to call the operation on, a is the output of the overloaded operation default.

      1. 0

        You want to allow library authors to introduce concepts, and then library users to introduce their own user-defined types satisfying these concepts (as interface types allow for methods as opposed to operators); but this design forces library authors to decide in advance of a fixed list of admitted types.

        I don’t think this is true - the example you give is special, because the standard library knows only those types can be comparable, since there’s no operator overloading.

        Can you give an example of your issue without using that special case? My understanding is that library authors can define a type constraint that uses an interface, and downstream can then just implement that interface.

        Eg.

        interface Comparable {
            CompareTo(other Comparable) int
        }
        
        func SomeAlgo[T Comparable](input []T) {
            ..
        }
        
        1. 2

          As I said: it depends on how your concepts can be formulated. If they fit the mold of interfaces (they are about methods that you can call on the values of the parametrized type), then this works well. But when they don’t, it does not. Binary methods / multi-parameter functions like “compare” are not easily presented as interfaces.

          In your example, nothing tells you that the two elements to be compared have to be of the same type, while you get this assurance for free with a signature such as (<) :: a -> a -> Bool (it says that the operator < takes two a and returns a Bool). In practice the CompareTo function for a user-defined type will do a type check and fail if the argument does not have the same type, but you can still easily mix things up, pass an argument of the wrong type to a CompareTo method, and get no warning that things are wrong. This is precisely the sort of imprecision that people are trying to solve by adding genericcs in the first place.

          1. 1

            As I said: it depends on how your concepts can be formulated. If they fit the mold of interfaces (they are about methods that you can call on the values of the parametrized type), then this works well. But when they don’t, it does not.

            “Methods that you can call on values” is the only way to express concepts in Go. So I don’t think this is really a restriction. Or, at least, not a new restriction. I understand why some people would feel encumbered by this lack of expressivity — I do too, sometimes — but it’s an important part of what keeps Go so simple.

            1. 2

              This restriction was apparently problematic enough to the authors of the proposal that it’s their third version trying to devise mechanisms to avoid it (two previous versions used “contracts” presented under different forms). In my top comment, I am making the point that the mechanism they currently propose to work-around the restriction, namely “type lists”, is un-modular and may turn out to cause friction in practice, because it assumes that the person who defines a generic bound knows about all potential users of the bound (technically, this is often called a “closed-world assumptIon”).

              (The previous reply made the point that the example is specific to a hardcoded operator of the standard library, so this is fine. I think that if the mechanism was only meant to apply in a couple hardcoded cases, it could be hidden underneath a few predeclared “primitive contracts” (such as comparable, compiler-defined without showing the definition) and not exposed to users. If it is exposed to users, it is probably because there are intended uses in user-libraries.)

              1. 2

                I believe type lists only exist to allow generic functions to specify which built-in language syntactic forms are allowed on the generic types. The reason why comparable is compiler defined is because it’s impossible to actually define with type lists. Equality (== and !=) is indeed an open set of underlying types (namely, some struct types). Every other built-in bit of syntax does follow the closed-world assumption: there is a finite list of underlying types that support <, for example. I cannot, at least, think of any reason to use type lists except to allow support for things like indexing, range, operators, etc.

                I think the reason why they did not define a set of built in primitive contracts is because there’s something of an explosion of possibilities and required names. Here’s a table I made of the different ways a type can be used and what underlying types support them (that I make no claims about being fully correct or exhaustive, it was just a quick attempt):

                operation              underlying types
                --------------         ----------------
                <, >, <=, >=           [u]int?, string, float?
                ==, !=                 [u]int?, string, float?, complex?, some structs
                +                      [u]int?, string, float?, complex?
                -, *, /                [u]int?, float?, complex?
                |, ^, %, &, &^         [u]int? 
                <<, >>                 [u]int? but right hand side must be uint?
                &&, ||, !              bool
                x <- y                 chan T, chan<- T
                <-x                    chan T, <-chan T
                x[n]                   [n]T, *[n]T, []T, map[K]V, string
                x[a:b]                 [n]T, *[n]T, []T, string
                x[a:b:c]               [n]T, *[n]T, []T
                range                  [n]T, *[n]T, []T, map[K]V, string, chan T, <-chan T
                T{}                    [n]T, []T, map[K]V, structs
                
                builtins               underlying types
                ---------------        ----------------
                len(x)                 [n]T, *[n]T, []T, map[K]V, chan T, <-chan T, chan<- T, string
                cap(x)                 [n]T, *[n]T, []T, chan T, <-chan T, chan<- T
                make(T)                map[K]V, chan T, <-chan T, chan<- T
                make(T, n)             map[K]V, chan T, <-chan T, chan<- T, []T
                make(T, n, m)          []T
                delete(m, k)           map[K]V
                
                other special cases    why
                -------------------    ---
                append(t []T, ts ...T) but if T is byte, then ts can be a string
                copy(d, s []T)         but if T is byte, then s can be a string
                complex(a, b)          float? and outputs complex?
                real(a), imag(a)       complex? and outputs float?
                

                I hope that demonstrates some of the complexity of attempting to come up with a taxonomy of the allowed syntactic forms and why it might be reasonable to just leave that up to the users, especially when it is, indeed, a closed world except for equality. If I recall, the idea of defining builtin contracts was brought up many times during the design iteration, and it always failed when working out the specific details.

                All that said, I find type lists to be particularly inelegant, and my least favorite aspect of the design. I hope something else is found to solve the problem: The desire to explicitly specify what syntax you are allowed to use on generic types so that you cannot silently break users of your generic function during a refactoring. For example, changing a usage of T{} to make(T) isn’t always valid for every T.


                Also, as you noted, the example above that gave a non-type safe compare interface and function using it. The type safe version would be

                type Comparable[T any] interface {
                	CompareTo(T) int
                }
                
                func SomeAlgo[T Comparable[T]](input []T) {
                	..
                }
                

                which requires methods of signature like

                func (m MyType) CompareTo(n MyType) int { ... }
                

                to satisfy the interface.

                1. 1

                  Your example defines an interface ComparableWith[T] of things that can be compared with T (I made a slight renaming to clarify my question). Is it possible in Go to express the interface of “being comparable to oneself”? I can always use [T ComparableWith[T]] to express the same bound, but it would be nice to be able to write

                  type Comparable interface {
                    CompareTo(Self) int
                  }
                  
                  fun SomeAlgo[T Comparable](input []T) { ... }
                  

                  I’m not aware of a way to express those “self types” in Go (and I can’t find them in the specification), but I’m not a Go expert so I thought I would ask.

                  1. 1

                    There is no “self type” currently in the language and I don’t believe anything like that exists in the current proposal, so I do not believe so.

                  2. 1

                    Great reply, thanks!

        2. 8

          When I first came to go from C#, I found the lack of generics really noticeable, and missed them a lot, but over the years of writing go, I can only count the times I have really wanted them on one hand.

          I am looking forward to getting them in the language, but I hope it doesn’t mean everyone uses them everywhere, and we end up with a bloat of type parameters and type constraints, which C# suffers from if you use type constraints (e.g. in and out, representing covariance and contravariance.)

          1. 1

            yeah, in my modest experiments with golang, i must confess, i don’t miss generics all that much, i would rather trade fast compile times with that any day. nevertheless, it would be a welcome addition to the language.

            what is even cooler is this

            “This design does not support template metaprogramming or any other form of compile time programming.”

            good !