Go 1.18 Beta 1 is available, with generics
go.dev
go.dev
Go felt good, a bit verbose, but my issue was that map/reduce/filter are a much better abstraction over imperative loops, and with generics, the dream of Go with more functional and high-level concepts is closer.
This comment is going to be very controversial, so let me remind you this is my very own opinion.
A for-loop has what's called "mechanical sympathy", and will go through things in the order that it is in memory. It'll allow the compiler and runtime to manage things quickly through memory, CPU registers, and CPU pipelines.
A great post on the subject is "Why Go Getting Generics Will Not Change Idiomatic Go": http://www.jerf.org/iri/post/2955
Go's compiler is just particularly naive.
The compiler has definitely gotten more optimal over the years, but it's a different beastie than the rust one. Because go is not rust.
The zero of Error is nil, so logically this would be Result(0, nil). But what do you set the internal state? It's not Ok, cause that int isn't used, and it's not Err either.
Option would work fine though and it would be amazing to map over options instead of nil checks.
I think you'd need to build Result on top of Option, it's naturally kind of ternary - "I have value" "I have error" "idk", either internally or externally.
What might be awesome would be if the Error were a slice type, nil would be "uninitialized" and empty slice means "initialized". still not...pleasant.
[0]: https://doc.rust-lang.org/std/default/trait.Default.html
Since it's implemented for Option<T> you can do it the same way [3] for that, but that's not available for Result<T,U> since the semantics are different from Option which is basically just Rust's alternative for 'null'/'nil' (but verifiable by the compiler).
Thus you'd have to choose what variant of the enum you want to have at initialization, after which you can use the Default implementation of the enclosed type, if available (and so on, if there are more nested types).
Though honestly I can't recall using the kind of pattern in Rust that would "need" something like that. Through the handling of code blocks as expressions, early returns etc. you for example don't need to initialize a variable before having an if-/match-/for-/while-/etc. block, you can simply use that block as an expression on the right side of the assignment [4].
[0]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
[1]: https://doc.rust-lang.org/stable/error-index.html#E0381
[2]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
[3]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
[4]: https://play.rust-lang.org/?version=stable&mode=debug&editio...
It would also allow defer-like syntax to be used on the value a function returns, to return early from the calling function. Rust's ? is an excellent example of this, allowing explicit delegation of error handling that is still very lightweight syntactically.
Foo, err requires either that all receiving functions accept (foo, err), or manually nil-checking at every stage.
Try/catch is the least composable, because deeper error types can "jump out" from any layer. Which means you have to keep wrapping catches, until you hit some base layer you can recover from. You get stack traces, but you lose type information along the way.
Sum (result) and product (foo, nil) types preserve type information. Meaning you can operate more generically over it. Meaning less code duplication.
I disagree that it would be without sacrifice. That's kind of the whole point.
Unless I'm missing something, go has iterators with for loops.
Go's for loops can only iterate over builtin types: slices, maps, channels, etc. You can't produce a custom type that is to be iterated over.
The main compiler is the self-hosting gc go compiler. Iirc ~~it's the reference implementation~~ erm I guess it's determined by spec, not reference implemenation. Gc is definitely "the default" though.
Then There's the gofrontend, which is a compiler frontend to other compilers, which can work with gcc and llvm. I am not aware of the state of the art for these, but I wonder if there are performance differences.
Very few languages outside of Rust and Haskell actually do this reliably and that's including languages like Ocaml and F#. Rust and Haskell are certainly the only even remotely popular languages that do it.
This is perhaps not the right way to measure, but: When I last checked the programming language benchmark game [0], the Haskell submissions were basically as fast as C/C++/Rust. But, this came at the cost of writing non-idiomatically/non-functionally – essentially abusing the language to write C in Haskell. I would be curious how well "idiomatic" Haskell compares to C++/Rust, in terms of the compiler optimizing away the functional abstractions.
[0] https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Look at both
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
and
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
I can't bet on what Java and JavaScript JITs make of map / forEach / filter / reduce, but I suspect that they can be pretty efficient, given the clear delineation and the repeated patterns.
> with flambda under sufficiently strong optimization flags, such compositions of operators should be compiled to an actual loop with no overhead!
Nowadays I use Fastapi, pydantic, Result, and static typing everywhere. Protocol interfaces are great.
Fwiw - I do dabble in Go. I’ve used it for some very targeted systems work where I don’t have to install a python interpreter on the target machine and hacking some k8s operators. I really like Go - but the ML ecosystem is pretty heavily python.
Do it, do it nao! And by that, I mean at your earliest convenience. It's such a step up from Flask. All the serde is handled for you via Pydantic. No more manually vetting data. Just write constraints.
I'm hoping generics makes it easier to write things like numpy, without doing `meao_u8`, `mean_u16` etc.
+ there's bool, int/uint8,16,32,64, float32/64, complex64/128, that's 13 right there, plus object, a bunch of extended sizes on some architectures, and c-alias types.
But as someone who used to write a LOT of Python, Go has replaced it for anything service or business-logic oriented, and while I do very little data wrangling and machine learning, Julia would be my go to for that. Julia and Go are in, many ways, complementary opposites. If you need to use PyTorch/TensorFlow, okay, fine Python (I'm not going to seriously advocate for just wrapping it all in PyCall.jl unless it's just incidental usage), but Julia is a really strong language for this stuff nowadays.
Are you seriously saying that github.com/somelib is a better package management? Of course nobody forces to use that, but it's what actually been used in most go projects. Even NPM looks better in comparison.
The best quality package management I've seen is Maven, followed by Cargo.
Github repos can vanish, NPM will only let repos be deleted if they have no direct dependencies also on NPM. So you have some guarantees your dependencies wont vanish.
You can also configure go to use your own for internal projects or if you just want to avoid the global proxy.
for private Go packages all I need is to point it to another git repo. for nuget/maven etc - I need an artifact registry, with it's own bells and whistles
This doesn't mean people _will_ name their things sensibly of course. I've seen golang projects with packages named after gardening, meteorology, and aviation… and each is totally confusing when you're new to it, especially when the naming theme is completely unrelated to what the project actually does.
Here maven would be acceptable (similar to go, there is a domain identifier embedded) whereas cargo (bare words) wouldn't be great.
Well, yeah, I mean if one can say Maven is best package management then I think I can get away by saying Earth is flat or stationary or some such interesting facts.
It is hard to be done with OOP when Go also does OOP, even if in a different way than Python does it.
And python does have map / reduce and much more... so I'm not sure what you mean there.
OTOH, I feel Go and Python are mostly used in quite different domains. To me, Python is the new bash. Go is more for those who dislike C++ and Rust (or Haskell or...) and prefer a certain kind of simplicity.
But Go has solid answers for these problems and an excellent static typing story (generics are gravy, but sum types would be nice). I’ve been very happy with Go on balance.
For things that are performance-critical or require high reliability, I have successfully used Rust, even if I am still much slower writing it. One thing I like is that the resulting code is not more verbose. Which also means that it is easy to read even after months of not touching it.
I would love some of the proposals surrounding errors to move forward, of course. But I'm OK with the current state, and will be happier with 1.18.
for bar := range list {
}e.g. in C++, where one has to make capture by reference explicit
int i = 0;
auto f = [x = i, &y = i] { //x captures i by value, y by reference
std::cout << "Value:" << x << ", Reference:" << y;
};
i++;
f();
will print out "Value: 0, Reference: 1".>at least a differently scoped variable of the same name referring to the same value, but that's not lexical scoping anymore
not lexical scoping? That's literally just how closures work under the hood.
And that's exactly why closures have to deal with references (or copies) - every implementation of them is a function pointer and a record for the lexical environment, the latter of which requires that at closure creation time the relevant bindings are captured, either by creating a reference or making a copy.
I also don't understand why C++'s (or Rust's) closures would be 'fake'. If you capture by reference, they work exactly like the closures of other languages. Not having a GC just means you have to be wary of lifetimes. If you use a reference counting pointer (/some other GC'd pointer) you literally have the same closures as other languages.
In the presence of references in the language, these two cases may be observationally indistinguishable (maybe, I'm not a C++ guru?) but they're still different language features. Lexical scoping does not require taking a reference.
> And that's exactly why closures have to deal with references (or copies)
No, they don't. They deal with bindings. References are either an explicit thing you create (as in, int &b=a; or such), or a parameter passing mechanism...but a free variable is neither (it's not a parameter to the closure in any case), and it exists even in languages with no notion of references whatsoever, such as for example Scheme.
> I also don't understand why C++'s (or Rust's) closures would be 'fake'. If you capture by reference, they work exactly like the closures of other languages. Not having a GC just means you have to be wary of lifetimes.
Yeah, and that's at least one of the things that make it fake. Why would you have to be wary about lifetimes? Closures in languages that support them Just Work(TM).
> The moment you do stuff with set-car! and friends you are very explicitly working with references
No, you're not. At least not in the sense that references are references in C++ or in the established CS term call-by-reference. Likewise, you probably wouldn't say that C has references because you can "do stuff" with pair->car=foo; in C.
Notwithstanding the fact that C pointers are syntactically not really references, at least in my opinion, the way to spell an example for what they are referring to in C would be "pair = foo". You aren't modifying *pair, but the reference object stored in your own private stack frame.
As long as mutability is out of the picture and the aptly named notion of referential transparency is preserved, there is no way to discern whether any two symbols reference the same value.
The moment you flip the proverbial page to chapter 3 of SICP and start using setter-functions!, you lose this. You should very much become aware of the fact you are working with references whenever you mutate things, because the mental model of lists as values falls apart when you write, for example:
(define a (list 1 2 3)) (define b (cdr a)) (set-car! b 'ta-da!) (display a)
And then get left to perplexedly stare at the REPL's output.
It is true that references are not, in some sense, a language-level/opt-in feature of Scheme, as in that you cannot arbitrarily create a reference to an integer, but cons cells very much are represented by references to them only, in much the same way as class types in Java are.
This strikes me as an unusual definition. I'd say that references are simply a pointer that is treated like the underlying value i.e. they're 'boxed' values. This is a notion that is essentially present in all languages. e.g. Java (or Scheme) which don't have some explicit reference feature, still have 'unboxed' values (e.g. integers) and 'boxed' values (e.g. lists).
>They deal with bindings.
Sure, to create a closure, you need to capture the relevant bindings (the lexical environment). But how does on deal with bindings? Well, a binding associates a variable to a value. Now, when you capture a binding, you can either point to the original binding (capture by reference) or copy the value (capture by value).
>Why would you have to be wary about lifetimes?
I mean, that's just how these languages are, right? Even if you do something incredibly mundane, like returning a value or calling a function with a parameter, you constantly have to be aware of lifetimes. That's why to me it seems like a completely orthogonal issue to closures.
C++ lambdas do the same thing if you pass a pointer.
Python would do the same thing if you had a mutable non-threadsafe objects passed to threads. Eg if you iterate over a list of dicts, and modify the dicts, you'll get the same effect.
So then the question becomes, why does v get re-used, instead of assigning a new pointer each time? Dunno, probably performance. I could see the latter generating a lot of garbage, which can be totally avoided if you don't pass mutable references to goroutines.
What go really lacks IMHO is some kind of const modifier for these kind of references. Maybe it does, all I remember are const scalars.
You'll have the same problem with a simple integer (for example, as in https://go.dev/play/p/uqvRoO3ynZt). Nothing to do with v being a reference to something.
>>> funcs = []
>>> for i in range(10):
... def f():
... print(i)
... funcs.append(f)
...
>>> [f() for f in funcs]
9
9
9
9
9
9
9
9
9
9
[None, None, None, None, None, None, None, None, None, None]If you seriously think that's gonna happen, you haven't been to Go mockery stations on the interweb.
There are only 2 kinds of languages: Those people complain about, and those nobody cares about.
For me, Go is essentially pointless without generics and better error handling (I love that there are no exceptions, but since 99% of the code just return the error anyway, that needs to be the default path), but even with it, it is not much more than C with garbage collection, a few nice features and better syntax in some cases.
So even if it got these problems solved well, I still don't really think I would be using it.
That said, with generics, it should be possible to change to the 'Either' style [0] of error handling. And with another compiler feature to be added, er, 'exhaustive switch/case' or whatever you want to call it, they can make it a compiler error to not handle the error case. That said, I don't see the language developers or community making it a standard anytime soon.
The current style of error handling doesn't hurt enough. It's verbose and repetitive, sure, but if the alternative is something clever or magic, it's not the way to go.
[0] https://www.scala-lang.org/api/2.13.6/scala/util/Either.html
The main problem of error handling in Go is not the tediousness of writing
if err != nil {}
after each function (it's not nice but bearable).The main problem is how hard to actually process errors.
In Go, there are three types of errors: custom structs with contextual info, predefined error constants of type stringError aka sentinels, and wrapped stringErrors, produced in-place by fmt.Errorf.
The first ones can be matched by type coersion or errors.As function. The second ones can be compared with == operator. The last ones can be either parsed by regex, or matched by the error they wrap and then unwrapped.
Oh, and the signature is nearly always just error, so basically to process errors gracefully you have to read the docs for each function instead of just looking at the signature.
It's so extremely inconvenient and tedious so nearly nobody process errors in Go, despite what language creators say. People either return error to the top of the callstack, maybe wrapping it in additional string, thus emulating poor man's exceptions without stack traces, or panic in-place. Nearly nobody catches and matches errors, as many in C++, Java, Scala or OCaml|Haskell world do.
My IDE helps a lot the typing of the error conditions and is collapsing it nicely. This is not the problem, but the error handling.
Anyway yeah, I'm glad Go is resistant to change.
(BTW, 2011 on that. I would not today agree with the statement "the Haskell community is the only community I know that is doing new things in the field of language and API design". I think Rust is doing interesting new things now. In 2011 they were still working on basic functionality. There's a couple of other interesting little up-and-coming languages.)
I remember something similar with their approach to package management which they said wasnt necessary for years.
They really do seem to resist doing the right thing for as long as possible.
Generics aren't a cutting edge feature that they wanted to see how it would develop. Their objection against it was ideological.
I can quite believe that the Go team wanted to be sure that the approach made sense for Go, that it was the right solution to the right problem and to keep to their backward compatibility promise. Experience is a good teacher. I really don't know why it matters in the slightest that they didn't arrive at that on day one.
This just isn't how it happened. The Go team never said that they were opposed to generics. Their stance was (i) that it was not a top priority and (ii) that it was difficult problem and they'd take the time to implement generics correctly.
The idea that this is about one group of people finally proving that they're 'right' is a distortion based on viewing events though the lens of snarky comments on the internet.
I'm paraphrasing and I wish I had a source for you but I'm pretty confident I didn't make that up.
What they sacrificed was runtime correctness, because the type erasure lets you sneak invalid types into unsuspecting code. How much of a problem that's caused in the real world is debatable.
There were trade offs and they didn’t have a solution on day one.
They’ve always said they were open to them with the right implementation.
https://web.archive.org/web/20091113154906/http://golang.org...
> Generics may well be added at some point. We don't feel an urgency for them, although we understand some programmers do.
> Generics are convenient but they come at a cost in complexity in the type system and run-time. We haven't yet found a design that gives value proportionate to the complexity, although we continue to think about it.
12 years later, here they are.
I speak here to those who slag on Go and haven't so much as run "Hello World" in it. Those who have used it, especially for some period of time, and especially those who tried long enough to learn to program Go-in-Go (as opposed to Rust-in-Go or C++-in-Go, etc.), and decided they didn't like it due to their experience, I invite to continue discussing. You're not who I'm talking about here. Real experiences with the language are valuable contributions.
Those who haven't had real experiences are not making valuable contributions. "Lol generics, lol they realized they're wrong and I was right all along" and "Lol error handling is bad in Go" are not valuable contributions.
Cruise through the Go issues and look at all the closed issues. Then, for context, cruise through Rust's or Python's, or your favorite open source project of some size that isn't even a programming language. It's a constant deluge.
What did it was real use cases from using the language for a long time, offered by serious people who weren't just berating the designers. If you read the generics design, and especially if you read the whole history of the design, you can see where this has had an impact, where the features are tuned for the use cases presented by the community, and how the design has changed at least twice in a major way (depending on how you count, of course) as people work through the use cases.
You'll also come to understand better why it's not like you can just flip a switch and "turn on generics" and why everyone running around claiming it was easy is dead wrong. You can also learn some of this by paying attention to discussions of generics in other languages when Go is not a topic, because it'll become clear when not discussing Go, there's a lot of broken generics and strange edge cases and poor decisions made by existing languages that we have to live with forever. Not that all of them are broken, but, again, the idea that some care needs to be taken wasn't something the Go team just made up. There's plenty of languages, even big name languages like Java, that "flipped the switch and turned on generics" and it turned out not to be as awesome as presented, because it's a legitimately hard problem even in 2021.
Java was created in an age where very few languages with mainstream relevance had userland generics.
Go was not, and generics had already had to get (somewhat painfully) retrofitted in two of the above.
When Go came out in 2009, though, Java and C# had pushed generics into the mainstream. Additionally, they'd shown some of the problems with trying to retrofit generics onto a language and ecosystem that didn't previously support them.
A tad different from the computing world in 2009.
you hear about firefox everywhere, everyday, and yet only has 3% marketshare
"The reason is simple and compelling: It's too much to do all at once, and we might get it wrong."
If only people realized this about most software projects. There's a lot of hubris and under-estimation in software engineering.