I haven’t dug too far into the Fo code yet, but I’d guess that it’s doing something akin to type erasure in Java, and adding conversions and type checks transparently when compiling down to a go binary. Sort of like how if you ever pop open a class file and dig into it, you’ll never see any references to generics. Everything compiles down to objects in the end with generated type checks.
How do recursive types and self-referencing types work? What about generic types that themselves require generic types? Can type constraints be applied? What do generics look like on interfaces? How about in slices? When compiled, are specialized functions written or is the code shared? How is runtime reflection for type params implemented?
Also, oblig ref of a now-defunct lang on top of Go: https://oden-lang.github.io/
Finally, good work, I hope it was fun! Don't take criticisms too seriously, they are good things (as opposed to silence) and par for the course on this site.
My perspective was that when I find myself wishing for generics in Go, what I really want is not full-blown generic types and functions, but rather a few helper functions like map/filter/reduce, or converting a map to a slice, etc.
Having "true" generics is undoubtedly useful when you really need them, but the Go ecosystem has shown that they aren't strictly required in order to write large, maintainable programs. And you pay for them in the form of misuse. In that sense, I view generics much the same as operator overloading.
Anyway, I ran into some tricky edge cases when working on Ply and I'm curious how you address them. First, Go lets you define local types, i.e. with a function scope instead of top-level scope. You can't define methods on these, nor can you reference them in top-level functions. How does Fo handle the case where I want to call a generic function/method on a locally-scoped type?
The second issue I ran into was imports. I didn't devote a ton of time to this, but it seemed like it could be tricky to properly fetch both Go and Ply packages and pre-compile the Ply packages to Go. How does Fo handle this?
Local types are an edge case I'll need to spend more time working on. The type-checker won't have any problems; the real question is how to generate the appropriate Go code. One solution might be to move the local type into a higher scope during code-generation. Another might be inlining the relevant generic types in the same scope as the local type. Finally, the best solution might just be to say this sort of thing isn't allowed and the compiler will return an error.
Imports aren't supported right now, but it's one of the most important things I need to work on next. It'll be pretty tricky, but I'm confident I can find a solution.
The fortunate thing about the approach I'm using with Fo is that I have complete control over the parser, type-checker, and code-generator. At the cost of having significantly more complexity, I have the flexibility to tackle these sorts of edge cases without relying solely on existing tooling.
package stack[t]
type Type []t
func New() Type { return Type{} }
...
Then it can be used so: import s “stack”[int]
var s1 = s.New()I made gotemplate to explore that idea
https://github.com/ncw/gotemplate
This requires a round of `go generate` for the actual code generation, but otherwise quite a similar experience.
Having it built in would be great!
Generic interfaces are pretty fundamental IMO. It's something I plan to add to the language before the v1 release.
https://github.com/albrow/fo/compare/faf3c9f96a8ecd9d17ca968...
https://github.com/albrow/fo/compare/f76c531f839e5e76e5f6b7f...
And from Java I know that there's a pretty trivial example that showcases how its Generics implementation is unsound when mixed with inheritance.
And I know that the Go maintainers have been hesitant for a long time because they didn't know a good version of Generics to add.
So, is there any version that just works, and never leads the user down any rabbit holes? And that doesn't lead to ever increasing type boilerplate?
Because I've been super happy ever since I decided that worrying about occasional usage of void pointers in C is just not worth my time. And where configurability is really needed, function pointers are totally fine - I don't think there is any need for static polymorphic dispatch (function pointers are probably even preferable, to avoid bloated machine code).
I think you can identify a core set of features that are pretty reasonable. I think that'd be type classes, constraints, and functional dependencies.
If they made a handful of extensions standard (GADTs, etc.) it would mostly Do What You Want without a lot of prodding.
> And from Java I know that there's a pretty trivial example that showcases how its Generics implementation is unsound when mixed with inheritance.
I'd be curious to see that. There's a well known limitation that mutable containers (and they're all mutable in Java) need to be invariant, but that doesn't make them unsound.
The type system was also already unsound due to covariant arrays and nulls being a member of all classes, but if you don't break those rules or disable checks, Java generics work as far as I can tell. By "work", I mean I've yet to get a ClassCastException in a fair amount of work with some gnarly Java generics.
And, really, 99% of the boilerplate in Java's typing is that you can't declare aliases for types; that seems to be more due to engrained hostility to syntactic sugar than any technical difficulty.
> So, is there any version that just works, and never leads the user down any rabbit holes?
Most of the "gradual typing" projects for languages like Javascript, Python, Ruby all seem to accomplish what you're looking for, by virtue of the fact that you can just ignore it when you don't want it.
I see. Yeah I don't know precisely what these terms mean. If the creators of Generics mistakenly made them covariant, that only goes to show that maybe it's a little too complicated. IMHO.
To be more precise, what we want to do might be too complicated for practical (i.e. relatively simple) type systems to describe. So, why bother at all? Better learn how to structure programs simple enough to make them obviously correct (i.e. mistakes will be obvious and can be easily fixed). Instead of catering to the needs of impractical type systems. I think that's why C is still so popular: It removes most of the boilerplate (i.e. strides for array indexing, arithmetic operators, structs, other ABI things) but gets out of the users way if s/he needs to disregard these constraints for a while.
Even in C, there is a similar problem with const compatibility of pointers of more than 1 level of indirection. Example taken from [1]
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; /* not allowed, but suppose it were */
*pp2 = &n; /** valid, both const, but sets p1 to point at n */
*p1 = 10; /* valid, but changes const n */
[1] https://www.sanfoundry.com/c-tutorials-concept-pointer-compa...There are many levels of generic capabilities, across multiple languages in about 45 years of research, we don't need the full shop, CLU generics would already be quite usefull versus interface{} everywhere.
If so, this is known wrong in the programming language theory community. Java just gets this wrong; if you remove it, generics in Java work correctly, I think.
That aside, I don't think I've ever really been bothered by the lack of generics for actual work code.
Do you have any recommendations for how to write this code without generics, without sacrificing performance, and without hundreds of lines of duplicated code?
I could understand not missing this if you're coming from a dynamically typed environment, but understand that many people aren't.
Starting with generics seem to lead down the dark path of C++ template metaprogramming..
I would rather do something like
type StrBox = MakeBoxType!(string)
...and have clean hygienic macros to generate my "generic". Syntax candy can be added to that to get generics...but starting with generics and only supporting that went really really badly for the C++ ecosystem.
Using the existing packages is a great approach as I think that Go standard library has one of the best packages supported for AST/lexing/parsing family, in comparison to Python and Ruby. Haven't worked with Rust.
Just not for user-defined methods and types.
https://golang.org/src/runtime/hashmap.go
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implem...
The standard reason I see for people wanting exceptions in Go is because they're sick of writing the following:
```go
thing, err := things.New()
if err != nil {
log.Fatalln(err.Error()) // Or `return err`
}```
But I would argue that it means they're not writing idiomatic go. A good reference for the value of error values is this go blogpost[0]. The HN discussion[1] of that post was also interesting, I like this comment in particular:
> In Go I not infrequently make use of a non-nil value AND a non-nil error. A canonical example of this is the io.Reader interface in Go's standard library [1]. I think it is a very useful idiom particularly when dealing with things where failure is more normal - e.g. dealing with network services, disk IO, decoding partially corrupt exif data from images. Often you want a best-effort at the value even if you run into problems.
Handling every error from every function that returns an error can be verbose. But for that verbosity you get a much easier to understand failure model, and the tools (defer) to handle failures reliably no matter what comes up. Exceptions are part of the reason that RAII is so critical in the C++ world.
I don’t think that’s quite true. You definitely want RAII with exceptions, yes, but RAII is extremely useful even without exceptions. I believe it’s common to disable C++ exceptions but still use RAII.
If you have multiple returns from a function (which can very easily happen with manual error checking) RAII is a big win.
You’re free to write exception-full code using those constructs, although the community tends to use error return values for the most part.
https://stackoverflow.com/questions/44504354/should-i-use-pa...
> You should assume that a panic will be immediately fatal, for
> the entire program, or at the very least for the current
> goroutine. Ask yourself "when this happens, should
> the application immediately crash?" If yes, use a panic;
> otherwise, use an error.
In Java, say, exceptions are the standard for raising a normal error. A class of those exceptions are runtime errors which are equivalent of "panics". Yes you can handle them, but they denote a problem with the program that can't be solved by the interpreter (divide by zero error, etc)