Hacker News new | past | comments | ask | show | jobs | submit login
Open-sourcing Sorbet: a fast, powerful type checker for Ruby (sorbet.org)
617 points by abhorrence on June 20, 2019 | hide | past | favorite | 268 comments



It's been funny to watch how more and more static type systems are getting bolted on to dynamically typed languages in recent years.

Typescript (with stellar adoption), native type annotation support in Python, Sorbet, PHP 7, Elixir + Dialyzer, ...

I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.

I guess in part because it's a big challenge to come up with a coherent type system that allows this, the bifurcation in the ecosystem, and often a somewhat leaky abstraction. Eg in Typescript you will often still run into bugs caused by malformed JSON that doesn't fit the type declaration, badly or insufficiently typed third party libraries, ....

Google's Dart is the only recent, somewhat popular language (only due to Flutter) that allows this natively - that I can think of right now.

I do think such a language would be very beneficial for the current landscape though, and projects like this show there is a clear need.

Edit: just remembered another option: Crystal. Also Julia, as pointed out below.


For whatever it's worth and without wanting to start a language war (I like Python just fine), I think Python/Ruby-style typing is a false economy for prototyping. There are a lot of things that make Go slower to write than Ruby, but mandatory typing isn't one of them. Rather, Ruby's total lack of typing makes it harder to write: you effectively end up having to write unit tests just to catch typos.

I wonder whether the perception that type safety slows down Ruby (or ES6) development comes from the fact that the type systems are bolted on after the fact.


I would say that dynamically-typed languages are great to prototype as they do not emphasize on being correct too much.

Being correct from day one will cause too much unnecessary friction. You (usually) don't know the entire program architecture until you make a full prototype, and even if you have a plan, there will always be some part where some unexpected consequences force you to rearchitect some parts. And since you don't know the entire program architecture you architect your program bottom-up, and most of the dynamically-typed languages allow an interactive development environment at a flexibility that typed languages can't provide.

Think about developing Python inside a Jupyter notebook, or Common Lisp inside a REPL. You turn on a REPL, open up a file, write a function, send the function to the REPL, test the function you just wrote, and when I find a mistake I can just redefine any function I would like to change. This process allows fixing mistakes on-the-fly. Typed languages don't allow this (even ones that have a so-called REPL) at this flexibility, (since they emphasize on being correct all the time), and cause too much unnecessary friction while prototyping.

Thus a need for a dynamically-typed language that can enforce types after prototyping.


Yes, it's this flexibility that allows you to experiment. Static typing proponents often say, since a type-error means that the code doesn't make sense, why would you ever want to run code that doesn't typecheck? But I find that when I'm experimenting, I want to do this all the time.

I ran into this recently when I was working with Rust. And don't get me wrong; I love Rust. But I was experimenting with a new way of doing things in my codebase and was adding a new kind of statistical analysis, and I wanted to run a test case to see what the result was... However, I couldn't because the compiler's typechecking refused to even compile my code, when I knew for a fact that the type errors had nothing to do with the code path of my test case.

I get that that's the whole point of static checking. I can change a large codebase and know exactly when/where I've broken something even if I'm not familiar with all the code, and it will barf at me. But the fact that I can outsmart it, even some of the time, leads me to believe that there will always be a place for dynamic languages.

If 99% of my code has type errors, but there is one code path that is true, who is the compiler to say that that isn't the one code path that I want to take? When experimenting, I build my code dozens of times. The vast majority of the time, I am the only one who consumes the build result. Once in a rare while, I will reach a steady state and build a release. It's really only then that I want the compiler to barf at me and refuse a broken build.

I must not be the only person on the planet who wants to do this sort of thing.


The eclipse java compiler can be instructed to substitute (most) compile errors with runtime exceptions for those intermediate builds. You still get all the compile time error messages, but the code is ready to run, right up to the point where the compiler complained.


That's a cool feature. I'd like to see this in other languages and become the norm.


Yes, eclipse java compiler definitely stretches the definition of possible.


that's clever, but how does it work?


It doesn't when the error is in something clever you did with the type system (e.g. wildly interdependent generics pretending to be scala).

But for many simpler cases (e.g. the majority of idiomatic java), it can be as simple as substituting an offending imperative line with a throw RuntimeException statement that parrots the compiler error message. I'm sue that they are doing a lot more than that.


It looks like what you want is type-mismatch as warnings. I'm not sure it would be viable though, because dynamic languages can throw an exception in this case, while static languages will segfault. Unless the compiler inserts instrumentation code where it saw a type mismatch?


Yea, that seems like a use-case I’ve felt like I wanted before. TypeScript supports this, as the JS output can be produced even if type checking fails, though in practice I have it turned off so that I don’t accidentally end up with type errors I’ve not fixed.


Statically typed languages that employ type inference (eg. Hindley–Milner) do exactly this, ie. you just write your function definition and the inference engine figures out its type. Doing this in the repl gives you comparable flow to doing it in dynamic langs but with full type safety.


If you are thinking Lisp is not typed, that is not true. I am caught with type errors all the time.


I think about this a lot, as someone who loves Ruby and loves Rust. I think it’s because we do a bad job of looking at the total costs. It feels like you can knock something out really quickly in Ruby. And in some sense, you can! But the time you spend debugging later doesn’t get factored into the way that it feels when you’re just cranking out features.


Yes, and, just to keep this clear: that debugging (or unit-testing-for-typos) time is coming out of your prototyping budget. Your Ruby program will fail to run for stupid reasons the first time you run it, which is not the experience Java and Go and Rust programmers have. I just remember when I first picked up Go, after about 5 years writing Ruby exclusively, how amazing it was that all my Go programs "worked" the first time I ran them. Obviously, they had bugs. But they didn't blow up for stupid reasons.

I'd been a C/C++ programmer for about 10 years! I guess I just forgot that compilers can catch typos if you let them.


> Your Ruby program will fail to run for stupid reasons the first time you run it, which is not the experience Java and Go and Rust programmers have.

I know exactly the feeling, but I don't think it's just the types that give it but that it's compiled. I similarly moved on from ruby after 5 years but to Elixir and was similarly surprised to (re-) learn that a simple compile step catches so many braindead typos, misnamed variables, not imported function calls, and the like.

Types go above and beyond this, but I think there's a huge step up just going from interpreted to compiled.


I know a lot of people in the Ruby community tend to believe in using text editors with limited (or no) semantic understanding of the code. I've found that using, for instance, JetBrains Rubymine tends to make typos and related bugs apparent as they happen. Not to mention providing autocomplete that usually feels like using a statically typed language.


New to Ruby, veteran Java/C++, while I'm liking what RubyMine adds, it's nowhere near what I get from a simple language with a simple type system like Java.


The ability to refactor, for one.


I think this is a Ruby thing more than a dynamic languages thing. (Not exclusively a Ruby thing mind; Javascript code is also always full of weird edge cases and gotchas.)

I wouldn’t necessarily recommend it, but I have written 500-line Python programs without executing/testing any functions along the way, that “just worked” the first time I ran them with no serious bugs. And not because I am especially clever or have an amazing memory or unusual attention to detail, but just because the code is easy to make clear and straightforward.

(But this was just with basic data types and a few standard library modules; dealing with other people’s poorly documented / confusing APIs makes this gets a lot harder.)


You never had a go program fail to compile, only to fix the problem then try to compile again? Replace 'run' with 'compile' and you could still type a bunch of go code and have to make changes before you deploy or no?


Tooling makes this argument moot. I use Ruby and TypeScript, both with VSCode. The experiences are vastly different. Ruby is slow and difficult to work with, and TypeScript is a breeze. I don’t have to wait for my TypeScript to compile before it tells me about the typo I have, the editor tells me immediately after typing the word.


It's odd to complain about the first run not working, when the first run is significantly earlier. I'd also say the interactivity of Pry and binding.pry more than compensates for anything lost by not having a compilation step.


I’d argue that type checking combined with editor auto completion leads to faster development.

The feedback loop is within your editor, that’s a step before the app console!


100% going from Java+IntelliJ to Ruby+VSCode was shocking. In regards to tooling, the developer experience is way behind with Ruby. Sure there’s a lot more boilerplate to look at, but with tolls you don’t actually end up writing much. And then you get refactoring tools that are actually super useful and robust.


Prototyping in Java is as fast as prototyping in Ruby once you understand how to use the tools equivalent to Pry in Java, and standard types instead of rolling your own types. JIT isso fast today the 20ms required to JIT what you type is immaterial.


More so now with better type inference and repl integration.


What REPL do you use on the JVM? I've added JRuby to Java projects in the past just to get a REPL.


The one that comes with it, starting with Java 9, jshell.

Before that, there was the Rhino and Nashorn ones, which although JavaScript based, did the job.


Not strictly Java, but Groovy is a great environment to prototype on the JVM. Prototyping was lightning fast and turning it into Java code was pretty quick too. There’s great IDE support for it too (IntelliJ’s Groovy scratches).

These days Kotlin is also pretty good. Perhaps not quite as fast to prototype but there’s little need for converting afterwards.


Yes, add an environment like BeakerX [1] (essentially, Jupyter notebooks) and it's quite amazingly good to figure out your approach even if you intend to write Java.

[1] http://beakerx.com/


This.

I inherited legacy Python projects at work and I'm shocked by the time I'm wasting fighting with the lack of types. Most of the time I have no idea (and my IDE neither) what methods and properties are available on variables and function parameters. And what's the most insane to me: I'm sitting next to people with LOT of experience in Python and I see them losing as much time as me when maintaining and debugging some basic piece of code they have written not even 2 weeks ago.


> Most of the time I have no idea (and my IDE neither) what methods and properties are available on variables and function parameters.

If I am really this lost with a piece of code in Python or Ruby, I usually start an integration test with an added line to call the debugger.

With introspection you can then easily see what methods a given object has.

> And what's the most insane to me: I'm sitting next to people with LOT of experience in Python and I see them losing as much time as me when maintaining and debugging some basic piece of code they have written not even 2 weeks ago.

That is insane. What kind of code coverage do you have and what kind of tests?

It is true that you have to write more tests in a dynamic language than in a static typed language but as tests often convey the information of what was intended to be done much better than code, I don't consider this a drawback.

But if the tests are lacking, you are at a much greater loss than with static typed languages.


> With introspection you can then easily see what methods a given object has.

With VS and C#, I can hit ctrl+space and get a list of absolutely everything that's in the contextually relevant public API. So much faster than introspection. If I'm using an object initializer to instantiate something, I can hit ctrl+space inside the object initializer to get a list of all the available public properties -- no typing the names required. No having to remember the order of arguments of methods, or even their types. Autocomplete means I can usually get away with somewhere between zero and three keypresses to get exactly what I want highlighted in the intellisense.

Forget efficiency via vim/emacs keybindings. Static typing + a strong intellisense/intellisense clone lets me code literally at the speed of my thoughts most of the time. I end up feeling like I'm stuck in molasses the few times I have to write some JS.

> It is true that you have to write more tests in a dynamic language than in a static typed language but as tests often convey the information of what was intended to be done much better than code, I don't consider this a drawback.

I understand your point, but for me, the extra tests I write in dynamic langs just end up making me feel like I've duplicated the work of the static checker in other languages, and in a non-reusable way.


I am a ruby developer who tried rust. I don't think its the language thats the issue, its the libraries and tooling. I tried rust with rocket and I spent a week trying to get the rust app to connect to postgres because of having to deal with so many things like connection pools and setting up the ORM. I eventually just gave up and used rails where you just tell it the username and password for your database and you are pretty much good to go.


Ouch, that seems like a long time. It is true that Rust isn’t as integrated here. I’d love a rails too. Which ORM did you try?


You actually spent 40+ hours trying to connect to a db with rust? Please tell me that's a gross exageration...


It was a free time project so I probably spent about 4-5 hours spread over a week or two


This is an advantage depending on the projects and organisation.

For instance if features need to be demonstrated or prototyped on a test env. for some time before they are green lighted to be fully baked, loosely typed languages are ideal. You get the speed of prototyping, and all the heavy cost comes after the main parts have been validated.

It's less painful than having to do prototypes after prototypes in a more rigid language.

But of course this advantage disappears when working more in small waterfall iterations, where everything is basically set in stone from the start.


I've never quite understood the notion that languages like Python and Ruby are amenable to fast prototyping. Ultimately, you wind up just having to carry complicated type information in your head rather than write it all down in your code, all whilst the compiler utterly fails to help you in any way in case your memory ain't what it used to be.

I'm sure those languages and their ilk have advantages for prototyping, but I agree, mandatory typing in other languages isn't a burden. If you already have to reason about what arguments are acceptable in functions, what objects can receive what messages and what those messages should contain, you already have typing — just inefficiently stored in short term memory.

Those who are the biggest proponents of these languages as useful for prototyping are also not the ones who rewrite their code in type-safe languages, so being able to add type annotations for the sake of their colleagues who eventually have to turn these prototypes into code upon which a team can collaborate can only be a useful thing.


>I've never quite understood the notion that languages like Python and Ruby are amenable to fast prototyping.

I've thought about this for a while and I think the "fast prototyping" reputation is an accidental feature that sticks because of history.

Python and Ruby were just better languages than Java 1.4, C++, Perl and PHP were at the time. They were so much better for not only prototyping, but writing - it was easier to get something working and iterate in Python than it was in Java. And not only were they better, they were better at the right time when the internet exploded.

Now, it largely feels like languages like Java and newer languages like Go have caught up, but Python and friends still enjoy the feature of "fast prototyping." Now I think this is because since they had gotten so popular, they now have a massive ecosystem of libraries and idioms that makes it so easy build applications.


Java and Go today are much better to work with than Java was in, say, the early 2000s, but to realize the productivity boost of a dynamic language within a statically typechecked language, you need type inference and a REPL.

You still don't get this with Java or Go, but you certainly do with ML-like languages (Haskell, SML, OCaml, F# etc.)

A lot of Haskell developers will tell you that writing Haskell feels a lot like a functional Python, and they use Haskell for scripting as well as application development.


Until Java gets data classes it will make you miserable. Lambdas are clunky in Java at best. "Much better to work with" than Java 1.3 is setting a pretty low bar.


> Until Java gets data classes it will make you miserable.

https://projectlombok.org/features/Data


Java has repl support since Java 9 and before that there were interactive sheets like Eclipse scratch pad, which alongside incremental compilation were already quite good.

And while Java's type inference is not the same has having HM around, it is already q89te friendly.


I think ruby has a reputation for "fast prototyping" for a few reasons

1) readable (somewhat english-like syntax) 2) terse (short and to the point, no extra setup code needed) 3) meta-programming/DSLs

It still remains unbeatable for readability and terseness..


Yes, and it's for this reason that adding a type system will to it will alienate a lot of its users. You can say "optional" or "gradual" as much as you like but adding types will split the community which is the last thing Ruby needs right now. The trouble with tech is we just can't let something be and stop fiddling with it. Ruby is fine just as it is.


> Ruby is fine just as it is.

Which Ruby do you mean? 1.8, 1.9, or a more current one?


Don't forget 4) no compilation/ultra fast test cycle and 5) REPL (IRB or Pry)


This response makes sense to me.


This is a straw man argument. Sure, if your types are simple like int, string, or float, you might as well write them down rather than keep them in your head. They double as documentation. However, the other cases are where it matters.

1. When the types are complicated enough, people no longer read them. They skim over them and don't bother to ever "load" them into short term memory. Sometimes it makes sense to make a type alias for the subcomponents, but many times they're one-off types and it wouldn't make sense. As the person writing the code, figuring out what the types actually are and writing them down doesn't actually help anyone. It's just busy work that the compiler or interpreter could have inferred. For example, if I have a hash nested in an array, nested in a set, you can access them all with brackets chained together. I don't actually need to know the full type in order to use it. So why bother figuring out the exact type that it is? Which brings us to...

2. When the types are abstract, with type variables and type constraints, someone has to put in the work to come up with the most sensible type signature. Sometimes that is the most general type that the code satisfies (using a Rust trait, Haskell typeclass, or a Java interface), but sometimes it's not. It really depends on the context. Should I abstract over this type and introduce a type parameter to the function? Or should the type parameter be for the entire struct/class? These are oftentimes complicated questions, with no clear answer. Weighing your options and choosing is real work. If I now say that this function will accept different sized ints, I may no longer be able to just call i64::something(). I literally need to change how I write my code or even import a new helper library to do it. (I have had to do that in Rust. And I love Rust, BTW.)

OTOH, in a dynamic language, all of that work is just gone.

What I'm saying is that types for a compiler must be precise and general. But types that you keep in short term memory are oftentimes vague (something that I can index, or some kind of number) or specific to a test case. And it doesn't matter.


> This is a straw man argument.

That would imply ill intent on my part, and there was nothing of the sort. All I said was that I couldn't see the sense in certain things and, without further explanation, I didn't. Other comments helpfully pointed out the context for why the assertion is so often made and I was happy with it.

On the other hand, I think I'd need to see some hard evidence for the following assertions:

> When the types are complicated enough, people no longer read them

What is a complicated type?

> As the person writing the code, figuring out what the types actually are and writing them down doesn't actually help anyone.

Citation needed. I find static typing tremendously helpful in my own code, especially if it's code I haven't worked on in a long while.

> It's just busy work that the compiler or interpreter could have inferred

… such that I only notice errors when the code is running, not beforehand. Without type annotations, interpreted languages like Python and Ruby are happy to let wrong types in function calls and message passes slide — which is why frameworks like Sorbet exist, to help eliminate that class of error.

> 2.

All of that is language-specific, surely, not really anything to do with static or dynamic typing. That whole mechanism is quite straight-forward and has a well-prescribed mechanism in Swift, for instance, with protocols, extensions, and conditional conformance.

> OTOH, in a dynamic language, all of that work is just gone.

I mean, it's not just gone. There are trade-offs, of course there are; but the assertion that the work is gone also implies the potential associated errors are gone seems a bit ingenuous to my eyes.

Yeah, types can be a bit constraining — but at the same time, if you reason about them properly and according to the language's prescribed mechanism rather than fight them, it can and does become second nature. At that point, using types becomes as simple for some as not using types is for others.


A better example of a complicated type that no one ever loads into their short term memory is the type of a parsed JSON object from an API response. No one ever bothers to figure out or reason about the type. They have documentation or an example response that they just navigate.

If you want to advocate something like protobufs, that's a separate issue from static vs. dynamic typing. The point is that in a dynamic language you literally _don't have to_ reason about many types at all. Or you do it in a completely different way that doesn't involve holding the type in your short term memory.


In most statically typed languages you end up with a class/struct type that encapsulates the API response you are either generating or parsing. When you don’t have this, you have to either assume or defensively program around the presence or absence of certain fields in certain contexts. “Everything is a hash/dict/JSobject” doesn’t absolve you of having to reason about which fields an API payload contains, it just forces you to do it all by hand rather than simply defining a data type and telling your serializer/deserializer “this is what I want” (and then operating on a strongly typed object rather than an arbitrary hash table).


I am an "expert" in static type systems (I'm familiar with Java, Scala, OCaml, Haskell, Rust, Go, TypeScript, C++ ... and keep up to date with latest type systems research like 1ML, MLsub, Liquid Haskell, ...), but I have a really hard time imagining how one would develop a statically typed library that would even approximate the usefulness and convenience (for rapid prototyping and interactive data analysis) of Python's Pandas (although if I was a betting man, I'd wager the best language to implement it in would be Scala, with it's advanced macros & almost-dependent type system).


I think it has less to do with the languages themselves, and more to do with the frameworks available, and the metaprogram-y things those frameworks are free to do without a type system stopping them.

I've yet to see the equivalent of Django or Rails when it comes to rapidly assembling a database-backed CRUD app in a statically typed language.


My job has internal tool that reads MySQL schemas and outputs a CRUD golang api and json-based form uis to interact with them.

I think it could be it's own product, but leadership doesn't care.


I assure you, it could not be its own product- but it could be its own open source project.


That is what I meant, don't know why I put product. Too many meetings.


I am working as a java developer now for about 3 years. Before I worked 5 years with rails. You are completely right, a spring boot application with Lombok doesn’t come near a rails application. Also what people tend to oversee is that the recompilation step really takes a lot of time.


Their reputation for fast prototyping was established before type inference became a widely available language feature.

Duck typing is also a minor win for "do what I mean" generics - you never have to worry about the type specification being too narrow, just pass an object with the necessary methods and it'll work until it breaks.


Python and Ruby are extremely malleable, like Smalltalk and lisp before them. You can inspect anything, you can override / replace / proxy anything. You can use a few built-in collection types for nearly anything.

This is by design. This is great for prototyping.

These languages are to programming what breadboards and wires are for electronics.

Of course, as your system grows, you start to want static checks, or having your schematics on a PCB. But at the very start, tweaking a piece of Python code, or a breadboard, is easiest. Then, of course, you may not want to afford a a rewrite to rust, or would put your breadboard in a box and ship it :)


>Of course, as your system grows, you start to want static checks

I've been working on large scale python systems for about 10 years. For me, the most cost effective checks as a system grew have been asserts.

They allow extremely sophisticated pre/post condition checking (partly helped by pythons flexibility), they're cheap to write, they don't take up much space, they catch a shit ton of bugs, they clearly highlight the source of the bug, they work together really well with higher level tests and I've almost never had them trigger in prod (so the fact that they technically could, as static type fans keep reminding me, doesn't really bother me).

For me, asserts and integration tests are what make large scale python systems a pleasure to write.


Funny; I'm a Lisp programmer, not to mention implementor, but whenever I do electronics, even something simple, I go straight to PCB. I can make those things at home with a quick turnaround time. I prefer manipulating traces in the PCB routing program than messing around with flaky wire connections on a breadboard. If it's worth doing, it's worth doing well. After I made my first PCB, there was no going back. I'm confident my circuits will work because I calculate and simulate; and in any case, usually even if the circuit won't work initially, the printed circuit topology will, modulo adjustment of component values. You can anticipate experimentation in a printed circuit, too.


I think you just replaced breadboard with simulation. It makes sense, but kinda supports the point :)


My experience with writing prototypes in Python is that one week later I have difficulties understanding it and one year later (yeah "temporary, my ass" kind of thing) it's completely alien to me.

It's absolutely not the same with OCaml, F# and lately, TypeScript. And I don't think I write the prototype in more time in this languages (if we are talking about something that takes up days, not minutes, of course). But the experience some months down the road is completely different.


> There are a lot of things that make Go slower to write than Ruby, but mandatory typing isn't one of them. Rather, Ruby's total lack of typing makes it harder to write: you effectively end up having to write unit tests just to catch typos.

No, you don't. You need unit tests to verify behavior (values) whether or not you have static typing (except for output types with only zero or one values). Now, it's true, that having such tests also verifies, at no extra charge, things that Go’s type system would verify, but without the additional effort of type annotations. But there's no added cost there, it's a net savings.


To abuse Greenspun's 10th - Every untyped codebase contains ad-hoc, informally specified, bug-ridden, slow implementation of types (written as unit tests).

edit: as pointed out - I need type checking on my words


This "unit tests substitute for static types" is a common straw man. Asserts catch similar kinds of bugs to static type checkers with a similar level of effectiveness and cost.

Moreover, they're typically able to do more sophisticated checks than most languages (e.g. similar to the kind of sophistication of Haskell's type system).

(yes, I know what is going to be replied... IME, they actually fail in prod vanishingly rarely)


I think you meant "types as unit tests", not the other way around.


Tests don't catch all bugs, even well written ones. Infact if tests could catch all bugs, you'd be able to write a test to determine if any program halts (!).

Now let's look at the nature of programmers. What mistakes are they most likely to make on a regular basis? Well they are human, and they are whacking these big fingers at a keyboard. Maybe typos? Also programmers are bad at spelling, from the source code I've read.

Now could a unit test catch all typo-led mistakes? Well I doubt it. Not for a big program with 10's of KLCs.

Does typing help? Yes - and I'd say typing speeds up programming by allowing autocomplete of code (because of less er... typing as in the finger kind). Since the IDE knows the type it can help you find the write method.


> Yes - and I'd say typing speeds up programming by allowing autocomplete of code

Autocompletion is available in numerous IDEs and editor plugins for many popular dynamic languages (and some dynamic language REPLs, too); static typing certainly can provide a source of data for autocompletion, but it's not necessary for it.


> you'd be able to write a test to determine if any program halts

Halt != bug, so this problem doesn't really apply.

You can write provably correct software, even if we still haven't solved the halting problem.


In most languages you can definitely introduce a bug which causes a program to not halt.


Yes, but do you often need to automatically determine if a bug causes a program to run indefinitely or only much longer than expected? That would be the halting problem, yes (strictly speaking, some subset of it, and for many programs a decidable one).


You’ve misconstrued my argument. I’m not arguing that unit tests are unnecessary in typed languages.


If you admit that tests are necessary in statically typed languages (which I agree with), then why does having to write tests in dynamic languages make them harder to use? Presumably, you were going to write them in the statically typed language anyway.


Yes and no. It’s true that cleverly written tests encompass bad input, but this also means you need to add an additional layer to your test thinking, which depending on who is writing those tests might might yield results of varying quality... Or you could try fuzzing?


> It’s true that cleverly written tests encompass bad input, but this also means you need to add an additional layer to your test thinking

