Live data from Hacker News

Go(lang): Robust generic functions on slices

go.dev

41–50 of 85 posts

Re: Go(lang): Robust generic functions on slices

#41
post #38
post #35

Earlier quoted context omitted.

Why do you need a tilda for `S ~[]E`, but not for `E comparable`? Both are type-constraining annotations, so why two syntaxes?

It is like S: where S = []E where E: comperable. I like the way Rust has it a lot more but it is still imperfect and feels arbitrary in ways. I find myself unsure where to put the templatization parameters and it doesn't feel perfectly symmetrical. Unsure if impl MyType where T: X is comperable to impl MyType Sometimes it needs impl MyType where T: X

Agreed, the Rust syntax is no golden standard either - but at least it's regular; this one really weirds me out.

Re: Go(lang): Robust generic functions on slices

#42
post #35

Earlier quoted context omitted.

type Something []string ensure that the underlying type is a slice

Why do you need a tilda for `S ~[]E`, but not for `E comparable`? Both are type-constraining annotations, so why two syntaxes?

Because when you define a "type Foo []int" you're definig a new type Foo that is not the same type as []int.

So if the type parameter said "[]E" where E is a comparable then "Foo" wouldn't be allowed because Foo is not the same type as "[]int" (for that you'd need a type alias i.e "type Foo = []int". This is by design since it allows you to define new types you don't want to get accidentally used where you don't want them to.

When defining library functions that are truly generic you may want instead to let them be used on the "underlying type". For example, Sort library authors decided that generally you want it to just work on the underlying type and that's why they added ~ in front of the generic type. Otherwise every invocation of Sort you'd need to convert the type e.g. "slices.Sort([]int(myfoo))".

The other type parameter "E comparable" doesn't need the tilde because "comparable" is not an actual type but a type constraint.

A type constraint is an interface that defines the set of permissible type arguments for the respective type parameter and controls the operations supported by values of that type parameter.

You don't need a tilde because any type that implements that constraint will be accepted. Also any type implementing a constraint that embeds that constraint also works

Re: Go(lang): Robust generic functions on slices

#43
post #39
post #36

I feel like I’m taking crazy pills. How are these APIs even remotely defensible? slices.Sort(s) // fine slices.Compact(s) // broken s = slices.Compact(s) // fine s := slices.Compact(s) // broken (!!!) slices.Delete(s, …) // broken s = slices.Delete(s, …) // fine How is one intended to remember which functions require overwriting (due to invalidating) their input and which don’t? Why does the language make it so easy…

Without defending this API, the easiest way to go about avoiding bugs when working with slice mutating functions is to consider all those "fine" scenarios as not fine. Always assume that only the return value of slice mutating functions are the valid reference and the old one always invalid. This is not always completely accurate, but it is very useful in that, it is also never "wrong".

> Without defending this API, the easiest way to go about avoiding bugs when working with slice mutating functions is to consider all those "fine" scenarios as not fine. Always assume that only the return value of slice mutating functions are the valid reference and the old one always invalid.

The first "fine" scenario is fine because `slices.Sort` works in place, it doesn't return a value at all.

And the other "fine" versions do essentially what you advocate, by overwriting the invalid reference with the new one.

Re: Go(lang): Robust generic functions on slices

#44
post #36

I feel like I’m taking crazy pills. How are these APIs even remotely defensible? slices.Sort(s) // fine slices.Compact(s) // broken s = slices.Compact(s) // fine s := slices.Compact(s) // broken (!!!) slices.Delete(s, …) // broken s = slices.Delete(s, …) // fine How is one intended to remember which functions require overwriting (due to invalidating) their input and which don’t? Why does the language make it so easy…

It's pretty simple really: Go's slice API was compromised from the start by making it the unholy hybrid of a list/vector and an actual slice, it's one of the original sins of the language. And it's not really fixable.

It must be said that even splitting them properly you'd have issues as long as you mix slices and mutability (e.g. take a full slice out of a vector then delete() on the vector, the slice will see the hole). Though the issues would be less prominent.

Re: Go(lang): Robust generic functions on slices

#45
post #36

