Hacker News new | past | comments | ask | show | jobs | submit | polymathist's comments login

Thanks for sharing and I'm excited to see what comes next!



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


Hi there! I worked on something similar a while ago: a compile-to-Go superset that adds some generic functions/methods. You may have come across it: https://github.com/lukechampine/ply

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?


Ply looks really cool! I can certainly understand the appeal of that kind of approach in terms of simplicity, stability, and better interop.

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.


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


Just as additional info to others, this is how Ada, Modula-2 and Modula-3 generics kind of work.


I think parameterized packages are a great idea. It would be a light-weight way of getting just a bit of generic code into go.

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!


Parameterized packages are interesting but they come with their own problems. It can get tedious if I want to use, e.g., a stack with multiple different types. They also lose some of the flexibility that comes with scoping type parameters to any function/method/data structure.


On the other hand writing such packages becomes easier as only two syntax extensions are needed (package header and import). You can easily abstract a package by replacing a concrete type with a package level parameter. More importantly, there is no other mechanism to tie together a bunch of objects, functions, methods etc. to the same parameter types.


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.


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


Think of it as a package macro.


Any plans to add formal sum types (as opposed to interface hacks)?


Definitely something I'm thinking about. It's one of a handful of features that I have in my head but haven't created an issue for.


> haven't created an issue for

You have one now: https://github.com/albrow/fo/issues/7

:)


What does the implementation look like under the hood? What are the limitations of your approach (i.e. generic interfaces) and are they fundamental or just on the todo list?


The Fo compiler parses and type-checks Fo source code and then generates and outputs Go code. It generates a unique concrete type for each usage of a generic type. You can look at the examples directory of the repo to see what this output looks like: https://github.com/albrow/fo/tree/master/examples/box.

Generic interfaces are pretty fundamental IMO. It's something I plan to add to the language before the v1 release.



It's very likely monomorphizing the types, i.e. creating a unique type for each generic invocation.


Can you do ad-hoc or bounded polymorphism? What about recursive types?


Yes, I've already been thinking about constraining type parameters to an interface. Internally, the type-checker considers type parameters to have underlying type interface {}. A constraint on a type parameter should be as straightforward as changing it's underlying type to a specific named interface instead of an empty one. The only tricky bit I can see is thinking about how this would work with generic interfaces or whether we should allow other kinds of type constraints (e.g., union types).


If it considers parametric types to be interface {}, is that a kind of type erasure? I don't know much about Go, but it's my understanding that interface {} types have an extra layer of indirection and are essentially just pointers.


Did you consider incrementing the first letter instead of decrementing when choosing a name?


The name would be "Ho", maybe he did.


Does this have anything in common with Go besides the language syntax? Is it binary-compatible with Go?


It seems to compile ("transpile") to Go, and then invoke Go compiler on the generated source. So I guess that would make it also binary compatible?

https://github.com/albrow/fo/blob/master/main.go#L70


Briefly looking at source one can tell it just transforms Fo into Go and invokes go run


In a non-NN world, nothing will stop Comcast or ATT&T from also offering an "email only" plan. Except in their version of the plan, it costs $0/month if you use their own hosted email service. And when the grandkids come over they can watch Netflix for free because Netflix is paying the ISP to subsidize bandwidth.

Eliminating NN will make competition even worse for smaller and medium sized ISPs because the big ISPs have disproportional leverage to extract subsidies from media companies and make all sorts of deals. In some cases, the ISPs even own the media companies (for example, Comcast owns NBC). How can you possibly expect to compete with that?


What about the way Go handles errors today makes them not "strongly typed"?


> What about the way Go handles errors today makes them not "strongly typed"?

Error as type or error as values ? the std lib promotes error as values (i.e. check equality) instead of errors as type (i.e. check the type). Go error system WAS written with errors as value in mind. There is no point having errors in place of exceptions if errors were intended to be used as types (which they are not, as said previoulsy). Basically developers are implementing their own mediocre exception system on top of Go errors.

The error as value thing made sense in C 30 years ago, it doesn't in a language created less than 10 years ago.

There are a lot of C inspired patterns in Go that make the language half modern/ half dated in strange ways. That's fine when one comes from C though, that isn't when one comes from anything remotely modern. But I guess it's why Go is successful, it's basically C with garbage collection.


error is an interface type is Go, which means an error contains both a type and a value, or "error as type" and "error as value" are both true in Go.


I said that already. And that's not the problem at end. When you test an error, do you test against a value or a type in order to know what kind of error it is ?


You can test the interface. A type is just an interface around memory, albeit more consrained.


> You can test the interface. A type is just an interface around memory, albeit more consrained.

