That being said for a lot of other uses, you really do want high quality data structures beyond "array" and "dictionary."
The verbose patterns (if err!= nil for example) make the code predictable to read, you notice the code smell of missing error handling really fast.
For example here's a code in Go to look for a Prime:
func IsPrime(n int) bool {
if n < 0 {
n = -n
}
switch {
case n < 2:
return false
default:
for i := 2; i < n; i++ {
if n%i == 0 {
return false
}
}
}
return true
}
It's readable as it is simple to understand what each line does.Here for example is a code that does the same thing in Rust:
fn is_prime(n: u64) -> bool {
match n {
0...1 => false,
_ => !(2..n).any(|d| n % d == 0),
}
}
It's might seem more complex at first (what does match do, what 0...1 means, !(2..n) what is any() doing. But if you understand the language it actually this seem much simpler and you can quickly look at it and know exactly what it is doing. And because it is less verbose it is easier to grasp the bigger code.I also noticed that while individual functions in Go are simple to understand and follow, you can still create complex, hard to follow and understand programs in Go.
That seems unfair when comparing:
Removing negatives from the Go implementation removes 6 out of 16 lines, bring it from 3x Rust to 2x Rust in line length.
Anyway in Go (ironically because of lack of generics) if you use any numeric type other than int, int64, float64 you will be in the word of hurt. Rust doesn't have that issue.
So in practice you will likely use int, and I suppose you can add an assertion.
BTW: I only see that it would remove 3 lines though, where are the other 3?
result = (float32)Max((float64)a, (float64)b)
I ended up switching the type to float64, and wonder why they even offer float32 if it's practically unusable. I had similar experience when I needed to use int8 or int16 etc.An alternative was to make own version of Max/Min and other math functions, but this is what generics would solve.
Rust is very fussy about types and some operations are bound to a single one (e.g. stdlib sequences only index with usize) but utility functions tend to either be genetic or be mass-implemented using macros (or by hand probably for f32::min and f64::min though I did not check).
So what your issue comes down to is Go's designers couldn't be arsed to duplicate the entire `math` package to also work on `float32`. Some members of the community did rise to the challenge[1] tho.
[0] well recent revisions of IEEE-754 have a total ordering predicate but I wouldn't say that it's really useful for such a situation as it positions NaNs on "outside" of the numbers, so a negative NaN is smaller than a negative number and a positive NaN larger than a positive number
Anyway my point was that with generics, they wouldn't need to copy anything, the math package would work with both float32 and float64 and many functions likely would also work on all integers.
The Rust code needs maintenance coders of way higher caliber, not something you'd usually find. It's super fun for the top-tier developers who love to be expressive and concise with their code, but all code is pushed down to maintenance mode eventually when the hotshots move on to the new shiny project.
Go has removed pretty much every footgun by sticking to the basics. You have one way to do a loop, one way to do comparisons etc. There are very few ways to hide non-obvious functionality.
It _is_ possible to create complex programs, that are hard to follow but that's a larger design problem. Not something the language can force on developers.
Thing is, care with what you wish for when easy to outsource is a goal, a welcomed feature mostly relevant to IT managers that don't care about the final quality of delivery, nor what consequences it makes to the home job market.
So yeah to all Wipros, Infosys, TCS, .....
static boolean isPrime(int n) {
return switch (n) {
case 0, 1 -> false,
default -> !IntStream.range(2, n).anyMatch(i -> n % i == 0)
}
}All our microservices return a { result: ... } or a { result: ..., nextPageToken }
We would definitely benefit from generic SingleResult<T> and PagedResult<T>.
Instead, you copy-paste the same definitions over, and over, and over, and over again for every call.
type NextPager interface {
NextPage() PageSpec
}
func (c *Client) FetchNextPage(ctx context.Context, current NextPager) (interface{}, error) {
...
}
Then for each type of paged object, you write: func (c *FooClient) FetchNextFoo(ctx context.Context, current Foo) (Foo, error) {
next, err := c.client.FetchNextPage(ctx, current)
...
if n, ok := next.(Foo); ok {
return n, nil
}
return Foo{}, fmt.Errorf("unexpected type: got %T, want Foo", next)
}
That's annoying. But, this problem has come up before with `sql`, which has rows.Next() and rows.Scan() to iterate over arbitrary row types, and you could use that as a model: pages := client.Query(...)
defer pages.Close()
for pages.Next() {
var foo Foo
if err := pages.Scan(&foo); err != nil { ... }
// do something with the page
}
Generics would let you enforce the type of `foo` at compile time, but it wouldn't save you many lines of code. I think you still have to write (or generate) a function like `func (c *Client) ListFoos(ctx context.Context, req ListFooRequest) (Paged[Foo], error) { ... }`. We hand-wave over that in the above example with a "..." passed to query (potentially possible if you retrive objects with a stringified query, like SQL or GraphQL), but that sounds like the hard and tedious part.Let me conclude with a recommendation for gRPC and gRPC-gateway as a bridge to clients that don't want to speak gRPC. Then you can just return a "stream Foo", and the hard work is done for you. You call stream.Next() and get a Foo object ;)
You literally showed that "for each type of paged object, you write <multiple lines of entirely unnecessary code>".
Where with generics you just have a single generic function.
> We hand-wave over that in the above example with
The problem is: there's no hand-waving in reality.
- repeat unnecessary bolierplate code for every single return type
- skip types completely and use a function that accepts interface{} as a parameter and assigns whatever to that variable
Neither are really acceptable in my opinion. And both will be greatly simplified and improved by generics.
But some people like offloading that to the language: not having it in a language means you don’t have to control for it on your team/project contributors + some 3rd party library.
I felt Go filled this minimalist category well. It’s always nice having a modern mainstream option doing so, not just a niche one on the fringes (ala LISP), even if I’m not personally a fan.
I guess it’s hard to keep saying no.
let call1 arg = ...
let call2 arg = ...
let error_handler call arg = ...
let call1 = error_handler call1
let call2 = error_handler call2
Python made this kind of technique famous, but languages with generics can do it pretty well too.At the end of the day, whatever gets the product built is what matters. I just fail to see how generics could be a hindrance.
Confessions of a used programming language salesman
https://dl.acm.org/doi/10.1145/1297027.1297078
> As a result, functional programming has finally reached the masses, except that it is called Visual Basic 9 instead of Haskell 98.
Also higher-order functions are a moot point. Higher-order functions give you convenience, but no increase in expressive power (defunctionalization is an homomorphism).
Of course, generics give you a true increase in power.
Don't get me wrong, I'd take Rust generics over codegen 8 times out of 10. But that ORM worked well, and was the only time I ever needed to write a code generator in Go.
If they didn't, they would just not create another solution.
Having said that you might be right with ORM though. One of great reasons why ORM might be superior on statically typed language is that you can rely on the type system to ensure you writing correct code (you also get benefit of autocompletion, refactoring in IDE etc). The problem though is that the type system including generics might not be sufficient to express it. So code generation could be still superior here.
The JOOQ (not exactly ORM though) generates java code, even though Java has generics.
BTW: I personally think though that actual proper way to handle this problem is to what JetBrains did. They integrated DataGrip into their IDEs (I think it's available in the paid version though) after you connect IDE to the database, it starts detecting SQL statements in the code and treat it the same as rest of the code (i.e. auto completion, some refactoring (they still need to improve that more) etc). It makes an ORM no longer necessary for me. I think that's probably the way to solve the impedance problem.
As someone who hasn't used Go, what would I do, then? The question about a generic data structure was very practical, your response was philosophical, and I still need a linked list.
- Option, Result, and other stuff from the standard library.
- My own B-Tree implementation w/ domain specific enhancements, in about 4 different contexts
- A heap based priority queue
- A custom 2 level vec, to support arbitrary insert & delete without shuffling elements around. (This turned out to be faster than my b-tree but less memory efficient.)
- Several different custom iterators, and iterator combinators. Eg, I have an iterator over some data which gets computed on demand. I have an iterator which consumes and run-length encodes each item in another iterator. And so on.
All of this stuff has generic type parameters everywhere. The b-tree is generic not just over the stored data, but also over the way its index works. I can specialize it to do a bunch of different tricks by just changing a type parameter.
All of this code is fancy and hence difficult to read for the uninitiated. And that isn't the Go way. But there's a big, valuable middle ground here. I'm glad Go is adding some options to let people lean a little bit into cleverer code when it becomes appropriate for the problem domain. Emulating generics with interface{} seems worse than just adding generics into the language.
* manually instantiating (handrolling) every version, possibly duplicating it if you don't realise somebody else did
* same except using codegen, which is easier to maintain but has more semantic overhead (now you need to add codegen to the project and there's an extra build step to consider)
* or manually type-erase and cast back, risking type safety and most likely performances
A flagrant example from the stdlib is the `sort` package with its weird and alien interface (and Interface), inability to use key functions, and which can only work in-place.
Talking about in-place but circling back to generic structures / collections, a big use-case I expect to see for generics would be type-safe (immutable) concurrent collections e.g. HAMT, RRB, …. Current Go significantly limits the ability to "share by communicating" (as well as safely "communicate by sharing") as there's no good way to build and make any sort of high-quality concurrent collection available, whether persistent or mutable.
It isn't as if we never coded without generics before.