Releases: dry-rb/dry-monads
v1.2.0
v1.2.0 2019-01-12
BREAKING CHANGES
- Support for Ruby 2.2 was dropped. Ruby 2.2 reached its EOL on March 31, 2018.
Added
-
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
gathersSome
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 likeMaybe
ofMaybe
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 asArray#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 ofTask.pure
, accepts an exception and returns a failed task immediately (flash-gordon)
v1.1.0
v1.1.0 2018-10-16
Fixed
- Do notation was made to work nicely with inheritance. This shouldn't break any existing code but if it does please report (flash-gordon)
Added
Success()
,Failure()
, andSome()
now haveUnit
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
v1.0.1
v1.0.1 2018-08-11
Fixed
- Fixed behavior of
List<Validated>#traverse
in presence ofValid
values (flash-gordon + SunnyMagadan)
Added
to_proc
was added to value constructors (flash-gordon)[1, 2, 3].map(&Some) # => [Some(1), Some(2), Some(3)]
v1.0.0
v1.0.0 2018-06-26
Added
-
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 callyield
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. WithDo
you don't need to chain monadic values withfmap/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 asreturn None
. Internally,Do
uses exceptions, notreturn
, 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 onPromise
from theconcurrent-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 sodry-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 withDo
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 ofTask
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 addedlist = 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 withResult
because when youtraverse
aList
ofResult
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 toList::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 withList::Validated[validate_name(...), ...]
, choose what you like (flash-gordon). -
Failure
,None
, andInvalid
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 gotNone
and this tells you nothing about what variable was assigned toNone
. It makes sense to useResult
instead ofMaybe
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 constructedFailure(:invalid_name).trace # => app/operations/create_user.rb:43
-
Dry::Monads::Unit
which can be used as a replacement forSuccess(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) toUnit
.# 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)
Deprecations
Either
, the former name ofResult
, is now deprecated
BREAKING CHANGES
Either#value
andMaybe#value
were both droped, usevalue_or
orvalue!
when you 💯 sure it's saferequire 'dry/monads'
doesn't load all monads anymore, userequire 'dry/monads/all'
instead or cherry pick them withrequire 'dry/monads/maybe'
etc (timriley)
v0.4.0
v0.4.0 2017-11-11
Changed
- The
Either
monad was renamed toResult
which sounds less nerdy but better reflects the purpose of the type.Either::Right
becameResult::Success
andEither::Left
becameResult::Failure
. This change is backward-compatible overall but you will see the new names when using oldLeft
andRight
methods (citizen428) - Consequently,
Try::Success
andTry::Failure
were renamed toTry::Value
andTry::Error
(flash-gordon)
Added
Try#or
, works asResult#or
(flash-gordon)Maybe#success?
andMaybe#failure?
(aliases for#some?
and#none?
) (flash-gordon)Either#flip
inverts aResult
value (flash-gordon)List#map
called without a block returns anEnumerator
object (flash-gordon)- Right-biased monads (
Maybe
,Result
, andTry
) now implement the===
operator which is used for equality checks in thecase
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
Deprecated
- Direct accessing
value
on right-biased monads has been deprecated, use thevalue!
method instead.value!
will raise an exception if it is called on a Failure/None/Error instance (flash-gordon)
v0.3.1
v0.3.0
Added
- Added
Either#either
that accepts two callbacks, runs the first if it isRight
and the second otherwise (nkondratyev) - Added
#fmap2
and#fmap3
for mapping over nested structures likeList Either
andEither Some
(flash-gordon) - Added
Try#value_or
(dsounded) - Added the
List
monad which acts as an immutableArray
and plays nice with other monads. A common example is a list ofEither
s (flash-gordon) #bind
made to work with keyword arguments as extra parameters to the block (flash-gordon)- Added
List#traverse
that "flips" the list with an embedded monad (flash-gordon + damncabbage) - Added
#tee
for all right-biased monads (flash-gordon)