Hacker News new | past | comments | ask | show | jobs | submit login
Ruby 3.2’s YJIT is Production-Ready (shopify.engineering)
481 points by onnnon on Jan 17, 2023 | hide | past | favorite | 286 comments



I've set this up in a staging environment of one of our apps to take a look. The staging environment we use for one-offs is on Heroku (can stand one up/down quickly), and the first issue is that a lot of the 'easy to deploy is a feature' PAAS platforms is that they bill by web/worker size restricted by memory rather than just pure virtual CPU power. Render etc all does this as well, and the memory headrooms are low.

This means that you often have to deal with, say 512 MB per instance, and then if using something like Puma, have to work out how to split concurrency vs memory footprint. What I'm finding is that v3.2 YJIT loves memory, so I have to trade off that, which means less concurrency per process. Benchmarking it quickly, the 15% gains I might get on the thread aren't worth having to move to just 2 threads on a 512 MB instance versus the 3 threads I can get with YJIT not enabled.

I think it's really neat and will continue to track it, but the performance in terms of memory trade-off isn't there quite as yet for our app profile. Not sure if others will find the same, but I guess its if their production environments aren't PAAS's with low memory headroom or not.


OT(ish) but a t3.medium on ec2 with 2v cores and ~4gb of mem is $33 a month ondemand. $20/mo on ri, and $10/mo spot.

I'm mostly done with Heroku. With one kinda big app left, all other envs, and projects are now just on aws without the tax of heroku.

And I was a big heroku fan, but their recent decisions made me shop around. I do miss the price $$ of metal in a dc, but not the price :clock: of metal.


Have you considered fly.io?


yeah. but their 2cpu with 4gb of ram aint competitive enough at ~$22/mo. That and swearing off vendor locked ci, switched to Terraform and Capistrano.


you can use dokku + hetzner. cheap prices(https://www.hetzner.com/cloud)


I start to have less love for technical pride languages and love business backed languages like Ruby more. It also means wide ranges of library, more jobs. Great or elegant niche languages put me in bad position so far. (Also sick of developer complex)


I'm with you on this as I get older. 'Been fascinated with Clojure for years but now sick of being out in the cold/on the fringe as it hasn't really converted into anything which brings home the bacon so I've turned to Java and Javascript lately. Will even do PHP if necessary if it means more paid work as a solo dev.


Hence why I embrace the FP stuff that lands on Java, C# and co.

It might not be as nice as having the original package, but it is good enough approximation while using boring technology (TM).

I leave the cool languages for hobby coding.


I understanding where you're coming at when I use Mastodon and see how RoR has enabled it but at the same time I cannot avoid thinking how much money the poor maintainers can save if it was written in Go or Rust.


Glad to see Ruby still going strong it will forever be the 1st programming language I actually loved working with, and the combination of it with Rails was amazing. Had to move to the other side (Typescript, Django) but I think unless ChatGPT will write all the code 10 years from now - Rails and Ruby will still be there, providing teams with a lot of power and a lot of fun.


Funny that you say that. If AI writes code, it should be Ruby, because it's shorter and easier to read. I'm making a site and love ChatGPTs Ruby code.

PS: Any Microsoft people here? Ruby could be 10x faster on Windows, if we figure out how to load files faster! https://bugs.ruby-lang.org/issues/19325#note-11


Ah I meant if the AI is so capable in writing code there won't be any need for a human to read the code anymore. The "code" will be a bunch of english requirements, like an essay describing the program. Its unclear what language ChatGPT would use in that case (and who cares if the human is no longer important?).


If an AI were meant to write code it would be Python. That is english with very few syntactic sugar mainly parens and colons.

Unless the result wouldn't be meant to be read nor understood by anyone.

IMHO Python is the closest to pseudo-code specifications.


Rails is like that first college girl/boy friend one had. Long gone, due to practical considerations, but that their fragrance, memories and love you felt for them then, is still buried somewhere deep in your heart.

Rather troubling experience are those bad days with your current real one, and you suddenly remember those sun kissed, beautiful days with your dearest ruby :D


This is awesome. I am impressed.

I recently wrote a toy compiler for x86_64. I use an external assembler.

My dream is to write a JIT compiler runtime.

https://GitHub.com/samsquire/compiler

My understanding is that you generate machine code and then mprotect the code to be executable then jump to the void * as a function pointer to execute generated instructions.

What I would like to understand more is tracing compilers and how they optimise hot loops. And how they deoptimise and optimise at runtime based on runtime information.



That looks so cool.

Do you have any resources you would recommend to someone who want's to explore (or develop as you have) in this subject?


Write a C program that does something interesting or useful then compile it with -S and inspect the assembly.

I am a beginner at assembly and learned everything from GCC.

There's so many details. I learned about rbp+rsp registers recently. And from Reddit post on the compiler someone told me I need to keep the stack aligned.


I'm wondering about what impact would this have in Basecamp's hosting bills...


If you look at the breakdown per service, EC2 (assuming mostly Ruby) seem to be at best 20% of the bill: https://twitter.com/dhh/status/1613508201953038337

So if you are to make Ruby ~10% faster, you might reduce Basecamp hosting bill by 2% at best.


> Remember, this excludes the current Basecamp and Basecamp 2, which use our own hardware for this

https://dev.37signals.com/our-cloud-spend-in-2022/


Thanks for spotting that, I had missed it! This goes to show (again) that the "Ruby/Rails does not scale" myth is fictional.


There are organisational scaling issues that you may hit with a language like Ruby that strongly typed languages like Java can solve.

And so Ruby may scale in cpu performance, but not with your number of teams.

That’s why Spotify switched to Java (I think they were a heavy Python user before that, not Ruby, but not sure anymore - but the organisational problem is the same for those two).

Note that I still choose Rails for all my projects, but my company is small.


> There are organisational scaling issues that you may hit with a language like Ruby that strongly typed languages like Java can solve.

Such as?


This also interested me.

I'd guess that the idea parent is trying to convey is that for a given size of a Ruby codebase the engineering organization needs to be structured like successful cases like Github, Shopify or whatever, but I'd be interested in the technical aspect to this. I agree with this idea when reasoning about microservices, but this is the first time I've seen it applied to the choice of language. It's intriguing.


Judging by parent poster's comments everywhere, they have an habit of saying such facts without much substance. I wouldn't hold my breath but I'm interested as well.


I would be super interested to read about how many servers they run, etc.


Does this work put Ruby into a faster category than Python until if/when Python gets a JIT in its official implementation?


If you believe the language benchmark game, Ruby is faster than Python on many of the microbenchmarks: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

And the benchmark game is using Ruby 3.1, whereas 3.2 is significantly faster. YMMV though, it is going to depend on your use case, but we are always working on making Ruby faster, and if you run into a use case where Python is a lot faster, you can ping me on twitter @Love2Code and tell me about it. We'll take a look.


Python 3.11 also claims [1] to be 10-60% faster than 3.10, which is the version that the benchmarks game is using at this time. The difference in used RSS is also quite interesting in this comparison.

  [1]: https://docs.python.org/3/whatsnew/3.11.html#whatsnew311-faster-cpython


> The difference in used RSS is also quite interesting in this comparison.

YJIT 3.1 was eagerly allocating it's executable memory region regardless of whether it was needed or not. With the default settings that meant an extra 256M of RSS right at boot.

As mentioned in TFA that's no longer the case in 3.2, so expect this RSS number to go down by at least a good 200MB once they upgrade to 3.2.


fyi now

Python 3.11.1


If only someone would contribute programs that used Ractors.


fyi now

ruby 3.2.0 +YJIT


Ruby was already slightly faster than Python in many micro benchmarks before 3.2 and YJIT, so maybe? It depends on your workload.


I thought Shopify wanted to migrate to TruffleRuby which would likely give them at least a 2x speedup?


Tragically, one of the principals behind TruffleRuby (and researcher at Shopify) was Chris Eaton and he passed away a few months ago.

I'm not sure where this leaves TruffleRuby itself, or plans to implement it at Shopify.

(Rest in peace, Chris, and may your memory be a blessing to those who knew you)


You have his name slightly wrong, it was Chris Seaton.

Here was the announcement:

https://twitter.com/flavorjones/status/1600436490885947393


thank you for the correction! typing with a hand injury today :-/


There are still folks working on TruffleRuby at Shopify.


so sad, I had no idea. We lost a brilliant mind...


Chris Seaton


Thank you.



With due respect to the tireless efforts of the TruffleRuby team TruffleRuby always seemed fated to be forever almost ready. I first came across it in 2013, I think it was, but there was always a not-quite-ready factor. If I remember the show-stopper was that it was unlikely to ever be Rails compatible but I may be wrong.


https://ruby.social/@eregon/109280839376585363

Tl;Dr it runs rails for a while already, it even runs mastodon.


Oh, yikes, I didn't realize this. Felt like I had seen him around on the site just yesterday :(


You can enable YJIT and get a ~30% speedup by passing an extra environment variable to your Docker container. Moving away from CRuby to an alternative Ruby is a lot harder than that.

Also Oracle.


> Moving away from CRuby to an alternative Ruby is a lot harder than that.

In what way? Truffleruby passes over 97% of CRuby's specs and it runs on my command-line without any problems so far. It was easy to install too. I'm sure if I dig into those failing specs I'll find something but will it be important? I'd love to know.


From the Truffleruby repo [1]:

> Regarding performance, TruffleRuby is by far the fastest Ruby implementation on the yjit-bench benchmark suite which includes railsbench, etc. To achieve this performance TruffleRuby needs a fair amount of warmup, as other advanced JIT compilers do. If you find any performance issue, please see this guide.

One benefit of YJIT + cruby is it doesn't have the same warmup costs. If you're deploying many times a day, this JIT warmup becomes a dominant factor.

[1] https://github.com/oracle/truffleruby


It’s probably not hard for someone to run the interpreter and try a few things out - but large private code bases are where the struggles would begin I imagine.


Sure, but the Devil's in the detail, especially when there's nothing obvious.


> Also Oracle

Very little of Graal/Truffle is locked behind an oracle license. The community edition is FOSS and within 5-10% of the performance of the paid version (so still an order of magnitude above MRI with or without YJIT). The situation is very similar to HotSpot/OpenJDK.

https://www.graalvm.org/community/opensource/


Shopify invests in both.

Source: I work in a team that's nearby as the org chart flies.


I don't quite understand the YJIT excitement. A few benchmarks show that TruffleRuby generally beats YJIT, sometimes by a 10x margin.

[0]: https://eregon.me/blog/2022/01/06/benchmarking-cruby-mjit-yj... says


And by many accounts Oracle makes the fastest database yet everyone still uses Postgres.

Also our experience with truffleruby is death by a thousand subtle differences. It might be 97% compatible but chasing down that 3% undocumented behavior difference on every minor version update for every gem got exhausting fast. Everyone uses and tests against cruby.


I have only seen one set of benchmarks, apparently posted by someone willing to tempt fate by violating the benchmark ban of the license, which compared them and in those PostgreSQL crushed Oracle. Of course those could have been misleading or straight up lies but it is not at all a known fact that Oracle has better performance than PostgreSQL. And the database consultants I know who have worked on both seem to think performance is roughly equal.


Yea, I’ve always seen Oracle’s big selling point is the admin tooling that they provide. At least that’s what I’ve been told.


I've always seen their big selling point as it being required by other giant software packages. At least that's the way it threatened to enter burrow into various places I've worked.


> by many accounts Oracle makes the fastest database

I've never seen that credibly claimed. Why would their license ban benchmarking if their performance was any good?


Their licensing does not ban benchmarking; it only bans publishing the results. A subtle but important point.

By all means, run benchmarks on your workload profile and choose the technology that works best for you - that is perfectly OK. But not make claim that xxx beats oracle in yyy.

Oracle goal here is to make a good enterprise ready DB solution; not to fight the open-source community on hundreds of different performance claims, half of which have been specifically engineered to put the competing, leading product in a bad light.

You might not like what oracle does or how they do it, but it’s clear that they have a target on their back and sometimes the best way to play the game is not to play at all.

Just look at the « independent and fair review » that Top Gear made about Tesla.


> Oracle goal here is to make a good enterprise ready DB solution

Maybe. Or maybe it's to soak naive clients for all they're worth. The remedy for bad benchmarks is good benchmarks, not no benchmarks.

> Just look at the « independent and fair review » that Top Gear made about Tesla.

What about it? It was accurate and informative (unless you think Tesla lied about their range stats). The good points about electric cars are real but they don't mean we should ignore the downsides.


It was later shown that the whole part were the Tesla goes out of power was staged.

The car did inform them it would run out of battery on the way. They made a segment about it and failed to inform the viewer that they had ignored several warnings given by the car before reaching the « oh no we are out of battery in the middle of nowhere » point.

This is not what I would qualify as a « fair review ». There are no « false claims » (they did run out of battery, after all !) but the information are given and presented in a way that paints the reviewed product in a specific light.


The way I remember it it was the other way around. They had the voiceover saying "and after x miles..." with a video of their crew pushing the Tesla into their pit lane. Tesla's argument in the lawsuit was that based on their telemetry they hadn't actually had zero battery when filming this, which seems beside the point when they quoted the mileage number from Tesla's own materials.


> The remedy for bad benchmarks is good benchmarks, not no benchmarks.

The comment you replied to just said Oracle allows running benchmarks internally, though.


Sure. How likely is it that someone running bechmarks internally - who is unlikely to be a benchmarking expert - is going to do a better job than a dedicated benchmarking project like TechEmpower that has a whole lot of people looking and contributing?


TechEmpower accepts a wide variety of contributions without much vetting. It allows quite a lot of benchmark gaming. The mantra of every good engineer about benchmarks is, 'Don't trust others' benchmarks, do your own with realistic workloads'.


Not everyone, other than for a specific customer project, I have mostly worked in SQL Server or Oracle, and slowly approaching 50 now.

There are still quite a few enterprise workloads that only the likes of stuff like DB2 are a match for them.


What? When was oracle free or opensource?


and when was oracle the fastest database? You are not allowed to even publish that metric....


I do have some sympathy for companies who don’t want to have to deal with a constant barrage of benchmarks that are egregiously wrong.

HN has been host to quite a few of these spats over the years and I wouldn’t want to deal with it, there are pretty much only downsides to letting people poorly benchmark your product and have every incentive to get it wrong.


well to be fair, most benchmarks are of course not comparable with the real world. People should always use what works best for them.


TruffleRuby can’t run production Rails traffic today, and YJIT can.


The README states "TruffleRuby runs Rails". Anything specific?


It passes most (maybe all?) of the Rails test suite. But it can't run a randomly-selected production Rails application.

This was based on my conversations with Chris Seaton before his passing, about 6 months ago.


That doesn’t sound like a very good test suite then.


i'm pretty sure eregon is biased...


The core team is split across oracle and shopify. They are still actively interested in it, even for research purposes.


I'm still a shameless ruby fanboy at heart. To be fair, its almost all devops type tasks but there is something about that language that just clicks with me, and while I def have to use python more than ruby, I still love ruby.


I'm pretty sure (there isn't a lot of info on how to enable it in production workloads) we're running YJIT in production and didn't notice any improvement in load times, CPU usage or anything really. If any slightly higher metrics across the board.

