Maybe adding generics to Go is about syntax after all
dave.cheney.net
dave.cheney.net
There are many times I remember where I've had to implement some sort of application specific data structure that's not built into the language, and generics would have made it a lot nicer. I made it generic by using interfaces, but then you have the problem of throwing away type safety, so to preserve it, you need wrappers which cast.
It's not a given that adding generics would ruin the ergonomics of the language. I hope that whatever form generics do take in the end, that they remain simple. If I see template meta-programming in Go of the sort that occurs in C++, I think I'll scream, but that meta programming was to work around limitations in C++'s typing system which Go doesn't share.
Interfaces do not throw away type safety if they are used correctly. They are also better suited for the consumer of the type rather than the definer. One of your problems might be that you are prematurely abstracting your interfaces for your callers.
My experience (admittedly limited) is that application writing in Go is nice, but as soon as I try to build a library for use in several similar (but not quite identical) cases, maintaining type safety can quickly become a hassle.
With generics, I could create a generic data structure which is strongly typed - what goes in is what comes out.
Right now, the way to ensure that is to wrap your data structure in something that converts those Sortables into specific classes, say PatientRecords.
Interfaces are wonderful, I like working in Go more than any other language so far, but they have their limitations.
and the future seems to be more weirdness. magic methods like handle. list comprehensions still uncertain. contracts sound like a backwards compatible break and duplicate of existing functionality.
i used go to use libraries and low memory foot print for my small projects. but i bet there’s simpler routes out there
func Join(type T strseq)(a []T, sep T) (ret T)
This below version is different, each variable could be a different type (that satisfies strseq). func Join(a []strseq, sep strseq) (ret strseq)It work quite well for package like a Map so any data structure/algorithm. I suspect it would be less ideal for filter/map/reduce.
Considering how conservative, bordering on reactionary, the philosophy of Go seems, I think it's politically untenable.
But I think Go is more worried about compiler performance, so if there are significant complexities in the surface syntax of generics or in their downstream consequences, it will be an impediment there. I think the people behind Go consider most type theory to be an impediment to productivity and would rather accept something simple with shortcomings they understand than try to do it "right." I think this is a perfect example of Unix's "worse is better" philosophy. Whether we agree with it or not is another question.
if err != nil {
return errors.Wrap(err, "some message that is unique to this line")
}
and the handle construct doesn't really handle (excuse the pun) this properly (you have to re-define handle each time or have a local variable or something similar). Which means that the new construct won't actually be massively helpful unless the only thing you want to do is errors.WithStack or just bubble the error back up.But I do get why they couldn't get .map_err() -- because that's something you can only really do if you have a Result type.
”If you write a program which generates source code, such as the bison parser generator, you may want to adjust the preprocessor’s notion of the current file name and line number by hand.
[…]
#line linenum
linenum is a non-negative decimal integer constant. It specifies the line number which should be reported for the following line of input. Subsequent lines are counted from linenum.
#line linenum filename
linenum is the same as for the first form, and has the same effect. In addition, filename is a string constant. The following line and all subsequent lines are reported to come from the file it specifies, until something else happens to change that. filename is interpreted according to the normal rules for a string constant: backslash escapes are interpreted. This is different from ‘#include’.”
handle err { log.Fatal(err) }
in quick "scripts". And even there I should probably provide more context. All clean-up work is already done in defer.Overall, check/handle feels like a poorly-thought slap-on to me, on par with ON-units from PL/I[1]. And PL/I is not something you want as an inspiration.
[1] https://en.wikipedia.org/wiki/PL/I#ON-units_and_exception_ha...
func something() (Err error) {
defer func() {
if Err != nil {
someCleanup()
Err = fmt.Errorf("wrapping: %v", Err)
}
}()
return blah()
}
And unlike handle you can actually give an arbitrary mapping function, while as far as I can tell you'd need to do something like handle err { return mapError(err) }
Rather than the more ergonomic handle mapError
Or handle func(err Error) { return mapError(err) }
But whatever -- all of this is pretty useless. The main benefit of check in my mind is actually that you would be able to easily bump your test coverage -- because Go test coverage is based on lines and so the repeated use of if err != nil {
return err
}
artificially dilutes your test coverage (you don't need to test that every error is propagated -- that's just doesn't make any sense and might not be possible if you are returning directly from os.* functions that don't even have fault-injection).At risk of cargo culting or not knowing enough about the problem space, I can only think of what I've recently read from The Psychology of Computer Programming^0. The entire reason that generics and error handling are difficult is because of the decision to avoid a syntax table.
This to me feels like an artificial constraint because the Go authors are optimising for compiler simplicity and not developer ergonomics. So the developer has to parse a whole lot more because the compiler guys don't want to take a hit. The reason why generics are such a pain in the ass, and errors are such a pain in the ass, is because the emotional attachment to how Go currently runs and what it stands for is still far more powerful than any attempt to innovate on the language's own status quo.
So what Cheney says makes some sense in that in a lot of ways, it feels more intuitive than implementing language grammar that isn't executed but looks like real code. But at the same time, the whole debate is basically around how to change go without changing it.
Either do it or don't.
My main issue with it (aside from making memes about RSI) is that it actually dilutes your test coverage artificially. 'go tool cover' instruments line-by-line (technically I think it's statement-by-statement but that's not relevant here) and so having a dummy line like
if err != nil {
return err
}
where you aren't doing anything useful with the error decreases your test coverage over a line which is not only obviously correct but might also be impossible to test (some APIs have an error return even though they can never fail in most usecases -- and as a user of the library it's better to be safe and check the error anyway). A perfect example of this is the Read() interface from math/rand -- it is impossible for this to return an error and yet you definitely should check the return value from Read() and it would be irresponsible not to.So, "thankfully", because acknowledging reality here means not having to live with the pain of using a language that is sub-optimal for the problem you're working on.
Put another way, do you have evidence that it is important? Has it let people write better programs? How would you prove that?
This is akin to saying grammar is important. It is, to an extent; but over adherence to grammar is usually not an effective way to communicate.
I would be interested in studies on this. Most I ever see is posturing and assertions with little to no evidence. Frustrating, because I think both sides have valid points. And I fully believe there is a learning curve both ways. I suspect they both converge on total ability. I am genuinely interested in ideas on how that could be explored.
My best try at a data point is that Lisp was #4 on the TIOBE index back in the 1980s. Now it's on the fringe. I assert that people had been exposed to Lisp, it was widely used. It became less and less popular. It's not that industry didn't know Lisp; they knew it and turned away from it.
Of course, the big flaw in my argument is the growth of the industry. A huge number of new people came in. Did they know Lisp, even at a college level? Maybe, maybe not.
My counter-argument to that is that industry knew Lisp, even if the new programmers didn't. If industry wanted them to learn Lisp, it could have made knowing it a job requirement, or trained people.
Unlikely, given that TIOBE the company wasn't founded until 2000, and the TIOBE index is based on web search engine results, and there weren't any web search engines, or a web for them to search, in the 1980s.
But I remembered wrong. They list Lisp as #2 in 1988.
Another interesting offer was MacScheme.
LeLisp had a Mac version and it was used to implement an interface builder, which was demoed to Steve Jobs, who then hired the developer to create a version for NeXT.
Both Symbolics and TI delivered Lisp Machine boards for the Mac II.
Procyon developed a Common Lisp with UI for both Macs and PCs.
ExperCommon Lisp was another Lisp for Macs.
XLisp had Mac version.
On the PC there were numerous Lisp implementations.
My personal hunch is that lisp was just too expensive when most industry took off. In near every way. C compilers being basically free is a ridiculous advantage. Yes, there are free lisp implementations today. However, early mover advantage is working against them, now.
My personal hunch about the experiment (based on nothing more than my intuition) is that people think differently, and that Lisp's syntax is a good match for how some people think, but not for how the majority of programmers think. But as I said, that's just my current intuition. My intuition a year ago, for example, was different...
Lisp has syntax. More than any other programming language.
What you think Lisp syntax is, are really simple function calls like
(+ 1 2)
which are based on the function + various args pattern.But something like Common Lisp has a few dozen syntactic built-in forms. For example the syntax for LET is in an EBNF (extended backus naur form) form:
let ({var | (var [init-form])}*) declaration* form*
Then Lisp has macros. Zillions. Each of them implements syntax.Lisp has a two-stage syntax:
1. stage are s-expressions, a data format: symbols, numbers, lists, strings, ...
2. stage are Lisp forms. They are written as s-expressions. But they have to follow a defined syntax. not every s-expression is a valid Lisp form.
(let ((a 1) (b 2))
(declare (integer a b))
(+ a b))
Above is a valid LET form. (let ((a 1) (b 2))
(+ a b)
(declare (integer a b)))
Above is not a valid LET form, because the syntax requires that the optional declaration is directly after the binding form.Without parentheses it might look like:
let (a 1) (b 2)
declare integer a b
a + b
You would think that it has a syntactic structure. Just because Lisp uses s-expressions as a base mechanism, does not mean Lisp has no syntactic structure - but it is on top of s-expressions.Since Lisp has user-programmable syntax, there is basically an unlimited amount of all kinds of syntactical constructs.
I think the difference is most people, myself included, are looking at basically the syntax as where the capital letter and the sentence ending punctuation go. Which is really the only mostly constant thing in most sentences. You raise a vital point that that is a small part of the syntactic makeup of a sentence.
Which is funny, because so many people are convinced you get no static help in lisp. Which is demonstrably wrong.
Just adding s-expressions is actually a huge jump up in syntactic complexity and structure, since now you can define a whole hierarchy statically, "on the page", instead of having to use a series of stack operations to manipulate memory from afar. And then as you lucidly put it, going from s-expressions to a practical Lisp dialect adds another level of context on top of that.
(And yeah, it's possible to bolt structure onto a Forth dialect, too, but the "Chuck Moore way" is that you do that custom every time you need it, instead of trying to generalize. Generalizing a concatenative structure leads to something like Factor.)
What I remember struggling with is that it was surprisingly kind of hard to remember when parens are necessary where. For example you might define an if statement:
(if (> x 3) (printf ...))
Looks nice, but what about if you want multiple things in the body? Your options are
(if (> x 2) (progn (printf a) (return b)))
Where "progn" means "curly braces" effectively, or instead always requiring a list-shaped argument
(if (> x 2) ((printf a) (return b)))
which means in even the single-expression case you have to remember to always double-wrap
(if (> x 2) ((printf a)))
And now throw in handling of 'else' into the above. It gets surprisingly hard to use surprisingly fast. This problem repeats everywhere (function definitions, type declarations, for loops, and so on).
What all the above really clarified for me is that sexps work great in languages like lisp/scheme that are designed around them, but they don't solve syntax for free.
(if* (> 3 2) then (foo) (bar) else (baz))
https://franz.com/~jkf/ifstar.txt (when (> x )
(printf a)
(return b))
(cond
((> x 3) (printf a)
(return b))
((< x 2) (printf b)
(return a))
(t (return c)))
Nobody who works with Lisps thinks in terms of "do I need double parentheses". You just know that if takes two or three expressions, when takes an expression followed by zero or more forms, cond takes a sequence of zero or more clauses which consist of a test followed by a body of zero or more forms.If you're designing an S-exp syntax for something else, it's probably best to keep the familiar things the same; don't make some different if and such.
The Lisp if maps naturally to the ?: ternary operator; cond, when and unless can compile to cascaded if/else if/else.
My guess is because most languages don't do that, so people are used to variables not having a symbol denoting their basic type. It largely comes down to what sort of syntax people are accustomed to.
And so they are in fact very related.
For instance with a macro you could do (made up syntax)
SortIntSlice = mymacros.MakeSorter!(int)
...then at compile time have code for sorting a slice of ints generated.
This is again similar to C++ templates (main difference is C++ will implicitly instantiate instead of explicitly).
And C++ doesn't need generocs because people use templates instead.
They really are in the same problem domain.
var _ SomeIface = SomeConcrete{}
var _ AnotherIface = SomeConcrete{}
Just to see if you actually implemented all the signatures on the interface correctly.However quite a few Go developers now use interfaces with an unexported method specifically to stop users from misusing their types in a context they didn't intend. I guess it's come full circle.
What a horrible hack. Why bother with that? Either export your interface, your don't.
Indeed. It's used in the standard library for describing Go's AST if I recall correctly.
The whole point is that the caller can make new interfaces to fit existing structs in other libraries.
All this pain for what boils down to not having to make a wrapper. Sure wrappers are dumb, but pushing a static analysis out to a run time error dumber, especially since implementing interfaces is a lot more common than wrapping 3rd party structs.
The latter is wonderful, and other languages support this without needing to resort to the former.
The tooling is also annoying with the most glaring deficiency being the lack of package versioning, which I believe is going to change (or it has already? I don't remember). And then there are tools like `go vet` which basically just attempt to make up for the compiler deficiencies in sub-par ways...
Go has a very nice runtime, but the language itself and the tooling feels extremely half-baked. I'm curious whether Go 2 will improve upon this.
The world of programming languages has improved since then.
Incredible mental gymnastics you've got there.