|
|
Subscribe / Log in / New account

Ruby 3.0 brings new type checking and concurrency features

By John Coggeshall
October 7, 2020

The first preview of Ruby version 3.0 was released on September 25. It includes better support for type checking, additional language features, and two new experimental features: a parallel execution mechanism called Ractor, and Scheduler, which provides concurrency improvements.

According to a 2019 keynote [YouTube] by Ruby chief designer Yukihiro "Matz" Matsumoto, type checking is a major focus of Ruby 3. In his presentation, he noted that Python 3, PHP, and JavaScript have all implemented some version of the feature. In fact, Ruby already has type-checking abilities in the form of a third-party project, Sorbet. For Ruby 3.0, type checking has been promoted into the core project, implemented as a new sub-language called Ruby Signature (RBS). This mirrors the approach taken by Sorbet, which implemented a sub-language called Ruby Interface (RBI). Sorbet allows annotations to exist within Ruby scripts, something that the community wanted to avoid, according to a presentation [YouTube] (slides [PDF]) by contributor Yusuke Endoh; by keeping RBS separate from Ruby, he explained, the project doesn't have to worry about conflicts in syntax or grammar between the two languages. In a recent blog post, the Sorbet project committed to supporting RBS in addition to its RBI format.

In a post introducing RBS, core developer Soutaro Matsumoto provided a detailed look at the feature. Conceptually, RBS files are similar to C/C++ header files, and currently are used in static code analysis with a project called Steep. As a part of the 3.0 release, Ruby will ship with a full collection of type annotations for the standard library.

Here is an example of an RBS file defining the types for an Author class:

    class Author
        attr_reader email: String
        attr_reader articles: Array[Article]

        def initialize: (email: String) -> void

        def add_article: (post: Article) -> void
    end

This declaration defines the types for two properties of the Author class: email (of type String) and articles (Array[Article]). attr_reader signifies that a property should provide an attribute accessor, which generates a method to read the property. The initialize() method is defined to take a single parameter, email, which is typed String and the method returns void. Finally, the add_article() method takes a single parameter, post, declared as an Article type; it also returns void.

Type unions, used when there are multiple valid types, are represented using the | operator (e.g. User | Guest). An optional value can be indicated by adding the ? operator to the end of the type declaration (e.g. Article?), which would allow either the specified type(s) or a nil value.

A new interface type enhances Ruby support for the duck typing design pattern. An interface in Ruby, as in other languages, provides a means to describe the methods that an object needs to implement to be considered compliant. In Ruby, classes are not explicitly declared to implement an interface. Instead, when an interface is used in RBS, it indicates that any object which implements the methods defined by that interface is allowed. Here is an example of an Appendable interface:

    interface Appendable
        def <<: (String) -> void
    end

As shown, Appendable defines an interface that requires an implementation of the << operator often used by classes like String as an append operation. This interface then can be used in other type definitions, such as this example of an RBS declaration for an AddToList class:

    class AddToList
        def append: (Appendable) -> void
    end

By specifying Appendable as the parameter type for the append() declaration shown above, any object which implements the << operator (for a String operand) can be used when calling the method.

Parallel execution

Another (currently experimental) addition coming in Ruby 3 is a parallel-execution feature called Ractor. According to the documentation, Ractors look a lot like processes, with no writable shared memory between them. Most Ruby objects cannot be shared across Ractors, save a few exceptions: immutable objects, class/module objects, and "special shareable objects" like the Ractor object itself.

Ractors communicate through "push" and "pull" messaging, with the mechanisms being implemented using a pair of sending and receiving ports for each Ractor. Below is an example of using the Ractor API to communicate between a Ractor instance and the parent program:

    # The code within the Ractor will run concurrently to the
    # main program

    r = Ractor.new do
        msg = Ractor.recv  # Receive a message from the incoming queue
        Ractor.yield msg   # Yield a message back
    end

    r.send 'Hello'       # Push a message to the Ractor
    response = r.take    # Get a message from the Ractor