We're using Rails as API and web frontend with a pretty high number of requests per second, so I was hoping we'd see something. Does anyone have any experience rolling it out?


    Does anyone have any experience rolling it out? 
FWIW, the linked article has prod benchmarks.

    I'm pretty sure (there isn't a lot of info on how 
    to enable it in production workloads) we're running 
    YJIT in production
Sure you're running it? You have to compile with YJIT support, and then pass the command line arg. (It doesn't support YJIT out of the box because, I presume, they didn't want to force a Rust dependency on everybody?)

Here's how on MacOS + asdf, others will be similar. Note that I think --enable-yjit might work (but do nothing) even if you don't have support compiled in.

    # if you already installed 3.2.0
    asdf uninstall ruby 3.2.0
    
    # install Rust
    asdf plugin-add rust
    asdf...      
    
    # now install Ruby 3.2
    asdf plugin-update ruby
    export RUBY_CONFIGURE_OPTS=--enable-yjit
    asdf install ruby 3.2.0
    asdf global ruby 3.2.0

    # verify it's installed and ready
    ruby --enable-yjit --version

    # now run your workload
    ruby --enable-yjit foo.rb


Yes, I'm pretty sure because:

1. Compiled with jyjit confrmed (ruby --yjit) returns the correct value

2. RubyVM::YJIT.enabled? returned true

But the information available on how to confirm YJIT is running is not super clear. Since I didn't notice any improvements I started wondering.


Sounds like you're running it!

    Since I didn't notice any improvements I started wondering. 
It only speeds up Ruby itself. In a "real world" web app performance, performance is mostly dominated by Ruby/PHP/Java/whatever sitting around and waiting for database queries to execute.

So for a lot of web workloads the perf increase won't be huge. If your average endpoint is e.g. 5ms of Ruby and 500ms of DB queries, your max speedup will be negligible if you switch to YJIT... or even if you rewrite the whole app in hand-optimized assembler.

