Crimes with Go Generics
christine.website
christine.website
fib = func(n int) int {
if cache[n].o.IsSome() {
return *cache[n].o.val
}
return fib(n-1) + fib(n-2)
}
Should be: fib = func(n int) int {
return cache[n-1].Force() + cache[n-2].Force()
}Here is a hacky workaround to measure the actual time using a syscall: https://go.dev/play/p/kF6Bh048siE
It shows 979ms for the original version and 0.212ms for the fast version.
If you call `time.Sleep()` the reported duration will reflect that call, but it's only simulating the delay. The duration of a CPU-heavy task is always reported as 0.00s.
Demo: https://go.dev/play/p/UZToqFseAwO
Output:
=== RUN TestSleep // calls time.Sleep(2 * time.Second)
Real duration: 554µs
--- PASS: TestSleep (2.00s)
=== RUN TestBusyLoop // for i := 0; i < 1000000000; i++ { ... }
Real duration: 390.826ms
--- PASS: TestBusyLoop (0.00s)
PASS
The simulation is quite good: if you have multiple threads that use `time.Sleep()`, their output will be interleaved correctly. The output panel will even display the lines one at a time as if it's streaming them from the server, but in reality the entire output is received at once and the "streaming" effect is simulated on the client.Concurrency is one of Go's core features, so it figures the playground would allow some form of concurrency even when the clock is fake.
Thanks for pointing that out!
I chuckled at the juxtaposition of the “expressivity and clarity” comment:
>> you can create composite types (types out of types). This lets you get a lot of expressivity and clarity about how you use Go
Followed by long winded code:
>> However, we can make this a lot more complicated with the power of the Thunk[T] type
And confusion:
>> Why is this so much slower?
This article touches on a lot of the frustration i see with abuse of static typing.
You’re going to write more code and take longer to write it and longer to change it (read: you’re going to cost me more money to build and maintain this code), for the unproven claim that you’ll write less bugs.
The author then goes on to write a logic bug in their memoization code.
But only after they’ve created a generic MPMC queue type by taking an already generic MPMC queue type (a go channel) then writing logic that breaks it (can’t handle an empty queue state) which they code their way out of by throwing more code i have to own.
I dread refactoring code that hasn't made extensive use of types, because it makes it harder to know if you've missed anything conceptually (typing reduces the potential behaviors of a piece of code) or literally (e.g. forgot to update a class).
https://planetscale.com/blog/generics-can-make-your-go-code-...
I also could not find where the author explains their "crimes".
What I am missing here?
The Option is just a good idea and sum types should have been in the language from the beginning to handle errors instead of product types. But that ship has sailed and sunk.
With the huge difference that Go’s pointers don’t statically require checking if they’re `null`.
Furthermore, you can add functor and monadic APIs to `Option` which provide completely safe (and somewhat efficient) usage patterns even if you build the option out of product types.
Though that isn’t the case here, an other interesting item is that you can build an option type out of non-pointer, thus avoiding the indirection and allocation (though hopefully unlike the C++ committee you don’t do it just so you have a pointer without an allocation)
>Furthermore, you can add functor and monadic APIs to `Option`
There's nothing preventing you from defining these functions directly on pointers, they'd be just as safe.
Whetger or not that’s the way you want to program is a whole different question, but you can get a level of safety from constructs like this you can’t get from a pointer.
Whether or not that’s the way you want to program is a whole different question, but you can get a level of safety from constructs like this you can’t get from a pointer.
It's consistent with the way Go's error handling works. Turns out it's not anywhere near as much of an issue as you might think. In practice you always notice that the function you're about to call returns an error and handle it.
> There's nothing preventing you from defining these functions directly on pointers, they'd be just as safe.
Pointers aren't Optionals. They happen to be nilable, yes, but their semantics are broader. A pointer can be used to avoid copying large structures around. How would you distinguish such a pointer vs one that's used as an Optional?
You choose if you want an exception. Map will only run if the optional contains a non-null value, so it won't throw. orElse will safely give you a value if the optional contains a null, so it won't throw either. The only time you throw is when you use orElseThrow, but that's in the name and you know what you're doing.
Ex.
Optional<Integer> x = Optional.ofNullable(null);
// This won't throw!
x.map(i -> i + 1);
// This won't throw either, and safeValue will *definitely* be an int!
int safeValue = x.orElse(5);
// This *will* throw, but you specify what to throw
x.orElseThrow(() -> new RuntimeException())
These three methods cover nearly everything I did with options in OCaml as well, so I think that's about everything you need.The latter. Java's Optionals are half-baked and don't provide as much safety as you'd think because it's easy for a `null` value to slip in. Notice how they used `Optional.ofNullable` — a common footgun is using `Optional.of(value)`, which throws a NullPointerException if `value` is null[1].
[1] https://docs.oracle.com/javase/8/docs/api/java/util/Optional...
Is it possible to church-encode these with Go generics?
As a casual Go user, it wasn't obvious whether those particular implementations were bad, whether the patterns themselves are a bad fit for the Go language, or whether there was just a better (more Go-like) way of doing things.
A better function to add to Optional[T] / *T would have been Map:
func Map[T any, V any] (o *T, foo func (T) V) *V {
if o == nil {
return nil
}
v := foo(*o)
return &v
}
or, with Optional: func Map[T any, V any] (o Optional[T], foo func (T) V) Optional[V] {
if o.IsNone() {
return Optional[V]{}
}
return Optional[V]{foo(o.Yank())}
}Just a side note: Go test supports benchmarking (https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-i...) which gives you few useful tools such as `b.ReportAllocs()` and `go test -bench . -cpuprofile cpu.out -memprofile men.out`. These tools can give you far better information than just the execution time of a test case.
https://planetscale.com/blog/generics-can-make-your-go-code-...
Go 1.18 added generics to the language. This allows you to have your types take types as parameters so that you can create composite types (types out of types). This lets you get a lot of expressivity and clarity about how you use Go.
Aren't structs already composite types?the website is called christine.website so it is probably not a man
Please forgive my ignorance! :)
But you could probably achieve that using go's not-well-known sum types, e.g. like this
https://github.com/FSMaxB/type-safe-builder-experiment/blob/...
It's a kind of sum type, but it can only be used to constrain types for a generic parameter.
That is, you can define `func foo[T StaticOptional[int]](x T)` and then call it as `foo(StaticNone{})` or `foo(StaticSome[int]{123})`, but you can't do `func foo() StaticOptional[int]` or even `func foo[T StaticOptional[int]]() T {return StaticNone{} }`.
For example, this doesn't work [0]:
//compilation error: interface contains type constraints
func TryParse123(s string) StaticOptional[int] {
if s == "123" {
return StaticSome[int]{123}
}
return StaticNone{}
}
[0] https://go.dev/play/p/1CnieCLqESCWhich of course defeats the point of using generics
_edit: clarify that my attempt is a bad practice_