Wow, again, that's not the problem here. Errors in the standard libraries are defined at values. There is no point testing it as interfaces, it will not give you the nature of the error, since they are all defined with fmt.Errorf . Do you understand now the problem ? the problem is being consistant across codebases between errors are values and errors as types.


That's because single error instances are much cheaper than always creating a new instance of a given type. No need to create garbage for lots of common errors. Of course you could have a dedicated type plus a singleton implementation of it in Go. But what would be the advantage? Checking if err.(type) == io.EofType does not give you more information the only checking if err == io.Eof, as long as you don't store any error instance related information in it. Which makes sense for custom errors and which Go absolutely allows you to do.


> they are all defined with fmt.Errorf

No, an error implements the error interface. It means that it can be a value of any type that implements the constraint of having an Error method.


> No, an error implements the error interface. It means that it can be a value of any type that implements the constraint of having an Error method.

It doesn't matter what a value implements if you don't test its content by type. Std lib errors are meant to be tested by value, not by type. It has nothing to do with interfaces again. When you get a std lib error in order to know what error it is you compare it to an exported value, not its type. I don't know why you keep on insisting on interfaces that's not the core of the issue here.


We do have the same problem in Java and .NET though.

I have already used libraries that catch the exceptions internally and return some kind of error value instead. :(


Neither the Java std lib or the .NET std lib do that though, they don't declare an exception as a static constant you need to compare with. Because it's (rightfully so) considered bad coding. Exceptions give you some valuable informations like stack traces. Go errors gives you nothing. They are inferior to Exceptions and a relic of C programming patterns.


The compiler provides you no help at all with them, and no syntax that makes error conditions and handling separate. It also mixes application logic and recovery logic.

Basically everything that's problematic with returning a status int in C, but all new, hip, and backed up by Rob Pike's pseudointellectual bullshit and a bunch of Silicon Valley 20somethings.

They could at least, you know, have an Either type or something. Anything?


>It also mixes application logic and recovery logic.

When did this separation become law ? What if the "application logic" requires recovery ?

>They could at least, you know, have an Either type or something

(int64, error) in func ParseInt() (int64, error) is your Either type. And checking if you got the "left or the right side of the Either" is IMHO much shorter and clearer than in Scala.

https://golang.org/pkg/strconv/#ParseInt

http://www.scala-lang.org/api/rc2/scala/Either.html

>backed up by Rob Pike's pseudointellectual bullshit and a bunch of Silicon Valley 20somethings

Why the ad hominems ?


cringe.

How ecactly is having to check twice the amount of cases an improvement (note btw, that checking for Left/Right is doing it wrong)?


You only have to check 1 case in Go (`if err != nil { ... }`). What language lets you check half a case?


  (int64, error)
gives you exactly four possibilities.

Either gives you exactly the two you want.


Please feel free to enumerate them.


  (value, no error)
  (value, error)
  (no value, error)
  (no value, no error)


The convention is that if the err == nil then the value is not nil. The exceptions to this rule are very few and usually specified in the documentation. Normally you only have to check for error.


The fact that it's a convention, and not a compiler error is the entire issue at debate here.


There is no "no value" representation for int64. The only two cases are "<int64>, nil" or "<int64>, <error>".


Yes, but at least they are making the tools we may eventually have to rely on due to market pressure (Docker, K8s, ...) in Go instead of C.


Don't forget Brian Kernighan and Ken Thompson!


Typed vs untyped nil means that, in practice, all functions must return an error of type 'error', and force clients to downcast at runtime:

https://golang.org/doc/faq#nil_error

Of course, there are ways around this such as returning a struct, but then that's no longer compatible with the error interface.


> Of course, there are ways around this such as returning a struct, but then that's no longer compatible with the error interface.

Which your struct can easily fulfill. That's the beauty of Go interfaces.


The linked FAQ specifically talks about returning pointers to structs that fulfil the 'error' interface and why it's a bad idea.


It's not that it's a bad idea, just that because of the "nil interface" absurdity it can happen if you accidentally mix concretely typed variables and interfaces, as in the example.

This is perfectly valid and doesn't cause the nil issue:

    return someErrorStruct{}
...where someErrorStruct is a strict that implements the "error" interface. Using structs for errors is fine, and is in fact generally preferable to singletons like io.EOF, which can (by their very nature) never be extended with more data about the error.


> absurdity

There's nothing absurd about it; interfaces are a reference type. If you have a reference to a reference, then checking the "outer" reference for nil doesn't tell you anything about the nullity of the "inner" reference. The advice is just a special case of "don't needlessly use pointers-to-pointers".


Go chose to rely heavily on nil pointers, which is a design mistake (see Tony Hoare's apology for inventing it). The resultant tension between interfaces and nils is, in my opinion, an absurd side effect that cannot be explained away as anything except an ugly wart. We should have something better than this in 2016.

I say this as someone who uses Go daily for my work and mostly likes it (despite the many warts).


I don't especially love nil either, but people make too big a deal of it. The only arguments against it are hypothetical scenarios, anecdotal examples, and appeals to Hoare's authority. While there's probably a more ergonomic design, there's no substantial evidence that nil is the catastrophe it's made out to be. Using terms like "absurdity" and "catastrophe" seems overly dramatic without some decent evidence.


I don't think I'm being overly dramatic, actually. I deal with nil oversights on a daily basis during development, and I would say that it is is the main source of unexpected crashes in anything. It equally applies to other languages such as Ruby and Python.

It's exacerbated by the fact that Go chose to make things like maps, slices and channels pointers, too. It has an excellent philosophy about zero values (so you can't get nil strings, even though they are pointers internally), yet goofed when it came to something as intuitive as "var foo []string", and claimed this behaviour to be a feature. The (nil, nil) interface is just icing on a crappy cake.

The fact that such a new language as Go doesn't have a way to express missing values safely should be disappointing to developers.


By a wide margin, the biggest production issues I see are index out of bounds or key errors (regardless of language). When I'm treating `nil` as a valid state for some object, I take extra care to test its permutations, and uninitialized maps/interfaces/etc are quickly discovered during testing (every test has to initialize, so this logic is well-covered).

> The (nil, nil) interface is just icing on a crappy cake.

The same problem exists with languages without nil. For example, if you choose to do the stupid thing and return Option<Option<error>> when you only need Option<error>, then checkout the outer Option<> for None is not going to magically guarantee that the inner Option<> is not None.

> It has an excellent philosophy about zero values (so you can't get nil strings, even though they are pointers internally), yet goofed when it came to something as intuitive as "var foo []string", and claimed this behaviour to be a feature.

What are you talking about? nil slices are every bit as safe as an empty slice or an empty string (which is just an immutable empty slice).

> The fact that such a new language as Go doesn't have a way to express missing values safely should be disappointing to developers.

I agree, but I'm mildly annoyed by it, but as it's the least of all of my problems, I find words like "absurdity" to be too heavy-handed.


Nil slices do cause problems. One when it's an aliased type that satisfies an interface (again with the nil interfaces!). Another is that it leads to inconsistencies: json.Marshal(([]string)nil) returns "null", for example, not "[]". Yet another annoyance caused by nils (including nil slice) is that reflect.Value becomes a lot more byzantine than it ought to have been, requiring careful use of IsValid(), Kind checks etc. when you want to deal with something that is either an interface or something else.

As for Option: Not sure how that's an argument. And anyway, a language with real sum types will never allow going down a branch that doesn't receive a correctly matched value.


> One when it's an aliased type that satisfies an interface (again with the nil interfaces!)

It sounds like you're again confusing nil interfaces with an interface holding a nil value (in particular, there is no way to get a nil interface from a nil slice). Here's an example that demonstrates nil slices do not cause problems: https://play.golang.org/p/tSA_otqg3-

> Another is that it leads to inconsistencies: json.Marshal(([]string)nil) returns "null", for example, not "[]".

1. This is unrelated to the language; it's the behavior implemented by the JSON library

2. This behavior is correct; a nil slice is not the same as an empty slice:

        fmt.Println([]int(nil) == nil) // true
        fmt.Println([]int{} == nil)    // false
3. This behavior is consistent: https://play.golang.org/p/NjdO0boHln

> As for Option: Not sure how that's an argument.

You posited that Go's nils are bad because you can satisfy an error interface with a pointer type, and then when you return (*ConcreteType)(nil), your error handling code executes. The problem here is unrelated to nil or to interfaces; the problem is that you're nesting one binary type inside another (binary in the literal sense of having two states, either nil or not nil in this case). You would have the same problem in Rust had you done Result<Foo, Option<ConcreteError>> or (in the case of a single return value) Option<Option<ConcreteError>>. You would fix the problem in either languages by recognizing that you only have 2 states (error or not error) and removing the nesting (in Rust: Result<Foo, Error> or Option<Error>; in Go `return ConcreteType{}`).

> And anyway, a language with real sum types will never allow going down a branch that doesn't receive a correctly matched value.

I agree, and it would be nice if Go had this, but this is also not a very significant problem--this problem is blown way out of proportion.


The return type would still be the error interface. If you want more information than the error interface you can just use a type switch/assertion.


Wow! I think it's worth emphasizing that in the given example, the time taken to install all dependencies went from 8m10s to 7.5s. Seems like a huge productivity boon for any teams working with Node.


Is there really no way to get closer to normal performance for networking in Virtual Box?

Also, why not use Linux VPS? Also, take a look at Docker as an alternative to Vagrant/Chef. Also, take a look at pnpm.


Infrastructure guy at Plaid here.

> Is there really no way to get closer to normal performance for networking in Virtual Box?

Unfortunately not! It takes many different tweaks, tunes, and hacks to squeeze performance out of VirtualBox. To make matters worse, NPM as a tool is pretty slow in general -- in fact it's become somewhat of a joke in the community[1]. npmserve solves this by offloading the work to a remote server, so the npm install process becomes as simple as fetching and expanding a tarball.

> Also, why not use Linux VPS?

This is something we may move to if performance becomes an even bigger issue. We use vagrant-aws[2] in our integration testing environment to achieve a similar effect.

> Also, take a look at Docker as an alternative to Vagrant/Chef.

We don't currently use Docker (in this use-case at least). It's definitely an option, though! Using Chef here is pretty handy, insofar as we can use the same infrastructure code we use on production, so our developer environments are as close to production as possible.

> Also, take a look at pnpm.

Nice, this is the first I've heard of this project. We looked at ied[3], but we weren't comfortable moving over to it without also moving our production infrastructure.

    [1]: https://www.npmjs.com/package/nplaym
    [2]: https://github.com/mitchellh/vagrant-aws
    [3]: https://github.com/alexanderGugel/ied


The Moto X and its successors have had this feature for a while. Your phone can be sitting on the other side of the room, completely off, and you can say a certain catch phrase to activate Google Now.


I can usually activate Google Now from a few feet away on my Moto X (2013) but it can't understand what I'm asking after that. I have to get much closer for it to work.


Makes sense I guess. The "listen for the trigger" bit is done locally on those Motos (I've got the 2014 variant) but really all it needs to do is keep an "ear" open for that one phrase so it's probably got a better margin, only helped by local processing. The bit of audio that comprises the query has to get compressed and interpreted on the server side (not to mention the fact that it consists of a much wider variety of possible content to be parsed) so that can't help. I guess a dedicated chip that's just constantly asking "did anyone say 'OK Google?" has an easier job than a remote process asking "wtf does that fuzzy bit of compressed audio mean?"

(obviously being silly and oversimplifying but same idea)


There is a small discrepancy in the article:

> Go supports the := assignment operator, which works like this ... All this does is look at the return type of bar(), and set the type of foo to that.

This is a misunderstanding of the := operator and how type inference works in Go. If you look at the language spec (https://golang.org/ref/spec#Short_variable_declarations), you can see that the := operator is nothing more than a shorthand variable declaration.

x := "foo"

is shorthand for (and functionally equivalent to)

var x = "foo"

Type inference is orthogonal to the := operator and is a little more powerful than the author implies. For example, Go is able to infer the type of literals, including inferring the type of a numeric literal based on the presence of a decimal point or the symbol for the imaginary number, i (complex and imaginary numbers have native support). It can also infer the type when you assign a variable to an element in array, slice, or map or when you receive from a channel. Of course it can also infer types for values inside of a struct, map, slice, or array and for keys in maps. The only obvious difference I see between type inference in Go and Rust or Haskell is that in Go you must always define the return types for functions, but there may be more differences I am unaware of. See this playground example for a demo of a few different ways that types can be inferred in Go: http://play.golang.org/p/8ep340vLky.

(edit: formatting)


From the article: "Functional testing of the isolated limbs showed that electrical stimulation of muscle fibers caused them to contract with a strength 80 percent of what would be seen in newborn animals. "


Ah. Thanks.


The most realistic virtual reality I have ever participated in was the Duke immersive Virtual Environment (DiVE). There is a program that simulates a kitchen. There are cereal boxes, silverware, etc that you can pick up and even throw across the room. When I opened the refrigerator door, I instinctively moved my body out of the way. The crazy thing is that it's not even the best tech out there. The resolution was crappy and the physics were a little bit off. But if you stopped actively paying attention to the details, even for just a moment, it was enough to make some part of your brain think it's real. VR doesn't have to much better (if at all) in order to be really immersive.

If you're at Duke or somewhere nearby I highly recommend checking it out. They have visiting hours fairly frequently. http://virtualreality.duke.edu/


How did it compare to Oculus Rift? I've never experienced either but I'm curious....


I've tried the Oculus DK1 but not the DK2. They are similar in terms of resolution and tracking accuracy. There are two main differences. The DiVE is a cube-shaped room with six projected walls. So you can fit multiple people into the simulation at once (only one person gets head tracking and the proper perspective). The DiVE also had a "wand" which you could use to interact with your environment. I would bet there are similar peripherals for use with an Oculus Rift, but I haven't tried them.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: