5 ms·
After a couple of years with Go, I'm neither particularly happy about, not particularly unhappy. It's a decent language. Productivity is high, which trumps many
by lobster_johnson 10y ago
After a couple of years with Go, I'm neither particularly happy about, not particularly unhappy. It's a decent language. Productivity is high, which trumps many other concerns.
There has been so much controversy about generics, but one related area where Go is consistently annoying is when you want to define a sort of language of data types -- where in a different language such as Haskell would define algebraic data types. For example, an AST of syntax nodes: There's no way define this (simplified):
data Node = Literal | Identifier | And | Or | Not | EQ | NEQ | ...
Instead, you must define a bunch of structs and use a dummy interface that all nodes implement. Even then, Go has no pattern matching, so if you want to handle a node, you need a switch statement on type, then check the data with an if. And even then, there's no way to prove at compile time that the switch statement is exhaustive (you could do some runtime magic with reflection-based lookup tables, but it's still poor). If you want a generic mechanism to traverse and rewrite such deeply nested data structures, there's a lot of exhausting boilerplate in the way of the core logic, because, ahem, no generics.
I'm in the middle of a project that has three sets of such data structures (a low level AST, a high level expression grammar, and a query plan graph), and it's an area where the Go development experience turns from pretty good to pretty dismal. I've considered writing a mini-DSL and using "go generate" to generate Go code, but I've not yet decided on the best design.
- jghn 10y agoAnecdata, but my experience has been that people who really enjoy Go either don't care about or don't want at all the kinds of language features you are describing. As with many language trade offs, at the end of the day people tend to gravitate towards languages that fit their own personal preferences
- gbersac 10y agoMaybe a language like scala should fit your needs better ? I am using it since 8 months and give me all those abstractions you speak about and its elegant synthax allow me to be very productive. I don't know if I am as productive as I would have been using go, but I am certainly more productive than using Java. But this not a language as easy to pick up as go (even though I didn't have much problem to pick it up right out of school). I also think it is much more difficult to write reliable software in go than it is in scala. The much stronger type system (generics, algebraic data type) help me to check the correctness of my program at compile time.
- lobster_johnson 10y agoScala is too overengineered for my taste — the combination of Java-style OO with functional programming doesn't sit right. I like the functional part, less the other stuff. I also want a slim, natively compiled language without a VM.
- StavrosK 10y agoSo, Rust?
- lobster_johnson 10y agoDefinitely going to be looking at Rust soon.
- paulddraper 10y agoRust has a ton of complexity so it can remove GC. It's a big leap, and something Swift and Scala don't attempt. Haskell would be a closer match.
- StavrosK 10y agoHmm, is it "a ton"? I've only written a few things in Rust, so I'm not overly familiar with it, but, for the few things I wrote, it was straightforward to at least reason about why something might not work (even though I may not know how to actually make it work). I imagine that, in a few weeks, one can become so familiar that the compiler won't have too many remarks about one's code.
- daysforbeef 10y agohear hear! That is the one thing stopping me from going full-steam ahead with Scala, reeks of java, promises the future yet is abused forever because it cannot decide what it wants to be ... do I want to be Haskell? do I want to be Java?
- lowmagnet 10y ago> Instead, you must define a bunch of structs and use a dummy interface that all nodes implement. This point interests me. I personally eschew 'dummy' interfaces in my Go code, preferring to work with the commonalities of such an algebraic type. For your Node example, I'd look for things that all of the nodes do or represent and work from there. The interface type built on that would have other bound functions using those commonalities to synthesize common functionality. Remember that in Go interfaces allow one to add disjoint configurations, such as things that (to borrow from webdriver) accept Click() (like buttons) or Set() (input fields). Those interfaces can apply independently from the meat of being a webelement. By implementing (or mixing in) the behavior, you can have a richer whole.
- lobster_johnson 10y agoThe problem is that these data structures don't have behaviour. They're pure, immutable data. The fact that I have to wrap them in an interface is also a performance concern, because an interface devolves to a pointer if it can't fit the contents into a single machine word. So in effect, every single struct becomes a pointer, which in my case seems unnecessary. There are some concerns that cut across all uses of the data that could be methods, but even things like mapping over (transforming) the data shouldn't really need to be built into the data structures via interfaces. Not least because it's horrible to write. For example, consider this: type Expression interface{} type And struct { LHS, RHS Expression } You want to be able to do something like this: newExpr := Map(expr, func(e Expression) Expression { if and, ok := e.(And); ok { if and.LHS == True { return and.RHS } if and.RHS == True { return and.LHS } } return e }) How do you implement Map? You have several choices. You can let Expression have Map(), and then every expression needs to implement it: type Expression interface { Map(fn func(e Expression) Expression) Expression } func (e And) Map(fn func(e Expression) Expression) Expression { e.LHS = fn(e.LHS) e.RHS = fn(e.RHS) return fn(e) } Or you could do something like: type Expression interface { GetChildren() []Expression SetChildren(...Expression) Expression } func (e And) GetChildren() { return []interface{e.LHS, e.RHS} } func (e And) SetChildren(children ...Expression) Expression { e.LHS = children[0] e.RHS = children[1] return e } Now you can implement Map() once for all expressions: func Map(e Expression, fn func(e Expression) Expression) Expression { children := e.GetChildren() newChildren := make([]interface{}, len(children)) for i, child := range children { newChildren[i] = fn(child) } return e.SetChildren(newChildren...) } ...but at the cost of being expensive, due to slices. Less expensive idea: Since all my expressions are nonary, unary or binary, I could have a Nonary/Unary/Binary to avoid GetChildren() and SetChildren() operating on slices, and then Map() could simply do a switch. Or you could simply skip all of that and build a switch statement: func Map(expr Expression, fn func(e Expression) Expression) Expression { switch e := expr.(type) { case And: e.LHS = fn(e.LHS) e.RHS = fn(e.RHS) // ... } return fn(expr) } This is why I was considering generating code, because it's terrible having to write and quality-check all that boilerplate. (There's a lot of boilerplate: Map() is not the only function I need. I also need a top-to-bottom version (the above is depth-first) and a Find(), and multiple other things that need to recursively determine something about an expression, without knowing all the types involved.)