(Also gets to the root of why perf-based criticism of Ruby is usually dumb, IMHO. It's usually not the problem)


There might be a number of reasons why YJIT isn't effective in your application.

The best way to know it to enable YJIT runtime statistics [0] to see whether YJIT is doing a lot of side exits and if so why. But it requires a bit of YJIT knowledge to interpret them.

It's also entirely possible that your application isn't CPU bound in the first place, or that it's bottlenecked by things YJIT can't do anything about (e.g. GVL).

That's close to impossible to guess what it might be without being able to instrument the application.

[0] https://github.com/ruby/ruby/blob/df6b72b8ff7af16a56fa48f3b4...


The top comment in HN is one where the poster is not 100% is using the thing we are discussing.


> I'm pretty sure (there isn't a lot of info on how to enable it in production workloads

The YJIT is not on by default in ruby 3.2, you have to specifically enable it. If you aren't sure if you have enabled it... what makes you pretty sure you have enabled it? It seems possible you have not enabled it, if you aren't confident you know how to do so?

I am not using it yet myself, and don't want to put any possibly incorrect info here about how to enable it. I agree that it's not clear to me where to find this documented. I am pretty sure it is not on by default.


Also worth noting that the Ruby 3.2 Docker image can't run with YJIT yet, so if your production setup is in a container, it's most likely not using YJIT.

GitHub issue link here: https://github.com/docker-library/ruby/pull/398


It was supported immediately for Alpine based images because rustc 1.60+ was available in that distro.

Initially when 3.2 was released Debian based images didn't support it because Bullseye ships with a version of rustc that's too old to compile YJIT.

But https://github.com/docker-library/ruby/commit/6db728e addressed this a few days ago by installing a pre-compiled version of rustc straight from Rust and YJIT is available in Debian now.

You can confirm if it's available to use by running:

    $ docker container run --rm ruby:3.2.0-slim-bullseye ruby --enable-yjit -v
    ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [x86_64-linux]
If it weren't available then you would get an unknown argument error.


    $ docker container run --rm ruby:3.2.0-slim-bullseye ruby --enable-yjit -v
    ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [aarch64-linux]
Runs on Arm as well!


That's not correct:

    $ podman run -it -e RUBYOPT="--yjit" ruby:3.2
    irb(main):001:0> puts RUBY_DESCRIPTION
    ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [aarch64-linux]
    => nil


Yes, I'm pretty sure because:

1. Compiled with jyjit confrmed (ruby --yjit) returns the correct value

2. RubyVM::YJIT.enabled? returned true

I didn't notice any improvements at basically so I can't really say _for sure_ if it was working as intended.


RubyVM::YJIT.enabled? returning true does seem to settle it, and no need to say "pretty sure" with qualifications!


The gist of it is that you need to build Ruby with `RUBY_CONFIGURE_OPTS=--enable-yjit` true and verify it is enabled with `ruby --enable-yjit --version` and then finally run your code with `ruby --enable-yjit foo.rb`. For Rails you'd want to just edit the executable Rails scripts like `bin/rails` to include that flag.


Thanks! Is there a "canonical" place to find this documented, like with the ruby distro?

> For Rails you'd want to just edit the executable Rails scripts like `bin/rails` to include that flag.

Alternately, you can edit your shell init scripts to put the correct thing in `RUBY_OPTS` ENV variable, yes?

I would consider this preferable to editing executable rails scripts with implementation-specific stuff (and then hoping that the app is always launched with those edited shell scripts, which I'm not sure eg `bundle exec rails` will do). I don't think editing Rails scripts is probably the "right" or recommended way to do this.

But I'm not going to try to specify what you put in `RUBY_OPTS` since i haven't done it myself and don't want to spread misinformation! (Again, is this documented in a centralized trustworthy place? As I try to google it... I am somewhat frustrated with the ruby documentation situation!)


Perhaps this is what you are looking for:

https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md


thank you!


We're using puma and set the RUBYOPT=--yjit in a systemd environment variable for our puma service.

The performance impact of enabling yjit was quite obvious in our charts. Another giveaway that it's properly enabled is the servers will use consume more memory.


You can also set RUBY_YJIT_ENABLE=1 to enable it.


That’s what I ended up using:

passenger_env_var RUBY_YJIT_ENABLE 1;


It's been a sec since I've used Ruby. How's the typing story? Seems like Sorbet is doing quite well, but are there comprehensive typings for the ecosystem, like TypeScript? Because with ergonomic, comprehensive type checking and a JIT, Ruby might be a tempting option again.


The problem, I think, is the restrictions that not having a compilation step places on what typing you can do without a big breaking change. Typescript didn't have this problem because everyone already used bundlers/minifiers/etc. Adding another compiler like step to that process was pretty natural.

Ruby doesn't have that and adding it in poses problems for REPL development. They chose to go with a separate file for types. Which gets around the breaking change problem but the ergonomics are terrible. Think the hope is that IDE support eventually gets around that issue. But thus far not a whole lot of adoption.


Sure, this is an advantage that Typescript has, but it doesn't explain Ruby's lagging progress compared to Python and PHP.

Some of that can probably pinned on the greater dynamism of Ruby, making all manner of static analysis quite challenging.

But from what I've seen, it seems like the main reason is indifference or even mild hostility from the language maintainers. It looks a lot like they saw Sorbet taking off and slapped together a competing spec (without any associated tooling) that would ensure all type information stays outside of the `.rb` file.

It seems like the only impact it has had is to dissuade people who would like some static types from adopting it, in favor of this vaporware. Contrast this with Python's embrace gradual extension of mypy type hints.

I'd love to be proven wrong, that there are thriving projects building atop RBS. I'd still _prefer_ a system that allowed me to type a function signature so that anyone inspecting it could immediately know its shape, but at least the shadow types would be _something_.


I've seen more than one ruby dev proclaim they specifically don't want types; if you want a language that has it, there are plenty to choose from.

My take here is that typed-ruby is something SOME people want (I'm one), but a lot don't.

I'm not sure what level of force that provides to anyone who has either tried or is considering adding it to the language.


I agree that Sorbet is a better solution but it's also a breaking change. It's a relatively small breaking change and, IMHO, would have been more than worth it but alas it was not my decision to make.


Speaking for sorbet - no compilation step is a feature, not a bug. Imagine adding some kind of build step to every ruby app out there! Sorbet annotations are just valid ruby code you add to your files (you don't need to write separate type files - the sorbet system might do this for you though).

Sorbet also provides a gradual typing system where you can start adding types to a large code base and gradually make the checking more strict. Stripe claims to be using it on a huge ruby code base and that their developers generally like it.

However I think many devs don't want static types on Ruby. We like our duck typing!


Couldn't you still have duck typing?

If a method expects to be passed an argument that it can call "quack" on, can't you define a type that has "quack"? The actual argument could be a Duck or a Goose but as long as it has a "quack" method the type will be OK

I haven't used types in Ruby (or much at all other than dabbling in Typescript), so I might be missing something


This thread on Rubyists' impressions of the gradual typing options is relevant: https://www.reddit.com/r/ruby/comments/105sdax/whats_the_lat...

Spoiler alert: not a lot of fans


No, I'm afraid I'd say Sorbet/RBS haven't really been catching on widely.


https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet looks really cool.

You know the yjit folks are going to take this and make yjit even faster right?


Ruby's typing story is still the worse-than-modern-Java boilerplate of writing loads of unit tests just to make sure an object is the type that you think it is.


This is not true.

In Ruby, tests are the same you'd write in any other language. You test that things do what they are supposed to do, not the types of parameters or return values.


Ok but if you can't constraint the types passed to a function then the universe of objects you have to test is massive. For instance you may have a performance benchmark that checks that a certain operation runs in a certain big O -- say, you want to make sure a contains(collection, elem) functions runs in constant time. If you can't constrain the type of collection (to be some kind of Map, say) then you are left testing that all paths that call contain do so with a Map and not, say, a List. In a typed language the type system would ensure that for you.


Try sorbet in a real project before you make this claim. It’s not perfect but it’s pretty good.


That example is not very grounded on reality, isn't it?


Matz poisoned the well once sorbet was gaining traction by releasing a nearly useless competitor (RBS) and refusing to allow any interop.


I'm so grateful for these geniuses working tirelessly on improving the language and runtime, and it allows me to just add an argument and magically my code runs faster.

It's been fascinating following Maxime working on Yjit.


Can someone 'explain it like I'm 5' why someone would just JIT instead of compiled Ruby, or even what the difference is please.


It’s hard to generate efficient code ahead-of-time because the compiler doesn’t know what the types of values will be, so it has to emit very general code that can cope with all possibilities at the expense of performance. A just-in-time compiler has access to this type information because it’s executing at runtime, so it’s able to generate more efficient code which only works in the more specific situation that is actually happening right then.

See Natalie [0] for a work-in-progress Ruby compiler which works ahead of time. It’s a super cool project but it’s just for fun [1] so I don’t believe it’s trying to compete on performance.

[0] https://natalie-lang.org/

[1] https://justforfunnoreally.dev/


JIT VMs often rely on runtime tracing to form assumptions about a program, and then JIT native code that fits those assumptions. If/when those assumptions break, the VM can fall back to interpreting the original code.

That means there is no need to write a fully-featured Ruby compiler, instead you only have to emit native code in places/situations where it will have the maximum benefit.


What does 10% speedup mean? Doing 10% more work in the same time or needing 10% less time for the same amount of work?


10% less time for the same amount of work, since we are measuring the total response time of the workload in question and looking at median (p50), p90 and p99 numbers.


I have to admit my brain seems to be short circuiting on this one. Aren't these 2 ways of saying the same thing?


At 10% it's approximately the same. But say 50% instead.

If it takes 1/2 the time to do something, you can do double the work in that time.

If on the other hand, you can do 1.5x the work in the same time, it means you've made it use 33% less time than before.

With 10% less time you're taking 1/0.9 = 1.11111x more work in the same time.


Not exactly, "doing 10% more work in the same time" means needing 9(.1)% less time to do the same work (100 / 110 versus 90 / 100).

Needing 10% less time for the same amount of work means you can do 11(.1)% more work in the same time.


At it's heart they seem to be asking kind of a latency vs. throughput question. The answer is frustratingly "it depends" but for some real world Rails benchmarks, Shopify has published quite a few benchmarks including the ones in the linked article.


The percentage is applied to two different things. One for work and the other for the time.

It took sometime to me too.


That's some good news


For anyone thinking Ruby is dying or slow, it's not the reason people like me used it and sticked with it in first place! It's about the experience when you write the code itself. It's natural, like a flow of water, and you're suddenly in Zen mode, where your thought just naturally flow without even you're aware or not.

I first time learn Ruby from zero to "hero" in production confidently is in just under a week. And as far as i know, no other language could bring me such experience.


The main issue I have with Ruby / Python is the fact that it's duck typed, it makes the maintenance and refactor pretty hazardous.

You get objects you don't know what's in there, 6 month later someone changed it, no compile error but it will break when you run it.

And so to overcome those major issues they added really ugly stuff that is not core to the language, linters, annotations etc ...


It's not like strong typing came after Ruby. In fact, duck typing was a _feature_ of Ruby on its days. At the end of the day, you choose your battles, it's not a black-or-white decision.


Static typing was a nightmare (by modern standards) in the 1990s. Dynamic scripting languages were such a breath of fresh air that we were willing to pay the steep performance penalties without hardly a thought. Sort of like the saying "deleted code is debugged code", code that you actually write always performs better than the code you didn't write because it was too soul-sucking.

But static typing languages, library stacks, and environments have come a long way in the last 30 years. Now I prefer even to "prototype" in static languages because the crossover to when they start providing me net benefit is around a week or so into my development process.

I will not claim one is completely better than the other, but the balance is a lot more even than I would say it was ~20 years ago.


> Now I prefer even to "prototype" in static languages because the crossover to when they start providing me net benefit is around a week or so into my development process.

I've moved from Ruby to Crystal as much as I can, and at first I found it painful but now I appreciate the myriad complaints from the type checker. It's eliminated 99% of the specs I used to have to write.


Actually almost all the stuff that's gone mainstream in the last 30 years was already around in the '90s if you used OCaml (or indeed Haskell). But in fairness not many people did.


Even the languages that were painful in the 1990s have gotten better. Some of it was the system libraries, some was just the lack of experience in API design, some was just a lack of awareness that better was even possible. While I don't like C++ at all, certainly C++ in 2023 is a lot more tolerable than C++ in 1997.

I learned programming in the late 1990s. Looking at Windows programming at the time looking like gibberish to me and I looked forward to the day I would understand it. Honestly, it still looks like gibberish to me. I understand the whys and the wherefores better now and if I had no choice, yeah, I could learn it and write it, but the code from that still reads absolutely awfully to me, just unbelievably bad by almost every modern standard. Even when you're writing approximately equivalent code in C# at the same low level of acquiring handles and manipulating them with very low level APIs, it's just so much better now.


No, but gradual/optional typing did come after Ruby. Back in 2005, you could choose between static typing with type checking but lots of verbosity (e.g. Java "Point point = new Point();" or C++ "for (std::map<int, BlaBla>::iterator it = myMap.begin(); it != myMap.end(); it++)") or dynamic typing without type checking but also without the verbosity. Since optional typing, dynamically typed languages got the guarantees of type checking, and JavaScript/TypeScript and Python have embraced this. With type inferencing, statically typed languages lose their verbosity, and Java and C++ have embraced that (as have newer languages like Rust, Go, and Swift). It feels like Ruby hasn't progressed on this front...


Ruby does have optional type annotations, if you want them:

https://github.com/ruby/rbs


I'll pedantically remind the audience that type inference dates back to the 70s and has been present in the industry at least since the 90s.

But in truth, you're right. Very few people know/cared about type inference at the time.


> Since optional typing, dynamically typed languages got the guarantees of type checking

Optional typing, at least in TypeScript, will not give you the guarantees of type checking. It finds many of your type errors, but not all of them because the type system is deliberately unsound.


Isn't RBS for Ruby a way of having optional typing? (Not challenging here, honest question :P)


I’m not aware of anyone using it, we evaluated it at work and decided it wasn’t worth using despite us making heavy use of TypeScript and Rust.


>"for (std::map<int, BlaBla>::iterator it = myMap.begin(); it != myMap.end(); it++)")"

All you really needed is:

  for (auto it : myMap)
and you still get compile type safety


Well, in 2005, you didn't.


Well we are not discussing 2005 version of Ruby. Why limit C++ to that particular time?


Well, the GP was very explicitly discussing the situation in 2005, which led to the then appeal of dynamic languages. Comparing with the situation in 2023 doesn't seem to contribute?


Ruby is one of the few successors to smalltalk with "true" everything is an object and message passing. As such, duck typing is in fact a feature and if you can internalize how it differs from traditional OOP then it becomes very powerful. It is a double edged sword however, and can be wielded poorly.


I’ve been doing Rails development for 10 years now and I just don’t think it’s possible to safely update anything in Ruby. Our app has 20,000 rspec unit tests and 5,000 cypress browser automation tests and basically every ruby patch version creates breakages that somehow slip through all of that despite nothing being mentioned in the changelogs.


What about recording and replaying traffic to the servers? It's not a magical solution, but it should catch regressions.


Replicating the production env to do that sounds beyond our capabilities tbh. We are slowly replacing Ruby with typed languages like TS and Rust which is working wonders for reliability.


Great, it's just a usual extra tool, although RoR is amazing, I fell in love with SvelteKit


I used to have that attitude. Then I wrote my first Ruby project as an experiment. That was 17 years ago, and the majority of the code I've written since has been Ruby. What clinched it was that the first Ruby project I wrote involved trying to reimplement a piece of C code we had. It ended up 10% the size with more functionality, and it took me a tiny fraction of the time to write.

If you write code the way you write statically typed code, then sure, you're in for a world of hurt. I could imagine using a statically typed language again if it made me as productive as Ruby, but most statically types languages have near useless type systems that I wouldn't wish on my worst enemy.


I don't know if I'm just a bad programmer and everyone knows something I don't, but it seems to be the benefits of a strongly typed language are wildly overblown. It's a different way of thinking about building software and it saves you from a few mistakes you could otherwise make, but you'd think I was writing code with a hammer and chisel the way people on HN talk about languages like Ruby. It feels a lot like the people who used to try to convince everyone functional programming was the only way to go and object-oriented programming was for dinosaurs.


For me, it comes down to the fact that static typing completely eliminates large classes of bugs. It therefore dramatically lowers the testing load. The compile step essentially performs its own tests. Think about all the test cases I save myself from having to write...

Beyond that, it also forces me to think structurally from the start. But I tend to be working on rather complex applications, I almost certainly wouldn't use a static lang for data processing or similar tasks.

> It feels a lot like the people who used to try to convince everyone functional programming was the only way to go and object-oriented programming was for dinosaurs.

What do you mean "used to"?? Haha


Strongly and statically typed languages are not very common. Most languages that pretend to be strongly + statically typed aren't, by a fairly large margin (I'm looking at you, C++, Objective C or Dart).

However, once you have strongly + statically typed languages, you can very often create safe-by-design (tm) APIs, APIs that are simply impossible to misuse (for some definition of misusing). A trivial example (which among industrial languages works only in Rust and perhaps Ada) is a file system API in which the compiler detects that, in some codepaths, you're attempting to try to write to a file after closing it.

Exactly how much value this has for your programming depends on how many invariants you need to guarantee. If you're writing (for instance) a CRUD for non-critical data, that's usually not many. If you're writing a network protocol or a garbage collector, this can save you from worlds of pain. I have had to fix data loss and privacy issues in Firefox that would have been detected years earlier if we had used a strongly-typed language such as TypeScript (which didn't exist at the time) instead of JavaScript for the front-end.

In fact, I wear a large number of scars from fixing Firefox bugs in JavaScript (or C++) code. These days, I prefer strong, static typing. I sleep more soundly :)