Multiple Ractors can be created to produce additional concurrency or construct complex workflows, see the examples provided in the documentation for details.

While most Ruby objects cannot be shared between the main program and its Ractors, there are options available for moving an object between these contexts. When sending or yielding an object to or from a Ractor, an optional move boolean parameter may be provided in the API call. When set to true, the object will move into the appropriate context, making it inaccessible to the previous context:

    r = Ractor.new do
        object = Ractor.recv
        object << 'world'
        Ractor.yield object
    end

    str = 'hello '
    r.send str, move : true
    response = r.take

    str << ' again' # This raises a `Ractor::MovedError` exception

In the example above, we define a Ractor instance r that receives an object, uses the << operator to append the string "world", then yields that object back using the yield() method. In the program's main context, a String object is assigned a value of "hello "; this is then passed into the Ractor r with a call to send(), setting the move parameter to true and making str available to r as a mutable object. Conversely, str in the main context becomes inaccessible, so the attempt to modify it will raise a Ractor::MovedError exception. For now, the types of objects that can be moved between contexts is limited to the IO, File, String, and Array classes.

Other updates

The preview release included another experimental concurrency-related feature, the scheduler interface, designed to intercept blocking operations. According to the release notes, it "allows for light-weight concurrency without changing existing code." That said, the feature is designated "strongly experimental" and "both the name and feature will change in the next preview release." It appears that this feature is largely targeted to be a wrapper for gems like EventMachine and Async that provide asynchronous or concurrency libraries for the language.

Ruby 3 also includes some new syntax like rightward assignments using the => operator (e.g. 0 => x to assign x). The new release will also have several backward-compatibility breaks with Ruby 2.7. Per the release notes on backward compatibility, "code that prints a warning on Ruby 2.7 won't work"; see the provided compatibility documentation for a complete description of breaking changes.

A significant number of changes are being made to the default and bundled gems in Ruby 3.0, including the removal of two previously bundled gems: net-telnet and xmlrpc. Likewise, 25 gems were promoted to "default gems"; the core development team maintains these gems, and, unlike bundled ones, they cannot be removed from a Ruby installation. Many of the new default gems provide implementations of various protocols such as net-ftp, net-http, and net-imap, while others, like io-wait and io-nonblock, improve Ruby's I/O functionality; see the release notes for a complete listing.

Yukihiro Matsumoto recently confirmed [YouTube] that he expects Ruby 3.0 to be completed on December 25 this year. It will be interesting to see if that date holds; the project only released the first 3.0 preview a few days ago. With multiple features in the current preview release still designated experimental, his timetable looks aggressive. On the other hand, the project has been delivering significant releases on Christmas for many years; it seems likely, given that tradition, that they will find a way to make it this year too. Schedule aside, Ruby 3.0 is sure to have many new features fans of the language will enjoy when it is released.



to post comments

Ruby 3.0 brings new type checking and concurrency features

Posted Oct 9, 2020 0:55 UTC (Fri) by ms-tg (subscriber, #89231) [Link]

Very impressed with the effort to deliver these new features in ways that allow existing code to continue working largely unchanged. Seems like a great deal of thought has gone into all of this.

Ruby 3.0 brings new type checking and concurrency features

Posted Oct 9, 2020 1:02 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link] (2 responses)

We have a big Ruby application, so we're eager to start using the types. But I'm not really a fan of separate header files.

Ruby 3.0 brings new type checking and concurrency features

Posted Oct 9, 2020 7:39 UTC (Fri) by cdamian (subscriber, #1271) [Link] (1 responses)

I agree, separating this into different files was already annoying in C and C++.
I am curious how this will affect adoption.

Ruby 3.0 brings new type checking and concurrency features

Posted Oct 9, 2020 7:40 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

Oh, it's workable. And in most cases this affects you only when you are writing library-ish code. It's fine for users.


Copyright © 2020, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds