For all Rubyists,2020 was a special year.Why wouldn't it be?Ruby 2 was released in 2013.We have been using Ruby 2.xfor almost 7 yearsandwe have been waiting to seeRuby 3 get released.Finally,the wait is over now.Ruby 3.0.0 has been released.It's time to unwrap the gift boxand see all the Ruby 3 features we got.
Ruby 3 major updates
The number 3 is very significantin the Ruby 3 release.Be it release version number,making performance 3x faster,or the trio of core contributors(Matz, TenderLove, Koichi).Similarly, there were 3 major goals of Ruby 3: being faster,having better concurrency,andensuring correctness.
1. Ruby 3 performance
One of the major focuses for Ruby 3was the performance.In fact,the initial discussion for Ruby 3was started around it.Matz had set a very ambitious goalof making Ruby 3 times faster.
What is Ruby 3x3?
Before discussing this,let's revisit Ruby's core philosophy.
"I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy." - Matz
About Ruby 3x3,some askedwhether the goal was to make Ruby the fastest language?The answer is no.The main goal of Ruby 3x3 wasto make Ruby 3 times faster than Ruby 2.
“No language is fast enough.” - Matz
Ruby was not designed to be fastestandif it would have been the goal,Ruby wouldn't be the same as it is today.As Ruby language gets performance boost,it definitely helpsour application be faster and scalable.
"In the design of the Ruby language we have been primarily focused on productivity and the joy of programming. As a result, Ruby was too slow." - Matz
There are two areaswhere performance can be measured:memory and CPU.
CPU optimization
Some enhancements in Ruby internalshave been made to improve the speed.The Ruby team has optimizedthe JIT(Just In Time) compilerfrom previous versions.Ruby MJIT compilerwas first introduced in Ruby 2.6.Ruby 3 MJIT comes with better securityandseems to improve web apps’ performanceto a greater extent.
MJIT implementation is differentfrom the usual JIT.When methods get called repeatedlye.g. 10000 times,MJIT will pick such methods which can be compiledinto native codeandput them into a queue.Later MJIT will fetch the queueandconvert them to native code.
Please checkJIT vs MJITfor more details.
Memory optimization
Ruby 3 comes with an enhanced garbage collector. It has python's buffer-like APIwhich helps in better memory utilization.Since Ruby 1.8,Ruby has continuously evolved inGarbage collectionalgorithms.
Automatic Garbage Compaction
The latest change in garbage collectionisGarbage Compaction.It was introduced in Ruby 2.7where the process was a bit manual.But in version 3 it is fully automatic.The compactor is invoked aptlyto make sure proper memory utilization.
Objects Grouping
The garbage compactor moves objects in the heap.It groups dispersed objects togetherat a single place in the memoryso that this memory can be usedeven by heavier objects later.
2. Parallelism and Concurrency in Ruby 3
Concurrency is one of the important aspectsof any programming language.Matz feels thatThreads are not the right level of abstraction for Ruby programmers to use.
“I regret adding Threads.” - Matz
Ruby 3 makes it a lot easierto make applicationswhere concurrency is a major focus.There are several featuresandimprovements added in Ruby 3 related to concurrency.
Fibers
Fibers are considereda disruptive addition in Ruby 3.Fibers are light-weight workerswhich appear like Threadsbut have some advantages.It consumes less memory than Threads.It gives greater control to the programmerto define code segmentsthat can be paused or resumedresulting inbetter I/O handling.
Falcon Rack web serveruses Async Fibers internally.This allows Falcon to not block on I/O.Asynchronously managing I/Ogives a great upliftto the Falcon server to serve requests concurrently.
Fiber Scheduler
Fiber Scheduleris an experimental featureadded in Ruby 3.It was introducedto intercept blocking operations such as I/O.The best thing is thatit allows lightweight concurrencyandcan easily integrate into the existing codebase without changing the original logic.It's an interfaceandthat can be implemented by creating a wrapperfor a gem like EventMachine or Async.This interface design allowsseparation of concerns betweenthe event loop implementationandthe application code.
Following is an example to send multiple HTTP requests concurrently using Async.
1require 'async'2require 'net/http'3require 'uri'45LINKS = [6 'https://bigbinary.com',7 'https://basecamp.com'8]910Async do11 LINKS.each do |link|12 Async do13 Net::HTTP.get(URI(link))14 end15 end16end
Please checkfibersfor more details.
Ractors (Guilds)
As we know Ruby’s global VM lock (GVL) prevents most Ruby Threadsfrom computing in parallel.Ractorswork around the GVLto offer better parallelism.Ractor is an Actor-Model like concurrent abstraction designedto provide a parallel executionwithout thread-safety concerns.
Ractors allows Threads in different Ractorsto compute at the same time.Each Ractor has at least one thread,which may contain multiple fibers.In a Ractor,only a single thread is allowedto execute at a given time.
The following program returns the square root of a really large number. It calculates the result for both numbers in parallel.
1# Math.sqrt(number) in ractor1, ractor2 run in parallel23ractor1, ractor2 = *(1..2).map do4 Ractor.new do5 number = Ractor.recv6 Math.sqrt(number)7 end8end910# send parameters11ractor1.send 3**7112ractor2.send 4**511314p ractor1.take #=> 8.665717809264115e+1615p ractor2.take #=> 2.251799813685248e+15
3. Static Analysis
We need tests to ensure correctness of our program.However by its very nature tests could mean code duplication.
“I hate tests because they aren't DRY.” - Matz
To ensure the correctness of a program,static analysis can be a great toolin addition to tests.
The static analysis relies oninline type annotationswhich aren't DRY.The solutionto address this challengeis having .rbs files parallelto our .rb files.
RBS
RBS is a languageto describe the structure of a Ruby program.It provides us an overview of the programandhow overall classes, methods, etc.are defined.Using RBS,we can write the definition of Ruby classes, modules, methods, instance variables, variable types,andinheritance.It supports commonly used patterns in Ruby code, and advanced types like unionsandduck typing.
The .rbs files aresomething similar to .d.ts files in TypeScript.Following is a small exampleof how a .rbs file looks like.The advantage of having a type definition isthat it can be validated against both implementationandexecution.
The below example is pretty self-explanatory.One thing we can note here though, each_post accepts a block or returns an enumerator.
1# user.rbs23class User4 attr_reader name: String5 attr_reader email: String6 attr_reader age: Integer7 attr_reader posts: Array[Post]89 def initialize: (name: String,10 email: String,11 age: Integer) -> void1213 def each_post: () { (Post) -> void } -> void14 | () -> Enumerator[Post, void]15end
Please checkRBS gem documentationfor more details.
Typeprof
Introducing type definition was a challengebecause there is alreadya lot of existing Ruby code aroundandwe need a tool that could automatically generate the type signature.Typeprof is a type analysis toolthat reads plain Ruby codeandgenerates a prototype of type signature in RBS format by analyzing the methods,andits usage.Typeprof is an experimental feature.Right now only small subset of ruby is supported.
“Ruby is simple in appearance, but is very complex inside, just like our human body.” - Matz
Let's see an example.
1# user.rb2class User34 def initialize(name:, email:, age:)5 @name, @email, @age = name, email, age6 end78 attr_reader :name, :email, :age9end1011User.new(name: "John Doe", email: 'john@example.com', age: 23)
Output
1$ typeprof user.rb23# Classes4class User56 attr_reader name : String7 attr_reader email : String8 attr_reader age : Integer910 def initialize : (name: String,11 email: String,12 age: Integer) -> [String, String, Integer]1314end
Other Ruby 3 features and changes
In the 7-year period,the Ruby community has seen significant improvement in performanceandother aspects.Apart from major goals,Ruby 3 is an exciting update with lots of new features,handy syntactic changes,andnew enhancements.In this section,we will discuss some notable features.
“We are making Ruby even better.” - Matz
One-line pattern matching syntax change
Previously one-line pattern matchingused the keyword in.Now it's replaced with =>.
Ruby 2.7
1 { name: 'John', role: 'CTO' } in {name:}2 p name # => 'John'
Ruby 3.0
1 { name: 'John', role: 'CTO' } => {name:}2 p name # => 'John'
Find pattern
Thefind patternwas introduced in Ruby 2.7as an experimental feature.This is now part of Ruby 3.0.It is similar to pattern matching inElixir or Haskell.
1users = [2 { name: 'Oliver', role: 'CTO' },3 { name: 'Sam', role: 'Manager' },4 { role: 'customer' },5 { name: 'Eve', city: 'New York' },6 { name: 'Peter' },7 { city: 'Chicago' }8]910users.each do |person|11 case person12 in { name:, role: 'CTO' }13 p "#{name} is the Founder."14 in { name:, role: designation }15 p "#{name} is a #{designation}."16 in { name:, city: 'New York' }17 p "#{name} lives in New York."18 in {role: designation}19 p "Unknown is a #{designation}."20 in { name: }21 p "#{name}'s designation is unknown."22 else23 p "Pattern not found."24 end25end2627"Oliver is the Founder."28"Sam is a Manager."29"Unknown is a customer."30"Eve lives in New York."31"Peter's designation is unknown."32"Pattern not found."
Endless Method definition
This is another syntax enhancement that is optional to use.It enables us to create method definitionswithout end keyword.
1def: increment(x) = x + 123p increment(42) #=> 43
Except method in Hash
Sometimes while working on a non Rails appI get undefined method except.The except method was available only in Rails.In Ruby 3 Hash#except wasadded to Rubyitself.
1user = { name: 'Oliver', age: 29, role: 'CTO' }23user.except(:role) #=> {:name=> "Oliver", :age=> 29}
Memory View
This is again an experimental feature.This is a C-APIthat will allow extension librariesto exchange raw memory area.Extension libraries can alsoshare metadata of memory areathat consists of shapeandelement format.It was inspired byPython’s buffer protocol.
Arguments forwarding
Arguments forwarding (...)now supports leading arguments.
It is helpful in method_missing,where we need method name as well.
1def method_missing(name, ...)2 if name.to_s.end_with?('?')3 self[name]4 else5 fallback(name, ...)6 end7end
Other Notable changes
- Pasting in IRB is much faster.
- The order of backtrace had been reversed.The error message and line number are printed first, rest of the backtrace is printed later.
- Hash#transform_keysaccepts a hash that maps old keys with new keys.
- Interpolated String literals are no longer frozen when # frozen-string-literal: true is used.
- Symbol#to_proc now returns a lambda Proc.
- Symbol#namehas been added, which returns the symbol's name as a frozen string.
Many other changes can be checked atRuby 3 Newsfor more details.
Transition
A lot of core libraries have been modifiedto fit the Ruby 3 goal needs.But this doesn't meanthat our old applications will suddenly stop working.The Ruby team has made surethat these changes are backward compatible.We might see some deprecation warningsin our existing code.The developers can fix these warningsto smoothly transition from an old versionto the new version.We are all set to use new featuresandget all new performance improvements.
Conclusion
With great improvements in performance,memory utilization, static analysis,andnew features like Ractorsandschedulers,we have great confidencein the future of Ruby.With Ruby 3,the applications can be more scalableandmore enjoyable to work with.The coming 2021 is not just a new yearbut rather a new era for all Rubyists.We at BigBinary thank everyone who contributed towards the Ruby 3 release directly or indirectly.
Happy Holidays and Happy New Year folks!!!