Useful, common monads in idiomatic Ruby
Success()
, Failure()
, and Some()
now have Unit
as a default argument:
include Dry::Monads::Result::Mixin
include Dry::Monads::Do
def call
yield do_1
yield do_2
Success() # returns Success(Unit)
end
do
-like notation (the idea comes from Haskell of course). This is the biggest and most important addition to the release which greatly increases the ergonomics of using monads in Ruby. Basically, almost everything it does is passing a block to a given method. You call yield
on monads to extract the values. If any operation fails i.e. no value can be extracted, the whole computation is halted and the failing step becomes a result. With Do
you don't need to chain monadic values with fmap/bind
and block, everything can be done on a single level of indentation. Here is a more or less real-life example:
class CreateUser
include Dry::Monads
include Dry::Monads::Do.for(:call)
attr_reader :user_repo
def initialize(:user_repo)
@user_repo = user_repo
end
def call(params)
json = yield parse_json(params)
hash = yield validate(json)
user_repo.transaction do
user = yield create_user(hash[:user])
yield create_profile(user, hash[:profile])
Success(user)
end
end
private
def parse_json(params)
Try[JSON::ParserError] {
JSON.parse(params)
}.to_result
end
def validate(json)
UserSchema.(json).to_monad
end
def create_user(user_data)
Try[Sequel::Error] { user_repo.create(user_data) }.to_result
end
def create_profile(user, profile_data)
Try[Sequel::Error] {
user_repo.create_profile(user, profile_data)
}.to_result
end
end
In the code above any yield
can potentially fail and return the failure reason as a result. In other words, yield None
acts as return None
. Internally, Do
uses exceptions, not return
, this is somewhat slower but allows to detect failed operations in DB-transactions and roll back the changes which far more useful than an unjustifiable speed boost (flash-gordon)
The Task
monad based on Promise
from the concurrent-ruby
gem. Task
represents an asynchronous computation which can be (doesn't have to!) run on a separated thread. Promise
already offers a good API and implemented in a safe manner so dry-monads
just adds a monad-compatible interface for it. Out of the box, concurrent-ruby
has three types of executors for running blocks: :io
, :fast
, :immediate
, check out the docs for details. You can provide your own executor if needed (flash-gordon)
include Dry::Monads::Task::Mixin
def call
name = Task { get_name_via_http } # runs a request in the background
email = Task { get_email_via_http } # runs another one request in the background
# to_result forces both computations/requests to complete by pausing current thread
# returns `Result::Success/Result::Failure`
name.bind { |n| email.fmap { |e| create(e, n) } }.to_result
end
Task
works perfectly with Do
include Dry::Monads::Do.for(:call)
def call
name, email = yield Task { get_name_via_http }, Task { get_email_via_http }
Success(create(e, n))
end
Lazy
is a copy of Task
that isn't run until you ask for the value for the first time. It is guaranteed the evaluation is run at most once as opposed to lazy assignment ||=
which isn't synchronized. Lazy
is run on the same thread asking for the value (flash-gordon)
Automatic type inference with .typed
for lists was deprecated. Instead, typed list builders were added
list = List::Task[Task { get_name }, Task { get_email }]
list.traverse # => Task(List['John', '[email protected]'])
The code above runs two tasks in parallel and automatically combines their results with traverse
(flash-gordon)
Try
got a new call syntax supported in Ruby 2.5+
Try[ArgumentError, TypeError] { unsafe_operation }
Prior to 2.5, it wasn't possible to pass a block to []
.
The Validated
“monad” that represents a result of a validation. Suppose, you want to collect all the errors and return them at once. You can't have it with Result
because when you traverse
a List
of Result
s it returns the first value and this is the correct behavior from the theoretical point of view. Validated
, in fact, doesn't have a monad instance but provides a useful variant of applicative which concatenates the errors.
include Dry::Monads
include Dry::Monads::Do.for(:call)
def call(input)
name, email = yield [
validate_name(input[:name]),
validate_email(input[:email])
]
Success(create(name, email))
end
# can return
# * Success(User(...))
# * Invalid(List[:invalid_name])
# * Invalid(List[:invalid_email])
# * Invalid(List[:invalid_name, :invalid_email])
In the example above an array of Validated
values is implicitly coerced to List::Validated
. It's supported because it's useful but don't forget it's all about types so don't mix different types of monads in a single array, the consequences are unclear. You always can be explicit with List::Validated[validate_name(...), ...]
, choose what you like (flash-gordon).
Failure
, None
, and Invalid
values now store the line where they were created. One of the biggest downsides of dealing with monadic code is lack of backtraces. If you have a long list of computations and one of them fails how do you know where did it actually happen? Say, you've got None
and this tells you nothing about what variable was assigned to None
. It makes sense to use Result
instead of Maybe
and use distinct errors everywhere but it doesn't always look good and forces you to think more. TLDR; call .trace
to get the line where a fail-case was constructed
Failure(:invalid_name).trace # => app/operations/create_user.rb:43
Dry::Monads::Unit
which can be used as a replacement for Success(nil)
and in similar situations when you have side effects yet doesn't return anything meaningful as a result. There's also the .discard
method for mapping any successful result (i.e. Success(?)
, Some(?)
, Value(?)
, etc) to Unit
.
# we're making an HTTP request but "forget" any successful result,
# we only care if the task was complete without an error
Task { do_http_request }.discard
# ... wait for the task to finish ...
# => Task(valut=Unit)
Either
, the former name of Result
, is now deprecatedEither#value
and Maybe#value
were both droped, use value_or
or value!
when you :100: sure it's saferequire 'dry/monads'
doesn't load all monads anymore, use require 'dry/monads/all'
instead or cherry pick them with require 'dry/monads/maybe'
etc (timriley)Either
monad was renamed to Result
which sounds less nerdy but better reflects the purpose of the type. Either::Right
became Result::Success
and Either::Left
became Result::Failure
. This change is backward-compatible overall but you will see the new names when using old Left
and Right
methods (citizen428)Try::Success
and Try::Failure
were renamed to Try::Value
and Try::Error
(flash-gordon)Try#or
, works as Result#or
(flash-gordon)Maybe#success?
and Maybe#failure?
(aliases for #some?
and #none?
) (flash-gordon)Either#flip
inverts a Result
value (flash-gordon)List#map
called without a block returns an Enumerator
object (flash-gordon)Maybe
, Result
, and Try
) now implement the ===
operator which is used for equality checks in the case
statement (flash-gordon)
case value
when Some(1..100) then :ok
when Some { |x| x < 0 } then :negative
when Some(Integer) then :invalid
else raise TypeError
end
value
on right-biased monads has been deprecated, use the value!
method instead. value!
will raise an exception if it is called on a Failure/None/Error instance (flash-gordon)Either#either
that accepts two callbacks, runs the first if it is Right
and the second otherwise (nkondratyev)#fmap2
and #fmap3
for mapping over nested structures like List Either
and Either Some
(flash-gordon)Try#value_or
(dsounded)List
monad which acts as an immutable Array
and plays nice with other monads. A common example is a list of Either
s (flash-gordon)#bind
made to work with keyword arguments as extra parameters to the block (flash-gordon)List#traverse
that "flips" the list with an embedded monad (flash-gordon + damncabbage)#tee
for all right-biased monads (flash-gordon)