golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
120.42k stars 17.29k forks source link

proposal: spec: generic programming facilities #15292

Closed adg closed 3 years ago

adg commented 8 years ago

This issue proposes that Go should support some form of generic programming. It has the Go2 label, since for Go1.x the language is more or less done.

Accompanying this issue is a general generics proposal by @ianlancetaylor that includes four specific flawed proposals of generic programming mechanisms for Go.

The intent is not to add generics to Go at this time, but rather to show people what a complete proposal would look like. We hope this will be of help to anyone proposing similar language changes in the future.

smasher164 commented 6 years ago

@jimmyfrasche To me, it sounds like the key problem here is getting the dynamic type/value of a variable. Putting aside embedding, a simplified example would be

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

The value that is assigned to eneeds to have a dynamic (or concrete) type of something like *net.DNSError, which implements error. Here are a couple of possible ways that a future language change might tackle this problem:

  1. Have a magical unbox-like function that uncovers a variable's dynamic value. This applies to any type that is not concrete, for example unions.
  2. If the language change supports type variables, provide a means to get the dynamic type of the variable. With type information, we can write the unbox function ourselves. For example,
    func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
    }

    wrap can be written in the same way as before, or as

    func wrap(ec extraContext, err error) error {
    if err == nil {
    return nil
    }
    t := dynTypeOf(err)
    return MyError{
    e: v.(t),
    extraContext: ec,
    }
    }
  3. If the language change supports type constraints, here's an alternative idea:
    
    type E1 which_is_a_type_satisfying error
    type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 { if err == nil { return nil } return MyError{ e: err, extraContext: ec, } }

In this example, we accept a value of any type that implements error. Any user of `wrap` that expects an `error` will receive one. However, the type of the `e` inside `MyError` is the same as that of the `err` that is passed in, which is not limited to an interface type. If one wanted the same behavior as 2,

var iface error = ... wrap(getExtraContext(), unbox(iface))

chowey commented 6 years ago

Since no one else seems to have done it, I'd like to point out the very obvious "experience reports" for generics as called for by https://blog.golang.org/toward-go2.

The first is the built-in map type:

m := make(map[string]string)

The next is the built-in chan type:

c := make(chan bool)

Finally, the standard library is riddled with interface{} alternatives where generics would work more safely:

There may be others I'm missing. The point being, each of the above are where I would expect generics to be useful.

(Note: I am not including sort.Sort here because it is an excellent example of how interfaces can be used instead of generics.)

liu-xuewen commented 6 years ago

http://www.yinwang.org/blog-cn/2014/04/18/golang I think the generic is important.otherwise, cannot handle similar types.sometime interface cannot solve problem.

bxqgit commented 6 years ago

Simple syntax and type system are the important pros of Go. If you add generics, language will become an ugly mess like Scala or Haskell. Also this feature will attract pseudo-academic fanboys, who will eventually transform community values from "Let's get this done" to "Let's talk about CS theory and math". Avoid generics, it's a path to abyss.

dlsniper commented 6 years ago

@bxqgit please keep this civil. There's no need to insult anyone.

As for what the future will bring, we'll see, but I do know that while for 98% of my time I don't need generics, whenever I get to need them, I wish I could use them. How are they used vs how are they wrongfully used is a different discussion. Educating users should be part of the process.

kamalfarahani commented 6 years ago

@bxqgit There are situations in which generics are needed, like generic data structures (Trees, Stacks, Queues , ...) or generic functions (Map, Filter, Reduce, ...) and these things are unavoidable, using interfaces instead of generics in these situations just adds a huge complexity both for code writer and code reader and it also has a bad effect on code efficiency at run-time so it should be much more rational to add to language generics than trying to use interfaces and reflect to write complex and inefficient code.

rolurq commented 6 years ago

@bxqgit Adding generics doesn't necessarily adds complexity to the language, this can be achieved with simple syntax too. With generics, you are adding a variable compile time type constraint which is very useful with data structures, as @riwogo said.

The current interface system in go is very useful, nevertheless is very bad when you need, for example, a general implementation of list, which with interfaces need a execution time type constraint, nevertheless if you add generics, the generic type can be substituted in compile time with the actual type, making the constraint innecesary.

Also, remember, the people behind go, develop the language using what you call "CS theory and math", and are also the people that "are getting this done".

bxqgit commented 6 years ago

Also, remember, the people behind go, develop the language using what you call "CS theory and math", and are also the people that "are getting this done".

Personally I don't see much CS theory and math in Go language design. It's a fairly primitive language, which is good in my opinion. Also those people you are talking about decided to avoid generics and got things done. If it works just fine, why change anything? Generally, I think that constantly evolving and extending language's syntax is a bad practice. It only adds complexity which leads to chaos of Haskell and Scala.

kgtkr commented 6 years ago

The template is complicated but Generics is simple

pciet commented 6 years ago

