A Scala inspired gem that introduces Trys to Ruby while aiming for an idiomatic API.
Add this line to your application's Gemfile:
gem 'ytry'And then execute:
$ bundle
Or install it yourself as:
$ gem install ytry
The Try type represents a computation that may either result in an error, or return a successfully computed value.
If the block passed to Try runs with no errors, then a Success wrapping the computed value is returned.
An instance of Failure wrapping the error is returned otherwise.
require 'ytry'
include Ytry
Try { 1 + 1 } # Success(2)
Try { 1 / 0 } # Failure(#<ZeroDivisionError: divided by 0>)Success and Failure provide a unified API that lets us express a sequence of tranformations in a fluent way, without error handling cluttering the flow:
def load_and_parse json_file
Try { File.read(json_file) }
.map {|content| JSON.parse(content)}
.select {|table| table.is_a? Array}
.recover {|e| puts "Recovering from #{e.message}"; []}
end
load_and_parse(nonexisting_file) # prints "Recovering from No such file..." # Success([])
load_and_parse(wrong_format_file) # prints "Recovering from Element not found" # Success([])
load_and_parse(actual_file) # Success([{"id"=>1, "name"=>"Lorenzo", "dob"=>"22/07/1985"}])Try#map and Try#recover are means to interact with the value wrapped by a Try in a safe way - i.e. with no risk of errors being raised.
Try#select transforms a Success into a Failure when the underlying value does not satisfy the given predicate - i.e. the given block returns false. That can be useful when validating some input.
Try#get_or_else provides a safe way of retrieving the possibly-missing value it contains. It returns the result of the given block when the Try is a Failure. It is equivalent to Try#get when the Try is a Success.
invalid_json = "[\"missing_quote]"
Try { JSON.parse(invalid_json) }
.get_or_else{ [] } # []
Try { JSON.parse("[]") }
.get_or_else { fail "this block is ignored"} # []It is preferable to use Try#get_or_else over Try#get, as #get will raise an error when called on a Failure. It is possible to check for failure via #empty?, but that tipically leads to non-idiomatic code
Using Try instead of rescue blocks can make your software both clearer and safer as it
- leads to less verbose error handling
- simplifies the way we deal with operations that might fail for several reasons (such as IO operations)
- privileges method chaining thus reducing the need for auxiliary variables to store intermediate results in a computation
- encourages programming towards immutability, where the data is transformed rather than mutated in place.
Given a Try instance try, a value c and a lambda f,
try.reduce(c, &f)
returns f.(c, try) if try is a Success AND the evaluation of the lambda f did not throw any error, it returns c otherwise.
This is a shortcut to
try.map{|v| f.(c,v)}.get_or_else {c}
When dealing with nested Trys we can use flatten to reduce the level of nesting
success = Try{:ok}
failure = Try{fail}
Try{success}.flatten # Success(:ok)
Try{failure}.flatten # Failure(RuntimeError)
flatten accepts one argument, defaulting to 1, which indicates the depth of the flattening operation. Mind that a Success can only be flattened as long as it wraps a Try instance. A Failure can be flattened an arbitrary number of times, as it always returns itself.
Try{success}.flatten(1) == Try{success}.flatten # true
Try{success}.flatten(2) # raises TypeError: Argument must be an array-like object. Found Fixnum
Because of it's ary-like nature, instances of Try play well with Array instances. In particular, flattening an Array of Trys is equivalent to filtering out the Failures from the array and then calling #get on the Success instances
(1..4).map{|v| Try{v}.select(&:odd?)}
.flatten # [1, 3]
Behind the scenes Array#flatten is iterating over the collection and concatenating the ary-representation of each element.
Now Failure#to_ary returns [], while Success#to_ary returns [v] - where v is the value wrapped by Success - and that does the trick.
We can squeeze the code listed above even more with Array#flat_map
(1..4).flat_map{|v| Try{v}.select(&:odd?)} # [1, 3]
Again, there is no magic behind this behaviour, we are just exploiting Ruby's duck typing.
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 tags, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/lbarasti/ytry. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.