
Show HN: Fo: An experimental language which adds generics on top of Go - polymathist
https://github.com/albrow/fo
======
gtt
I liked certain aspects of Go, but lack of generics was a deal-breaker for me.
You approached it a way better than I (and many others) did. Instead of
arguing on forums you started writing code.

~~~
awb
Isn't interface{} a generic?

~~~
cowboysauce
No, interface {} is more like void pointers in C and C++ or Object in C# and
Java. It can hold values of any type. One of the major selling points of
actual generics is that they provide type safety, but interface {} doesn't
provide that.

~~~
jj2
Go type assertions are quite safe on the other hand, unlike C type casts. The
only thing generics will do is to avoid that extra assertion step and the bit
of overhead that's involved in that. You could argue wether it will actually
increase readability of your code.

~~~
pionar
Generics (when used/implemented correctly) provide way more than just
elimination of type assertions. It provides compile-time type safety, rather
than runtime safety. It also provides elimination of the overhead (which isn't
trivial, reflection is not inconsequential) and boilerplate code. It also
allows you to use the same function/method for many different types without
having to resort to boxing everything to an interface/object.

~~~
eropple
Depending on the implementation, too, generics can be more friendly in other
ways. C# emits code for both value types and reference types when appropriate
and uses the correct paths as needed.

------
kodablah
For generics:

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/](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.

------
polymathist
Author here. I've been working on Fo part time for 4 months. Feel free to ask
me anything.

~~~
bakul
Did you consider parameterized packages? The idea is you can declare a set of
related objects/types/methods as well as have concrete type specific
initialization. E.g.

    
    
      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()

~~~
weberc2
I’ve seen this in ocaml and always thought it strange. I think it’s useful,
but binding it to a package seems odd since packages are units of code
distribution or some such.

~~~
pjmlp
It is not strange if you think of objects in OOP as modules/packages you can
inherit from.

------
jstimpfle
Nice work. But, serious question - When going with parameterized types I know
from Haskell how you always end up needing one more language extension and
always end up banging your head against the wall that separates types and
values a little more. At least if you're not a math genius, but probably even
then.

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).

~~~
ben509
Haskell is a proving ground, so it's picked up a number of ideas that never
quite panned out.

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.

~~~
jstimpfle
> 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.

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...](https://www.sanfoundry.com/c-tutorials-concept-pointer-
compatibility-programming/)

------
symlock
The only time I find I need generics is when I'm prototyping things and making
lots of changes. Node, Ruby, & Python are great for this. In fact, PHP's
associative array is really powerful/sloppy since it can be used as a list,
hash/dictionary, iterable, or object.

That aside, I don't think I've ever _really_ been bothered by the lack of
generics for actual work code.

~~~
marcrosoft
I've been working full time in Go since 2012. I've never had a use for
generics.

~~~
jjoonathan
How does Go statically type check collections, then? It seems like a pretty
common, desirable use-case to me.

I could understand not missing this if you're coming from a dynamically typed
environment, but understand that many people aren't.

~~~
anonymoushn
Go can statically type check hash maps, arrays, and channels. If you want some
other container type it must be written with a concrete element type in mind.

~~~
jjoonathan
Wait, really? The language designers gave themselves (effective) generics and
then kicked down the ladder afterwards so that other container implementers
couldn't follow? That's hilarious.

~~~
skj
Why does it need to be fair? Serious question.

~~~
jjoonathan
It doesn't, obviously, it's just hypocritical and patronizing.

------
dagss
Has anyone worked on Lisp-like macros in Go? Executing code at compile time to
emit AST and compile it.. That is what I feel is really missing. Even with
strictly a compile-time phase (no run-time macro evaluation) it could be quite
useful.

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.

~~~
kodablah
Completely agree. Go has parsing and type checking and what not as part of its
stdlib. However they choose a code generation approach which is a separate
step and often relies on code comments. Even if security fears of compile time
execution were allayed, the Go stewards are very unlikely to support altering
the very strict-yet-simple grammar for multiple reasons. Macros would be very
welcome, but they can't help improve the language if the language is
intentionally inflexible.