But, as usual, we're not in a one-size-fits-all industry. There are developments for which static typing gets in the way of zero-to-deployment.

As usual, tradeoffs everywhere!

Note: Strongly + statically typed languages also typically offer strong support for other forms of static analysis (e.g. model checking, abstract interpretation, etc.) that are considered critical in some industries (e.g. aeronautics – or writing Windows device drivers). But we're getting into something of a niche.


> Most languages that pretend to be strongly + statically typed aren't, by a fairly large margin (I'm looking at you, C++, Objective C or Dart).

What about Dart doesn't feel sufficiently typed for you?


Last time I tried Dart (2020?), it was pretty easy to confuse the type system and get it to just abandon all hope of typing a fairly simple expression. I remember the early Dart presentations in which the developers very clearly stated that they didn't even try to make the Dart type system sound, because they felt that it would complicate the life of users.

Apparently, these days, Dart claims to be sound... except they seem to have redefined the meaning of type soundness along the way: https://dart.dev/guides/language/type-system#runtime-checks


Yes, sounds like you used it before Dart 2.0. As of Dart 2.0, the type system is sound in the same way that C#, Java, and Haskell have sound type systems, though a combination of mostly static checking with some runtime checks in a few places.


It comes down to project size. Small projects with a small team tend to be fine. Bigger ones tend to break down a bit and the application becomes ossified because there's no safety when changing things. You can power through it but productivity takes a nose dive. The big ones that stay on Ruby tend to invent typing like things to solve the issue.


