Hacker News new | ask | show | jobs
by johnfn 944 days ago
> 1. The typesystem will be sound, ML-like, and so simple that any code that doesn't interact with external data will not need _any_ type annotations.

I've tried using languages with this promise, such as Haskell, and also spent a lot of time with TypeScript, which makes a different set of tradeoffs, and I feel like I've spent enough time on both to know this is the wrong tradeoff to make. It sounds flashy to be able to say that no type annotations are necessary, but in practice what it ends up meaning is that you end up tracking down errors in the wrong parts of your code because the compiler can't figure out how to reconcile problems.

e.g., you have function A incorrectly call function B. How does the compiler know if A has the wrong arguments, or B has the wrong signature? It can't! I know that's a toy example, but it really does lead to a lot of real-world frustration. Sometimes the type errors are very far away from where the actual issues are, and it can lead to a lot of frustration and wasted time.

The TS approach of "please at least annotate all your function signatures" isn't nearly as flashy, but it strikes a much better utilitarian balance.

5 comments

Oh I totally agree that it's a good idea to annotate top-level functions even if you don't have to, and better compiler error messages is one of the benefits of doing that. Personally I basically always choose to annotate them except in the very specific situation of writing beginner introductory materials, when it's not a given that the reader actually knows how to read the annotations yet.

One of the practical benefits of having full inference is that these signatures can be inferred and then correctly generated by an editor. Like I can write the implementation of my function, and then tell my editor to generate a type annotation, and it can always generate a correct annotation.

That saves me time whenever I'm writing the implementation first (I often write the annotation first, but not always), even if I end up wanting to massage the generated annotation stylistically. And unlike having Copilot generate an annotation, the type inference system knows the actual correct type and doesn't hallucinate.

To me, the main benefits of type inference at the top level are that they offer beginners a more gradual introduction to the type system, and that they offer experts a way to save time through tooling.

> Personally I basically always choose to annotate them except in the very specific situation of writing beginner introductory materials, when it's not a given that the reader actually knows how to read the annotations yet.

Having just gone through your tutorial (albeit not yet with a computer in hand, that's the next step), might I suggest putting those annotations in anyway? I suspect most of the people reading the tutorial will already be programmers, and one of the most useful things I've found when reading tutorials and guides is when the code examples look as much like real code as possible. When I'm reading that initial documentation, I'm not just trying to figure out what's different about this language from other languages, but I also want a sense of what it looks like to actually read and write idiomatic code in that language - what sort of formatting conventions are there, what do variable or type names typically look like, are there any common idioms, etc. Ultimately, my goal is to get up to speed and begin writing productive code as quickly as possible, so seeing type annotations everywhere is a sign to me that type annotations are good practice and something to get used to.

In general, I found the tutorial a bit too aimed towards someone learning their first programming language, which is a demographic that I suspect are unlikely to be using Roc any time soon! Even if they are a demographic you're targeting, I wonder if they'd be better served by a separate explicit "Roc as a first programming language" document that goes through the basics. Then in the main document, do some repl stuff at the beginning, but move quickly on to what regular development work might look like - starting a new project, writing functions with types, using tasks/effects, adding tests, dependencies, etc.

With all that criticism out of the way (sorry!) I do want to say that I love pretty much everything that I've seen so far, especially the focus on practical usage. It's great to see an example CLI and an example web server right on the home page - I feel like these are often left as complete afterthoughts for these sorts of languages, but they're the sort of real-world programs that dominate software in the industry.

I'm also really excited to have a play around with the effects/tasks system. It looks really powerful, but not too complicated to actually use as a base abstraction.

And I agree that having powerful type interface can be a great tool, even if you supplement it with type annotations for the sake of explicitness. Is there an explicit type hole mechanism as well, for getting the compiler to spit out the types it expects?

Thanks for the kind words, and thanks for the feedback!

> Is there an explicit type hole mechanism as well, for getting the compiler to spit out the types it expects?

Not currently, although you can write `_` for any part of a type annotation that you don't want to bother annotating (which means that part of the type will be inferred as if you hadn't written any annotation for it), and we either have or want to have "hover to see the type" in editor extensions.

The Haskell approach is "annotate all top level declarations" (even if not exported) and OCaml has module signatures. But both (and Roc) don't make up new "types" like Typescript does.
I think a general 'explicit > implicit' priority is good enough to cover most cases - a written signature takes precedence over an inferred one. The compiler can also simply emit errors at both sites and let the writer figure it out.

I usually think of writing explicit type annotations as 'pinning' the type in situations where things are inferred/generic by default.

> The TS approach of "please at least annotate all your function signatures"

The whole signature or just the parameters? I thought typescript is pretty chill about inferring the return type on its own.

It usually is as long as you don't do anything recursive (and a few select polymorphic instances), I can see people getting bitten by it.

Having written inferring compilers and used C++ extensively I appreciate the workings, but I can also see people getting stuck with it.

Rust takes the "at least annotate all your function signatures" approach as well. It's essential for making borrow-checking tractable (for both the compiler and the programmer).
Rust has the "you must annotate your functions signatures, because there is no global type inference possible" approach.
It's a carefully chosen, deliberate design decision [1]. Interface boundaries should be stable.

[1] https://steveklabnik.com/writing/rusts-golden-rule