The next item on my wishlist are algebraic data types. In complex projects I've opted for Rust over Go just because of the abstraction power of being able to represent things like:
Dest = Register (A | B | C)
Src = Register (A | B | C) | Immediate value
OpCode = Nop | Add(Dest, Src) | ...
In Go, I'm so tired of `interface{}`.Edit: To respond to your edit about map iter order, that's related to my complaint of Go's lack of dev vs release build. Randomizing map iter order in dev builds is acceptable to me. Go makes you pay for that in prod.
You can spell out all your types into explicit structs (where you can choose between a simple implementation with a little worse performance or to make the implementation a little more complex) and pass them around.
There are many complex code bases (kernels & databases come to mind) which use C (not C++), and they don't resort to passing void* around the "business logic" parts.
The idea that for a complex project you would choose a different language with so many different characteristics due to a minor detail about the type system (this is not exactly Haskell vs JS...). This kind of decision making would not pass in any reasonable organization...
No, it's more like Haskell vs C, considering the expressiveness of Rust’s vs Go’s type system.
An example is rolling your own discriminated union:
type Avariant struct {
a int
}
type Bvariant struct {
b string
}
type SumChoice int
const (
SumA SumChoice = 0
SumB SumChoice = 1
)
type Sum struct {
type SumChoice
A *Avariant
B *Bvariant
}
sumA := Sum{choice: SumA, A: &{a: 7} }
sumB := Sum{choice: SumB, B: &{b: "abc"} }
func foo (x Sum) {
switch(x.choice) {
case SumA: {
fmt.Printf("Value is %d", x.A.a)
}
case SumB: {
fmt.Printf("Value is \"%s\"", x.B.b)
}
}
}
As usual with Go, it's ugly and verbose and error prone, but it just about works.The previous poster was probably thinking of something similar but using interface{} (equivalent to Object or void*) instead of Sum, and then pattern matching using an explicit type switch:
func foo (x interface{}) {
switch actual := x.(type) {
case AVariant: {
fmt.Printf("Value is %d", actual.a)
}
case BVariant: {
fmt.Printf("Value is \"%s\"", actual.b)
}
}
}
This is slightly less risky and has less ceremony, but it's also less clear which types foo() supports based on its signature. Since Go has only structural subtyping [0] for interfaces, you would have to add more boilerplate to use a marker interface instead.[0] In Go, a type implements an interface if it has functions that match (name and signature) the interface functions. The name of the interface is actually irrelevant: any two interfaces with the same functions are equal. So if you declare an interface MyInterface with no methods, it is perfectly equivalent to the built-in `interface{}`: any type implements this interface, and any function which takes a MyInterface value can be called with any type in the program.
On the first approach, with "error prone" you mean the tag could be incorrect, right? (or even have an impossible variant with 0 or multiple variants set).
type Sum struct {
type SumChoice
A Avariant
B Bvariant
}
This is what I meant by saying that it depends on exactly what you mean by "sum types".Still, I think most people don't worry so much about efficient packing of sum types, and instead the safety and convenience factors are more important. Of course, YMMV based on exact usage.
I'm not in anyway claiming that Go supports sum types. Someone just asked how they may be emulated, and I don't think it should be surprising that emulation has an overhead.
It'd be nice to have syntactic sugar for splatting structs together, or a library that let you do it in a typesafe and efficient way.
EDIT: I will defend the random map order, though. Golang maps were only predictable up until a set number of items, which caused hard-to-detect bugs where your small test case would be in order but your large real-life data would be out of order.