If there's no safety when changing things, then you've failed to define and verify adherence to contracts along component boundaries, and the lack of a proper test suite.

I've certainly seen that happen in Ruby, but I've also seen that happen in every other language - most of them statically typed - I've worked with, as very few languages have type systems expressive enough to prevent it (and it tends to massively destroy productivity to try check everything strictly enough anyway).


Unfortunately static typing has turned into a religion.

IIRC someone looked at Github issues a while ago to understand the impact of dynamic typing on defect rate. What he found was that around ~1% of bugs in JS/Python/Ruby enterprise systems were type-related.


Did that include bugs due to encountering unexpected nulls?


My understanding is that he was looking for instances of TypeError, AttributeError, undefined is not a function and so on. So yeah, nulls should've been included.


Its all guts, feels, and "everybody knows" - but afaik there's been no hard proof about statically typed languages being superior to dynamically typed ones, or vice versa.


The big difference between the two that I noticed is that dynamic camp doesn't aggressively proselytize. And it doesn't shame infidels (it pities them, if anything).


Oh, it does. It just happened back when Python and Ruby were new and hot. These days, Rust is new and hot, and so ...


I do remember dumping on Java back in those days, not so much on static typing itself. Seems like those were mostly language wars: Python vs PHP, Python vs Java, Ruby vs PHP, etc.

It might be just anecdotal, but I noticed people often say that writing in Ruby makes them feel happy.

Static typing (or functional programming, or TDD) doesn't seem to make people happy. It makes them feel superior.

Whether it truly makes them superior, or just taps into their insecurities is an open question.


As someone who was gung ho about static typing, what made me happy about Ruby was losing the aggressive type annotations, and the pain of having to change it all around, and C++ and Java were among the worst offenders. I spent a lot of time looking for statically typed alternatives that'd make me happy back then before I finally was convinced to try Ruby.

These days more inferred typing in many of the static languages has lessened the gap, so the reason to hate on them has been reduced.

I still would love better static analysis tools, including for Ruby, but at the same time the way I write code has changed drastically in ways that makes that harder (e.g. leveraging the dynamic features of Ruby more)


YMMV. I used to be very gung-ho about dynamic typing 10 years ago. Since then, my experience using it in a fairly large Python code base is that it doesn't make me happy, and the code patterns that make it workable tend to be very similar to how statically typed code would look anyway.

Ruby specifically is a separate question IMO because it has a lot of convenience features that are kinda orthogonal to the whole typing debate. Crystal is a good example of how you can retain those in a statically typed language.


To me, it’s as much of a preference as text editor or spaces vs. tabs.


Have you looked into Crystal? It's supposed to be a statically-typed language, with the syntax of Ruby.

https://en.wikipedia.org/wiki/Crystal_(programming_language)


I have, and last time I looked it deviated from Ruby in entirely pointless ways that makes it fairly uninteresting to me. I get they want it to be its own language, but it'd have been far more interesting to me if it stayed as close as possible wherever it could.


C is barely statically typed. In an ML-family languge you can write pretty much the same code you'd write in Ruby, just it'll be typesafe.


If there's an ML-family language that reads similar to Ruby I'd love to use it. I'm not being facetious - I still love the notion of as much static testing as I can, but I'm not willing to give up the pleasantness of Ruby for it, so if there's something that can get me there I'd love to see, even if it's not "good enough" to replace Ruby for everything.

But keep in mind this includes run-time dynamic meta-programming. E.g. one of my projects bootstrapped a large proportion of the web frontend by having the backend introspect the database schema and dynamically generate models, routes, access control and metadata endpoints that the frontend then used to instantiate the UI.

The typing in that case was controlled by the database migrations.


I was a huge dynamic language fan (although admittedly more Python than Ruby) until I found Scala. It has a reputation for incomprehensible arcane symbolic code, which certain libraries do, but you can write ruby-like code if you avoid doing that.

I've done "generate a whole CRUD backend from a single source of truth for what my domain looks like", although I've done it by defining the model classes, generating the database schema from them, and deriving everything else from them. All the building blocks are there but I haven't found a framework/library that puts it all together (that's one of those projects I keep meaning to get around to). I've heard there are some database access libraries that let you use a macro to derive model classes from the schema, but I haven't used them yet.


Scala to me is unreadable. It reads like an unholy mix of Python and Java, with a lot of the ceremony of Java still hanging in there coupled with introducing partial significant whitespace. It's not bad - it's better than most alternatives - but it's not good enough to meet my standards.

If you see static typing as essential, then I can see the appeal. But my starting point is that it's not essential to me (and we're increasingly getting part of the value via optional and gradual typing via things like Sorbet which also let us keep the clutter out of sight, reducing the gap even further), and so losing even some of what brought me to Ruby is too much of a sacrifice.


The solution is to be religious about testing and writing tests. If you've done it well, refactors are pretty safe and your objects are pretty well understood.


The point is that compilation automates all this. It's just one more failure point you have to devote man hours to.


Compilation won't tell you whether your code follows the business logic it's intended to do. You do need to rely on tests even if the implementation of the programming language compiles it or not.


Actually, very often, with a strongly, statically-typed language, in a well-designed API, it actually can.

Unfortunately, most people got a taste of static typing with Java and the initial language and library design were done in such a way that types were well, if not entirely useless, then certainly under-used. But if you look at many libraries for languages such as Rust, Scala, OCaml, Haskell, F#, ... (I haven't looked at Java in a while, but I don't hold high hopes on this specific front) you'll find many examples in which the API and the type system cooperate to guarantee that high-level protocols are enforced.

That being said, I absolutely agree that strong static typing does not mean that you don't need tests. As everything, if you want to be safe, you need a defense in depth, with good API design and many test layers.


I'm not talking about business logic tests, and never implied that compilation removes the need for them.


Both religions are leading you astray.

The solution is to become a software atheist and just break things down when they no longer fit in your head.

Interestingly, this seems to be what microservices originally were about until they got twisted by hype.

It is also what OOP was about, until it got twisted by another religion.


Also, you need to be religious about testing and writing tests _anyway_.


As someone who spent a good chunk of their career teaching TDD, I agree and disagree. Yes, tests are crucial. However, which tests to write varies among other things by language. When I write Ruby, I benefit from testing even the simplest things. When I write Rust or even Type Script or Swift I'll focus much more on tests for complex logic and on integration and acceptance tests. Static typing eliminates an entire, larger category of issue I need to address with tests.


When I write Ruby, I test expected behaviours, and if there are type errors in there those tends to fall out from tests I needed anyway. If you need to test specifically for errors due types, then generally that suggests that either your application does not normally exercise those code paths at all and/or you fail to test behaviours of your application.


100% on testing behavior. That said, I still end up writing more and lower-level tests in Ruby to get fast feedback on silly errors, both in terms of locality of the failing test and in terms of not executing the entire stack to test variations of behavior somewhere deep inside.


Does this include writing libraries for use by third-parties?


To an extent. In libraries for third parties you do need to test contracts specified by your docs, but a well written Ruby library should intentionally avoid over-testing typing (both in separate tests and in code) and focus on testing behaviours.

Sure, test for sane failure modes in line with the documented contract, and that may include the occasional test that is de facto a type test, but often testing for types, especially in languages with poor type systems, but also in Ruby where we have alternatives, ends up with tests for classes which is frequently the wrong thing.

E.g. in Ruby never, ever check for somearg.kind_of?(IO) if all you ever do is somearg.read - if you absolutely must typecheck somearg, the Ruby way is to check for presence of "#read", e.g. somearg.respond_to?(:read), or try and fail responsibly (and often just allowing the NoMethodError to bubble up is the right way to fail).

Also think it's just fine for people to add and ship Sorbet type declarations for gems etc. to then signal those contracts so people can verify them if they choose. There's no reason not to offer that when it's reasonable to do so.


I have zero to no experience coding in Ruby, so I cannot comment in that case.

In Python or JavaScript/TypeScript, though, my experience is that failing to validate types at the borders (i.e. every single function/method/constructor/generator/... exposed in your API) is pretty much guaranteed to end up, months later, with developers attempting to sherlock out surprising breakages in production from logs that make no sense and traces that do not show anything remotely close to the actual culprit.

I have the scars to show it :) Of course, YMMV.


The "Ruby way" in this respect tends to be to avoid being over-prescriptive. That doesn't mean "do no checks", but "do only the checks actually needed" with a very different expectation of "what is needed" than in many other languages.

Hence don't check for an IO object when what you care about is the presence of a "#read" method.

Or don't check if something is an Array if what matters is that it supports "#map". Instead, either somearg.respond_to?(:map), or if it needs more than map, somearg.respond_to?(Enumerable) (this seems broad, but supporting Enumerable only requires implementing "#each" and including the module, so a caller "worst case" can reopen a class or wrap their object), or call one of Array(somearg) (tries "#to_ary" then "#to_a" then falls back to returning [somearg]) or Array.try_convert(somearg) (tries "#to_ary" then falls back on nil).

