[HN Gopher] Go generics draft design: building a hashtable ___________________________________________________________________ Go generics draft design: building a hashtable Author : mdlayher Score : 100 points Date : 2020-06-17 18:26 UTC (4 hours ago) (HTM) web link (mdlayher.com) (TXT) w3m dump (mdlayher.com) | lsllc wrote: | Interesting article. I think that using <> for the type | specifiers would possibly be better! For example one could | quickly end up with something like: func (obj | *SomeType(Q, Z)) Foo(type K, V comparable)(key K, val V) | (*OtherType(Q, V), error) { ... } | | ... Lots of Infuriating & Silly Parentheses? | sacado2 wrote: | I'm ready to bet that with <>, you'd introduce ambiguity in the | grammar, making parsing code (and, as a consequence, writing | tools for the language) much more complicated. | monkeyfacebag wrote: | On <> syntax, this has been discussed quite a bit. () were | chosen to avoid ambiguous parsing. | | 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. | twblalock wrote: | On the <> syntax, why not improve the parser? Every language | that uses <> for generics has a parser that is able to | distinguish between generics and the "less than" operator, | and it seems odd that Go developers think it can't be done | efficiently. | 8192kjshad09- wrote: | Go developers like to claim that the reason the Go compiler | is fast is because it's "simple to parse". Unfortunately | this doesn't make a lot of sense as parsing is typically | only 1-5% of the total compile time. | sacado2 wrote: | It's not only about the main compiler. Go has tons of | third-party tools (linters mainly) that can parse go | code, because it's so easy to write one. Most of them | wouldn't exist if parsing was a PITA. | monkeyfacebag wrote: | In my opinion, the new design doc clearly characterizes | this as a design goal of the parser, rather than a hard | constraint that cannot be resolved. | | "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. | nemetroid wrote: | The design doc doesn't say "simple", it says "efficient". | | 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. | mpoteat wrote: | It's all a matter of where you accept the complexity - in | the compiler, in the language, or in the downstream | applications. In my view it's an obvious choice to choose | a more complicated compiler on exchange for simpler | downstream applications... | Sharlin wrote: | C++ and Rust require extra disambiguating syntax in some | contexts (C++ the `foo.template bar<T>()` syntax, Rust the | "superfish operator" `foo.bar::<T>()`. Java works around | the problem by awkwardly putting the generic argument list | in _front_ of the method name (`foo. <T>bar()`). I don't | know about C#. | DougBTX wrote: | C# is `foo.bar<T>()`, I'm not sure what compromises that | had to make for that to work but from an end-user | perspective it works well. | steveklabnik wrote: | (teeny tiny note: turbofish, not superfish) | Sharlin wrote: | Oops :D | mdlayher wrote: | Exactly. Thanks! | _ph_ wrote: | I never have been able toi read the syntax with < > well, so I | am soo happy they found some syntax based on parens. It is | perfectly readable for me. | nytgop77 wrote: | After seeing and trying dlang syntax for generics I was | hugely disappointed that rust chose angle brackets. | BurningFrog wrote: | Now that unicode is everywhere I think it's time to use more | brackets than [], (), {} and <>. | | 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. | a1369209993 wrote: | Dicts and sets are _supposed_ to both use {}; {a,b,c} is | (conceptually) shorthand for {a:dummy,b:dummy,c:dummy}, where | sets (in some cases, including python, opaquely) are actually | dicts without the space to store values. (This also shows up | in set theory and related mathematics, although there it 's | dicts that are unusual.) | | The rest of post is fair point, although I think you | underestimate the value of plain ASCII that can be typed on a | unassisted keyboard. | hombre_fatal wrote: | To me this is the ultimate "developer chasing shiny" at the | expense of everything else. | | 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. | asjw wrote: | That's a lot of effort for little benefits | | 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 | BurningFrog wrote: | The benefit is purely readability. | | 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 :) | throw_m239339 wrote: | There is absolutely no way this gets the vocal "conservative" | core of the Go community's approval. For better or worse. | mseepgood wrote: | The <> people are the conservatives, because they are afraid | to give new things a try. They want every programming | language to look the same. | DaiPlusPlus wrote: | Consistency is important, though. Using C-family syntax | makes it easy to get people to dip their toes into | something within the spending time to learn a new syntax. | Look at modern languages like Rust, Swift, Dart, and Go: | they all use C-style syntax. | | 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. | rob74 wrote: | Well, Go already picks and chooses elements from | different languages, e.g. Pascal-style declarations ("x | int" instead of "int x") and C-style blocks ("{...}" | instead of "begin...end"). So I'm not surprised that when | deciding on the syntax for generics they look more at | what fits best for Go than at what other languages are | doing... | iand wrote: | The author asks about using the same hash function as the builtin | Go map. This was recently exposed in the standard lib at | https://golang.org/pkg/hash/maphash/ | | Specifically | https://golang.org/src/hash/maphash/maphash.go?s=1316:1346#L... | links to the internal hash function. | mdlayher wrote: | Right, but you can only write strings or bytes to the hash, not | integers, booleans, structs, etc. So the problem remains. | ardit33 wrote: | I am not a GoLang programmer, but that just didn't look neither | pretty or readable... | | 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)... | galkk wrote: | The problem is that with current proposal Go severely lacks | type inference, required for generics to look nice and clean. | | 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. | mseepgood wrote: | > 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. | | https://go2goplay.golang.org/p/SbEXpyVl-V3 | mdlayher wrote: | In this particular case, the compiler can't infer the type | of V if you omit the type parameters at the call sites: | 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. | nicoburns wrote: | The limitation around implementing methods on built-in types | seems unfortunate. If I understand how Go interfaces work | correctly, that would mean that you also can't implement your own | interfaces on built-in types. Which would seem like quite a | severe restriction in being generic over them. Perhaps I'm | missing something? | mdlayher wrote: | The alternative is to do something like: | | 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. | skybrian wrote: | For example, the caller could cast `int` to `myInt` which | implements the appropriate interface. This is probably more | idiomatic than using a bare `int` type anyway. | sacado2 wrote: | You can only add methods on types that were defined in the | current package. As a consequence, you can't add methods to | built-in types. But that's rarely something you want to do | anyway (I can't think of a real-world situation where it would | be useful), what would make sense is creating a new string | type, and add methods to it: type myInt int | func (mi myInt) f() myInt { ... } | [deleted] | lspears wrote: | If you assume that the std lib creates a collections package, | there could be helper types will for primitives. No Java style | auto boxing, but more in the Go style. These could also be used | in a refactored math package. | benhoyt wrote: | What would be a real-world example of what you're referring to | ("implement your own interfaces on built-in types")? | uluyol wrote: | Perhaps like overloading in C++? Hash functions are sometimes | defined this way | (https://abseil.io/docs/cpp/guides/hash#making-hashable- | types) | _ph_ wrote: | Looks pretty nice and readable. Looks to me like a strong | indication that the updated generics concept is close to what | should go into Go. | malcolmgreaves wrote: | Since this post is illustrating the use of generics, why not go | all the way with the get implementation to return an Option[V] | type? It's a natural thing to do here. The return type is already | a kind of sum type: it's either the value you want (non-zero | value, true) or it's not (zero value, false). If the | implementation uses the optional type, it'll become impossible to | write code that uses the zeroed-out returned value incorrectly. | Calling code must always explicitly check the returned | Optional[V] value to access the value of V and continue or to | perform some code to handle the not present case. As it stands, | it's very possible to ignore the 2nd returned boolean value and | write code that'll easily break. | | 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. | cy_hauser wrote: | I'm not understanding this. Are you saying an Option[V] would | reduce the number of lines of code (the explosion) that Go code | uses for "err := ...; if err == nil"? | alkonaut wrote: | When you chain multiple things that can go wrong with | Result[T,E] or Option[V] it should be possible assuming there | are methods for chaining/fallback. E.g. opening a file, | reading the contents, parsing the contents etc. If all those | 3 return Result[T,E] and you want the overall result (parsed) | T or the first error to occur, then you should be able to | chain that. | jerf wrote: | I don't think it would in Go. You'd still end up with | 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. | [deleted] | Someone wrote: | I think they are saying using sum types will remove a source | of errors (forgetting to check for error conditions). | | I also think part of their remark is on 'either', not | 'option', but that's not important for the point being made. | mdlayher wrote: | I like the idea, I just haven't given it a try with the new | draft yet. Sounds like it's worth exploring at the very least. | masklinn wrote: | > If the implementation uses the optional type, it'll become | impossible to write code that uses the zeroed-out returned | value incorrectly. | | 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. | alkonaut wrote: | It's always possible, even in a Rust Option you'll panic when | you extract the value with unwrap() without knowing that it's | valid. | | 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. | weberc2 wrote: | This doesn't add anything. You get the same protection in | Go with pointers. For example, a `*T` can't be passed to a | function taking a `T` without explicitly dereferencing it. | The problem of course is that the type system doesn't | guarantee that the pointer isn't nil when you go to | dereference it, similarly your `Option<T>` doesn't | guarantee that the option.Value is set correctly. You need | sum types to provide this substantial guarantee. | alkonaut wrote: | It guarantees that the value is set _if_ the flag is | saying it is set (that 's the invariant of the type). It | screams "check flag before accessing the value". | | 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(); | weberc2 wrote: | I agree that there are semantic issues with pointers, but | my point was to illustrate that you need sum types in Go | to get any real benefit out of an Option type. If you | need an option type and you aren't content with a | pointer, you can use `(T, bool)`, but this is still a far | cry from a real Option type. | alkonaut wrote: | I use Option<T> types very happily in C# without sum | types. Most of what would be pattern matching can be done | with just methods. 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. | simias wrote: | I don't use Go myself but knowing its philosophy I wonder if | they'll end up replacing the idiomatic "if err == nil" with an | generic optional type even if they end up implementing generics | in Go. | | 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. | weberc2 wrote: | Generics aren't the gap (multiple return values are already | generic), but rather sum types. Sum types are what allow you | to express that this is either None/Sum(T) (Option) or | Ok(T)/Err(E) (Result) or Nil/Cons (List) or etc. | peter_l_downs wrote: | Take a look at the comment tree starting here in the Generics | announcement thread from the other day for some discussion of | Option[V] under the current proposal: | | 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. | [deleted] | MHordecki wrote: | > 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`. | peter_l_downs wrote: | I see what you're saying, and while I think you're right, I | think a lot more of the value is in being able to use | `Map()` /`FlatMap()` to be able to avoid thinking about | error checking at all, as I laid out in the second part of | this comment https://news.ycombinator.com/item?id=23549396 | . The convention of returning `(value, err)` goes a long | way towards enforcing the checking of errors; I have | golangci-lint's `errcheck` linter enabled and basically | cannot accidentally ignore the `err` values. In any case, | Option/Result types are extremely useful and I'm happy that | with generics the language will be able to include them! | weberc2 wrote: | > 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. | hombre_fatal wrote: | To do anything with a Result/Optional, you have to | explicitly get the value inside the box at some point. | But the container abstraction also gives you a nice | composition abstractions instead of the uncomposable top- | level `val, err := do()` destructure. | | 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. | [deleted] | nemetroid wrote: | > For my design, I decided to enforce that both key and value | types are comparable, so I could build a simple demo using two | hashtables as an index and reverse index with flipped key/value | types. | | 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 { ... | mdlayher wrote: | Yep that is true. I wanted both values to be comparable for an | easy demo but would write it as you've suggested if I intended | to make this a general purpose package. | [deleted] | nemetroid wrote: | What I mean is that as written, the demo doesn't need V to be | comparable. | mdlayher wrote: | Oh wow, for some reason I totally zoned out and finally | understand what you mean. Yes, you're right! I should fix | that. ___________________________________________________________________ (page generated 2020-06-17 23:00 UTC)