Look at the functions SortInts, SortFloats, SortStrings in the sort package. Or SearchInts, SearchFloats, SearchStrings. Or the Len, Less, and Swap methods of byName in package io/ioutil. Pure boilerplate copying.

The copy and append functions exist because they make slices much more useful. Generics would mean that these functions are unnecessary. Generics would make it possible to write similar functions for maps and channels, not to mention user created data types. Granted, slices are the most important composite data type, and that’s why these functions were needed, but other data types are still useful.

My vote is no to generalized application generics, yes to more built-in generic functions like append and copy that work on multiple base types. Perhaps sort and search could be added for the collection types?

For my applications the only type missing is an unordered set (https://github.com/golang/go/issues/7088), I'd like this as a built-in type so it gets the generic typing like slice and map. Put the work into the compiler (benchmarking for each base type and a selected set of struct types then tuning for best performance) and keep additional annotation out of the application code.

pciet commented 6 years ago

smap built-in instead of sync.Map too please. From my experience using interface{} for runtime type safety is a design flaw. Compile-time type checking is a major reason to use Go at all.

pierrre commented 6 years ago

@pciet

From my experience using interface{} for runtime type safety is a design flaw.

Can you just write a small (type safe) wrapper ? https://play.golang.org/p/tG6hd-j5yx

pciet commented 6 years ago

@pierrre That wrapper is better than a reflect.TypeOf(item).AssignableTo(type) check. But writing your own type with map + sync.Mutex or sync.RWMutex is the same complexity without the type assertion that sync.Map requires.

My synchronized map use has been for global maps of mutexes with a var myMapLock = sync.RWMutex{} next to it instead of making a type. This could be cleaner. A generic built-in type sounds right to me but takes work I can't do, and I prefer my approach instead of type asserting.

andrewcmyers commented 6 years ago

I suspect that the negative visceral reaction to generics that many Go programmers seem to have arises because their main exposure to generics was via C++ templates. This is unfortunate because C++ got generics tragically wrong from day 1 and has been compounding the mistake since. Generics for Go could be a lot simpler and less error-prone.

It is would be disappointing to see Go becoming more and more complex by adding built-in parameterized types. It would be better just to add the language support for programmers to write their own parameterized types. Then the special types could just be provided as libraries rather than cluttering the core language.

zerkms commented 6 years ago

@andrewcmyers "Generics for Go could be a lot simpler and less error-prone." --- like generics in C#.

ianlancetaylor commented 6 years ago

It is disappointing to see Go becoming more and more complex by adding built-in parameterized types.

Despite the speculation in this issue, I think this is extremely unlikely to happen.

thwd commented 6 years ago

The exponent on the complexity measure of parameterized types is variance. Go's types (excepting interfaces) are invariant and this can and should be kept the rule.

A mechanical, compiler-assisted "type copy-paster" generics implementation would solve 99% of the problem in a fashion true to Go's underlying principles of shallowness and non-surprise.

By the way, this and dozens of other viable ideas have been discussed before and some even culminated in good, workable approaches. At this point, I'm borderline tinfoil-hatting about how they all dissappeared silently into the void.

On Nov 28, 2017 23:54, "Andrew Myers" notifications@github.com wrote:

I suspect that the negative visceral reaction to generics that many Go programmers seem to have arises because their main exposure to generics was via C++ templates. This is unfortunate because C++ got generics tragically wrong from day 1 and has been compounding the mistake since. Generics for Go could be a lot simpler and less error-prone.

It is disappointing to see Go becoming more and more complex by adding built-in parameterized types. It would be better just to add the language support for programmers to write their own parameterized types. Then the special types could just be provided as libraries rather than cluttering the core language.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/15292#issuecomment-347691444, or mute the thread https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv .

sighoya commented 6 years ago

Yes, you can have generics without templates. Templates are a form of advanced parametric polymorphism mostly for metaprogramming facilities.

DemiMarie commented 6 years ago

@ianlancetaylor Rust allows for a program to implement a trait T on an existing type Q, provided that their crate defines either T or Q.

DemiMarie commented 6 years ago

Just a thought: I wonder if Simon Peyton Jones (yes, of Haskell fame) and/or the Rust developers might be able to help. Rust and Haskell have probably the two most advanced type systems of any production languages, and Go should learn from them.

tarcieri commented 6 years ago

There's also Phillip Wadler, who worked on Generic Java, which eventually lead to the generics implementation Java has today.

DemiMarie commented 6 years ago

@tarcieri I don’t think that Java’s generics are very good, but they are battle-tested.

ianlancetaylor commented 6 years ago

@DemiMarie We've had Andrew Myers pitching in here, fortunately.

Based on my personal experience, I think that people who know a great deal about different languages and different type systems can be very helpful in examining ideas. But for producing the ideas in the first place, what we need are people who are very familiar with Go, how it works today, and how it can reasonably work in the future. Go is designed to be, among other things, a simple language. Importing ideas from languages like Haskell or Rust, which are significantly more complicated than Go, is unlikely to be a good fit. And in general ideas from people who have not already written a reasonable amount of Go code are unlikely to be a good fit; not that the ideas will be bad as such, just that they won't fit well with the rest of the language.

For example, it's important to understand that Go already has partial support for generic programming using interface types and already has (almost) complete support using the reflect package. While those two approaches to generic programming are unsatisfactory for various reasons, any proposal for generics in Go has to interact well with them while simultaneously addressing their shortcomings.

ianlancetaylor commented 6 years ago

In fact, while I'm here, a while back I thought about generic programming with interfaces for a while, and came up with three reasons why it fails to be satisfactory.

  1. Interfaces require all operations to be expressed as methods. That makes it painful to write an interface for builtin types, such as channel types. All channel types support the <- operator for send and receive operations, and it's easy enough to write an interface with Send and Receive methods, but in order to assign a channel value to that interface type you have to write boilerplate plate Send and Receive methods. Those boilerplate methods will look precisely the same for each different channel type, which is tedious.

  2. Interfaces are dynamically typed, and so errors combining different statically typed values are only caught at run time, not compile time. For example, a Merge function that merges two channels into a single channel using their Send and Receive methods will require the two channels to have elements of the same type, but that check can only be done at run time.

  3. Interfaces are always boxed. For example, there is no way to use interfaces to aggregate a pair of other types without putting those other types into interface values, requiring additional memory allocations and pointer chasing.

andrewcmyers commented 6 years ago

I am happy to kibitz on generics proposals for Go. Perhaps also of interest is the increasing amount of research on generics at Cornell lately, seemingly relevant to what might be done with Go:

http://www.cs.cornell.edu/andru/papers/familia/ (Zhang & Myers, OOPSLA'17) http://io.livecode.ch/learn/namin/unsound (Amin & Tate, OOPSLA'16) http://www.cs.cornell.edu/projects/genus/ (Zhang et al., PLDI '15) https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (Greenman, Muehlboeck & Tate, PLDI '14)

pciet commented 6 years ago

In benchmarking map vs. slice for an unordered set type I wrote out separate unit tests for each, but with interface types I can combine those two test lists into one:

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

Testing removing an item:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

This way I’m able to put my previously separate cases together into one slice of cases without any trouble:

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

For each concrete type I had to define the interface methods. For example:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

These generic tests could use a proposed compile-time type check:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

Source: https://github.com/pciet/pathsetbenchmark

pciet commented 6 years ago

Thinking about that more, it doesn't seem like a compile-time type check would be possible for such a test since you'd have to run the program to know if a type is passed to the corresponding interface method.

So what about a "generic" type that is an interface and has an invisible type assertion added by the compiler when used concretely?

mandolyte commented 6 years ago

@andrewcmyers The "Familia" paper was interesting (and way over my head). A key notion was inheritance. How would the concepts change for a language like Go which relies on composition instead of inheritance?

andrewcmyers commented 6 years ago

Thanks. The inheritance part doesn't apply to Go -- if you are only interested in generics for Go, you can stop reading after section 4 of the paper. The main thing about that paper that is relevant to Go is that it shows how to use interfaces both in the way they are used for Go now and as constraints on types for generic abstractions. Which means you get the power of Haskell type classes without adding an entirely new construct to the language.

pciet commented 6 years ago

@andrewcmyers Can you give an example of how this would look in Go?

The main thing about that paper that is relevant to Go is that it shows how to use interfaces both in the way they are used for Go now and as constraints on types for generic abstractions.

My understanding is that the Go interface defines a constraint on a type (e.g. "this type can be compared for equality using the 'type Comparable interface' because it satisfies having an Eq method"). I'm not sure I understand what you mean by a type constraint.

I'm not familiar with Haskell but reading a quick overview has me guessing that types that fit a Go interface would fit into that type class. Can you explain what is different about Haskell type classes?

A concrete comparison between Familia and Go would be interesting. Thanks for sharing your paper.

andrewcmyers commented 6 years ago

Go interfaces can be viewed as describing a constraint on types, via structural subtyping. However, that type constraint, as is, is not expressive enough to capture the constraints you want for generic programming. For example, you can't express the type constraint named Eq in the Familia paper.

pciet commented 6 years ago

Some thoughts on the motivation for more generic programming facilities in Go:

So there’s my generic test list that doesn’t really need anything added to the language. In my opinion that generic type I proposed doesn’t satisfy the Go goal of straightforward understanding, it doesn’t have much to do with the generally accepted programming term, and doing the type assertion there wasn’t ugly since a panic on failure is fine. I’m satisfied with Go’s generic programming facilities already for my need.

But sync.Map is a different use case. There’s a need in the standard library for a mature generic synchronized map implementation beyond just a struct with a map and mutex. For type handling we can wrap it with another type that sets a non-interface{} type and does a type assertion, or we can add a reflect check internally so items following the first must match the same type. Both have runtime checks, the wrapping requires rewriting each method for each use type but it adds a compile-time type check for input and hides the output type assertion, and with the internal check we still have to do an output type assertion anyway. Either way we’re doing interface conversions without any actual use of interfaces; interface{} is a hack of the language and won’t be clear to new Go programmers. Although json.Marshal is good design in my opinion (including the ugly but sensible struct tags).

I’ll add that since sync.Map is in the standard library ideally it should swap out the implementation for the measured use cases where the simple struct is more performant. The unsynchronized map is a common early pitfall in Go concurrent programming and a standard library fix should just work.

The regular map has just a compile-time type check and doesn’t require any of this scaffolding. I argue that sync.Map should be the same or shouldn’t be in the standard library for Go 2.

I proposed adding sync.Map to the list of built-in types and to do the same for future similar needs. But my understanding is giving Go programmers a way to do this without having to work on the compiler and go through the open source acceptance gauntlet is the idea behind this discussion. In my view fixing sync.Map is a real case that partially defines what this generics proposal should be.

Azareal commented 6 years ago

If you add sync.Map as a built-in, then how far do you go? Do you special case every container? sync.Map isn't the only container and some are better for some cases than others.

pciet commented 6 years ago

@Azareal: @chowey listed these in August:

Finally, the standard library is riddled with interface{} alternatives where generics would work more safely:

• heap.Interface (https://golang.org/pkg/container/heap/#Interface) • list.Element (https://golang.org/pkg/container/list/#Element) • ring.Ring (https://golang.org/pkg/container/ring/#Ring) • sync.Pool (https://golang.org/pkg/sync/#Pool) • upcoming sync.Map (https://tip.golang.org/pkg/sync/#Map) • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

There may be others I'm missing. The point being, each of the above are where I would expect generics to be useful.

And I'd like the unordered set for types that can be compared for equality.

I'd like a lot of work put into a variable implementation in the runtime for each type based on benchmarking so that the best implementation possible is usually what's used.

I'm wondering if there are reasonable alternative implementations with Go 1 that achieve the same goal for these standard library types without interface{} and without generics.

sighoya commented 6 years ago

golang interfaces and haskell type classes overcome two things (which are very great!):

1.) (Type Constraint) They group different types with one tag, the interface name 2.) (Dispatch) They offer to dispatch differently on each type for a given set of functions via interface implementation