Consistently picking the most generic applicable options when faced with choices like that still enforces the contract on the boundary, but also tends to lead to code with much less ceremony. E.g. far fewer [something]Adapter classes, or glue code to convert data before calling methods that are being overly prescriptive.

But for most statically typed languages, when people talk about static typing, they still talk about a class and a type as interchangeable (there are exceptions, and it's getting better, and with increased type inference coupled with increased support for type annotations and analysis of dynamic languages, I expect there to be an increasing convergence, though).


This makes sense, although there are downsides to not naming types, as these serve as documentation. Python documentation has a strong tendency to using descriptions to avoid giving names to types, which imho makes the documentation needlessly confusing.

Note that I'm writing "types" and not "classes". Interfaces do just as well, if not better!

For what it's worth, in the static realm, OCaml introduce features that let code check for presence of a method (instead of implementing an interface) ~25 years ago. Unfortunately, this proved rather unwieldy and, to the best of my knowledge, nobody uses that feature.


The problem to me of naming types is that a lot of the time there is no meaningful name to give, and so it becomes noise.

When there is a meaningful name to give, in Ruby you might define a module, and include it and use that as an interface:

   module HelloWorld
      def hello
        raise "Not implemented"
      end
   end

   class Foo
     include HelloWorld
   end

   Foo.new.hello # Raises an exception

   Foo.new.kind_of?(HelloWorld) # Returns true
(And anytime you want something that behaves somewhat like an ordered collection, you typically just want object.kind_of?(Enumerable)).

There are plenty of contract-checking frameworks for Ruby [1] but they see little use, because most of our contracts tend to be extremely simple. Along the line of "implements method X" or "includes module Y".

Sorbet w/inference tools and optionally storing the types in a separate files (it supports inline too) might slowly change that (and effectively let you use modules as "proper" interfaces), since the biggest opposition to types in Ruby tends to be visual noise and forcing refactoring, and if it can be turned on/off in your IDE and regenerated, a lot of the objections fall away. It's not that we (well many of us) don't want the extra type checks, but that we don't want to pay the cost of making the code less readable.

[1] Here's one: http://egonschiele.github.io/contracts.ruby/ and of course there's Sorbet: https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet


> The problem to me of naming types is that a lot of the time there is no meaningful name to give, and so it becomes noise.

Do you have examples? That's not my experience.

Of course, we may simply be working on very different types of code :)


It's hard to give a neat example, because it really changes how you write code and it's been a long time since I had to suffer through much static code.

But e.g consider basically almost any case with abstract member functions / methods. While that happens in Ruby (or the equivalent: Leaving a method which raises an exception unless overridden) it's far less common.

Typically you'll instead implement defaults in a module if there is lots of functionality that a client might want for itself). These are generally fine to name, and type check by the module name.

But when that is not* the case (any class consisting mostly of abstract methods), a Ruby implementation would be more likely to instead expect the presence of a single method and embed the default functionality in the calling class. That's the kind of scenarios where you end up with the kind of long, contrived class names people tend to mock Java for in particular.

But going further, a Ruby implementation will instead often take an argument that is expected to support "to_proc" or a block argument, so that you can provide a lambda/closure to do what you'd otherwise put in an "adapter" or "shim" class.

Conversely, any time where you demand (whether in a static language by type annotiations or in a dynamic one by checking for the class) an object of a very specific class and only call one or a few methods on it, you're sidestepping this problem by avoiding the creation of a more precise interface that'd clutter up the code. You don't create and name interfaces that encompass the specific operations needed by each method because it'd be a total mess.


Fair enough.

A few common examples for which I rely upon static typing: - I have a proof that operation X is complete, because I have a protocol to follow; - this string is a Matrix room id (and not a Matrix user id or a Matrix server id, also strings); - this time is in milliseconds.

Sounds like very different use cases :)


Sure! But you need to be a testing practitioner anyway. I agree that perhaps the number of tests is different, but the teams processes and cultures should already be there.

