type IntBox struct { v int }
type StrBox struct { v string }
func (b IntBox) MapToStr(f func(int) string) StrBox {
return StrBox{v: f(b.v)}
}
(Please forgive any typos I made on mobile.)It wasn't a great example because "Box" isn't really a useful type. But the point is that you no longer need to define a separate "MapToXXX" method for every type you might want to map to; now you can have just one type-generic "Map" method.
Without generics:
‘’’
type Stream[T any] struct{ ... }
func (s Stream[T]) MapInt(f func(T) int) Stream[int] { ... }
func (s Stream[T]) MapString(f func(T) string) Stream[string] { ... }
‘’’
Then later you need to map floats:
‘’’
func (s Stream[T]) MapFloat64(f func(T) float64) Stream[float64] { ... }
‘’’
Then later someone outside the package wants to map a custom type. Too bad! They’ll need to make some custom wrapper that doesn’t follow the pattern.
With the genetics approach the semantics are defined once and usable in all scenarios. You don’t need to keep adding methods; instead the caller can provide the mapping function. Common mapping functions could be pre defined for convenience.
I think it's trying to show a mapping operation for a generic container where the container values are of one type and the mapping function is allowed to return a container with values of a different type.
Without generics, something along the lines of the following (with runnable example at https://go.dev/play/p/KHBI1uAhbO0):
type MySlice []int
// Map maps from a slice of ints to a slice of float64s.
func (s MySlice) Map(f func(int) float64) []float64 {
var out []float64
for i := range s {
out = append(out, f(s[i]))
}
return out
}
From a quick search, this seems to be better explanation of this new 1.27 feature:https://www.gopherguides.com/articles/golang-generic-methods
(That uses an example that seems similar in spirit to the Interactive Tour's example, but with a more useful type of a Stack[T] and corresponding explanation seem clearer.)
// Map maps from a slice containing type In to a slice containing type Out.
func (s MySlice[In]) Map[Out any](f func(In) Out) []Out {
var out []Out
for i := range s {
out = append(out, f(s[i]))
}
return out
}
In short, you could always have methods on a generic type since Go first introduced generics in Go 1.18, but with 1.27, the methods on the generic type can also introduce their own additional type parameters.(Previously, you could achieve the same net effect with a top-level generic function, but then the code would not be grouped as nicely as hanging it off of the type, and arguably it now can have slightly better ergonomics in some cases. You can see more of the rationale from Robert Griesemer at https://github.com/golang/go/issues/77273.)
This is like building a very crude general-ish DSL inside the language. Because the tools are intentionally limited (as to limit the scope of the feature), the result looks ugly. Also, like with C++ templates, people find exploits to do what the designers didn't want them to, with even more elaborate workarounds.
I liked Go before generics. It had a clear identity. If you wanted to get cute, you could use go generate and generate code. They should've made that much more convenient and ergonomic, if they wanted to make the language more powerful (and the nice thing is that it still sits outside of the language).
I think the point Go was making is that these complex things generally have little use in application code, and 99% of the time they're there for people who want to show how smart they are, at the expense of code readability, and accessibility.
@hatchet.task()
async def my_task(...) -> SomeOutputType:
return SomeOutputType(...)
## imagine this is an API handler:
@api_handler("/some/path")
async def handle() -> ...:
result = await my_task.run(...)
# we now know `result` is of type `SomeOutputType` without any sort of type assertion, etc.
Admittedly, I'm not a Go expert, nor am I a programming languages expert. But I do feel that this type of behavior is really only possible (with nice ergonomics) with generics, and it's always been upsetting to me that somehow Python's type system feels more complete than Go's in this arena, or at least it has until more recently.Maybe this falls into the 1% of cases, but I'd suspect this sort of thing is more common than that.
Edit: I should have mentioned - in the Python example above, `@hatchet.task` is generic with the output type of the task it wraps.
Now, the verb "map" is the traditional name for this operator, but I agree it sounds confusing when the noun "map" is also a collection type. C# and SQL call this Select, if that helps.