But,

1.) Sometimes you want only anonymous groups like a group of int, float64 and string. How should you name such an interface, NumericandString?

2.) Very often, you do not want to dispatch differently for each type of an interface but to provide only one method for all listed types of an interface (Maybe possible with default methods of interfaces)

3.) Very often, you do no want to enumerate all possible types for an group. Instead you go the lazy way and say I want all types T implementing some Interface A and the compiler then search for all types in all source files you edit and in all libraries you use to generate the appropriate functions at compile time.

Although the last point is possible in go via interface polymorphism, it has the drawback to be a runtime polymorphism involving casts and how do you restrict the parameter input of a function to contain types implementing more than one interface or one of many interfaces. The go way is to introduce new interfaces extending other interfaces ( by interface nesting ) to achieve something similar but not with best practice.

By the way. I admit to those who say that go already has polymorphism and exactly therefore go is not any more a simple language like C. It is a high level system programming language. So why not expanding the polymorphism go offers.

pciet commented 6 years ago

Here’s a library I started today for generic unordered set types: https://github.com/pciet/unordered

This gives in documentation and testing examples that type wrapper pattern (thanks @pierrre) for compile-time type safety and also has the reflect check for run-time type safety.

What needs are there for generics? My negative attitude toward standard library generic types earlier centered around the use of interface{}; my complaint could be solved with a package-specific type for interface{} (like type Item interface{} in pciet/unordered) that documents the intended un-expressible constraints.