But to counter myself, the perceived lack of developer speed and flexiblity of a strongly typed language (with Rust, I always get the feeling that I'm fighting with the compiler!) is also solved in the long run with practice and tooling.


You really don't. You need to test the business logic, but for the "plumbing" (which is often 90%+ of the code) often if you have a decent type system and use it effectively there's only one way to write it that would compile.


Python now has an optional type system and if you add one of them such as mypy or pyre to your CI process and you can configure GitHub to refuse the pull request until types are added you can make it somewhat strongly typed.

If you have a preexisting codebase I believe the way you can convert it is to add the types that you know on commits and eventually you will have enough types that adding the missing ones should be easy. For the missing ones Any is a good choice.

https://pyre-check.org and https://github.com/python/mypy are popular.


Python's type hints are definitely an improvement and they're getting better all the time, but they're still frustrating to use at anything approaching the edge. I long for something as elegant and functional as TypeScript.

One hurdle I've stumbled over recently is the question "what is a type?", the answer can be surprising. Unions, for example, are types but not `Type`s. A function that takes an argument of type `Type` will not accept a Union. So if you want to write a function that effectively "casts" a parameter to a specified type, you can't. The best you can do is have an overload that accepts `Type` and does an actual cast, and then another that just turns it into `Any`. This is, in fact, how the standard library types its `cast` function [1]. The argument I've seen for the current behavior is that `Type` describes anything that can be passed to isinstance, but that's not a satisfying answer. Even then, `Union` can be passed to isinstance and still does not work with `Type`. Talk currently is to introduce a new kind of type called `TypeForm` or something to address this, which is certainly an improvement over nothing, but still feels like technical debt.

[1]: https://github.com/python/typeshed/blob/main/stdlib/typing.p...


My experience of mypy is that, even with the most restrictive settings, it's still quite weak with respect to all reasonably strongly typed languages I've used (e.g. anything at least as strongly typed as Java).

YMMV


I still think it's a great choice for smaller projects. I can't imagine it at the scale of shopify.


Apparently they couldn't imagine it either[1].

1: https://sorbet.org/


This is built by Stripe, not Shopify.


It is, but Shopify uses it as well and maintains the companion tool, tapioca.

https://github.com/Shopify/tapioca

Tapioca is used to generate type signatures on gems and code that creates functions at runtime. Sorbet ships with some of that behavior, but they updated their docs to recommend tapioca over sorbet where the behavior overlaps.


Oops, my bad, I should have doublechecked that!


What are you using that's strongly typed and as quick to get stuff done with as Ruby?


Ruby.

(Ruby is strongly typed in the "doesn't automatically coerce types" and "maintains a fixed type for an object" sense; using the term "strong typing" is inherently ambiguous because it has so many partially conflicting meanings)


I think TypeScript, especially with a full stack framework such as Remix or Next, satisfies these two constraints.


Yep, Next / Typescript


Nothing, it was just an opportunity to use buzzwords and inject negativity.


rbs / steep, and sorbet.

Monkeypatching is evil.

Rubocop custom cops to reign-in what's allowed in CI/CD.


Even in dev, you have to basically code in irb in order to verify everything works.


Nonsense. I used to be a professional Ruby developer and that is nowhere close to the truth. While I prefer static typing it is not hard at all to code in Ruby once you have learned the basics and I almost never used IRB other than when I was a beginner.


How many different ruby projects did you code in? Once you have memorized the global state on a project, you can rely on your intuition and rely less on irb.

But anytime someone introduced a new gem (namely act_as_paranoid) or writes a new concern that modifies defaults, then you're back to need to do live code.


It's good practice to use irb to quickly check stuff or inspect the programmatic surface of a new library. But that's a Ruby feature that you don't have in other languages! In no way its mandatory to code inside irb. Most of my time was spent in Textmate or Emacs.


A REPL such as irb is by no means unique to Ruby. Even many statically typed compiled languages have them these days.


Didn't mean it was unique (guess most interpreted languages have REPL loops) but my point was that it is a plus, not a fault.


Ruby was probably my "first love" as a programming language for the reasons you described, but nowadays I prefer statically typed compiled languages. Ruby is still my preferred scripting language, though I've seen the way large projects can start to suffer from high maintenance overhead when it comes to dynamic languages like Ruby.


Definitely, it gets extremely frustrating when you have runtime bugs that would've been trivially caught by a compiler.


Same. I wish I was always writing Swift.


Ruby definitely optimizes for the writer but it fails for the reader (a saying I say quite frequently at work).


I've been coding for 10+ years and I would argue that ruby is not kind to the writer.

Ruby requires you to be aware of much more "state" than any other language that I have coded in, because all assumptions about methods cannot be confirmed until the code is run. For example, a gem might modify the default_scope of a portion of active record models. You have to memorize all these tweaks and even then you don't know if the code you've written will do what you want until it is actually been run.


That is a Rails issue, not a Ruby issue. Outside some Rails libraries this is not a thing. Virtually all non-Rails libraries try hard to avoid modifying global state.


rspec uses monkey patching for stubbing methods. Even rails libraries avoid modifying global state, but its still very much a thing.


It _can_. Much like any other language, if you're not careful you can write some very convoluted Ruby code. What makes Ruby different is when you invest in readability you get far greater returns for much less effort.


People often say this but it's nowhere close to universal. Ruby is 24/42 of most loved langs.

https://survey.stackoverflow.co/2022/#technology-most-loved-...


This survey shows that slightly over 50% of ruby users dread using it.


I don't say ALL people, it's "people like me", why care about numbers ?


It shows that Ruby still has life left in it yet. The language itself is wonderful, there's a great ecosystem of tooling but the performance was always lagging. Hopefully some of this makes it a choice for people once more over other more esoteric languages. My only concern here would be the time and sunk cost fallacy of someone like a Shopify working on a Ruby JIT. It reminds me of when Facebook was working on the HipHop VM for PHP code and compiling PHP to large C++ binaries. I mean I get it, you have millions of lines of code, you don't want to rewrite it, but I often wonder, is there a better solution to this? Is there just a natural path towards, write new things in a new language and leave the old behind? Genuine question, because I'm not talking about rewriting the stack, I'm just talking about not investing significant amounts of time in low level infrastructure that's totally unrelated to business priorities. Yes fast code means better user experiences but often there's alternative ways around this than doing some literal JIT compiler work.


The big difference compared to HHVM is that this is part of Ruby. It's not a separate project. Shopify is sponsoring much of the effort, but it's owned by the community.

At Stripe, there are millions of lines of Ruby. Lots of it would be better off in a different language—if it had been written in another language initially. Ruby is easy to hire for, there are tens of thousands of pages of documentation for the code written in it, and there's a ton of operational knowledge about how to use it well. Switching is possible, but the cost to replace it is half a decade or more. During that time, you're not building new features, you're worrying about compatibility and making sure there's no downtime. Instead of making incremental improvements, your infra folks are worrying about migration paths.

At the end of the day, the major arguments against Ruby is lack of native type checking (solved with tooling) and performance. For Shopify, you can either undertake a major engineering effort to rewrite your stack (years of effort, incidents, no real feature development, morale killing) or kick a few million dollars at making the thing your engineers like faster. The cost of replacing Ruby is tens of millions of dollars: not just engineers putting pen to paper, but lost business from putting the company on hold, breakages, and throwing out immense amounts of operational knowledge and experience.

Said another way, telling a thousand+ people to drop what they're doing, learn a new language, and redo all their work is an expensive way to end up with a worse version of what you already have. It's not a sunk cost fallacy if you literally can't afford to change.


Love this post, although:

    Ruby is easy to hire for
I've always had the opposite experience!


In my time at Stripe, few of the candidates were fluent in Ruby, but it almost always took less than a couple weeks to get spun up with it. If you're looking for Ruby talent, it's more challenging.


I've worked at places that hire folks with Ruby experience only (more difficult) and places that just hired the best available engineer and let them learn Ruby (easier but EXTREMELY mixed results - specifically lots of Java ppl insisting on writing Ruby that looks like Java and insisting upon using e.g. Spring conventions in Rails)


This is super common, (and the mark of a bad programmer if you program the same in every language). What's funny is the opposite - people who get so into ruby that everything they write is cute and overthought - is actually worse.

Ruby is so powerful but simplicity is the best if other people are going to read and maintain your code. Go is great for this.


    Ruby is so powerful but simplicity is the best if 
    other people are going to read and maintain your code.
Amen. Keep it simple for a large shared Ruby application, such as a Rails app.

More advanced Ruby (writing your own operators/iterators/whatever, metaprogramming, extending the language itself, whatever) is best reserved for Ruby frameworks, not applications.


As someone experienced in Ruby (and Rails), I've also always had the opposite experience. Very little options!


There haven't been a ton of choices when I've looked for Ruby/Rails jobs, but I never had much trouble landing one. They usually seem pretty relieved to have found somebody.

When I've been on the hiring side of the equation, it's been very tough to find candidates.


> Switching is possible, but the cost to replace it is half a decade or more.

I would imagine if you're going to switch millions of lines from one language to another - you wouldn't do it by hand - you'd write something transpile it?

Has any company actually done something like this before - especially somewhat recently?

I can't imagine anyone doing it by hand...


That doesn't really work in practice at scale. There's "Ruby" ways of doing things that don't just transpile over. It's also the entire Rails framework that would have to be ported then, which is beyond a monumental undertaking.

That's not to even mention the massive amounts of new bugs that would be put into the code that have been ironed out over years of debugging and testing in the Ruby base. There's a reason so many legacy operators still run COBOL despite even C being a better candidate, it was just created twenty years too late and the costs of moving it over is well outside of the savings of not doing that.

"If it ain't broke, don't fix it" is very much the key here.


You need to produce code that not just runs, but is idiomatic in the target language. I'm not aware of ANY transpilers that do this for target and source languages that run in different runtimes (i.e., not coffeeacript to JS). You need every language feature to translate cleanly, and that means that a translation failure makes it impossible to convert a logical unit (module, package, etc) at once. Simply having a translated version of the code without making it idiomatic means it's impossible to read or modify.


How is hiring a few people to write a transpiler any different than hiring a few engineers to write a JIT compiler? Isn't a JIT just transpiling code at a different level (bytecode instead of source code)?


Many many years ago I was hired to convert a Fortran app to C. The first person to try it ran it through a software called "f2c". It worked fine - but the code was totally non human readable so it couldn't be extended later, so it provided almost no value in this case.


The JVM is as performant as it is because so much resources were invested in optimization (including various forms of JIT etc). It's an amazingly performant VM because of so much invested in tuning it. Or same about various Javascript VMs.

This kind of performance comes from lots of resources invested in tuning. Lots of resources are invested, generally, when there are lots of entities relying on a thing; that's a very good reason people invest in improving a thing, right?

So I'm not really sure what you mean -- you say "sunk cost", I say "Well, those who are using a thing are those who invest in improving it, how is it ever any different? And that there are people with current investments in ruby who will continue to invest to improve it is what will make ruby continue to thrive -- and how is it ever any different with any technology?"

What would make it "sunk cost", i suppose, is if you think ruby is a poor technology and everyone using it should switch to something else. I guess it is popular to hate on ruby right now, but that seems to be an orthogonal debate. Ironically, one of the most popular reasons to hate on ruby is "performance" (rightly or wrongly), so it seems especialy weird to me to show up with an argument like "Sure, they're drastically improving ruby performance, but is ruby the right choice of thing to improve it's performance? After all, ruby has such bad performance!"

To be fair, you didn't specifically say "because ruby has such bad performance", you didn't say anything about why ruby might be the wrong thing to invest in at all -- which makes it all the more just weird FUD, intentionally or not.

You are suggesting that just in general we should prefer switching to new languages over investing in the existing ones? Since you didn't supply any specific arguments, it makes it seem as if you suggest this as a general principle, regardless of details? I think many of our experiences is that this leads to always using immature technology; to reach the stability and performance of (say) the JVM or V8 requires... investing in the thing, not constantly chasing a new immature thing hoping it will be different this time.


I think it's almost a cliché that Ruby is dying, is dead, or is outdated and uncool. It comes up in almost every post about Ruby.

It's going as strong as ever and, if anything, it's stabilised into a robust, expressive and powerful language and in many cases that's a pretty acceptable trade-off. The fact it has long since matured into 'boring' technology (as in 'choose boring technology') is nothing but a good thing.

It's great that so much work is going into performance, though. I've been excited to try this new JIT out in prod, and I'm excited to see how Ractor, for example, evolves.


Indeed. Oh well, while people keep spouting these cliches Ruby will continue running the worlds code bases (Github), a lot of the world's payments (Stripe) and a lot of the worlds e-commerce (Shopify)


I see the point to make a parallel with HipHop, but here YJIT is directly integrated in CRuby, the main implementation of the language, and it’s just a matter of command line flag whether you enable or disable it — at least from what I remember that I red.

From what I remember, HipHop was distributed in a different toolchain than the vanilla PHP interpreter. Ruby also have other interpreters available by the way: https://github.com/codicoscepticos/ruby-implementations


Yes. Not only is YJIT directly integrated into CRuby, it's also 100% compatible with your existing Ruby code, which is why we chose to go that route.

We didn't want to independently reimplement Ruby because we knew that this would lead to a situation where we wouldn't be 100% compatible, which would stop people from using YJIT. If you think about PyPy for example, they have great performance numbers, but relatively few people are using it.


Yeah , it was because Guido himself was not promoting that project . PyPy team really tried hard to be compatible , now almost all c-extensions including DS libraries worked. I am using PyPy in production for 8 years and - it reduce memory load by a lot , ( 80 MB vs over 300 MB for tornado web service running) - it is so much faster than python that is comparable to node in high load peformance. - it would be compatibile with every lib if Main Stream Python community had even tried it and work on it.


Someone has to do this work, and I this is a good example of a company giving back to what was originally a non-industry project (Ruby).


Yes, I'm just asking whether that needs to be Shopify? And whether their time is best spent elsewhere.


The loss of human time in big corporations is immense. Coordination overhead and losses due to information logistics problems. I'd imagine a project like this with no dependencies on the rest of the stack can be run efficiently. From a bang per buck point of view it might beat other means (rewrite, using multiple languages, redoing ops to run on cheaper backbone) by a huge factor.

I get your sunk cost objection, but it's a bit depressing to hear people worry about someone doing genuinely useful work. Nobody wants to take responsibility or get their hands dirty. There's still plenty of low-hanging fruit in both language implementations and databases. The world today is just mountains upon mountains of bullshit.


Creating and sharing powerful tools for people to build the things that they want to exist is kinda in Shopify's DNA.

It's also important to me to give back to open source. It's done so much for us.


Well they have one of the bigger ruby/rails deployments so a 10% perf increase benefits them quite a bit.


I am sooooooooooo sick of all this language churn. Ruby is a fantastic language, full stop.


  irb > def method
  irb >   'yes'
  irb >   def method
  irb >     'it is not'
  irb >   end
  irb > end
  irb > method
  => :method
  irb > method
  => "it is not"


yeah don't nest your methods. When have I ever needed that anyway?


That’s true, method_missing is both much more common and much worse


I think you don’t like Ruby and would want investment in the your favourite language. Because why would you care what a corporation does with their money?


I think thinking of the "sunk cost" as being only or even primarily code is incomplete.

It's internal expertise in that language, understanding of its strengths and weaknesses and how they fit into your business needs, custom tooling around dev, debugging & deployment, practice evaluating candidates for expertise in it, particular strengths of that language not guaranteed to be in a replacement, just all of the "unknown unknowns" that you now know through bitter experience.

Surely there will still be a point where it's worth throwing all that out and starting fresh, but some of those are very hard to quantify or even see, when you have them, so the conservative choice is to stay put in the system that works.


You can often solve ~70% of this with language interop. Bindings and FFIs are in my opinion very under-appreciated. This is the approach taken by PHP+HipHop, Apple with Swift/ObjC, and many Python bindings over C++ internals (TF etc).

There is no magic bullet. You can't do an overnight transition. If you decide to make a language shift (or equivalently large architecture shift), you need an interop. Old and new have to coexist. The remaining 30% (where interop fails) is where engineering happens and is often where you had hidden technical debt anyway.


I use Ruby and Chef at work (MAANG).

For side projects, I use Rails 7 to get things going and replace components selectively with native extensions and separate processes.

It's all about productivity, especially when small, because performance is rarely an issue.


You love to see the continued investment in this language. I've been using Ruby for about a decade and I'm still enjoying writing code in it. I hope the language continues to improve and the community stays vibrant.

I can remember about 8 years ago someone telling me how they were worried Ruby (and Rails) was dying. At this point the community could dry up and I think I'd still stick with it.


Python have a 99% compatible jit implementation called PyPy and nobody much know about it. It was production ready since 5 years ago but ignored by mainstream. It is 400% faster than vanilla python on average and Guido and MS is working on for a few percent gain instead of promoting it or eventually making it default. PyPy.org


Even if GvR promotes it fully, it won't be adopted unless c extensions work out of the box.

Python without its crazy amount of extensions in c-api won't survive.


This is something that's up to the community really. The PyPy developers early on made a new FFI that is also available for CPython, sadly not entirely as easy as the classic API but completely functional and if there was a push to rewrite extensions to use that FFI (or provide dual-paths) then the gap could be bridged.


Are you referencing https://github.com/hpyproject/hpy?

I do hope it takes off.


They didn't update their website often , that is the main problem of people not knowing much about PyPy . But they had made CPyext compatible and works fine with several Data-science Libs over past 2-3 years . Even anaconda have a miniconda for pypy called pypyforge where many of CPyext are building fine with it and they have same performance comapre to CPython. ( Can't make C-Extension faster since they are already C anyways.)


