diff --git a/Gemfile.lock b/Gemfile.lock index 53ba9d0..57096ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,6 +85,7 @@ GEM sorbet (0.5.11274) sorbet-static (= 0.5.11274) sorbet-runtime (0.5.11274) + sorbet-static (0.5.11274-universal-darwin) sorbet-static (0.5.11274-x86_64-linux) sorbet-static-and-runtime (0.5.11274) sorbet (= 0.5.11274) @@ -117,6 +118,7 @@ GEM yard (>= 0.9) PLATFORMS + arm64-darwin-23 x86_64-linux DEPENDENCIES diff --git a/README.md b/README.md index 5995efd..1d26944 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,19 @@ end 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. +TODO: using `propagate!` and `try!` +```ruby +sig { params(info: Info).returns(R::Result[NilClass, StandardError]) } +def write_info(info) + R.propagate! do + file = file_create("my_best_friends.txt").try! + file_write_all(file, "name: #{info.name}\n").try! + file_write_all(file, "age: #{info.age}\n").try! + file_write_all(file, "rating: #{info.rating}\n").try! + end +end +``` + ## Development 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. diff --git a/lib/r/result.rb b/lib/r/result.rb index 2966bf3..a24f17e 100644 --- a/lib/r/result.rb +++ b/lib/r/result.rb @@ -580,6 +580,9 @@ def unwrap_or_else(&blk); end .returns(OkType) end def try?(&blk); end + + sig { abstract.returns(T.self_type) } + def try!; end end # Creates a new instance of {Ok}. @@ -597,6 +600,22 @@ def self.ok(value) Ok.new(value) end + sig do + type_parameters(:Ok, :Err) + .params( + blk: T.proc.returns(T.nilable(Result[T.type_parameter(:Ok), T.type_parameter(:Err)])), + ) + .returns(Result[T.type_parameter(:Ok), T.type_parameter(:Err)]) + end + def self.propagate!(&blk) + # TODO: using singleton class instance variables is obviously bad and not + # thread-safe but demonstrates how it might work. + @ball = T.let(Object.new, Object) + catch(@ball) { blk.call || R.ok(nil) } # rubocop:disable Performance/RedundantBlockCall + ensure + @ball = nil + end + # Contains the success value. # # @see R::Result @@ -1102,6 +1121,11 @@ def try?(&blk) @value end + sig(:final) { override.returns(T.self_type) } + def try! + self + end + # Calls the provided block with the contained value. # # Returns `self` so this can be chained with {#on_err}. @@ -1656,6 +1680,15 @@ def try?(&blk) yield(self) end + sig(:final) { override.returns(T.self_type) } + def try! + if (ball = R.instance_variable_get(:@ball)) + throw(ball, self) + else + self + end + end + # Does nothing. # # Returns `self` so this can be chained with {#on_err}. diff --git a/test/r/result_test.rb b/test/r/result_test.rb index 49340ae..9128cab 100644 --- a/test/r/result_test.rb +++ b/test/r/result_test.rb @@ -5,6 +5,34 @@ module R class ResultTest < Minitest::Test + describe "R.propagate!" do + it "propagates the first try! error" do + ok1 = T.let(R.ok(1), R::Result[Integer, String]) + err1 = T.let(R.err("err1"), R::Result[Integer, String]) + err2 = T.let(R.err("err2"), R::Result[Integer, String]) + + result = R.propagate! do + ok1.try! + err1.try! + err2.try! + end + + assert_equal(R.err("err1"), result) + end + + it "propagates the last try! ok" do + ok1 = T.let(R.ok(1), R::Result[Integer, String]) + ok2 = T.let(R.ok(2), R::Result[Integer, String]) + + result = R.propagate! do + ok1.try! + ok2.try! + end + + assert_equal(R.ok(2), result) + end + end + describe "R.ok" do it "returns a new instance of R::Ok" do x = R.ok(0)