Go generics draft design: building a hashtable
mdlayher.com
mdlayher.com
Now, I can see why the author would _not_ want to do this, since this "explosion" of sum-typed things is present in all go code (e.g. the err := ...; if err == nil { ... pattern). So, it might be easier for Go programmers to see how they could use generics in their own code by re-using this pattern. However, I think this is a disservice to why generics are an incredibly useful construct in programming languages. They can be used to align code more closely with the semantics that the programmer wants to convey.
Since Go doesn't have sum types, it would most likely be possible: the option type would just be a reification of the MRV. At best it could panic if you try to get the value of an "empty" optional but now you've got a panic.
What it does is preventing the accidental use of a missing value. You can’t pass the Option<T> on to a function taking a T without explicitly doing it.
Let's say I have a hashmap that returns a `&T` and I have a `func foo(s Stringer)`. Because 'Stringer' is an interface, it's possible for it to take both `T` and `&T` without a compile-time complaint.
In addition, I may wish to have my function `bar(t &T)` always take a pointer because I want the object to exist on the heap, or to be able to mutate its value for the caller.
Since pointers mean so many other things, they're not a good way to have a compile-error indicate optionality.
On the other hand, if my caller does `hm.Get` and gets an `Option<s: Stringer>`, it's clear what to do to pass it to foo, and if it's an `Option<&T>`, it's clear both that I want a pointer, and that it should be checked / unwrapped.
I agree that `T` / `&T` would be just as powerful as option types in go (without sum types) if pointers didn't already have other substantial meaning, and if they could sensibly interact with interfaces.
As it stands, I think you're off the mark though.
(note: All the asterisks are & because I dunno how to escape stuff on hn)
No, a pointer in Go doesn't mean that it's on the heap. The compiler keeps it on the stack if it's safe to do so, regardless of whether you're using pointers.
You can even write code like `t := new(T); t.Foo()` that very much looks like you're allocating on the heap, but it can stay on the stack, yet t is then a pointer to the stack.
Unlike C, you don't need to worry about the heap-vs-stack in Go. It's never even mentioned in the language spec as a concept people need to be concerned with. It's an implementation detail.
I'm really surprised to read that: yes a beginner can get his code working without wondering about stack vs heap (and that's one of the big reason why Go is easier to learn than Rust for people coming from non-system language), but as soon as you care about performance (which many Go users do!), you need to write code that reduces allocations to the minimum, because Go's allocator is really slow (compared to Java for instance). Interestingly enough, doing so forces you to think about the ownership and lifetime of your objects, like you'd do in C (or Rust).
I just found surprising to see a core person of Go declaring heap vs stack allocation to be “implementation detail”. Because if it was, that would mean that my carefully crafted zero-alloc code could become full of allocations one day because the underlying implementation changed! Obviously they don't want their users to be afraid of that.
To compare with references which are also 0-or-1-thing effectively: A reference where you as a developer know it's never null but always a ref to exactly one thing is denoted "* T" and a reference to "one thing or null" is denoted "* T"! There is no difference in the types! so you can accidentally send one that is "0-or-1-things" to a method accepting a * T that MUST be a thing. Type system didn't help you document which case it was.
Options, apart from the annotation benefit it also helps making the syntax nicer in many cases, with e.g. "or()" fallbacks etc.
let data = get_cached().or(load_from_disk()).or_panic(); Car c = maybeCar.GetValueOr(CreateCar()) // inline fallback
maybeDog.Do(d => b.Bark()) // only performs call if present
Sailboat s = mybeBoat.As<SailBoat>() // none unless of correct subtype
And so on.
With nullable reference types C# now has a builtin alternative to this, but it has worked we’ll for many years.With generics, discriminated unions can take on all sorts of user-defined shapes.
Without generics but with sum types, the sum types can still take on all sorts of user-defined shapes. It's just that the types of its various alternatives are fixed at declaration.
edit: oh. no.
https://go.googlesource.com/proposal/+/refs/heads/master/des...
For one thing it would generate a massive amount of churn to upgrade existing code, and if you don't update you'll quickly end up with very ugly mixed error handling patterns. On top of that Go seems to really value compilation speed, so I suspect that they won't want generics "contaminating" interfaces all over the place only to do error handling.
I'm really curious to see (from the outside) how all of this is going to coalesce in the end.
You can do that without generics, of course. But the developer overhead is large enough that it effectively does not happen, as you have to redo that by hand for every type. That's what generics bring - ergonomics good enough to stop using less safe workarounds (e.g. `interface{}`, multiple returns).
https://news.ycombinator.com/item?id=23545361
For what it worth, returning `(value, err)` is conceptually the same thing as returning a Result[V]. You can ignore the error case on a result / the None case on an option as easily as you ignore the `err` today.
The point of a result type is that you _cannot_ ignore the None case. Any method that would provide you with the value will also check that the error is not present.
In comparison, you can happily ignore `err` in Golang and continue with an invalid `value`.
I'm struggling to envision a Result type that requires you to be more explicit than `foo, _ := fallible()`. Seems like `fallible().Ok()` and similar are strictly less explicit.
Though perhaps not quite as compelling without real sum types. I'd have to play with Go's generic-typing sandbox more to form a stronger opinion.
No, you can't. If you want to pull the value out of a result and ignore errors, you have to explicitly do so. With `(value, err)` style error handling, you can write bugs by simply forgetting to check `err`.
result := ThingThatReturnsOption(...)
if result.Error() != nil {
// ...
}
happening in general, or some other equivalent construct.The main point of having Option in this case would be to make it so that where Go programmers normally write
result, err := ThingThatMayError()
you can get precisely one of a result or an error. At the moment with the current calling convention it is possible to both return a result and an error.However, I will say that while in theory this is advantageous (and I mean that seriously), in practice this is nearly a non-issue. I don't think I've ever had a bug because I had both things and misused them. I expect a dozen or more "Option" implementations to pop up nearly overnight once this is released, and for Go programmers to settle pretty quickly on not using it.
In non-Go languages, Options can have additional features that make them yet more powerful, such as chaining together optional computations in a way that makes it easy to shortcircuit whole computations, e.g., in Haskell:
do
x <- optionalFail
y <- somethingElseFail x
z <- moreMightFail x y
return (extractFromZ z)
In Haskell, assuming the right definitions of the various functions, while that may look like it's not handling errors, it actually is, because the machinery behind their Option type (called Either in Haskell) is handling all the short circuiting. Go comprehensively lacks the features necessary to make that sufficiently pleasant to use that anyone will, though.It is not unique in lacking those features, most languages are missing at least one thing to make it easy enough to use people will, but it does quite comprehensively lack them. It's not just a matter of adding this one little thing or that other thing, it'd be a whole suite of necessary changes, e.g., you might be able to write an .AndThen(...) function to operate on a Option type, but it's going to be too inconvenient to use, even post-generics, and even if you force it because it's the Right Thing to Do in languages that aren't the one you are currently programming in, it's still going to be a lot of disadvantages for not much advantage. Personally I don't value "Doing the Right Thing in language X while working in language Y" very highly, but some people seem to.
I also think part of their remark is on ‘either’, not ‘option’, but that’s not important for the point being made.
Specifically https://golang.org/src/hash/maphash/maphash.go?s=1316:1346#L... links to the internal hash function.
It seems powerful enough to cover most cases of generics. Architecture astronauts will never be happy, but tough.
An IDE / coloring scheme can help make things look more distinct if need be.
* the custom one: hashtable.Table(string, int)
* the builtin one: map[string]int
The syntax forms are quite different. Wouldn't it be better to make them consistent? Is it so hard to achieve this?
Looking at this, I don't see any fundamental differences from, say, C#. They're even using interfaces for generic constraints. Did they decide that they are "good enough", after all?
The good enough is more drawing the tradeoff line in a spot that’s useful and clean to maintain, and the place where the core team and the community draws that line is finally converging.
Surely the demo works equally well without this extra constraint? If the demo had a generic function for creating a reversible mapping it would have been necessary, but as it stands, this extra constraint comes across as avoiding having to write the less aesthetically pleasing
type Table(type K comparable, V interface{}) struct {
... type myInt int
func (mi myInt) f() myInt { ... }type Int int
func (i Int) Hash() uintptr { /* do the hash */ }
But I didn't want to deal with it in this code. I agree that it isn't optimal and would be curious to see if the situation can be improved.
It's emphasizing writeability over understandability, which is totally backwards from the point of view of engineering robust systems.
Except it's barely even writeability, it's some bizarre notion that code should read like English where possible. And of course, since it's not actually English, it's only possible in limited ways and trying to generalize past those limits will break. So you have to learn exactly where the limits are anyway, which is as much effort as learning to use a proper library, except harder because a proper library will have the decency to stay in its own namespace.
func (obj *SomeType(Q, Z)) Foo(type K, V comparable)(key K, val V) (*OtherType(Q, V), error) {
...
}
... Lots of Infuriating & Silly Parentheses?Also note that generic methods are not allowed in the current design, only generic functions.
The new draft design ( https://go.googlesource.com/proposal/+/refs/heads/master/des... ) discusses both of these points. I recommend everyone who is interested in this topic read the design spec, ideally before commenting.
Basically, unlimited look-ahead.
This reduces the amount of state that the parser has to carry around and makes the error messages for syntax errors easier to generate.
Languages that use <>'s for generics have to look at a larger amount of the code when parsing to work out what to do.
"Resolving that requires effectively unbounded lookahead. In general we strive to keep the Go parser simple."
It's totally fair to question the tradeoff - should having a simple parser outweigh the potential ergonomic benefit of <>? I don't know. The forum for this is probably the golang-nuts group.
I think "simpler" would have been a stronger argument. While using <> would make the parser more complex, I have a hard time seeing that making a meaningful performance difference in a compilation context. Maybe if you're parsing a lot of Go without actually compiling it, but that doesn't seem like a use case to optimize for.
https://go.googlesource.com/proposal/+/refs/heads/master/des...
Hmmm ... not sure how I feel about that. Ok so my original example becomes this:
func (obj *SomeType) Foo(type K, V, Q comparable)(key K, val V) (*OtherType(Q, V), error) {
...
}
Which is only slightly better [without the generic type].Don't get me wrong, I'm a big fan of Go, but I'm kind of on the fence about generic types. I've made do with casting interfaces and type-casts for many years and I'm OK with it (honestly, glad to not be a C++ or Java programmer anymore).
No, you still don't understand. Methods can't have additional type parameters, only functions. This part is not allowed in your "example":
(type K, V, Q comparable)> Generic types can have methods. The receiver type of a method must declare the same number of type parameters as are declared in the receiver type's definition. They are declared without the type keyword or any constraint.
So my original example should have been:
func (obj *SomeType(K, V)) Foo(key K, val V) (*OtherType(K, V), error) {
...
}
(hopefully this is now correct!).I’m not accusing you of advocating Go’s adopting a different syntax for generics solely to be contrarian - but using angle-brackets for type-parameters and template-parameters is a proven technique with few downsides - and certainly not any that downsides that would be fixed by using any other syntax I’m aware-of.
(I had to write a lot of Go code in Notepad for a while in 2017 - never again)
Python is the worst, where dicts and sets both use {} and tuples and "grouping" both use (), each causing real problems.
Some of many possibilities:
⦅⦆«» ⟦⟧ ⟨⟩ ⟪⟫ ⟮⟯ ⟬⟭ ⌈⌉ ⌊⌋ ⦇⦈ ⦉⦊ ||
Yes, you need a way to type it on non specialized keyboards.
An easy way is is to type it as (( or [[ etc, and let the IDE convert it.
Especially in the context of Go that hasn't even managed to use anything other than parentheses in its func definition syntax. No language has saturated the bracket options that come with the standard US keyboard so much that it's worth bringing in characters that you need additional tooling to type.
If (), <>, {}, and [] aren't enough for a language, something has gone very wrong.
In Perl / / was used to bracket regrexs in C and many C languages /* */ is used to bracket comments.
The rest of your post is a fair point, although I think you underestimate the value of plain ASCII that can be typed on a unassisted keyboard.
I can already see a lot of edge cases
Most notably font support
I like ligatures and ide/editor support for them, but I'm not sure I like this
You read code 1000x more often than you write it, so a little extra effort for even a small readability benefit is worth it.
Most fonts have quite full Unicode support since many years.
I'm often disappointed in how reluctant programmers are to use modern technology. The public thinks we're on the cutting edge of advanced technology. If they only knew :)
Honestly, it's just common sense.
This year mark my 30th year as a professional developer, but my first "program" was copied fro a magazine
10 PRINT CHR$(205.5+RND(1)); : GOTO 10
It generated random maze like structures in screen using two PETSCII symbolsI think everybody know this one liner here
So it's not symbols I don't like, it's the lack of ergonomics
My hands started hurting when I had to type a lot to write simple symbols like } that's why I switched to US layout and things have gone a lot better for my nuckles
What really surprises and disappoints me is how many people appoint themselves as engineers or computer scientists, but take for granted that their ideas are good and the rebuttals are "resistance to change" without even reading the most basic studies on the topic or testing their theories on the field.
Implement your idea, measure the results and then we'll discuss of why they didn't take off.
Because they won't, believe me.
There is a reason why I use font with the zero striked, otherwise capital O would look too similar.
What do you think of ‹‹ vs « ?
Do they really look different enough to be useful?
Cutting edge doesn't mean "stupid"
And even after typing in Spanish for five years, it's still objectively more annoying to write está (6 keypresses) vs esta (4 keypresses). And I'm always having to go back and correct the key sequence because I've written ´a instead of á.
Also, just today I saw someone use "⇸" (crossed out arrow) on HN and it was so tiny with HN's font that I had to zoom in to see what it was which only inhibited their message.
You have to consider this sort of overhead when making decisions about the glyphs you are going to impose upon everyone when designing a language.
I've seen pages and pages of bike-shedding over whether to use kebab-case over snake_case for a DSL because it's one less keypress to type a hyphen vs underscore.
I can appreciate your preference for those characters. It's nice how they can encode meaning in a single glyph where you would otherwise need multiple glyphs (like "!="). One solution that you can find people doing today are IDE plugins that simply rerender something like "->" as "→".
Seems like the best of all worlds. You get to work with higher level characters while the plaintext format remains in the most accessible common denominator.
https://www.reddit.com/r/programming/comments/a9tb2/secret_h...
[* *]
[= =]
[/ /]
(* *)
(= =)
(/ /)
Just kidding, requiring specialized tooling to type isn't quite acceptable..Or there's a bug in the IDE and all of a sudden you literally cannot type anymore?
(I'm not sure I'm convinced that using such symbols in a programming language is a good idea, of course, just that our current input tech is barely trying.)
Also, APL happened.
Perhaps, perhaps, hard-core generics are not a great idea after all, and they should only be in the annotation level of the language (especially for container types, which is the only place where generics are truly valuable)...
A lot of the information is already in the definitions, there's no reason to repeat it, but Go chose to do that.
// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce(type T1, T2)(s []T1, initializer T2, f func(T2, T1) T2) T2 {
s := []int{1, 2, 3}
sum := slices.Reduce(s, 0, func(i, j int) int { return i + j })
and the example from referenced article is even more outrageous t1 := hashtable.New(string, int)(8, func(key string, m int) int {
We have function types literally at the same exression, but Go requires to repeat it.It does not require it, the author chose to do it.
t1 := hashtable.New(8, func(key string, m int) int {
is perfectly valid. type checking failed for main
prog.go2:17:3: cannot infer V (prog.go2:46:27)
prog.go2:22:3: cannot infer V (prog.go2:46:27)
But generally you are right, yes. It is able to infer K at least. func Filter(type T)(s []T, f func(T) bool) []T { ... }
s:= []int{1,2m3}
evens := slices.Filter(s, func(i int) bool { return i%2 == 0 })
The type of predicate is already in generic definition, we don’t need this verbosity. Other languages (I’d even say most of languages, used in dev work today) let us write simply something like slices.Filter(s, i->i%2).