pypy is pretty well publicised and known about in the python community. Lots of popular libraries specifically call out compatibility etc.

For quite a while it used to have some terrible performance if you ever reached out to C libraries in a hot loop. It's still a bit iffy on the C / FFI front, though the performance issues are much improved

It's certainly good enough for production use, with careful evaluation.


Agreed, cpyext performance getting much better these days, still a little bit slower but general python performance improvement is massive. Non data science project get massive performance boost,for example web framework like Django.


I've tried multiple times to use PyPy on real projects and it was totally unusable if you rely on stuff like pandas, sklearn, keras, fastapi, etc. I found similar issues with Numba as well. These things only work if you keep to a relatively small subset of Python and stick to "pure python" functions. Which basically no one does (that I can see) except in toy projects and tutorials. Improving cpython by 1% (without breaking compatibility, which python 3.10+ did a poor job of in my view) would do more for the world than making PyPy even faster.


I wonder how much of it is related to how Python is being used for short running scripts more than Ruby, at least in my experience. In such cases startup time is more important than being faster in a long run.


"On average" is mostly meaningless because it depends so much on the kind of code you use it with that you need to do your own measurements to judge. I've seen a 30% improvement on one project, 200% on another, and none at all on yet another.


Man that is a terrible graph. Why use tiny fonts and smush them together, them make the image small as well. I wish I could read the charts...


You can see this chart and all the numbers and more at https://speed.yjit.org/, which this chart was taken from.


Thank you!


> Multiple large clusters of servers distributed across the world, capable of serving over 75 million requests per minute

That doesn't say anything at all sadly.

Ruby people seem to be sensitive when someone asks them about performance, my last question about that was downvoted heavily here.

I don't know any Ruby, I know how much Go can handle with the stdlib, a single non-parameterized route, returning "hello world". On my machine xeon e3-1275v5 with 8 threads 1000 concurrent wrk about 220k/ rps. Errors start appearing (aka non-200 results) between 5000 and 6000 concurrent. Don't forget to ulimit -n 10000 or more before you start wrking.

Alternatively can you provide a simple single route hello world in Ruby so I can bench it myself with this Ruby 3.2 YJIT? Maybe also how to build and run?

Yes, I'm being serious. I'm also guessing that Ruby with Rails has a large overhead compared to Go and just the stdlib. So that comparison wouldn't be fair, but I guess it is what it is, since we're talking semi real world scenarios (yeah don't get your ocd panties in a bunch) and Rails AFAIK is the standard for Ruby web development while the stdlib in Go is also used quite often.


Complaining about downvoting, demanding that other people do work for you, and preemptively using demeaning language about people who might disagree with you are all ways to reliably get downvoted.


> since we're talking semi real world scenarios

A single non-parameterized route returning "hello world" is a real-world scenario? You have an interesting business domain!


he has a saas that return hello world.


Is there a name for this syndrome? The same thing happened at Github. Ruby and Rails are slow enough in production that they dedicate a specialized, long-running internal team to it. This team doesn't contribute directly to the product. They hire core Ruby and Rails maintainers, also not to contribute to the product, just to try to help improve their slow software. It's a dream job that shouldn't exist.

If you tell me that Ruby and Rails scaled for Github and Shopify, be careful using that as an argument for choosing Rails for your company. Rails definitely didn't scale for Github, and doesn't appear like it scaled for Shopify, until a manager greenlit a year long JIT project unrelated to Shopify. If your company has as many resources, free cash, klout to hire core Ruby maintainers, and enough downtime you can throw a few engineers at a non-product problem for a few years, then sure, go ahead and use that reasoning.


Don't most large products have teams working on performance? With a VM-based language like Ruby, speeding up the VM is a sound strategy because it works across all of your applications. There are teams embedded in corporations working on the JVM, the Python VM, the PHP VM, and so on (sometimes brand new VM implementations). There are teams working on improving performance of the standard library and compiler in C, C++, Rust, and Go. There are teams working improving performance of applications in all of those languages.

Sure, if you're a startup and you can't afford it, you probably shouldn't be spending time speeding up your language. But, you're also unlikely to experience performance issues under your lighter load that can't be attributed to your application.


Shopify like many other successful Ruby shops were successful because of the speed of development. The cost of hosting is secondary and any company the size of Shopify can afford to involve a small team to reduce hosting costs... with impact for the developers happiness to boot.


I wonder if YJIT actually did decrease infrastructure costs for Shopify, seeing as it requires ~3x more memory. For my business, it’s come out about even. But a >30% decrease in response time is worth it, for me at least.

I run many small servers (2 processes each), but IIRC, Shopify runs beefy servers. So may be different for them.


> as it requires ~3x more memory.

Not sure were you've seen this, but that's absolutely not normal. The YJIT memory overhead is certainly sizeable, but is generally at least an order of magnitude smaller than that.


Reddit. Per this discussion here: https://old.reddit.com/r/ruby/comments/zzbrch/how_much_memor....

My production workload shows similar results. The YJIT app's memory sits at about 2.4x what my non-YJIT app utilizes. Sidekiq sits at 3x.


Languages don't scale (out), architecture does.

Well, maybe Elixir and Erlang scales out, since architecture is baked in.

Depending on how performant a language is, it can put off (sometimes forever) your need to scale out, which can simplify things. But both Github and Shopify are beyond the level of traffic that would allow you to get away with only scaling up rather than out.


Can you give some examples of companies that "scaled" without dedicating at least some engineering work to improving their runtime?


You should be asking specifically for examples of companies that don't do anything computationally intensive, aren't CPU bound, and still dedicated teams to work on the overall performance of the _language_ itself they use. Not tuning the VM, not tracking down memory leaks, trying to fix the thing itself. Which is the case here.




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

Search: