6 ms·
>For type parameters, prefer functions to methods >The Tree example illustrates another general guideline: when you need something like a comparison function,
by Groxx 5y ago
>For type parameters, prefer functions to methods
>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.
- cube2222 5y agoAgreed. I almost never use sort.Sort in practice, sort.Slice is just so much more usable. 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-slower https://planetscale.com/blog/generics-can-make-your-go-code-...
- staticassertion 5y ago> This means that a time.Time pointer has the same GCShape as an uint64, a bytes.Buffer and a strings.Builder. > 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.
- shp0ngle 5y agoYeah, exactly that. Faster compile times.
- kgeist 5y agoAnd also smaller binaries. For comparison, our templates-heavy C++ project's binary is 1 GB in debug mode and 200 MB in release mode. It takes 10-20 seconds just to link.
- deleted 5y ago[deleted]
- staticassertion 5y agoAll of that gets removed though
- nyanpasu64 5y agoAgreed. If I could retroactively alter Rust's design, I'd remove non-marker traits with methods, and instead have functions and data structures take method or vtable pointers. In case of static dispatch, the method pointers would be passed as compile-time const generic parameters. 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, or having individual types or functions add comparator parameters in an ad-hoc fashion (https://doc.rust-lang.org/std/primitive.slice.html#method.sort_by https://doc.rust-lang.org/std/primitive.slice.html#method.so...).
- twanvl 5y agoThe ergonomics of this are really bad without some kind of implicit parameters and trait resolution. Without trait methods, ad-hoc overloading becomes impossible. You would have to explicitly specify what + operator and what == operator you use everywhere. After all, there could be multiple different additions or comparisons defined for a type, so which one do you mean by `+`? 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.
- zozbot234 5y ago> This means that the comparison has to be stored in the struct, with the corresponding runtime overhead. 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.
- nyanpasu64 5y ago
- azth 5y ago> 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. Java 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));
- Groxx 5y agoI don't think I'd call this "neatly"... more "a weird but pragmatic hack to improve an incredibly common pattern that had no concise option prior to java 8". And it kinda encourages continuing the "keep declaring a lambda that could have just been a function reference if the API had used a function instead" pattern, which is not great. But I do very much like it. It improves Java's ergonomics quite a lot.
- layer8 5y agoWhat I like about Java functional interfaces is that they give a place to specify an interface contract. For example, with Comparator, it specifies that instances must implement a total ordering. Purely structural function types don't have that affordance. 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.
- watwut 5y ago> keep declaring a lambda that could have just been a function reference Yes, but those are less readable, too much syntax and not enough javadoc/names on them.
- suzuki 5y ago> 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. 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 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 // 109