Sure, you have to deal with more cases in running code if they aren't foreclosed by static guarantees, but you are either adding the thinking to code static guarantees and you are doing the thinking to code tests, and I've seen overpermissive types from too-shallow thought on that (even when it's not due to insufficient expressiveness in the type system) plenty of times to not think its substantially less of a risk than inadequate testing.

I'm not against static typing; I definitely think it has an important place in the toolbox, butnmy experience doesn't align with the idea that static typing is universally better for rapid prototyping.


fuzzing is kind of crude, you should go for a more sophisticated type of test, like property testing, which is "fuzzing" plus "shrinking"


Nowadays I write unit tests for any nontrivial code I create. It takes a little more time than writing type declarations, but IMHO they offer much more value.


Unit tests are great, but they're a stupid way to catch typos.


I agree that writing them only to catch typos would be stupid. But if you're going to write them regardless, catching typos comes for free.

Ps and luckily, even with dynamic languages any decent IDE will catch most typos


> Unit tests are great, but they're a stupid way to catch typos.

Catching typos is an important part of what unit tests do with statically typed code, since typos result in value errors at least as often as type errors.


That’s obviously true, but it’s equally obvious that there’s a whole (large) class of typos that types eliminate.


For prototyping sure, because the prototype might grow, but otherwise it depends on the type and scope of the project I think. For the kind of CRUD web app projects I pick Ruby for I still fail to see how I could benefits from types instead of slowing me down. I may be missing something, so I’m interested if you have any resources with concrete examples for common errors.


I've found strict typing, along with type inference and a good IDE, to be much faster than a dynamic language.


My point is that in my experience it depends on the project, and that dynamic languages still have their places. For example if I have to code a few web pages with some data visualisations in D3 I found that going straight with vanilla JS in a text editor is the best tradeoff. Any bug that would have been catched with typing will obviously be catched visually.

I don't challenge the benefits of static typing for more ambitious projects with a constantly updated codebase and multiple developers, or the usefulness of IDE with large codebases or frameworks.


> dynamic languages still have their places

I wasn't disputing this in my previous comment, but I do actually disagree with it at this point. I haven't found a use-case for a dynamic language in many years. I cringe at the thought of working with them now.

> vanilla JS in a text editor is the best tradeoff

This defies my understanding of the word "tradeoff". It costs nothing to fire up a full IDE instead of a text editor, so why not use one? It comes with autocomplete at the very least, but also static analysis of JS.

If something costs nothing and has a benefit, it must be the best tradeoff, right?

> Any bug that would have been catched with typing will obviously be catched visually.

Catching bugs at runtime is slower by definition than catching them while you're writing the code in the first place. Why make things harder when great tooling is a few clicks away?


You are focusing on the IDE aspect which I do not necessarily disagree with, honestly I haven’t tried the recent offerings for Ruby or JS so maybe it’s 0 cost now.

> I haven't found a use-case for a dynamic language in many years.

That’s actually my initial point. The use case is small scope projects, where you get the benefits of dynamic languages (faster and easier write and easier to read) without the maintainability cost and where you’re unlikely to ever get a type error.

Edit: concrete example: Standalone web page with a GET request to get some data, create a chart, and some user events like mouseover. No build tool chain, plain JS. I don’t see how I could benefit from types and what kind of errors would be prevented, but maybe the overhead is now very low with recent tooling and I should re-evaluate.


> I haven't found a use-case for a dynamic language in many years

This seems like a rather extreme statement given the number of people in the mathematics and science communities that find plenty of use cases for Python/Scipy/Numpy/Pandas/Tensorflow, etc.

What language ecosystem would you suggest for those people?


I agree: I think most people that look at typechecking as a serious friction are thinking about it wrong; the problem is probably languages with bad ergonomics. Many people who use TypeScript actually end up using it with strict defaults for even new or toy projects - it’s too valuable to not, honestly.


I agree about the ergonomics.

Another aspect of that which I have noticed is that so many fans of dynamic languages refuse to use IDEs. They'll even state it simply as "I don't want to use a language that makes me use an IDE". For me, the IDE is like a second part of my brain. I use Eclipse which does incremental compile at every keystroke and I see errors, warnings, autocompletes, hints etc in real time. This removes a huge amount of the ergonomic barrier they are talking about (having to save your file, run a compiler, only then see the errors, then find that line in the file, etc ...). But they are stuck at "I don't want to use an IDE" ....


Wasn't that perception around long before the 'bolted on' type systems appeared? To me, it seems like some of the clunkiness and sharp edges of the type systems of commonly-used typed languages at the time - C++ and Java had a lot to do with it along with RoR enthusiasts portraying static typing as some sort of tool of oppression.


Yes, it was; I wrote imprecisely, I'm referring to the perception of systems like Typescript being clunky for prototyping.


Oh I see what you meant now - I've mostly seen people being super-excited about their new typesystem JATO rockets.

My own theory is that we likely underestimate how much of this might just be fashion. I don't mean this dismissively or to suggest that the mood swings over typing in the last 20-odd years have been without technical basis or impetus. Just that, you know, at some point people liked bellbottoms or Members Only jackets and later they didn't.


I think it's mostly the REPL that allows for quickly spiking out things, with quick feedback. Elixir has a similar experience, but less of the warts you get with Ruby (like monkeypatching).


Absolutely. Ruby is faster to write than Go, and the REPL is a big part of that. I'm saying: it would be even faster to write if it had static types.


If it had static inferred types a la Ocaml.


Unless you've used a language with a high level of strictness, i.e. OCaml/Haskell/Rust, it can be hard to see the sheer power and utility of typechecking. If someone has only used Java, they may not understand the true power of types. But if you've familiar with OCaml/Haskell/Rust, why bother writing dynamically typed code? Sure there's some niche usecases where it's more powerful, but generally you can do as much with say, Rust, or even more pragmatically, C# 8/Kotlin.

While if you've only used dynamic languages or badly typed languages, then having to deal with this stupid naggy compiler is just annoying. A big part of learning a strongly statically typed language is learning that the compiler is your friend, and that errors are good. I've noticed that a lot of people new to TypeScript try to get the compiler to shut up, often resorting to any or @ts-ignore, while more advanced users will see it as a dialogue. The compiler complains? Okay, something's wrong: Let's find the root cause here.

TypeScript took off because people had no choice but to write JS, so any benefit was better than no benefit. Sorbet was also borne out of an existing codebase. But a new language wouldn't have this lock in factor.


Copying my comment from above...

I am an "expert" in static type systems (I'm familiar with Java, Scala, OCaml, Haskell, Rust, Go, TypeScript, C++ ... and keep up to date with latest type systems research like 1ML, MLsub, Liquid Haskell, ...), but I have a really hard time imagining how one would develop a statically typed library that would even approximate the usefulness and convenience (for rapid prototyping and interactive data analysis) of Python's Pandas (although if I was a betting man, I'd wager the best language to implement it in would be Scala, with it's advanced macros & almost-dependent type system).


I don't think it is hard to see the power of stronger typing - surely everyone who has written any code in Python or Javascript has made a typo that would have been caught in a more strongly typed language?


Yeah, you'd think. But a lot of people who write code in solely Python and JavaScript just see it as an inevitability of programming. Much as I'd imagine people thought you'd always have to manage memory when programming. Or that programming would always involve punching cards and having to debug via hex dumps. Often times when a new technology like automatic memory management or strict, sound typing becomes available, people will only see the loss of freedom/control and not the corresponding benefits. They won't see it as "oh I lose X to gain Y", but just "I lose X".


Add scala to the list. Or any typed functionnal language.


> a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.

You mention it in your edit, but Crystal has been exactly that for me. A rubyist for a decade I found Crystal to have the type system I was expecting all along.


Have you seen Julia?

https://julialang.org/

The first three selling points on their home page are Fast, Dynamic, Optionally Typed.


Meh. Julia does nothing to help me catch errors before runtime, it's no different than Python in this regard. Although it does use the types to generate fast code (and in my experience it does live up to its performance claims).

I've seen some talk of Julia doing compile time checks, maybe in the future it will?


Julia's type system works to make the language more dynamic instead of more static, instead of one slow program in which types have to be checked at runtime a Julia program is a superposition of every optimized implementation of an algorithm and explicit types are used to access and manipulate a subgroup of implementations. So in general you don't use types to increase performance (the compiler already infers all types to generate fast code, including optimized union types for nullable fields for example), but when you want to restrict the polymorphism to work in more specific code.

But since the JIT compiler infers all types, regardless of explicit hinting, it's possible to create tools to evaluate all types before a function is called [1], which combined with the efforts in better precompiling the code (which increases the amount of code that will have the type inferred before running) could make some good tooling for compile time checking.

[1] https://nextjournal.com/jbieler/adding-static-type-checking-...


> a Julia program is a superposition of every optimized implementation of an algorithm and explicit types are used to access and manipulate a subgroup of implementations.

That is an amazing description of what it is!


Isn't Julia optionally dynamically typed, not optionally statically typed?


I'll add Racket and its variant, Typed Racket, to the list. See https://docs.racket-lang.org/ts-guide/index.html


Dart isn't optionally typed any more. It's now a fully statically typed language, that also has a special "dynamic" type. That puts it in the same boat as C# and Scala, among others.

Optional or gradual typing does seem like an obvious brilliant idea from the outside. Start out dynamic when the program is small, layer in types when it grows to the point where you need them. Capture the union of both dynamically typed and statically typed users. Everyone wins!

In practice, we found ourselves in an uncanny valley where we were too typed for the dynamic typing folks, and too unsafe for the static ones. We couldn't deliver the user experience either camp expected. We learned, the hard way, that a statically typed language is not simply a dynamically typed language plus some type annotations. Everything about how you use the language is different.

---

The way you design APIs is different

Python's tuple type has a subscript operator to return an element at the given index. That's a perfectly reasonable, simple, clean API in a dynamically typed language. If you want to have statically typed tuples, that API doesn't even make sense:

   t = (1, True, "three")
   x = t[datetime.datetime.today().weekday() % 3]
What is the static type of x?

Another example: Python's list type has a sort() method. It takes an optional "key" argument that is a callback that converts each value to a key of some time and then sorts using those projected keys. If you pass a key function, then sort() needs to be a generic function that takes a type parameter for the return type of the key function, like:

    sort<R>(key: (T -> R))
But if you don't pass the key function, the R type argument is meaningless. Should it be a generic method or not?

An even gnarlier question is "What kinds of lists can be sorted at all?" The sort() method works by calling "<" on pairs of elements. Not all types support that operation. Of those that do, not all of them accept their own type as the right-hand operand. How do you design the list class and the sort() method such that you ensure you won't get a type error when you call sort()?

To handle this kind of stuff, the "best practices" for your API design effectively become "the way you would design it in a fully statically-typed language". But those restrictions are one of the main reasons people like dynamic languages.

You can mitigate some of this with very sophisticated type system features. Basically design a type system expressive enough to support all of the patterns people like in dynamically typed languages. That's the approach TypeScript takes. But one of the main complaints with static type systems is that they are too complex for humans to understand and too slow to execute.

This makes that even worse. TypeScript's type system is very complex and type-checking performance is a constant challenge. In order to let you write "dynamic style" code, TypeScript effectively makes you pay for a super-static type system.

---

User expectations are bimodal

Once you ask people to design their APIs such that they can be statically typed and then let them start writing type annotations, we observed that they very quickly flipped a mental bit and expected the full static typing experience. They expected real static safety where certain errors were proven to be absent. They expected the performance of a statically-typed "native" language.

But most optional or gradually typed languages are unsound in order to allow typed and untyped code to intermingle. That means type errors can still sneak through and bite you at runtime. It means you get none of the compile-time performance benefits of static types. Sorbet asks you to write your code with all of the discipline, restrictions, and cognitive effort of a statically-typed language. In return, it gives you the runtime performance of... Ruby.

Worse, actually, because it is checking your type annotations dynamically at runtime. It basically turns your type annotations into assertions. So you get even more potential runtime failures.

This was how Dart 1.0 worked. I used to joke that we gave you the best of both worlds: the brevity of Java and the speed of JavaScript. And then I cried a little.

---

This sounds like I'm criticizing this approach to languages. I actually think TypeScript, Flow, Sorbet, and others are a really smart solution to a very challenging problem. If you have a very large corpus of dynamically-typed code that you want to keep extracting value out of, they give you a way to do that while getting some of the benefits of types. If I was sitting on a giant pile of JS or Ruby that I had no plans to rewrite, I would absolutely use one of these tools.

But for new development, I think you're much better off choosing a modern statically typed language if you think there's a chance your program will grow to some decent size. By that, I mean C#, Go, Swift, Dart, Kotlin, etc. Type inference gives you most of the brevity of dynamic types and you'll get all the safety and performance you want in return for your effort to type your code.

If you're going to do the work to make your code typable, you should get as much mileage out of it as you can. So far, no one I know has figured out how to do that with an optionally or gradually typed language.

---

This is, of course, just my personal preference. And I'm biased because I've already walk the long painful educational road to understand static types. One of the real large benefits of dynamic types is there is much less to learn before you can start writing real code. For new users, hobbyists, or people where programming isn't their main gig, this is huge. I love that dynamically typed languages exist and can serve those people.

But my experience is that if you're a full time professional software engineer writing real production code eight hours a day, it's worth it to get comfortable with static typing and use it. The fact that basically every large software shop that had a big investment in dynamically typed languages is trying to layer static typing on now probably tells us something. Google with Closure Compiler and Dart. Microsoft with VB.Net, TypeScript, and Pyright. Facebook with Hack and Flow. Apple with Swift.


TypeScript actually handles the first example quite well. If you simply have a heterogenous array type, the type of its members will be the union type of `number | boolean | string`.

If you’ve used a typed tuple, then the type after access is based on what TypeScript statically knows. So array[0] would be number, but array[random() % 3] would be the union type.


That's pretty nice, but it's also a large amount type system machinery for what is in Python an idiot-simple API. Imagine you have:

    array[0] + 2
And you change it to:

    array[random() % 3] + 2
Now you get a type error on "+". Think about the level of type system sophistication you need to have as a user to understand why that change caused that error.

In some senses this is even worse than programming in either a dynamically typed language or a statically typed one. You still get the performance and unsoundness of a dynamically typed language. But you also get the type errors and cognitive load of a typed language, with a type system that is much more complex than a typical typed language.


The better feedback loop actually makes it worth it. Yes, we don't get soundness and performance when programming in TypeScript, but we do get better feedback in the editor along with great escape hatches for customizing types as needed. I don't know if I ever can go back to a sound statically typed language again, actually, TypeScript is just way too useful.


I feel exactly the same way. I thought I liked static typing more than dynamic typing until I tried TypeScript, but TypeScript feels like by far the best of both worlds.

I especially feel like null checking is impossible to live without, and I'm going to strongly prefer TypeScript over Go or Dart from munificent's list until they add null checking.


We're working on static null checking in Dart now. Unlike TypeScript's, Dart's approach to null checking will be sound.


> But for new development, I think you're much better off choosing a modern statically typed language if you think there's a chance your program will grow to some decent size.

> By that, I mean C#, Go, Swift, Dart, Kotlin, etc.

I think all of those require compilation. While I loved writing C# in a previous job, the compilation step added some small amount of friction to regular web development.

Though working with PHP requires more thought for the big-picture stuff (no PHP ORM can touch what C# offers) I find it easier to get into a state of flow when developing new features. Once I've completed a given task I can run a static analysis tool (I've made one at Vimeo, but there are others to choose from) that can automatically add most of the types I neglected to add, and can suggest more.


> Dart isn't optionally typed any more.

Gilad was (religiously) in favor of optional types. Did he come around to agreeing with the direction that Dart has taken (i.e. full static types)?


He left Google a while ago.


Lars, Kasper, and Gilad have all left the project.


Interesting. Looks like Kasper and Lund are on a mission to fix the IoT mess and "democratize embedded development". I'm looking forward to their product launch.


> What is the static type of x?

an anonymous (Integer | Bool | String) sum-type, of course.


Not a sum type, since if all the components were strings you'd have (String | String | String) = String. The sum type would be 3 × String instead.


Thanks for bringing up Dart (underrated IMHO). The built-in optional typing helps with productivity and readability. There is also a great official style guide which goes over when static/inferred/dynamic typing are preferred: https://dart.dev/guides/language/effective-dart/design#types


Agreed, I really hope Dart gets more adoption now with the help of Flutter. I would especially want it to get some more back end love.


Yes, I'm watching http://aqueduct.io and https://angulardart.dev with interest.

I'm loving Dart + Flutter, and I hope to be able to write backend code in dart as well, because I want to be able to reuse code.


I'd also encourage you to check out https://angel-dart.dev/.


Clojure allows one to start with essentially untyped code, then add type declarations for efficiency and safety.


core.typed never really caught on though, what seems more popular is Schema which focuses on annotating and validating the structure of lists and maps: https://github.com/plumatic/schema


And Schema died in favor of Spec, which is still in flux.


Spec isn't finalised but I wouldn't say it's in flux. It's 95% there.


I'm actually working on solving this problem at the moment with https://darklang.com. Our approach is to allow the quick prototyping of python via tooling built-into the editor, within a language that has strong static types (similar to Haskell or OCaml).

As an example, you never change types in Dark, you only make new types and switch over to them, so if you want to test out a type change for just one HTTP route, you can do that.

Dark also doesn't have nulls or exceptions because they're hard to reason about. The usual tools to replace them (Result and Option/Maybe types) require you to handle all the cases when you write code using them. We're allowing you to write code that doesn't handle these cases (again, using editor tooling). Instead it tells you exactly what errors can happen at every point in your code. Once you have your initial prototype/algorithm figured out, you can use that information to handle all the edge cases.


> Instead it tells you exactly what errors can happen at every point in your code. Once you have your initial prototype/algorithm figured out, you can use that information to handle all the edge cases.

While I can't say much about Dark (the available blog posts [1] are shallow), I do think that the automation may be one key aspect of future programming languages. For example, when I'm thinking about gradual typing I don't only want to mix the hodge-podge unitype and actual types but also convert the former to the latter, and a large portion of the process can be automated in various ways (for example, one can track the typical runtime types that unityped variables have; the programmer can solidify compile-time types using that fact).

[1] https://medium.com/darklang


Guilty for sure! Working on it.

Instagram had something similar [1] where they extracted mypy types from actual run-time instrumentation. I could see something like that working for typescript and sorbet too.

[1] https://github.com/Instagram/MonkeyType


Yeah, that kind of things! I think it should be a more frequent operation though, available from an editor with one or two keystrokes. I see Dark is heading to that direction; I wish you good luck.


Thank you!


Do you have any documentation?

> Dark also doesn't have nulls or exceptions because they're hard to reason about.

How do you handle OutOfMemory errors?


No documentation just yet, but we do have an introductory design doc: https://medium.com/darklang/the-design-of-dark-59f5d38e52d2. I'm working on an essay about this particular thing (email/twitter me for an early draft).

In the longer term, OutOfMemory errors will be handled by automatically rerunning the request with more memory enabled. (Or even longer term, requests will be split and parallelized and checkpointed, etc, automatically). But yeah, if you run out of memory, it will probably simple return a 500.


> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code

Unless I'm misunderstanding something, PHP7 can do exactly that.

With regards to the general trend you mention about adding static-ish typing to dynamically typed languages, the opposite is also happening to some extent. I work in a medium sized company that does mainly .NET and I see C# devs use `var` a lot (in order to let the compiler infer the type instead of having to declare it explicitly). I'm not sure if the `dynamic` type is also seeing increased use, but just the fact that it was added to the language in v4 says at least a little.

I think what is really happening is that the more popular languages will sort of naturally converge as development progresses and more and more people request features. So while Mr. PHP-dev-turned-to-C# will maybe want more dynamic-ish typing in C#, Mr. C#-dev-turned-to-PHP will request more static-like typing in PHP.


> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.

Popularity aside, Perl 6 supports exactly this.


So does php 7


Well, kind of.

I have an old website built on PHP, using a PHP 7 runtime, and I recently had to make a few small changes and discovered PHP had added typing in recent years.

My interest was piqued, and I dug into the docs a bit.

And was promptly disappointed - yes, PHP allows gradual typing, but the type system it has is woeful! Aside from the pitiful selection of types, basically only function arguments can have type hints (e.g. no typing of local vars).


Another huge problem is the lack of typing for arrays.

Considering the prevalence of array types in PHP, this considerably reduces the available safety.

Basically your only option is to use only classes for keyed arrays, and array wrapper classes like `MyClassArray` with typesafe methods like `push` and `get`.

Typescript solves this beautifully generics and interface types.


> the type system it has is woeful

Check out a thing I made: https://psalm.dev. It allows you to add more descriptive types in docblocks.

> no typing of local vars

Why do you want explicit types for local vars?


> Why do you want explicit types for local vars?

I suppose mainly because of deficits in the static analysis tooling that exists (or at least, that I used), as they usually cannot infer types correctly. Psalm looks pretty nice though, so I'll check that out next time I'm working in this PHP codebase :)

Even with exellent tooling though, there are still occasions where I like to use an explicit type.


Fast prototyping without types? Meh! I need types to be able to prototype and change things really fast, knowing that it won't break.

I feel a lot more confident to prototype in OCaml/F# and then "downgrade" to an 'ordinary' language that needs more people to understand what is written, than the other way around (prototype in Python and move to something 'real' later)

I think that recent movement to put types in dynamic languages is just because of the need to fix existing projects, people are finally "getting it".

TypeScript is awesome in this regard. Almost makes JS bearable.


Haskell, with deferred type errors and runhaskell, is quite a dynamic language.


Just to be a little pedant, Dialyzer (an Erlang success typing lib) precedes Elixir and the other static typing efforts you mentioned by many years, way before this so called “static typing renaissance”.


Another thing to point out is that (dialyzer) typespecs are extremely prevelant in both Erlang and Elixir libraries and especially the core language libraries. So not only does dialyzer precede the others, it’s become a core part of Elixir and Erlang. In contrast, mypy & sorbet appear to be largely second class tools. TypeScript though appears to have made more inroads.


eh I would say that dialyzer is kind of a 1.5-class citizen, at least of Elixir. It operates using a very different AST with somewhat different "ideas" of what is what, and writing typespecs in macros is not... easy.


Type inference is where it's at. Everybody loves types when there's hardly any overhead. Typescript is the best example of this.

My theory, the first languages of most tend to be untyped. Over the years you get tired of dealing with type errors and move to more complex languages with strong typing. After a while you get tired of typing a bunch of useless crap because the compiler isn't smart enough to figure out the type for you and land in Typescript or similar


here's one more (not so popular) one: https://inko-lang.org I'm also a fan of clojure.spec, though not a substitute for a proper type system, is extremely helpful in achieving the same goal.


> Eg in Typescript you will often still run into bugs caused by malformed JSON that doesn't fit the type declaration, badly or insufficiently typed third party libraries

there's a fantastic typescript library, io-ts (https://github.com/gcanti/io-ts), that provides the ability to declare runtime types variables that you can infer compile time types from that solves exactly this problem. it's deifnitely work taking a close look at if you want to ensure type safety at runtime for data coming from third parties.


Dialyzer is an Erlang thing, and it's been around a long, long time. That doesn't change the point your are making, and am just clarifying it a bit.


Erlang/Elixir + Dialyzer?


It would be great if it had a proper IDE. That's the only major hurdle to the success of this platform.


LSP support is pretty good for Elixir. Most of the actions are supported and it's pretty fast. There's definitely not as much drive for IDE tools as I'd like to see out of the core team/Elixir community but pretty good LSP support works in almost any IDE.


Yeah, but integrating the IDE with dialyzer is a pita. And if you don’t have that, it’s not a “proper” language


You forgot two of the very first ones in this regard, Lisp and Basic.

Also Dart 2.0 is strongly typed with type inference, they rebooted the type system.


Groovy is one language that is dynamic but also has an optional statically typechecked compilation mode.


> Groovy is one language

Apache Groovy is two languages. The Groovy 2.x download, first released in 2012, bundles two different compilers that were both forked from the Groovy 1 compiler. Only one of them has upgraded to the JDK-7 invoke-dynamic capabilities, and the other (which hasn't) is the one actually used by Gradle and Jenkins and everyone else. Last month, the Groovy project managers at the ASF announced they were keeping the upcoming Groovy 3 as two separate languages also. The long-awaited parser upgrade from Antlr 2 to Antlr 4 is being bolted on to the invoke-dynamic compiler only -- the compiler no-one uses. They talked about Groovy 4 reverting back to a single language, but i'm guessing that's many years away because the original purpose of making Groovy be two languages in the first place was to not change the language that users actually use in any way, while simultaneously appeasing their own developers by bundling the code they wrote (invoke-dynamic bytecode generation, Antlr 4 parser upgrade, etc) in the language.


And if it wasn't for Gradle it would be mostly irrelevant today, so much for Grails and JEE Java's companion language for bean implementation.

The recurring talks on Android build speed, with everyone moving into buck and bazel show how much everyone loves putting up with it.

Just last week AMA on Reddit had the recurring answer to add memory and profile build execution.

When a build tool requires profiling to make it usable, it already starts on the wrong foot.


I prefer how this evolves organically if / when there's a need. Once the need is proven by adoption numbers (say this ruby w/ type checking gets popular) you'll have a good idea of what people want (and any issues they might have with this implementation).


I’ve found Elixir to be exactly that.


As I understand, Racket seems to be the gold standard in that area.


stanza [http://lbstanza.org/] is optionally typed and compiles to native.


waves and roundabout


> I wonder why there isn't a popular gradually typed language that natively allows writing both dynamic and type-safe code, allowing quick prototyping and then gradual refactor to type safety.

With the addition of `var`, I think Java is that language.


A fascinating part about Sorbet is it did not have to introduce any additional syntax to Ruby (unlike TypeScript, for example). This really speaks to the expressiveness of Ruby. Very cool.


The type signatures are pretty noisy to read, though, some syntax can definitely help. Maybe with Ruby 3?


Was that additional syntax in TypeScript actually neccessary for type inferrence? Or is it rather to avoid API hazards when you change some internal code and suddenly the API of your library breaks because the inferred type has changed.


You can use TypeScript in a mode that only uses type inference and doesn't require type annotations or definitions. It works surprisingly well.


Additionally, TypeScript will parse JSDoc comments into type annotations: https://www.typescriptlang.org/docs/handbook/type-checking-j...


It is necessary because of some limitations, but IMO it's also a great idea nonetheless. Types' names are a great documentation, one which you can't get with pure inference.

Even languages that have (close to) the best possible inference, like OCaml, still have additional syntax for defining types, because it 1. gives you documentation and 2. allows you to do things that are mathematically proven to be impossible via inference.

TS is great, and also moving really fast and becoming better every 2-3 months.


Coincidentally, I announced the beta version of a Ruby type checker in Solargraph two days ago: https://github.com/castwide/solargraph/issues/192

It has a few overlapping features with Sorbet, with one major difference being that Solargraph type checking relies on YARD documentation instead of annotations.


Love the work you’re doing on Solargraph! Thx for it.


I wondered why no one had tried this instead. It's certainly a (much needed!) incentive to document methods.

I'm going to give Solargraph a look-see.


> To enable static checking with srb, add this line (called a sigil) to the top of your Ruby file:

> # typed: true

Isn't this called a directive/pragma? A sigil is a symbol on a name.

Either way, I'm excited to see this finally out after seeing the past presentations on it.


Thanks for pointing that out. It can be called all of those. We liked sigil from its connotation:

> Google defines sigil as, “an inscribed or painted symbol considered to have magical power,” and we like to think of types as pretty magical

https://sorbet.org/docs/static#fn1


That's a pretty unintuitive use, since "sigil" is more commonly used (in programming languages) as a single symbol, as in a non-alphanumeric character that's used as some kind of syntax.

https://en.wikipedia.org/wiki/Sigil_(computer_programming)


Right so the $ in PHP or @ in Perl would be a sigil, (and even the $ and % in QBasic, am I dating myself?) and the # in this example and all C code and some Swift code would be a pragma. Seems pretty cut and dry.


And the $ and @ in ruby...


I remember there was this CEO once who was new to the software industry and was looking for a word to describe non-small-business customers. When people suggested "Enterprise", he instantly dismissed it, and when they insisted this is already the word we all use to mean this, he actually opened the dictionary to prove that it wasn't quite accurate. What I took from this is that, when cultures or conventions already have momentum, sometimes you just have to go with it. This is the same reason I don't like Go.


How does this relate to Go? That flew over my head.


They throw every convention out the window and act like they have a true greenfield audience. They don't even allow [variable, Class, CONSTANT] convention, they don't follow the convention of letting devs choose their own project location, and a lot more.


Could be a reference to Go choosing to use different words for similar concepts that exist in other languages like C++? I'm not familiar enough to think of examples though.


Bummer. That kind of name overloading has the potential to be needlessly confusing down the line. "Sigils, I mean well, they're like pragmas but in Sorbet we call them sigils." [x a billion]

Now's the time to fix that stuff.

(In any case, I'm quite keen to start playing with sorbet, looks great!)


  test/test_corpus.cc
  364:        auto checkPragma = [&](string ext) {
  368:                    << "Missing `# typed:` pragma. Sources with ." << ext << ".exp files must specify # typed:";
  377:            checkPragma("cfg");
A quick look at the source shows that it may have been called pragma at one point.

EDIT: I'm guessing "sigil" was chosen because it's closely matching the "signatures" or Sigil.sig method name?


Nice spelunking! Yeah, this word means nothing really. Sigil, pragma, directive, typed comment, whatever. Feel free to call it "dohicky" if you'd like.


Ruby has perl-like sigils, eg: @i_am_instance_var, $i_am_global. Additionally, one might consider array literals sigil-like (like %w[a b c d e f g]).

https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/vari...


The terms all overlap a bit, but I would interpret "directive" or "pragma" to indicate that it's conveying meaning to the Ruby interpreter. This is exactly the opposite -- it's a comment as far as the Ruby language is concerned while conveying meaning to an external tool.


I've never understood the so called advantages of dynamic typing. To me it looks like a land mine in one's project waiting to blow at run time. And what for? Do developers code so fast that the time spent on typing something like "int i" will provide any real savings? Now vendors are trying to patch those with bolted on top syntax extensions/derived languages that need to be transpiled. What a mess.


Most people fixate on the terseness that it can afford in a language. I suppose I do like that but for me it is not a huge deal.

What I think is more important is the flexibility that it brings to express design patterns that in other languages, like Java for example, can become very cumbersome. I can’t tell you how many times I have been in the bowels of some Java code and found some method that takes a concrete implementation of something that could or should be an interface when I really want to pass in something different. Then you are like “let me extend and fix this class” and then you end up just extending and fixing 1/2 the code base to get done what needs to be done. In a language like Ruby I would just pass in an object that responds to all the needed methods and it would happily work. Ideally you wouldn't get into these type of messes in statically typed languages because people would follow good design principles all the time. But people are fallible and in reality messes are everywhere in statically typed languages.

So I think the approach of adding type enforcement if desired is a nice approach considering there is a large amount of code out there that probably doesn’t benefit much from it.


When you're consuming JSON that has deep nesting, arrays that contain multiple types, etc. Something that may be two lines of code in a dynamic language could be as much as 100 lines in some static typed languages.


Sure, you could write one line of code to read it in as maps and arrays, but at some point you need to validate your input from the outside world to convert it you your domain objects. Using a dymically typed language doesn't magically make that problem go away.


Depending on library used it can actually be the same amount of lines. The lines are a bit longer of course.

Example

  auto obj = JSON(text);
  int age = obj.["persons"][3]["age"];


That's probably the only real case, yes. And still, even this is a non-issue with things like io-ts https://github.com/gcanti/io-ts

This is my go to tool (and language) nowadays when I need to do something JSON heavy for a prototype.


It makes unit testing much easier. That’s the best explanation I’ve found. Rapid prototyping too, but that just means you’re backloading tech debt so that’s at best neutral in pure technical terms. In a startup context backloading tech debt is deeply desired.


Type checking isn’t really related to typing “int.” Many languages infer types. In fact, Hindley Milner type systems should be able to infer all types without explicitly specifying any of them.


Sometimes I do code that fast. When you’re messing around just trying to find out if something is possible you want to write as much code in as short a time as possible, and Python shines for that. Every second wasted typing is a second that could have been spent moving forward.

Of course the problem is that the prototypes are terrible to maintain and eventually need unit tests and typing. But you don’t want to waste time adding those things if you’re not even sure your idea will work. I use strongly typed languages in production and couldn’t imagine using Python for that.


If typing speed is your main limitation, it means either you can improve your productivity dramatically by improving your typing skills, or your language is limiting you so much that you don't have good abstractions to enable you to think at a higher level.


No argument on the first thing you said.


I don't see how typing F# is slower, longer or less convenient than typing Python (spoiler alert, it's not). And you also get things like actual lambdas, pattern matching, currying, real parallelism and more and more.

It's only that people believe there is no need to learn anything beyond Python because it's "easy", which it's not, once you go beyond several hundred lines of code. But the myth somehow continues to persist.


This logic transpiles(TM) to this in my brain: a) I am messing around and need to write 10 lines of code. Can I write it fast and without thinking? Sure and not needing to type int/float/whatever will save me couple of seconds. If I iterate and rewrite that short piece 10 times then I just saved 20 seconds. Chirp chirp ... . Do I need special language for just that? Lemme guess ... b) I am messing around and need to write few hundred or thousands lines of code. Can I write it fast? Maybe but it is likely that good chunk of time will be spent thinking. I doubt that at this point not declaring type will save anything worth noticing. But that of course is my opinion


Typing speed is only one constraint that type systems enforce. They also make it much slower to change input or output data from functions. I tend to use dynamic languages for projects between 100-500 loc and usually I’m not really sure what the final design will look like so need to try a lot of different ideas as fast as possible.


Awesome work.

    sig {params(person: Person).returns(Integer)}
    def name_length(person)
Not sure if I dig the syntax. Furthermore arguments seems to be the official names for method arguments, not parameters. eg, `ArgumentError`. `params` also feels like it's linked to Rails `params` variable in controllers. It can be confusing.

Something like this will also feel more Rubyist:

    def name_length person: Person, return: Integer
But it probably requires a deeper hack or a change in MRI.


Thanks for the idea.

We used `params` because Method#parameters was what they called it in the standard library. I actually had it as `args` originally until someone pointed this out. https://ruby-doc.org/core-2.6.3/Method.html#method-i-paramet...

As for the syntax change, we are actually on our 8th iteration of the syntax. We really wanted this to NOT be a fork of Ruby so finding something compatible was very important. For example that's why it has the weird `sig {` syntax too, we didn't want to have to cause load-time and cyclic dependencies from adding type signatures.


> We used `params` because Method#parameters was what they called it in the standard library

Super interesting. We should probably have being consistent for naming parameters vs. arguments in stdlib. It's too late though!


On a super pedantic level, "parameters" are the names that you write in the function definition, and "arguments" are the values you pass as parameters.

  def name_length(person)

  steve = Person.new
  name_length(steve)
Here, 'person' is a parameter, and 'steve' is an argument.

Most programmers use them interchangeably.


We called those 2 terms "formal parameters", and "actual parameters" if I remember my programming language concepts class from college correctly.


I've never been to programming college but in 10 years I have always heard them as def(parameters) and call(arguments). Probably a lot of that was when reading about how VMs and compilers work, though.


I'm not sure how consistent it is with everything in Ruby, but parameters is technically the correct term here. A parameter is a variable definition, while an argument is the value that is passed to the parameter.

ArgumentError is still consistent with this definition (it's an error with the value you passed, not with the definition). However, params in a Rails controller does violate this definition.


Different levels of abstraction/concerns. Params in Rails come from HTTP params i.e the conceptual merge between GET query strings and POST body (e.g forms, but also JSON).


In computer science, "formal parameters" is the name for those named variables that are established on entry into the function and immediately receive external values. "arguments" are the values that they receive. A function has only one set of parameters, but a new set of arguments in each invocation.


My view is you define methods with parameters, you call methods with arguments.

`ArgumentError` is consistent with an error during call time.


That might work in .rbi files because they could be parsed independently of Ruby itself (basically giving Ruby the C-style header+impl split).

As far as ruby goes, though, it would conflict with keyword arguments.


Dont know if this applies, but my understanding is that in functions, a parameter is the name of a declaration which when called will receive an argument.


Related, Square wrote a great article: "RubyKaigi and the Path to Ruby 3"[0]. The section titled "Static Analysis" high level compares Sorbet to Steep

[0] https://developer.squareup.com/blog/rubykaigi-and-the-path-t...


It’s my understanding that the Sorbet team is involved with bringing types to Ruby 3. I’m unclear on whether it will be Sorbet itself or if it’s elements of it. Can’t dig up the source right now, maybe someone can corroborate this?


This is true, we're part of a single working group that's working on types for Ruby3. https://twitter.com/darkdimius/status/1119130004313350144 has some detail


Though I haven’t yet used it for anything in production, I think if I were starting something greenfield and wanted “Ruby with static types” I would go with Crystal. I really enjoy writing it and the performance you can get is quite a significant boost over Ruby.


I’d still go Ruby. A language’s ecosystem and community are as much factors in why someone should choose or avoid it as its syntax. Both of those things are fantastic for Ruby — I’d argue that they’re some of its best features, in fact. Crystal? Not so much.


I’m a long time veteran of Ruby and someone who deployed production Rails apps in EARLY v1. I absolutely love and adore it and it’s by far my favorite language to work with. That being said, when I can write in a very stunningly similar language and get 10 to 100x performance with very little extra effort I am going to strongly consider it when deciding on my stack. Also the ecosystem for crystal is not terrible at all. I think it’s a great project and shouldn’t be ignored because “the ecosystem”


I also think Crystal is a great project that shouldn’t be ignored because of its smaller ecosystem. The performance boost is nothing to sneeze at and I wasn’t suggesting that Crystal’s ecosystem is terrible, but it’s nowhere near Ruby’s.

EVERY problem has been solved in Ruby. (Edit: Every problem that isn’t hamstrung by Ruby’s technical limitations. It’s certainly not the right tool for every job.) There’s a gem or a blog post or a service for everything and it will probably work very well. The language is stable and predictable. It won’t be as fast as we might want it to be but it’ll probably work with minimal effort out of the box. You’ll be able to put it into production with little to no fuss and it’ll just work. If it doesn’t just work, you’ll have no problem finding resources to get it resolved. I don’t think you can say any of that about Crystal.

Maybe that stuff doesn’t matter for every individual or every project but for me, they’re significant enough that I think they should come up whenever anyone tries to compare the two.


I'd agree with this. It comes down to the project and scope. For tasks where correctness is paramount it's hard to argue against Crystal, but for most apps, most of the time, Ruby and Rails are sufficient.


I have mixed feelings about adding type annotations to an existing project. IDEs become easier to use, you can avoid certain bugs, refactoring becomes a bit less error-prone. But this comes at a cost: you need a very high type coverage, which means you need to rewrite a lot of code to deal with the different style of polymorphism. It's very likely that you end up with code that looks as if it were written in a statically typed language but without any of the performance benefits of such a language.


Thanks for this. Major contribution to the Ruby community!!


Thank you! Ruby has been kind to us, we'd like to be kind back.


I'm interested in whether `.rbi` files are going to be the only official route for typing in Ruby, and if so, how that would end up impacting Sorbet?


It'd be nice to have an option to put that data inline.


Sorbet supports both formats. You can see the inline syntax on https://sorbet.run/


Does anyone know what Matz thinks of Sorbet? He has previously been opposed to adding type annotations to Ruby [1].

This is in sharp contrast to Python, where Guido has overwhelmingly embraced type annotations.

[1] https://bugs.ruby-lang.org/issues/9999#note-13


I saw some news saying that ruby 3 would have types built in.


I wonder what the reason for not supporting structural typing is. It seems like a very natural fit for Ruby.


We believe that the main goal of a typechecker is to give you nice error messages. We've found giving names to things makes it easier for folks to reason about their errors, and introducing names for interfaces isn't that onerous at Stripe or with our beta testers.

We aren't opposed to eventually support it, but we'd like to see how it goes with the current form first.


The "Getting Started" link at the bottom of the page is broke

https://sorbet.org/blog/2019/06/20/docs/adopting


Great find! Fixing now, thanks.


I've used contracts any time this type of thing was necessary. https://github.com/egonSchiele/contracts.ruby


We use Contracts too and are in the process of transitioning to Sorbet. In addition to the same runtime type checking as Contracts, Sorbet offers static type checking (and will re-use your runtime signatures in its static analysis).


Excellent work! I wonder if somebody already tried to run it against Rails codebase.


Original author of sorbet-rails here. I tried and it did take some work to integrate with Rails, because how dynamic Rails code can be.

But it's pretty useful once setup. It can know when an attribute is nullable or non-nullable, which is a big deal. We even use Sorbet to limit the usage of some dynamic Rails API so that it's more sane.


Companies that use Rails have also started working on https://github.com/chanzuckerberg/sorbet-rails, worth checking out!


While we're on the topic of static analysis of code that uses Rails... I suggest also using other static analysis tools as they make sense. I lead the Railroader project, an open source security static analysis tool for programs built on Rails. It doesn't guarantee finding all security problems, but it can help. More info here: https://railroader.org/


Yes, join the slack! While Stripe doesn't run rails all other companies in private beta did!


Both Shopify and Kickstarter are on Rails.

I wonder why Github didn't join the private Beta?


Maybe their forked version of Ruby had something to do with it.


IIRC last time I poked at their enterprise image and dumped the code there were bits of custom type annotations. That was around 2-3 years ago.


The parser for Sorbet was actually entirely given to us by GitHub. They were fabulous partners early on in the project and we're grateful for their contributions.


Understated, but awesome! Thanks for the insight!


The dependency on bazel is very off-putting to me. After having tried it for other projects and watched as rapid breaking backwards incompatible changes were made to the tool, I'm opting out of anything requiring it.


We only use it at build time, as for a user, it should be invisible for you


I'm having trouble finding the implementation of `sig` - could someone please point me to the right file? Thanks. I'm very curious how they pulled this off.


Here it is:

https://github.com/sorbet/sorbet/blob/master/gems/sorbet-run...

As you'll notice, `sig` doesn't actually do anything.


So....then....how does Sorbet use the type signature provided to sig?


Sorbet has two components, broadly speaking: the typechecker, which is a standalone application, and the runtime, which is a Ruby gem. The typechecker can parse Ruby code and find the sig blocks in the code to extract type information from them, and it then can use this type information to perform type-checking without ever loading a Ruby interpreter, to say nothing of the actual Ruby code in question. This is intended to happen in a CI pass or a pre-processing step, but it is entirely offline.

On the other hand, when you run your code, the sig blocks may or may not be used. There's a lot of machinery in https://github.com/sorbet/sorbet/blob/master/gems/sorbet-run... that handles understanding what a sig means and installing a wrapped version of a method that does type-checking on entry and exit. The intention is that the standalone sorbet executable should be used during development, but it can't catch every error, so the runtime system will double-check types at runtime, which is especially helpful when some parts of your code are typed and other parts are untyped, and control flow passes back and forth between those sections: the runtime will ensure that you don't accidentally pass an object with an unintended runtime type into code with static type expectations.


I still wonder what problem exactly static typing fixes. Is is only me that I'm too used to dynamic tech without correctness issues?


Well one thing: when you work on enough code for long enough you start to forget what's/what. Static typing let's you jump in and immediately know the shape of your data at any point in the code, without having to re-trace execution manually


Yep, many projects grow to a point where it’s literally impossible for anyone to keep track of the full data model in their head. At that point, you end up needing to refer back to schemas and/or jump around the codebase constantly in order to to get anything done. This is when static types and IDE autocomplete start saving you significant time and energy.


I know what you mean. I code in ways that compensate for that problem. Actually minimizing my need to remember anything but intentions. Smalltalk rewards that style vehemently for example. And I use Smalltalk and JavaScript in that style and I don't feel any less productive or particularly vulnerable to correctness issues. While in a TypeScript project I'm working I do feel a productivity hit (not in the good sense) and I (and my teammates) keep asking ourselves "what TS is helping us with, really"? So far the only thing I can say is that TypeScript is a fantastic solution for all the problems that it creates itself.


I'm genuinely curious how you code in such a way that compensates for the problem. I'm unfamiliar with Smalltalk but very familiar with JS/Typescript.

I'm sure that if tight-knit group held strongly to certain naming conventions, then you could convey structure through the semantics. My experience though has been that anything that relies heavily on "holding strongly to convention and form" falls quickly to business speed, forgetfulness, and laziness.

As a personal example, a couple of coworkers and I were working on a codebase in JS, and there was some function that helped manipulate user objects. There were however two types of user objects, that were _very_ structurally similar, but with slight variances. Because of the similarities, people started calling user functions with one type that were meant for the other type, and it would work. Future modifications would inevitably cause failures, as unexpected parameters were being passed in.


In point 10 here I've wrote about that https://www.quora.com/What-is-your-review-of-Smalltalk-progr...

About the problem of "falls quickly to business speed, forgetfulness, and laziness" are you guys using peer-reviews with 4 eyes principle?


I tend to think in terms of static types, so I prefer it, but a lot of people seem to be very productive with dynamic typing.

What's your workflow like? Strict TDD? Runtime contracts? How do you feel when you need to use a statically typed language?


Dude, I asked a genuine question and I get downvotes? WTF? Downvoters, I didn't asked anything off topic, can you elaborate why the downvotes?


Very cool stuff!


!remindme in 1 month to check out Sorbet case study posts


I really don't like the current movement of introducing static typing in dynamic typed languages. Why did we create dynamic typed languages after all? Because we know the pain of having to type everything and especially the pain of converting one type into another.

In C you mainly need static types because you cannot put 32 bits into a 16 bit CPU register, or you cannot do pointer arithmetic on values of different types, etc.. But that is not the reason why we want types in dynamically typed languages. We just want to prevent passing incompatible types as argument to a receiving method for example. And by adding static typing to dynamically typed languages we actually invalidate these languages entirely, full circle. First we create a dynamically typed language, then we change that into a statically typed language that transpiles back to a dynamically typed language, which is terribly inefficient, why would you want that? I have never been able to convince the proponents, they appear to be in some kind of higher state, having found the holy grail.

Almost all the benefits of static typing added to dynamic languages can be achieved by a better and smarter IDE. All these new 'typed' languages with all their own issues are only temporal I expect. We keep changing and rewriting while thinking we're doing it 'the right way' now.. Like Typescript, how long will that live? Flow is deprecated already. The very best thing of the C language is that it is still C, and that is fucking awesome for C developers, after all those years they can still write in the language they master.


> Almost all the benefits of static typing added to dynamic languages can be achieved by a better and smarter IDE.

That does what? Typecheck things? You'd probably want a tool or library to do that, so it's pretty fortunate that Stripe bothered to provide those to us.

> We keep changing and rewriting while thinking we're doing it 'the right way' now..

Humanity has yet to rescue type inference from the forges fire on Mount Olympus. After Milner tricked Hindley in a lunchtime debate about tabs versus spaces and code quality, Hindley punished us mere mortals by placing type inference in the fires of heavily recursive forges mortals dare not touch.




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

Search: