Hacker News new | ask | show | jobs
by jamespwilliams 1652 days ago
One annoying bit about Go's generics is that you can use type parameters in functions, but not in methods.

So for example, maybe you'd want to write a Map function for the Optional type in this article, which returns None if the option is None, or calls a given function with the value of the Optional otherwise.

You'd probably write it like this:

    func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
But that doesn't work: "option/option.go:73:25: methods cannot have type parameters"

The type inference is also a bit limited, e.g: let's say you have a None method:

    func None[T any]() Option[T] { ... }
And you call it somewhere like:

    func someFunction() option.Option[int] {
        if (!xyz) {
            return option.None()
        }
        // ...
    }
it isn't able to infer the type, so you have to instead (in this case) write option.None[int]().

Generics is a super cool addition anyway though.

Edit: I just found https://go.googlesource.com/proposal/+/refs/heads/master/des... which has some details on why method type parameters aren't possible.

5 comments

To hoist this comment a bit higher: https://news.ycombinator.com/item?id=29583557 and this one which as the "a hah" moment for me: https://news.ycombinator.com/item?id=29583866

While you can't do this:

    type Option[T any] struct{}
    func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }
You can do this:

   type Option[T,U any] struct{}
   func (o Option[T,U]) Map(f func(a T) U) Option[U] { ... }
So this is not quite as restricting as it seems. Though it is still likely to be annoying. Runnable example: https://gotipplay.golang.org/p/2w2y1KEjXVE
> func (o Option[T,U]) Map(f func(a T) U) Option[U] { ... }

I love GO and it's simplicity and yes I do want generics, but is it just me or is this reading much much harder now ? It reminds me of those ugly voidfuncptr signatures of C and C++ :(

Maybe my eyes should just get used to it but I do feel a little my simple Go now reads not as easy. YMMV

Personally, I think that Rust's Option and Result are not well suited to Go. Option may serve a purpose, but the language was not built around these constructs. It's best (still IMO) to use generics to avoid code repetition, not to build semi-mathematical abstractions for their own sake right now. With time, we'll know how to use generics properly.
Without generics this is

    func (o IntToStringOption) Map(f func(a int) string) IntToStringOption { ... }
    // ... but that has to be repeated dozens of times
tbh I don't see it as much different. The func argument is the most complex part of the whole thing. And much of the readability comes from not using single-char type names, but you do learn to see past that with time.
You should also be able to do functions instead of methods, just downgrading the receiver to a normal parameter, if I understand correctly. Other than losing the method call syntax, this is probably the most reasonable workaround, due to the interface problem discussed in the generics proposal.
> func (o Option[T]) Map[U any](f func(a T) U) Option[U] { ... }

This is totally opinionated, but I better not see code like this on a review. Is there a way to make it a bit more readable and a bit less like Perl?

It only looks messy. It’s not particularly difficult to read, and with what it’s expressing there’s really no way of expressing it in shorter terms. “A method on an Option[T] named Map, generic over type U, taking one argument, a function from a T to a U, and returning an Option[U].”

Mind you, I wouldn’t mind colons and arrows as separators which I think make it easier to read; here’s what it’d look like in a somewhat more Rust-like syntax:

  fn Map<U: any>(self: Option<T>, f: fn(a: T) -> U) -> Option<U> { ... }
I would also note that signatures like these are mostly found in foundational types; they take a bit more effort and practice to write, but you don’t often have to do so; and have the outcome that the API is more pleasant to use—no more interface objects and downcasting everywhere, in Go terms.
I got your point, and it's worth mentioning that there's that rare moment when Rust looks more readable than Go.

Concerning resulting API: isn't code generation solves a problem with interface objects and downcasting?

I’m not sure what you’re asking. Generics are code generation (look into the term monomorphisation), just automatic and managed by the compiler for better convenience than manual code generation by other means, and type safe for better correctness and efficiency than interface objects and downcasting.
Thank you, i wasn't able to decipher the syntax until i read your comment “A method on an Option[T] named Map, generic over type U, taking one argument, a function from a T to a U, and returning an Option[U].”
I’d argue a lot of the ugliness of this is a product of the basic syntax of genetics in go.

The syntax in other languages with generics (C#, Swift, Java, and even c++) for this construct is easy to read. And obviously there’s always Haskell where you often don’t need explicit type annotations at all :D

I guess in Java, it'd look something like this:

  public class JavaOption<T> {
    
    public <U> JavaOption<U> map(Function<T, U> func) {
        //todo
    }
  }
Kotlin might be a closer match in semantics if I use an extension function:

  fun <T, U> Optional<T>.map(func: (T) -> U): Optional<U>  {
      // todo
  }
What's un-readable about this ? I haven't even read the article but I can understand this. Probably because of prior experience in C++, Java. But it seems sweet and succint.

At the max, one can make a couple of type-aliases for a bit more legibility, but that's all one can squeeze.

I want to mention that Go doesn't have a ternary operator, because it is "used too often to create impenetrably complex expressions"[1]

[1] https://go.dev/doc/faq#Does_Go_have_a_ternary_form

There is another way, you can declare option as

   type Option[T any] *T

   nil is None

   opt == nil instead of IsNone()

   func Some[T any](t T) Option[T] { return &t }

   *opt instead of opt.Get()

   option.Map(opt, func(x int) double { return double(x) }) for the monadic behavior
I wish there was type inference for function arguments, so that you could write func(x) { return double(x) }. Maybe in a couple of years the Go team could be convinced.
you are programming go why do you expect consistency
>One annoying bit about Go's generics is that you can't use type parameters in methods.

i haven't written go in a long time (generics would/could get me to go back to it) but are you saying that functions can't be generic? or is members here vernacular for class (struct?) associated functions? i thought those were called "receivers", which you mention further down. so it looks to me like you're saying that functions can't be generic. to which i ask: wtf is the point of generics when functions can't be generic...?

You can use type parameters in functions, but not methods. Methods are functions which have a receiver, so:

    // This is a function, you can use type parameters here:
    func Foo[T any](g T) { ... }
    
    type bar struct {}

    // This is a method, you can't use type parameters here:
    func (b bar) Foo[T any](g T) { ... }
In the second case, "Foo" is a method which has a "bar" instance as a receiver.

I've edited my original post to make it a bit clearer.

Doesn't C++ have a similar restriction, in that virtual methods cannot be templates? And in Go, with its dependence on interfaces everywhere, every method is the equivalent of /comparable to a C++ virtual method.
Yes, and you are right, Go interfaces are similar to pure virtual base classes in C++ (to the extent of virtual dispatch being discussed).
so does this mean there are no generic structs in generic go?
You can do

    type X[T any] struct{}
    func (x *X[T]) Method() {}
just fine. What you can't do is

    func (x *X[T]) Method[T2 any]() {}
Not saying mixing OO and generics could never have any merit, but.. Isn't a method just a function having an object as first parameter. Does Go change this beyond "syntactic sugar" somehow? Been a while from coding Go, so interested to hear.

The rationale seems to me that generics be functions first (ok, procedural), and not complecting it with objects and OO too much, whatever that mix could mean..

> Isn't a method just a function having an object as first parameter.

Differences:

- Methods must be defined in the same package as the receiver.

- Methods can be used to implement interfaces.

- Methods can be discovered dynamically by inspecting the type of the receiver (either through reflection or with a dynamic cast).

In other words, methods cannot introduce generic types.