------
weberc2
This is suuuper cool. I've thought of doing something similar for a while, but
I tried approaching it from the "generate Go" angle. I also found the Go
syntax too tedious to write a parser for (because I've never written a parser
before, nor any kind of compiler, so the learning curve was too steep). So I
was just going to try to build a simple, expression-based language that
compiled to (and interoped with) Go. Unfortunately, I ran out of steam because
the learning curve was so steep.

~~~
nemo1618
One approach would be to use the existing Go parser packages and modify them
to suit your needs. Unfortunately (last time I checked) the standard library
packages for this (go/ast, go/types, etc.) differ from the actual packages
used by the Go compiler. But they might be close enough to suit your needs.

~~~
weberc2
I recall experimenting with that, but I had a hard time making them work
correctly (although I don't recall the details--might've been something to do
with their parser DSL or something). The compiler doesn't use the stdlib
because the compiler was originally implemented in C, and then they did a
mechanical C -> Go translation.

------
bigato
You really missed the opportunity here to call your language Foo

~~~
tunesmith
Or, "Go" but pronounced "Jo" for Generics.

~~~
munificent
"d͡ʒoʊ".

------
jcelerier
but go already has generics
[https://www.reddit.com/r/rust/comments/5penft/parallelizing_...](https://www.reddit.com/r/rust/comments/5penft/parallelizing_enjarify_in_go_and_rust/)

~~~
tomp
Go _actually_ has generics.

Just not for user-defined methods and types.

~~~
reificator
If you're referring to the map type, that is implemented in plain Go. (Though
it does import "unsafe") . There are no generics like you would see in other
languages.

[https://golang.org/src/runtime/hashmap.go](https://golang.org/src/runtime/hashmap.go)

[https://dave.cheney.net/2018/05/29/how-the-go-runtime-
implem...](https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-
maps-efficiently-without-generics)

~~~
nemo1618
Well, it's not quite fair to say it's "plain Go." The compiler converts
expressions like x, ok := m["foo"] into function calls. User-level code can't
define syntax sugar like that. Also, the hashmap implementation imports some
internal packages, so you can't just copy and paste it. (I would know, because
I mostly did copy and paste it for one of my own projects:
[https://github.com/lukechampine/randmap](https://github.com/lukechampine/randmap))

~~~
reificator
When discussing whether go has generics, "plain go" is a reasonable thing to
say.

Besides it's a compiler. It's going to take an input in format X and translate
it to an output in format Y. That's just what they do. Syntactic sugar debates
aside there's still no generics in there.

~~~
tomp
"generics" is mostly a matter of type system, not of implementation - as Java
and this Go implementation showcase. Indeed, "untyped" generics is trivially
implementable in Go, via `interface {}`, you just loes all the type safety
(and you have to write more verbose code).

My point is, Go _type system_ DOES support generics, but _only_ for
"primitive" types, not for user-defined types.

------
sagichmal
C++ style (compiler) or Java style (boxing) implementation?

~~~
en4bz
Looks more like C style macro token pasting with sugar.

------
gliese1337
Is there any support for bounded type variables? Or plans to add them?

~~~
polymathist
Yes, see
[https://news.ycombinator.com/item?id=17305563](https://news.ycombinator.com/item?id=17305563)

------
jeffrallen
Should be called mofo.

~~~
pjmlp
It would not be a good idea in Portuguese speaking countries (mold).

------
stunt
Nice work!

------
tonyedgecombe
Interesting, now all you need to do is add exceptions :)

~~~
reificator
Perhaps. But if Go itself adds exceptions they're going to have to catch a
MassDeveloperExodusException.

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.

[0]: [https://blog.golang.org/errors-are-
values](https://blog.golang.org/errors-are-values)

[1]:
[https://news.ycombinator.com/item?id=8877502](https://news.ycombinator.com/item?id=8877502)

~~~
HeyImAlex
Eh, rust’s result type has shown that you can do the same thing in way that is
more type safe and more ergonomic.

~~~
reificator
Sure, if I had proper sum types + pattern matching in go that'd be great. I'm
just saying that I'd prefer the status quo over adding exceptions
specifically.

~~~
HeyImAlex
Oh for sure.

------
gggguil
Shouldn't it have been called Ho ?

~~~
jrs95
And then we can take this, but put it on the JVM, and we end up with Jo!

~~~
kchr
Yo?

