R is an experimental Ruby gem which brings Rust's Result
type to Ruby, using Sorbet as the type system.
Install the gem and add to the application's Gemfile by executing:
$ bundle add r --github=olivierbellone/r
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install specific_install
$ gem specific_install https://github.com/olivierbellone/r.git
R::Result
is a type used for returning and propagating recoverable errors. (For non-recoverable errors, exceptions should be used instead.)
R::Result
is an abstract interface with only two possible concrete types: R::Ok
, representing success and containing a value, and R::Err
, representing error and containing an error value.
A simple method returning R::Result
might be defined and used like so:
class Version < T::Enum
enums do
Version1 = new
Version2 = new
end
end
sig { params(header: String).returns(R::Result[Version, String]) }
def parse_version(header)
return R.err("invalid header length") if header.size != 1
case header
when "1"
R.ok(Version::Version1)
when "2"
R.ok(Version::Version2)
else
R.err("invalid version")
end
end
version = parse_version("1")
case version
when R::Ok
puts "working with version: #{version.ok}"
when R::Err
puts "error parsing header: #{version.err}"
end
R::Result
is powered by Sorbet, and thus in most cases it is possible to statically assert the types of values contained by R::Result
instances. In the example above:
version = parse_version("1")
case version
when R::Ok
T.reveal_type(version.ok) # => Version
when R::Err
T.reveal_type(version.err) # => String
end
Refer to the YARD docs for the full API documentation.
In general, I have tried to stick to the Rust Result
API as closely as possible. Some notable differences are:
inspect
is renamed to#on_ok
, in order not to interfere with the#inspect
method available on all Ruby objectsinspect_err
is renamed to#on_err
to be consistent with the above- several methods that don't make sense in Ruby are missing (
as_deref
, etc.) - some methods have slightly different type signatures, to account for the differences in Rust's and Ruby's type systems
When writing code that calls many functions that return R::Result
s, the error handling can be tedious.
Rust has the question mark operator ?
to hide some of the boilerplate of propagating errors up the call stack.
I haven't figured out a way to reproduce this specific feature in Ruby, but it is possible to approximate it by using the #try?
method and leveraging the fact that calling return
within a block will return from the method that created the block.
So you could replace this:
class Info < T::Struct
const :name, String
const :age, Integer
const :rating, Integer
end
# Silly method to simulate Rust's `File::create`.
sig { params(name: String).returns(R::Result[File, StandardError]) }
def file_create(name)
R.ok(File.open(name, "w"))
rescue StandardError => e
R.err(e)
end
# Another silly method to simulate Rust's `file.write_all`.
sig { params(file: File, data: String).returns(R::Result[NilClass, StandardError]) }
def file_write_all(file, data)
file.write(data)
R.ok(nil)
rescue StandardError => e
R.err(e)
end
sig { params(info: Info).returns(R::Result[NilClass, StandardError]) }
def write_info(info)
result = file_create("my_best_friends.txt")
case result
when R::Ok
file = result.ok
else
return result
end
result = file_write_all(file, "name: #{info.name}\n")
return result if result.is_a?(R::Err)
result = file_write_all(file, "age: #{info.age}\n")
return result if result.is_a?(R::Err)
result = file_write_all(file, "rating: #{info.rating}\n")
return result if result.is_a?(R::Err)
R.ok(nil)
end
with this:
class Info < T::Struct
const :name, String
const :age, Integer
const :rating, Integer
end
# Silly method to simulate Rust's `File::create`.
sig { params(name: String).returns(R::Result[File, StandardError]) }
def file_create(name)
R.ok(File.open(name, "w"))
rescue StandardError => e
R.err(e)
end
# Another silly method to simulate Rust's `file.write_all`.
sig { params(file: File, data: String).returns(R::Result[NilClass, StandardError]) }
def file_write_all(file, data)
file.write(data)
R.ok(nil)
rescue StandardError => e
R.err(e)
end
sig { params(info: Info).returns(R::Result[NilClass, StandardError]) }
def write_info(info)
file = file_create("my_best_friends.txt").try? { |e| return e }
file_write_all(file, "name: #{info.name}\n").try? { |e| return e }
file_write_all(file, "age: #{info.age}\n").try? { |e| return e }
file_write_all(file, "rating: #{info.rating}\n").try? { |e| return e }
R.ok(nil)
end
#try?
is very similar to #unwrap_or_else
. The main differences is that it yields the Err
instance (instead of the underlying value) and that has T.noreturn
as its return type, so it cannot output a value. The only two valid options for the block are:
- using
return
(somewhat ironically, given theT.noreturn
return type), which will return out of the method in which the block is defined. This will only work with inline blocks, e.g.result.try? { |e| return e }
. - raising an exception.
If you think it's possible to implement something closer to Rust's ?
, I'd love to hear about it! Feel free to open an issue or PR to start a discussion.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/olivierbellone/r. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the TestGem project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.