Useful, common monads in idiomatic Ruby
Unit
destructures to an empty array (flash-gordon).value!
called on a Failure
value the error references to the value (rewritten + flash-gordon)
begin
Failure("oops").value!
rescue => error
error.receiver # => Failure("oops")
end
Result#alt_map
for mapping failure values (flash-gordon)
Failure("oops").alt_map(&:upcase) # => Failure("OOPS")
Try#recover
recovers from errors (flash-gordon)
error = Try { Hash.new.fetch(:missing) }
error.recover(KeyError) { 'default' } # => Try::Value("default")
Maybe#filter
runs a predicate against the wrapped value. Returns None
if the result is false (flash-gordon)
Some(3).filter(&:odd?) # => Some(3)
Some(3).filter(&:even?) # => None
# no block given
Some(3 == 5).filter # => None
RightBiased#|
is an alias for #or
(flash-gordon)
None() | Some(6) | Some(7) # => Some(6)
Failure() | Success("one") | Success("two") # => Success("one")
nil
values to None
with Some#fmap
is officially deprecated. (flash-gordon)
Switch to Some#maybe
when you expect nil
.
This behavior will be dropped in 2.0 but you can opt-out of warnings for the time being
Dry::Monads::Maybe.warn_on_implicit_nil_coercion false
#deconstruct_keys
. Now curly braces aren't necessary when the wrapped value is a Hash (flash-gordon)
case result
in Success(code: 200...300) then :ok
end
Result#either
(waiting-for-dev)
Success(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 2
Failure(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 3
Maybe#to_result
(SpyMachine + flash-gordon)
Some(3).to_result(:no_value) # => Success(3)
None().to_result { :no_value } # => Failure(:no_value)
None().to_result # => Failure()
Do notation can be used with extend
. This simplifies usage in class methods and in other "complicated" cases (gogiel + flash-gordon)
class CreateUser
extend Dry::Monads::Do::Mixin
extend Dry::Monads[:result]
def self.run(params)
self.call do
values = bind Validator.validate(params)
user = bind UserRepository.create(values)
Success(user)
end
end
end
Or you can bind values directly:
ma = Dry::Monads.Success(1)
mb = Dry::Monads.Success(2)
Dry::Monads::Do.() do
a = Dry::Monads::Do.bind(ma)
b = Dry::Monads::Do.bind(mb)
Dry::Monads.Success(a + b)
end
{Some,Success,Failure}#[]
shortcuts for building arrays wrapped within monadic value (flash-gordon)
Success[1, 2] # => Success([1, 2])
List.unfold
yields a block returning Maybe<Any>
. If the block returns Some(a)
a
is appended to the output list. Returning None
halts the unfloding (flash-gordon)
List.unfold(0) do |x|
if x > 5
None()
else
Some[x + 1, 2**x]
end
end # => List[1, 2, 3, 4, 5]
Experimental support for pattern matching! :tada: (flash-gordon)
case value
in Failure(_) then :failure
in Success(10) then :ten
in Success(100..500 => code) then code
in Success() then :empty
in Success(:code, x) then x
in Success[:status, x] then x
in Success({ status: x }) then x
in Success({ code: 200..300 => x }) then x
end
Read more about pattern matching in Ruby:
Keep in mind this feature is experimental and can be changed by 2.7 release. But it rocks already!
Most of the constructors now have call
alias so you can compose them with Procs nicely if you've switched to Ruby 2.6 (flash-gordon)
pipe = -> x { x.upcase } >> Success
pipe.('foo') # => Success('FOO')
List#collect
gathers Some
values from the list (flash-gordon)
include Dry::Monads::List::Mixin
include Dry::Monads::Maybe::Mixin
# ...
List[10, 5, 0].collect do |divisor|
if divisor.zero?
None()
else
Some(n / divisor)
end
end
# => List[4, 2]
Without block:
List[Some(5), None(), Some(3)].collect.map { |x| x * 2 }
# => [10, 6]
Right-biased monads got #flatten
and #and
(falsh-gordon)
#flatten
removes one level of monadic structure, it's useful when you're dealing with things like Maybe
of Maybe
of something:
include Dry::Monads::Maybe::Mixin
Some(Some(1)).flatten # => Some(1)
Some(None()).flatten # => None
None().flatten # => None
In contrast to Array#flatten
, dry-monads' version removes only 1 level of nesting, that is always acts as Array#flatten(1)
:
Some(Some(Some(1))).flatten # => Some(Some(1))
#and
is handy for combining two monadic values and working with them at once:
include Dry::Monads::Maybe::Mixin
# using block
Some(5).and(Some(3)) { |x, y| x + y } # => Some(8)
# without block
Some(5).and(Some(3)) # => Some([5, 3])
# other cases
Some(5).and(None()) # => None()
None().and(Some(5)) # => None()
Concise imports with Dry::Monads.[]
. You're no longer required to require all desired monads and include them one-by-one, the []
method handles it for you (flash-gordon)
require 'dry/monads'
class CreateUser
include Dry::Monads[:result, :do]
def initialize(repo, send_email)
@repo = repo
@send_email = send_email
end
def call(name)
if @repo.user_exist?(name)
Failure(:user_exists)
else
user = yield @repo.add_user(name)
yield @send_email.(user)
Success(user)
end
end
end
Task.failed
is a counterpart of Task.pure
, accepts an exception and returns a failed task immediately (flash-gordon)