Agreed. And if you prefer methods to functions, you can define methods on such a function type! In my LINQ in Go (https://github.com/nukata/linq-in-go), I define a higher order function type:
type Enumerator[T any] func(yield func(element T))
and several methods on it, for example, // Where creates an Enumerator which selects elements by appling
// predicate to each of them.
func (loop Enumerator[T]) Where(predicate func(T) bool) Enumerator[T] {
return func(yield func(T)) {
loop(func(element T) {
if predicate(element) {
yield(element)
}
})
}
}
Naturally, if you need another type parameter for a method, you must define it as a function. For example, // Select creates an Enumerator which applies f to each of elements.
func Select[T any, R any](f func(T) R, loop Enumerator[T]) Enumerator[R] {
return func(yield func(R)) {
loop(func(element T) {
value := f(element)
yield(value)
})
}
}
Now, with the function which converts a slice to an Enumerator, // From creates an Enumerator from a slice.
func From[T ~[]E, E any](x T) Enumerator[E] {
return func(yield func(E)) {
for _, element := range x {
yield(element)
}
}
}
you can write the following: seq := Select(func(e int) int { return e + 100 },
From([]int{
7, 8, 9,
})).Where(func(e int) bool {
return e%2 != 0
})
seq(func(e int) {
Println(e)
})
// Output:
// 107
// 109Java neatly solves this by means of Functional Interfaces. Interfaces that have a single method related to the generic type parameter can be called by using lambda syntax. Take for example sorting:
<T> void sort(T[] a, Comparator<? super T> c)
A `Comparator<T>` has a method `int compare(T o1, T o2)`, which allows the caller to invoke `Arrays.sort` by passing a lambda: Arrays.sort(list, (l1, l2) -> l1.compareTo(l2));But I do very much like it. It improves Java's ergonomics quite a lot.
A slightly problematic point, however, is that you can assign a method reference from one functional interface to a variable of another functional interface based purely on structural typing, even if they happen to have incompatible interface contracts. For example, if you had a PartialOrder functional interface with the same parameter/return types as Comparator, then you could assign PartialOrder::compare to a Comparator variable although the semantics aren't compatible. Whereas nominal typing prevents you from assigning a PartialOrder to Comparator (no "duck typing"), Java circumvents the nominal type system here for method references, with the result that one may assign incompatible implementations without noticing.
Yes, but those are less readable, too much syntax and not enough javadoc/names on them.
Also, based on the recent blog post[0] a method-based approach will be a tiny bit slower than the function-based approach. Not that it matters for most code though.
[0]: https://planetscale.com/blog/generics-can-make-your-go-code-...
> In the current implementation of Generics in 1.18, every runtime invocation of a generic function will transparently receive as its first argument a static dictionary with metadata about the arguments being passed to the function.
> That’s right, after the monomorphization step, the generated function shape needs to take as a runtime input the virtual method tables for all its generic arguments.
What an odd design. I'm curious to read the justification for this. I assume it's faster compile times.
Do you mean single-method traits? Because it already sounds like a pain in the ass for Hash or Ord, but it sounds absolutely horrendous for Iterator.
> This allows picking different ordering operators for different trees or sorts defined on the same type, instead of requiring wrapper types defining custom comparison operators
Sounds like an aggressive worsening of the default, and a solution in search of a problem really: if that’s a really common need you could have a generic wrapper rather than a specific one (like Reverse).
Your complaint about ad-hoc comparator seems odd as your method would essentially require always doing that, unless I’m misunderstanding what you mean.
No, the pointer to vtable can point to a struct value containing multiple function pointers.
> if that’s a really common need you could have a generic wrapper rather than a specific one (like Reverse).
The way I see generic wrappers, it's not the integer that's reversed, but the data structure defined over it, and requiring wrapping/unwrapping reversed integers/etc. is ceremony in the wrong location. With specific wrappers, you could argue it often has semantic significance representing how your specific type isn't just a regular integer, but a Uid or such.
> Your complaint about ad-hoc comparator seems odd as your method would essentially require always doing that, unless I’m misunderstanding what you mean.
My change to Rust would make all trait-bounded generic functions consistently allow supplying a trait implementation separate from the underlying type, effectively transforming this pattern from an ad-hoc change to a language construct more flexible than globally coherent traits. From there on, you could layer on optional ergonomic improvements, like implicit trait implementations or optional default traits (while keeping the first-class ability to supply any trait implementation desired).
Is the idea practical? Maybe. Currently structs can't supply default field values, even though traits can supply default methods. But the hard part is that default trait methods are effectively templates, not functions:
trait A {
fn f() -> i32;
fn g() -> i32 {
Self::f()
}
}
struct T1;
impl A for T1 {
fn f() -> i32 {
1
}
}
struct T2;
impl A for T2 {
fn f() -> i32 {
2
}
}
To preserve the current semantics (a debatable goal), instead of building method table structs out of free functions (the most minimal and elegant implementation, though less ergonomic) like `static T1_A = A<T1>{...}`, you'd still have to keep a compromise syntax of some sort, closer to current `impl` but allowing you to optionally specify a name for the impl.Another issue to be addressed is that global coherence can serve as a lint against accidentally implementing the same trait twice for a type (as opposed to deliberately providing two different implementations with different names the user can select.)
Another problem is that a data structure's invariant can depend on the trait implementation. For example a tree is balanced with a comparison. This means that the comparison has to be stored in the struct, with the corresponding runtime overhead. With traits you know that there is only a single implementation for any particular type, so if the trait v-table is an argument to every method call there is no risk of different implementations being used at different times.
The runtime overhead is solvable in principle by making the comparison a const-generic parameter of the data structure type. But to do this properly requires dependent types, because the type of that const-generic parameter is taken from an earlier parameter in the same definition. It's a dependent sum type, sometimes called a dependent record.
Some of the flexibility in C++ here is desirable, much of it is either wanking or outright dangerous. C++ will allow a float generic for example, which is obviously a footgun because good luck explaining why Thing<NaN> and Thing<NaN> are different types...
Rust expects to sooner or later ship Const generics for user-defined types, but to exclude the nonsense of "float generic" the likely rule goes like this: Your const parameter type must derive PartialEq and Eq. It won't be enough to implement them yourself as your implementation might be nonsense (e.g. misfortunate::Maxwell implements Eq but no Maxwell is equal to anything, including itself) you'll need to use the derive macro which promises the compiler these things actually have working equivalence so that Thing<A> and Thing<B> are the same type if A and B are equivalent.
It's not nonsense, there are legitimate use-cases for floating point template params. A classic example is something like generateInlinedVersionOfFunction<someInt, someFloat>() that generates a class or function with some particular values inlined for efficiency. Using nan as a template parameter is stupid, but it's not especially more dangerous than using nan in general, since any issues specific to its compile-time usage will manifest at compile time, not runtime.
It does nobody any favours to just dismiss advanced features Rust doesn't support as dangerous and unnecessary just because most Rust programmers haven't come across a use-case.
I think requiring decidable equality for const generics is a fine approach for a limited MVP like what Rust is going with at present. Sure there are more general settings but they would need dependent types and the user would have to write proofs of type equality/apartness anytime the issue came up in a compile. Seems like a bit of a non-starter for now, even though it's effectively what enables non-trivial logical features like homotopy types.
This smells very bad. Seems like a better choice is a macro, and I understand that in C++ the macros are awful and worth avoiding, but Rust has decent declarative macros for this type of work.
Do you have some real world examples?
>The Tree example illustrates another general guideline: when you need something like a comparison function, prefer a function to a method.
>We could have defined the Tree type such that the element type is required to have a Compare or Less method. This would be done by writing a constraint that requires the method, meaning that any type argument used to instantiate the Tree type would need to have that method.
Yes yes a thousand times yes.
If you have an interface with a single method, or something with a single varying piece of behavior, please strongly consider accepting a function instead. Going from method to function is trivial - pass `thing.method`. Going the other way is impossible at runtime, it requires package-level code at compile-time.
This is true for many/most? languages, but it feels like a lot of Go libraries have forgotten it, and they're sometimes a huge unnecessary pain to deal with for that reason.