I feel like I’m taking crazy pills. How are these APIs even remotely defensible? slices.Sort(s) // fine slices.Compact(s) // broken s = slices.Compact(s) // fine s := slices.Compact(s) // broken (!!!) slices.Delete(s, …) // broken s = slices.Delete(s, …) // fine How is one intended to remember which functions require overwriting (due to invalidating) their input and which don’t? Why does the language make it so easy…

It's golang, it has a long and proud tradition of being footguns all the way down.

Re: Go(lang): Robust generic functions on slices

#46
post #39

Earlier quoted context omitted.

Without defending this API, the easiest way to go about avoiding bugs when working with slice mutating functions is to consider all those "fine" scenarios as not fine. Always assume that only the return value of slice mutating functions are the valid reference and the old one always invalid. This is not always completely accurate, but it is very useful in that, it is also never "wrong".

> Without defending this API, the easiest way to go about avoiding bugs when working with slice mutating functions is to consider all those "fine" scenarios as not fine. Always assume that only the return value of slice mutating functions are the valid reference and the old one always invalid. The first "fine" scenario is fine because `slices.Sort` works in place, it doesn't return a value at all. And the other "fine…

By returning nil, the function makes it clear that it doesn't move the input reference

Re: Go(lang): Robust generic functions on slices

#47
post #36

I feel like I’m taking crazy pills. How are these APIs even remotely defensible? slices.Sort(s) // fine slices.Compact(s) // broken s = slices.Compact(s) // fine s := slices.Compact(s) // broken (!!!) slices.Delete(s, …) // broken s = slices.Delete(s, …) // fine How is one intended to remember which functions require overwriting (due to invalidating) their input and which don’t? Why does the language make it so easy…

Possibly that's mostly out of familiarity with the language? The only thing in your example that does things in-place (and thus looks out-of-place) is Sort(), but that's the way I'd at least expect it to work? If you take that away from the list all of them behave similarly to each other and return the modified slice:

    slices.Compact(s)       // modified slice in the return value is ignored
    s  = slices.Compact(s)  // s now points to the new changed slice
    s := slices.Compact(s)  // s already exists, this is not valid Go syntax.
    slices.Delete(s, …)     // modified slice in the return value is ignored
    s = slices.Delete(s, …) // s now points to the new changed slice

EDIT: Would prefer people not to downvote actual discussion. In this case there were was indeed good argument made on the reply that these also modify the underlying slice, but it's not like I was being rude in the comment.

Re: Go(lang): Robust generic functions on slices

#48

> func Index[S ~[]E, E comparable](s S, v E) int { After seeing this signature, I think that Go is giving up on it's simpleness principle.

Generics can be a bit of an eye sore, but go already had reflection & I recently was mucking around bigquery's internals full of `reflect` having to read through that, & it doesn't even get backed by a type checker

`func Index(s interface{}, v interface{}) int` both has to deal with incompatible types, & the function body is going to be anything but simple

(sure, without generics most people wouldn't even write that `func Index`, but ultimately there's plenty of libraries deep in reflect that'd greatly benefit from generics)

I've also been dealing with temporal.io using interface{} everywhere, got to a point where I wrote typed wrappers using generics to have type checking for signals

Re: Go(lang): Robust generic functions on slices

#49
post #47
post #36

I feel like I’m taking crazy pills. How are these APIs even remotely defensible? slices.Sort(s) // fine slices.Compact(s) // broken s = slices.Compact(s) // fine s := slices.Compact(s) // broken (!!!) slices.Delete(s, …) // broken s = slices.Delete(s, …) // fine How is one intended to remember which functions require overwriting (due to invalidating) their input and which don’t? Why does the language make it so easy…

Possibly that's mostly out of familiarity with the language? The only thing in your example that does things in-place (and thus looks out-of-place) is Sort(), but that's the way I'd at least expect it to work? If you take that away from the list all of them behave similarly to each other and return the modified slice: slices.Compact(s) // modified slice in the return value is ignored s = slices.Compact(s) // s now po…

> The only thing in your example that does things in-place (and thus looks out-of-place)

That is not really correct, and is much of the issue: Compact and Delete operate in-place on the backing buffer while copying the slice metadata. This is the same issue as the append() builtin and why it's so fraught (before you get in all the liveness stuff).

> s := slices.Compact(s) // s already exists, this is not valid Go syntax.

    s := []int{}
    if true {
        s := slices.Compact(s)
        _ = s
    }
Post reply on HN