I don’t see the need for an added language feature when just documentation could get us there now. There is already large amounts of battle-tested code in the standard library that provides generic facilities (see https://github.com/golang/go/issues/23077).

zerkms commented 6 years ago

Your code type-checks in runtime (and from that perspective it's in no way better than just interface{} if not worse). With generics you could have had the collection types with compile-time type checks.

pciet commented 6 years ago

@zerkms run-time checks can be turned off by setting asserting = false (this wouldn't go in the standard library), there's a use pattern for compile-time checks, and anyway a type check just looks at the interface struct (using interface adds more expense than the type check). If interface isn't performing then you'll have to write your own type.

You're saying maximized-performance generic code is a key need. It hasn't been for my use cases, but maybe the standard library could become faster, and maybe others need such a thing.

zerkms commented 6 years ago

run-time checks can be turned off by setting asserting = false

then nothing guarantees correctness

You're saying maximized-performance generic code is a key need.

I did not say that. Type safety would be a great deal. Your solution is still interface{}-infected.

but maybe the standard library could become faster, and maybe others need such a thing.

may be, if core dev team is happy to implement whatever I need on demand and quickly.

urandom commented 6 years ago

@pciet

I don’t see the need for an added language feature when just documentation could get us there now.

You say this, yet you have no problem using the generic language features in the form of slices and the make function.

mahdix commented 6 years ago

I don’t see the need for an added language feature when just documentation could get us there now.

Then why bother using a statically typed language? You can use a dynamically typed language like Python and rely on documentation to make sure correct data types are sent to your API.

I think one of the advantages of Go is the facilities to enforce some constraints by the compiler to prevent future bugs. Those facilities can be extended (with generics support) to enforce some other constraints to prevent some more bugs in the future.

pciet commented 6 years ago

You say this, yet you have no problem using the generic language features in the form of slices and the make function.

I'm saying the existing features get us to a good balanced point that does have generic programming solutions and there should be strong real reasons to change from the Go 1 type system. Not how a change would improve the language but what problems people are facing now, such as maintaining a lot of run-time type switching for interface{} in the fmt and database standard library packages, that would be fixed.

Then why bother using a statically typed language? You can use a dynamically typed language like Python and rely on documentation to make sure correct data types are sent to your API.

I've heard suggestions to write systems in Python instead of statically-typed languages and organizations do.

Most Go programmer using the standard library use types that can't be completely described without documentation or without looking at the implementation. Types with parametric sub-types or general types with applied constraints only fix a subset of these cases programmatically and would generate a lot of work already done in the standard library.

pciet commented 6 years ago

In the proposal for sum types I suggested a build feature for the interface type switch where an interface use in a function or method has a build error emitted when a possible value assigned to the interface does not match any contained interface type switch case.

A function/method taking an interface could reject some types at build by having no default case and no case for the type. This seems like a reasonable generic programming addition if the feature is feasible to implement.

dc0d commented 6 years ago

If Go interfaces could capture the type of the implementer, there could be a form of generics that is completely compatible with current Go syntax - a single parameter form of generics (demonstration).

pciet commented 6 years ago

@dc0d for generic container types I believe that feature adds compile-time type checking without requiring a wrapper type: https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

dc0d commented 6 years ago

@pciet You are right. In the provided code, No. 4, sample states that the type is captured for slices and channels (and arrays). But not for maps, because there is only one and only one type parameter: the implementer. And since a map needs two type parameter, wrapper interfaces are needed.

BTW I have to emphasis the demonstrational purpose of that code, as a line of thought. I'm no language designer. This is just a hypothetical way of thinking about the implementation of generics in Go:

shelby3 commented 6 years ago

Discussion of genericity and all possible use cases in the context of a desire to minimize impacts while maximizing important use cases and flexibility of expression is a very complex analysis. Not sure if any of us will be able to distill it down to short set of principles aka generative essence. I’m trying. Any way, here some of my initial thoughts from my cursory perusal of this thread…

@adg wrote:

Accompanying this issue is a general generics proposal by @ianlancetaylor that includes four specific flawed proposals of generic programming mechanisms for Go.

Afaics, the linked section excerpted as follows fails to state a case of genericity lacking with current interfaces, “There is no way to write a method that takes an interface for the caller supplied type T, for any T, and returns a value of the same type T.”.

There is no way to write an interface with a method that takes an argument of type T, for any T, and returns a value of the same type.

So how else could the code at the call site type check that it has a type T as the result value? For example, the said interface may have a factory method for building type T. This is why we need to parametrise interfaces on type T.

Interfaces are not simply types; they are also values. There is no way to use interface types without using interface values, and interface values aren’t always efficient.

Agreed that since interfaces can’t currently be explicitly parametrised on the type T they operate on, the type T is not accessible to the programmer.

So this what typeclass bounds do at the function definition site taking as input a type T and having a where or requires clause stating the interface(s) that are required for type T. In many circumstances these interface dictionaries can be automatically monomorphised at compile-time so that no dictionary pointer(s) (for the interfaces) are passed into the function at runtime (monomorphisation which I presume the Go compiler applies to interfaces currently?). By ‘values’ in the above quote, I presume he means the input type T and not the dictionary of methods for the interface type implemented by type T.

If we then allow type parameters on data types (e.g. struct), then said type T above can be itself parameterised so we really have a type T<U>. Factories for such types which need to retain knowledge of U are called higher-kinded types (HKT).

Generics permit type-safe polymorphic containers.

C.f. also the issue of heterogeneous containers discussed below. So by polymorphic we mean genericity of the value type of the container (e.g. element type of the collection), yet there’s also the issue of whether we can put more than one value type in the container simultaneously making them heterogeneous.


@tamird wrote:

These requirements seem to exclude e.g. a system similar to Rust's trait system, where generic types are constrained by trait bounds.

Rust’s trait bounds are essentially typeclass bounds.

@alex wrote:

Rust's traits. While I think they're a good model in general, they would be a bad fit for Go as it exists today.

Why do you think they’re a bad fit? Perhaps you’re thinking of the trait objects which employ runtime dispatch thus are less performant than monomorphism? But those can be considered separately from the typeclass bounds genericity principle (c.f. my discussion of heterogeneous containers/collections below). Afaics, Go’s interfaces are already trait-like bounds and accomplish the goal of typeclasses which is to late bind the dictionaries to the data types at the call site, rather than the anti-pattern of OOP which early binds (even if still at compile-time) dictionaries to data types (at instantiation/construction). Typeclasses can (at least a partial improvement of degrees-of-freedom) solve the Expression Problem which OOP can’t.

@jimmyfrasche wrote:

I agree with the above link that typeclasses indeed aren’t subtyping and aren’t expressing any inheritance relationship. And agree with not unnecessarily conflating “genericity” (as a more general concept of reuse or modularity than parametric polymorphism) with inheritance as subclassing does.

However I do also want to point out that inheritance hierarchies (aka subtyping) are inevitable1 on the assignment to (function inputs) and from (function outputs) if the language supports unions and intersections, because for example a int ν string can accept assignment from an int or a string but neither can accept an assignment from an int ν string. Without unions afaik the only alternative ways to provide statically typed heterogeneous containers/collections are subclassing or existentially bounded polymorphism (aka trait objects in Rust and existential quantification in Haskell). Links above contain discussion about the tradeoffs between existentials and unions. Afaik, the only way to do heterogeneous containers/collections in Go now is to subsume all types to an empty interface{} which is throwing away the typing information and would I presume require casts and runtime type inspection, which sort of2 defeats the point of static typing.

The “anti-pattern” to avoid is subclassing aka virtual inheritance (c.f. also “EDIT#2” about the issues with implicit subsumption and equality, etc).

1 Regardless whether they’re matched structurally or nominally because subtyping is due to the Liskov Substitution Principle based on comparative sets and the direction of assignment with function inputs opposite to return values, e.g. a type parameter of a struct or interface can’t reside in both the function inputs and return values unless it invariant instead of co- or contra-variant.

2 Absolutism won’t apply because we can’t type check the universe of unbounded non-determinism. IOW, increasing static typing is in tension with algorithmic flexibility. So as I understand it to be, this thread is about choosing an optimum (“sweet spot”) limit to the level of stating typing w.r.t. to the genericity issues.

@andrewcmyers wrote:

Unlike Java and C# generics, the Genus generics mechanism is not based on subtyping.

It’s the inheritance and subclassing (not structural subtyping) that is the worst anti-pattern you don’t want to copy from Java, Scala, Ceylon, and C++ (unrelated to the problems with C++ templates).

@thwd wrote:

The exponent on the complexity measure of parameterized types is variance. Go's types (excepting interfaces) are invariant and this can and should be kept the rule.

Subtyping with immutability side-steps the complexity of covariance. Immutability also ameliorates some of the problems with subclassing (e.g. Rectangle vs. Square) but not others (e.g. implicit subsumption, equality, etc).

@bxqgit wrote:

Simple syntax and type system are the important pros of Go. If you add generics, language will become an ugly mess like Scala or Haskell.

Note that Scala attempts to merge OOP, subsclassing, FP, generic modules, HKT, and typeclasses (via implicit) all into one PL. Perhaps typeclasses alone might be sufficient.

Haskell is not necessarily obtuse because of typeclass generics, but more likely because it’s enforcing pure functions every where and employing monadic category theory to model controlled imperative effects.

Thus I think it’s not correct to associate the obtuseness and complexity of those PLs with typeclasses in for example Rust. And let’s not blame typeclasses for Rust’s lifetimes+exclusive mutability borrowing abstraction.

shelby3 commented 6 years ago

Afaics, in the Semantics section of the Type Parameters in Go, the problem encountered by @ianlancetaylor is a conceptualization issue because he’s (afaics) apparently unwittingly reinventing typeclasses:

Can we merge SortableSlice and PSortableSlice to have the best of both worlds? Not quite; there is no way to write a parameterized function that supports either a type with a Less method or a builtin type. The problem is that SortableSlice.Less can not be instantiated for a type without a Less method, and there is no way to only instantiate a method for some types but not others.

The requires Less[T] clause for the typeclass bound (even if implicitly inferred by the compiler) on the Less method for []T is on T not []T. The implementation of the Less[T] typeclass (which contains a method Less method) for each T will either provide an implementation in the function body of the method or assign the < built-in function as the implementation. Yet I believe this requires HKT U[T] if the methods of Sortable[U] need a type parameter U representing the implementing type, e.g. []T. Afair @keean has another way of structuring a sort employing a separate typeclass for the value type T which doesn’t require a HKT.

Note those methods for []T might be implementing a Sortable[U] typeclass, where U is []T.

(Technical aside: it may seem that we could merge SortableSlice and PSortableSlice by having some mechanism to only instantiate a method for some type arguments but not others. However, the result would be to sacrifice compile-time type safety, as using the wrong type would lead to a runtime panic. In Go one can already use interface types and methods and type assertions to select behavior at runtime. There is no need to provide another way to do this using type parameters.)

The selection of the typeclass bound at the call site is resolved at compile-time for a statically known T. If heterogeneous dynamic dispatch is needed then see the options I explained in my prior post.

I hope @keean can find time to come here and help explain typeclasses as he’s more expert and helped me to learn these concepts. I might have some errors in my explanation.

P.S. note for those who already read my prior post, note I edited it extensively about 10 hours after posting it (after some sleep) to hopefully make the points about heterogeneous containers more coherent.


The Cycles section appears to be incorrect. The runtime construction of the S[T]{e} instance of a struct has nothing to do with the selection of the implementation of the generic function called. He’s presumably thinking that the compiler doesn’t know if it’s specializing the implementation of the generic function for the type of the arguments, but all those types are known at compile-time.

Perhaps the Type Checking section specification could be simplified by studying @keean’s concept of a connected graph of distinct types as nodes for a unification algorithm. Any distinct types connected by an edge must have congruent types, with edges created for any types which connect via assignment or otherwise in the source code. If there’s union and intersection (from my prior post), then the direction of assignment has to be taken into account (somehow?). Each distinct unknown type starts off with a least upper bounds (LUB) of Top and a greatest lower bounds (GLB) of Bottom and then constraints can alter these bounds. Connected types have to have compatible bounds. Constraints should all be typeclass bounds.

In Implementation:

For example, it is always possible to implement parameterized functions by generating a new copy of the function for each instantiation, where the new function is created by replacing the type parameters with the type arguments.

I believe the correct technical term is monomorphisation.

This approach would yield the most efficient execution time at the cost of considerable extra compile time and increased code size. It’s likely to be a good choice for parameterized functions that are small enough to inline, but it would be a poor tradeoff in most other cases.

Profiling would tell the programmer which functions can most benefit from monomorphisation. Perhaps the Java Hotspot optimizer does monomorphisation optimization at runtime?

shelby3 commented 6 years ago

@egonelbre wrote:

There is Summary of Go Generics Discussions, which tries to provide an overview of discussions from different places.

The Overview section seems to imply that Java’s universal use of boxing references for instances in a container is the only axis of design diametrically opposing C++’s monomorphisation of templates. But typeclass bounds (which can also be implemented with C++ templates yet always monomorphised) are applied to functions not to container type parameters. Thus afaics the overview is missing the design axis for typeclasses wherein we can choose whether to monomorphise each typeclass bounded function. With typeclasses we always make programmers faster (less boilerplate) and can get a more refined balance between making compilers/execution faster/slower and code bloat greater/lesser. Per my prior post, perhaps the optimum would be if the choice of functions to monomorphise was profiler driven (automatically or more likely by annotation).

In the Problems : Generic Data Structures section:

Cons

  • Generic structures tend to accumulate features from all uses, resulting in increased compile times or code bloat or needing a smarter linker.

For typeclasses this is either not true or less of an issue, because interfaces only need to be implemented for data types which are supplied to functions which use those interfaces. Typeclasses are about late binding of implementation to interface, unlike OOP which binds every data type to it’s methods for the class implementation.

As well, not all the methods need to be put in a single interface. The requires clause (even if implicitly inferred by the compiler) on a typeclass bound for a function declaration can mix-and-match interfaces required.

  • Generic structures and the APIs that operate on them tend to be more abstract than purpose-built APIs, which can impose cognitive burden on callers

A counter argument which I think significantly ameliorates this concern is that the cognitive burden of learning an unbounded number of special case re-implementations of the essentially same generic algorithms, is unbounded. Whereas, learning the abstract generic APIs is bounded.

  • In-depth optimizations are very non-generic and context specific, hence it’s harder to optimize them in a generic algorithm.

This is not a valid con. The 80/20 rule says don’t add unbounded complexity (e.g. premature optimization) for code which when profiled doesn’t require it. The programmer is free to optimize in 20% of the cases whilst the remaining 80% get handled by the bounded complexity and cognitive load of the generic APIs.

What we’re really getting at here is the regularity of a language and generic APIs help, not hurt that. Those Cons are really not correctly conceptualized.

Alternative solutions:

  • use simpler structures instead of complicated structures
    • e.g. use map[int]struct{} instead of Set

Rob Pike (and I also watched him make that point in the video) seems to be missing the point that generic containers aren’t sufficient for making generic functions. We need that T in map[T] so we can pass the generic data type around in functions for inputs, outputs, and for our own struct. Generics only on container type parameters is wholly insufficient for expressing generic APIs and generic APIs are required for bounded complexity and cognitive load and obtaining regularity in a language ecosystem. Also I haven’t seen the increased level of refactoring (thus the reduced composability of modules that can’t be easily refactored) that non-generic code requires, which is what the Expression Problem is about that I mentioned in my first post.

In the Generic Approaches section:

Package templates This is an approach used by Modula-3, OCaml, SML (so-called “functors”), and Ada. Instead of specifying an individual type for specialization, the whole package is generic. You specialize the package by fixing the type parameters when importing.

I may be mistaken but this seems not quite correct. ML functors (not to be confused with FP functors) can also return an output which remains type parametrised. There would be no way to use the algorithms within other generic functions otherwise, so thus generic modules wouldn’t be able reuse (by importing with concrete types into) other generic modules. This appears to be an attempt to oversimplify and then entirely miss the point of generics, module reuse, etc..

Rather my understanding is that that package (aka module) type parametrisation enables the ability to apply type parameter(s) to a grouping of struct, interface, and func.

More complicated type-system This is the approach that Haskell and Rust take. […] Cons:

Quoting @ianlancetaylor in the linked document:

If you believe that, then it's worth pointing out that the core of the map and slice code in the Go runtime is not generic in the sense of using type polymorphism. It's generic in the sense that it looks at type reflection information to see how to move and compare type values. So we have proof by existence that it is acceptable to write "generic" code in Go by writing non-polymorphic code that uses type reflection information efficiently, and then to wrap that code in compile-time type-safe boilerplate (in the case of maps and slices this boiler plate is, of course, provided by the compiler).

And that’s what a compiler transpiling from a superset of Go with generics added, would output as Go code. But the wrapping would not be based on some delineation such as package, as that would lack the composability I already mentioned. Point being that there’s no short-cut to a good composable generics type system. Either we do it correctly or don’t do anything, because adding some non-composable hack that isn’t really generics is going to create eventually a clusterfuck inertia of patchwork half-assed genericity and irregularity of corner cases and workarounds making Go ecosystem code unintelligible.

It's also true that most people writing large complex Go programs have not found a significant need for generics. So far it's been more like an irritating wart--the need to write three lines of boilerplate for each type to be sorted--rather than a major barrier to writing useful code.

Yeah this has been one of the thoughts in my mind as whether going to a full blown typeclass system is justifiable. If all your libraries are based around it, then apparently it could be a beautiful harmony, but if we’re contemplating about the inertia of existing Go hacks for genericity, then perhaps the additional synergy gained is going to be low for a lot of projects?

But if a transpiler from a typeclass syntax emulated the existing manual way Go can model generics (Edit: which I just read that @andrewcmyers states is plausible), this might be less onerous and find useful synergies. For example, I realized that two parameter typeclasses can be emulated with interface implemented on a struct which emulates a tuple, or @jba mentioned an idea for employing inline interface in context. Apparently struct are structurally instead of nominally typed unless given a name with type? Also I confirmed a method of an interface can input another interface so afaics it may be possible to transpile from HKT in your sort example I wrote about in my prior post here. But I need to think this out more when I am not so sleepy.

I think it's fair to say that most of the Go team do not like C++ templates, in which one Turing complete language has been layered over another Turing complete language such that the two languages have completely different syntaxes, and programs in both languages are written in very different ways. C++ templates serve as a cautionary tale because the complex implementation has pervaded the entire standard library, causing C++ error messages to become a source of wonder and amazement. This is not a path that Go will ever follow.

I doubt anyone will disagree! The monomorphisation benefit is orthogonal to the downsides of a Turing complete generics metaprogramming engine.

Btw, the design error of C++ templates appears to me to be the same generative essence of the flaw of generative (as opposed to applicative) ML functors. The Principle of Least Power applies.


@ianlancetaylor wrote:

It is disappointing to see Go becoming more and more complex by adding built-in parameterized types.

Despite the speculation in this issue, I think this is extremely unlikely to happen.

I hope so. I firmly believe that Go should either add a coherent generics system or just accept that it will never have generics.

I think a fork to a transpiler is more likely to happen, partially because I have funding to implement it and am interested to do so. Yet I’m still analyzing the situation.

That would fracture the ecosystem though, but at least then Go can remain pure to its minimalist principles. Thus to avoid fracturing the ecosystem and allow for some other innovations I would like, I would probably not make it a superset and name it Zero instead.

@pciet wrote:

My vote is no to generalized application generics, yes to more built-in generic functions like append and copy that work on multiple base types. Perhaps sort and search could be added for the collection types?

Expanding this inertia is going to perhaps prevent a comprehensive generics feature from ever making it into Go. Those who wanted generics are likely to leave for greener pastures. @andrewcmyers reiterated this:

It ~is~ would be disappointing to see Go becoming more and more complex by adding built-in parameterized types. It would be better just to add the language support for programmers to write their own parameterized types.

pciet commented 6 years ago

@shelby3

Afaik, the only way to do heterogeneous containers/collections in Go now is to subsume all types to an empty interface{} which is throwing away the typing information and would I presume require casts and runtime type inspection, which sort of2 defeats the point of static typing.

See the wrapper pattern in comments above for static type checking of interface{} collections in Go.

Point being that there’s no short-cut to a good composable generics type system. Either we do it correctly or don’t do anything, because adding some non-composable hack that isn’t really generics…

Can you explain this more? For the collection types case having an interface defining the necessary generic behavior of contained items seems reasonable to write functions to.