Open-sourcing Sorbet: a fast, powerful type checker for Ruby
sorbet.org
sorbet.org
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.
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.
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.
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.
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.
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.
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 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.)
The feedback loop is within your editor, that’s a step before the app console!
Before that, there was the Rhino and Nashorn ones, which although JavaScript based, did the job.
These days Kotlin is also pretty good. Perhaps not quite as fast to prototype but there’s little need for converting afterwards.
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.
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 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.
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'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 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.
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.
And while Java's type inference is not the same has having HM around, it is already q89te friendly.
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..
Which Ruby do you mean? 1.8, 1.9, or a more current one?
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.
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.
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.
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.
I think it could be it's own product, but leadership doesn't care.
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.
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 :)
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.
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.
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.
edit: as pointed out - I need type checking on my words
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)
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.
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.
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.
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.
Ps and luckily, even with dynamic languages any decent IDE will catch most 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.
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.
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?
> 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.
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?
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" ....
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.
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.
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).
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.
The first three selling points on their home page are Fast, Dynamic, Optionally Typed.
I've seen some talk of Julia doing compile time checks, maybe in the future it will?
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-...
That is an amazing description of what it is!
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.
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.
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.
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.
> 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.
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)?
an anonymous (Integer | Bool | String) sum-type, of course.
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.
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.
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).
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.
> Dark also doesn't have nulls or exceptions because they're hard to reason about.
How do you handle OutOfMemory errors?
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.
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.
Popularity aside, Perl 6 supports exactly this.
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).
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.
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?
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.
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.
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
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.
Also Dart 2.0 is strongly typed with type inference, they rebooted the type system.
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.
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.
With the addition of `var`, I think Java is that language.
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.
It has a few overlapping features with Sorbet, with one major difference being that Solargraph type checking relies on YARD documentation instead of annotations.
I'm going to give Solargraph a look-see.
> # 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.
> Google defines sigil as, “an inscribed or painted symbol considered to have magical power,” and we like to think of types as pretty magical
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?
https://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/vari...
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.
Example
auto obj = JSON(text);
int age = obj.["persons"][3]["age"];This is my go to tool (and language) nowadays when I need to do something JSON heavy for a prototype.
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.
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.
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.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.
Super interesting. We should probably have being consistent for naming parameters vs. arguments in stdlib. It's too late though!
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.
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.
`ArgumentError` is consistent with an error during call time.
As far as ruby goes, though, it would conflict with keyword arguments.
[0] https://developer.squareup.com/blog/rubykaigi-and-the-path-t...
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.
This is in sharp contrast to Python, where Guido has overwhelmingly embraced type annotations.
We aren't opposed to eventually support it, but we'd like to see how it goes with the current form first.
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.
I wonder why Github didn't join the private Beta?
https://github.com/sorbet/sorbet/blob/master/gems/sorbet-run...
As you'll notice, `sig` doesn't actually do anything.
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'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.
About the problem of "falls quickly to business speed, forgetfulness, and laziness" are you guys using peer-reviews with 4 eyes principle?
What's your workflow like? Strict TDD? Runtime contracts? How do you feel when you need to use a statically typed language?
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.
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.