I worked on a summer project to add type annotations to Ruby. Didn't get very far since I ran into some challenges with the internals of the parser and the parser library, Ripper. I'm extremely interested in seeing how the Ruby team designs the type system. It'll be gradual, of course, but also it'll be interesting what adaptations they'll have to make to accommodate existing code. JavaScript relied on a lot of stringly typed code, so TypeScript added string literal types. Perhaps Ruby's dynamic, block oriented style could lead to some interesting decisions in the type system.
Not to mention, the types will most likely be reified as per Ruby's philosophy.
Super excited for this. Between the JIT and types, Ruby could definitely see a renaissance in the near future.
And +1 on the Ruby renaissance! Super excited about all the exciting things that are currently being built!
Keep in mind, Ruby development is headed towards a goal that the dev team has called "3x3" as in Ruby 3 aims to be three times faster than current Ruby implementation.
I was disappointed to find out that adding more types in Perl6 actually slows down performance.
I wonder what the differences are that adding types in one language speeds it up, while adding types in another language slows it down.
Quite a few large companies have found themselves in this situation: Very large codebases in a programming language without types stop being fast to develop in. Then you get to either rewrite everything, with the well documented risks, or start doing all kinds of other things to make programming safer, like banning certain parts of the language, until eventually dedicating a team to improve the language is the most cost effective way to go.
In this case, I am also pretty certain that the interaction with data started having informal types a while ago too.
What I find really interesting here is that what starts as a library to help a single company handle the subset of Ruby they were using in the first place now aims to be good enough for general purpose Ruby outside of said company. It's one thing to have problems with an experimental, home-made thing, and just get support via slack, but adding this to the language has a far higher barrier. This is also probably the reason it's not OSS yet: The code that is enough for production use in Stripe's approach to Ruby might not be the greatest in a random codebase with different opinions on how many dynamic methods you want to have.
So it's not that a team decides to add types to Ruby instead of just picking a language that already has the types: It's solving a private problem and, a while later, realize that accidentally the solution is very close to being good enough for the language.
Speed is not the first and foremost benefit of types. Type checking is (and other stuff that comes with that, like better completions, self-documenting code, etc).
One of the reasons why Sorbet does both runtime checking[1] more than just static checking is so that we can know that signatures are accurate, even when a typed method is called from untyped code.
If the signatures are accurate, a future project could take advantage of method's signatures to make decisions about how the code should actually be run. If the signatures lie, then any runtime optimization made using the types would only be overhead, because the runtime would have to abort the optimization and fall back to just running the interpreter.
In certain basic blocks typed ints or floats can be unboxed, if they will not escape. This is what php7 made 2x faster. the stack will get much leaner. simple arithmetic ops can be inlined, using native variants. ops with typed vars cannot be overridden.
Another feature of even optional types is creating uniformity to allow JIT optimization. A great real-world example of this is Typescript or ReasonML. It's converted to JS, but still winds up faster on average. The JS JITs have multiple tiers of optimization. Changing data types and function signatures are the biggest performance killers. If you can ensure a list is always strings or numbers, then the optimizer can reach the top tier of optimization. When lots of people work together on untyped languages, there tend to be small changes in the signatures and structures that drop you out of that top optimization level. Even partial types are useful for preventing this.
Related to that is the potential for runtime type warnings. Even though the types aren't used by the JIT, it should be possible to give a warning message if the received types don't match up. That could be a huge assistance in finding where a bug is located.
now that ruby has an actual jit compiler, it could benefit from typing to optimize code further. And a gradual migration process will help people speed up parts of their code. Unless they mess it up like python where abstractions are costly.
One of the major things that has kept me using Groovy over the last 10 years was the reluctance to leave optional / gradual typing behind. Now, nearly every major dynamic language has given in and introduced types, so it seems like this idea of hybrid dynamic/typed languages is now fully mainstream. The problem of course, is they are all built on a legacy of untyped code, not to mention giant communities of people with no culture or habit of tying their code. So it's not clear to me that any amount of added language features can actually compensate for that.
Some languages are better from a static POV, and offer some auto features. Some languages are better from a dynamic POV and offer some hinting feature.
You don't want to type your code to do data exploration and analysis, but you may want to extend the original project later to something bigger and move on to types.
There is no such thing as the perfect language for everything anyway. Plus, it's very good that some languages integrates unnatural features to them, for the case where you want to go beyond their initial best case scenario. It won't be perfect, but I don't need perfect, I need programmatic.
The world of programming is vast, the pool of programmers very heterogeneous, and the constraints are super diverse.
People tend to forget this all to easily. For example most of the static type discussions for the past 10 years have taken place on a website built in a dynamic programming language, I'm talking about http://lambda-the-ultimate.org/ which afaik is built in Drupal (i.e. PHP).
You could theoretically do all of that before anyways with clever use of reflection, but this makes the compiler create all of that extra code for you from what looks like normal code.
Best use Groovy for dynamically typed scripty stuff only, and a JVM language built with static typing from the ground up for building the actual systems, such as Java, Scala, or Kotlin.
That could be the best of both worlds.
I have said for awhile that "Ruby with types" would be my favorite language to work in. I recently returned to Ruby briefly and had to integrate with a poorly-documented API. I spent more time digging through third-party code trying to figure out what certain parameters were supposed to be than writing the program itself.
* Can I emit typed REST API docs out of sorbet types?
* Can I coerce HTTP params out of sorbet types?
* Can I emit ActiveRecord columns? ActiveModel validations?
* Can I emit generative tests?
You can do all of those (and whatever else you imagine) with clojure.spec in a DRY manner, i.e. types are defined once, and reused in a variety of contexts.
As a Rails dev, I would greatly value all of those, particularly because they're practical things directly related to my webdev activity. Ensuring the type safety of the codebase is great, but also implicitly exercised by an adequate test suite.
Meanwhile, I've gotten myself more and more into Clojure. Which now that other dynamic languages seems to move closer to types, seems to be in a niche in that Clojure is moving further away from types.
It'll be interesting to see what happens at both extremes and in the happy middles.
What about clojure.spec?
Clojure seems to have double downed on dynamism and runtime construct, away from static types. It seems to have made the bet that better software (less defects, cheaper to maintain and extend, more targeted to the users needs) is better achieved through:
* Simpler primitives * Immutability * Interactive development * Higher level constructs * Data driven DSLs * Generative testing * Contract specifications * Data specifications
Which are all very good ideas, but they're non traditional compared to formal static type systems and proofs.
They're used to be more drive behind these in the past, Common Lisp and Eiffel embody a lot of these ideas, but miss on others. So Clojure is like a new take trying to fit in all these ideas of interactive, dynamic, safe languages together a new.
And I just find it interesting, because it is counter current. As others have pivoted back to static types, Clojure went all in on dynamism.
Time will be the true test, and I'm looking forward from the learnings in all directions.
I also feel that CL and Racket have embraced types a lot more. Doesn't CL have a fair bit of static typing already? And with Racket, Typed Racket has pretty much pioneered the concept of gradual typing now being applied to JS, Python and Ruby.
I know Racket also explored contracts, and has a lot of great ideas. But I feel overall it's missing the: "and we dog food it all on real business use cases in production" aspect that Clojure has.
And for CL, it doesn't seem to have as much in terms of contracts, data DSLs, immutability, simpler primitives, etc. It feels more like a traditional mutable, OOP, dynamic language. It has nailed down the interactive development part though. I don't want to put it down as I'm interested to try more of CL, but overall, it just doesn't seem as active or opinionated anymore. If anything, CL seems to lack any form of opinion, and goes more for the: we just add all features of every other language. Which is a quality on its own, but not driving the discussions forward either.
For example, Java is pretty terrible at type inference (still) and you have to annotate types almost everywhere (Java 8 had a very tepid improvement on that front.)
But languages like Haskell and Rust are very good at type inference, and you almost never actually need to specify the types.
It's still good Haskell style to always annotate the type sigs of top-level functions. Why? Because they serve as more than just hints to the compiler: they are part (and a very important part!) of the documentation. That is why they're in-line. Because A function like
zipWith:: [a] -> [b] -> (a -> b -> c) -> [c]
tells you what it does in its type signature.Java 10 and 11 introduced real type inference, at least for local variables and function parameters.
Half of the documentation will be in the header file and the other half in the implementation file and you will have to edit two files for every tiny change you make. No thanks. Types are part of the code and should be as close to the code as possible to reduce any possible source of friction while editing.
By now I think it is quite obvious that type annotations aren’t as helpful as initially expected and that a library approach seems more pragmatic and more powerful. See Clojure + Spec.
The thing is dynamically typed languages with type annotations tend to no longer feel like dynamically typed languages as the annotations and the tooling spreads and spreads and spreads. Not easy to put up boundaries.
PHP started added type hinting (aka specifying types for function arguments) in 5.0.0, back in 2004. Dartlang didn't exist until 2011, TypeScript until 2012, and Flow (I assume you mean the FB tool) didn't exist until 2014, as best I can tell.
>By now I think it is quite obvious that type annotations aren’t as helpful as initially expected
My only take away from this is that you obviously haven't used PHP's type system.
There seems to be a never ending cycle of new languages that are dynamically typed because it is easy for small codebases, which then become popular, get large codebases and then realise that static types are actually a really good idea.
Python, Dart, Ruby, JavaScript (via Typescript), etc...
“foo”.class # => StringWhy the privacy? Are programmers too dumb to understand something is a beta?
What if in the end adoption is marginal and Ruby Core's time was wasted?
Best adoption is organic, not hyped up.
The problem is that most popular dynamic languages are really quite terrible. They have atrocious runtime environments and usually quite limiting language semantics.
I agree that most popular dynamic are quite terrible. But, honestly, I think the real problem is not the particular implementations, but the whole idea of dynamic typing. At first it did make sense, but now that compiler writers have figured out "cheap" and general type inference, I don't see the point anymore.
However, I use Python on a daily basis because I have no decent alternative for the libraries I use.
Check it out. https://github.com/hernanwilkinson/LiveTyping
But Ruby used to advocate for them, and it's definitely what drew me in. I find it disappointing that we're moving away from that. More and more, it seems we’re attempting to make Ruby all things to all people. Which eventually makes it the right thing for no one.
If it's not, why not leave these solutions in gems?
Btw, I don't think static typing alone is Ruby becoming all things to all people. In recent history, it's also aliasing `Enumerable#filter` to `Enumerable#select`, numbered block arguments, a shorthand special notation for `Object#method` -- it feels like a trend of "hey these other languages do this, we should too". I'm not convinced that's always the case.
This might be misleading. That is, jump to around the 29 minute mark where he talks about the type profiler and .rbi file stuff.
also for some reason homebrew really likes to updates its index all the time (I think it got tamed in the newest version), but setting HOMEBREW_NO_AUTO_UPDATE to 1 helps a lot.
Ruby 3 static analysis will have four items:
1. Type signature format 2. Level-1 type checking tool 3. Type signature profiling / prototyping tool 4. Level-2 type checking tools
and so on. [1]: https://docs.google.com/presentation/d/1z_5JT0-MJySGn6UGrtda...
- https://sorbet.run/talks/RubyKaigi2019/#/53
- https://sorbet.run/talks/RubyKaigi2019/#/55
So I don’t think that the divide will be at Rails. And more than that, I think there will be very little divide at all. Sorbet is designed to be gradual, so it works 100% fine with untyped code:
But I personally wouldn't hire someone who maintained that dynamic typing produced as good results and was reasonable for an even mid-sized project. They've either never had a long running project or they've never dealt with a big enough code base at that point, or they're simply being dishonest or lack self awareness. None of those are good signals. Not having worked on a project that goes on for long enough is fine, but having opinions on software maintenance in that case is foolish.
sig {params(name: String).returns(Integer)}
... why not simply: sig {name: String, returns: Integer}In this case, your example is not valid syntax, which violates this rule. Not that I personally could tell you why the parser makes a distinction here, but it's at least part of the reason :)
irb(main):010:0> foo {a: "b"}
SyntaxError: (irb):10: syntax error, unexpected ':', expecting '}'
foo {a: "b"}
^
(irb):10: syntax error, unexpected '}', expecting end-of- input
foo {a: "b"}
^
from /Users/bhuga/.rbenv/versions/2.4/bin/irb:11:in `<main>'
irb(main):011:0> foo {params(a: "b")}
NoMethodError: undefined method `foo' for main:Object
from (irb):11
from /Users/bhuga/.rbenv/versions/2.4/bin/irb:11:in `<main>'
irb(main):012:0>
The `sig` syntax has gone through multiple iterations; within the boundaries of Ruby syntax this is the best we've had. sig {params(name: String).returns(Integer)}
... why not simply: sig [String]=>[Integer]
Yes, that's just ruby - see https://github.com/s6ruby/ruby-to-michelson/blob/master/cont... for example for live running code (in secure ruby) :-) sig {params(new_value: T.nilable(Integer)).void}
... why not simply: sig Integer? # or
sig [Option.of(Integer)]=>[] # longest form in sruby
sig [Integer?]=>[] # same as Integer?It was a design decision that all type annotation arguments be named as opposed to positional. As one example why, it makes the error messages better. You can always say "You're missing a type declaration for parameter 'foo'" as opposed to "You have four positional arguments and 3 types".
We could probably still bikeshed our annotations inside the `sig { ... }`. I'm not sure we'd make constants with unicode like BigMap‹Address→Account› for generics, though, how do you even type that? :)
If not then why are you using a dynamically typed language? If you're not using it's abilities then it doesn't sound like the right tool for the job.
It's like a cost/benefit analysis where none of the benefits you're using, and the cost is the total inability to validate, refactor, and navigate your code base.
Yes, yes, and no. I do most of my work in languages that prevent the first two, but when I do have access to this kind of runtime trickery I do use it when useful.
The issue is not "Do you purposefully do those things?", but rather "Do you have a call stack where you can't guarantee it won't happen by accident?" Type checking is not relevant when you know what will happen and want the dynamic/ducktyping behaviour.
Another issue is: I'd use a different framework which doesn't use Ruby, but this was the most productive framework at the time the codebase was started, and nobody will port that many lines of code to a non-dynamic language now. So the best course of action is to validate the current code is not overly-dynamic.
Will be interesting to see how ruby handles types vs duck typing etc 10 years from now, when the new best practices have been figured out.
I guess some companies started fast with Ruby/Python and similar and instead of rewriting to static/typed languages they pushed forward features that would allow them to just continue where they left off at the expense of having a more concise problem oriented programming language that's good for solving specific problems.
One buys into eco-systems, not language features bullet point list.
And there isn't something like an universal eco-system for any kind of business case, hence multiple languages.
I'm personally tired of staring at variables trying to figure out what they're supposed to be, then having to dive into source to see how its used. C/C++/C# solved that problem, why are we still dealing with it?
Depending on your type system, a well-typed program can eg run faster, because the compiler / interpreter can elide certain runtime safety checks that would be necessary in untyped code.
If your type system is crazy enough, you can even track the runtime complexity of your program at the type level, including whether your program runs in finite time. See eg Dhall (https://dhall-lang.org/) whose type systems only allows programs running in finite time.
But honestly, if you're asking your IDE to do it, that means you're asking your IDE to do static analysis of your code - and type-checking in a lot of ways is just another static analysis technique. And for a lot of us (myself included) we prefer to catch as many of these bugs as possible using static analysis, instead of waiting for someone to get paged when it causes an outage.
Yes, there's a trade-off, and types can be obnoxious (Java imports being probably one of the worst offenders, C++11 introduced `auto` for a reason), but that's the cost we pay.
So, please, be brave and evaluate a 100kloc codebase+deps that may contain a top level `rm -rf ~/`
Now the JS fanboys discovered and moved (from Coffeescript to Babel ESxx) to Typescript they apparently think that they can write beautiful and bugfree software just because of static type checking! Let them please move to C++ or whatever statically typed language and shoot themselves in the foot by making all those mistakes that have nothing to do with static type checking at all! Oh, and of course hitting the wall because they are missing their precious 'any' keyword!
I totally agree that type checking for dynamic languages should be done in the IDE, tooling. But static typing in the dynamic language world is a hype at the moment, so we'll have to go through a wave static type checking frenzy. For my work I look at horrible code bases, perfectly typed and strictly formatted by tslint..
But it is always an incomplete solution because a) old code needs to be retrofitted, see TypeScript's way of defining type maps for vanilla JS or b) more commonly you keep the code around that's using unsafe types, effectively passing void*|Object|"choose your poison" around.
Classes are types by default, but you can define non-class types as well: https://sorbet.org/docs/abstract
Having said that as far as I understand, type support in Ruby 3 will not prescribe which type checker is used and what limitations exist. Some of the mentioned projects are structural and I think even Sorbet might add support for it at some point.
In a duck-typed language, type is defined by the willingness of a message receiver to receive that message. Class, inheritance, composition are all means to achieve this, but the type of an object is determined by its signature, not its ancestor chain.
The earliest possible date (and somewhere I read it's a probable one, but I can't find it right now) is Christmas 2020.