All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Break Versioning.
1.10.0 - 2026-04-24
- Support for JRuby (via fixes to RSpec extension). (@katafrakt in #200 & #201, @flash-gordon in c6967b6)
- Fix param names and Ruby syntax in API doc examples. (@mensfeld in #196)
1.9.0 - 2025-06-24
- Add
pretty_printextension for improved output (@paul + @flash-gordon in #189 and #190)
- Fix nested modules not being extended in RSpec (@flash-gordon)
1.8.3 - 2025-04-04
- Fix signature of warning filter (@flash-gordon, issue #187)
1.8.2 - 2025-03-15
- Fix be_* matchers for non-monadic values (@flash-gordon, issue #186)
1.8.1 - 2025-03-12
- Exclude extensions from the Zeitwerk loader (@flash-gordon, issue #185)
1.8.0 - 2025-03-12
-
New extension for RSpec (@flash-gordon in #183): One of the pain points of testing monads is referencing class constants from specs. This extension catches missing class constants, analyzes the call site and returns a matching constant.
Before, this code would raise a
NameErrorbecauseFailureis a constant that is missing inObject:example "missing constant" do expect(call_operation).to eql(Failure[:some_error, "some message"]) end
Now, after enabling the extension, it will return the correct constant:
Dry::Monads.load_extensions(:rspec) example "missing constant" do Failure[:some_error, "some message"] # => Failure[:some_error, "some message"] end
Out of the box, the extension will check if
Success,Failure,Some, andNoneare referenced from a file ending with_spec.rb.More involved analysis is possible if you add
debug_inspectorto your Gemfile:group :test do gem "debug_inspector" end
This will allow referencing constants from other modules, such as rspec helpers.
The extension also adds new matchers for
Success,Failure,Some, andNonevalues.expect(Success(1)).to be_success expect(Success(1)).to be_success(1) expect(Success(1)).to be_success { |x| x > 0 } expect(Success(1)).to be_a_success { |x| x > 0 } expect(Failure(1)).to be_failure(1) expect(Some(1)).to be_some expect(Some(1)).to be_success expect(None()).to be_none expect(None()).to be_failure
-
New extension for super_diff (@flash-gordon in #184):
Adds support for improved diff output in specs when using the super_diff gem. This makes it easier to understand the differences between monad values in test failures.
To use this extension:
- Add super_diff to your Gemfile's test group:
group :test do gem "super_diff" end
- Load the extension:
require "dry/monads" Dry::Monads.load_extensions(:super_diff)
This will change the diff output for monad values to be more readable.
Before:
-Success({a: 2, c: 2}) +Success({a: 1, b: 2})After:
Success( - a: 2, + a: 1, - c: 2 + b: 2 ) - Add super_diff to your Gemfile's test group:
1.7.1 - 2025-01-21
- Fix warnings about unused block arguments (@flash-gordon)
1.7.0 - 2025-01-07
- Fix pattern matching for
Tryvalues (@alexkalderimis)
- Set 3.1 as minimum Ruby version (@flash-gordon)
1.6.0 - 2022-11-04
- This version uses dry-core 1.0 (@flash-gordon)
1.5.0 - 2022-10-16
- Use zeitwerk for auto-loading dry-monads classes (@flash-gordon)
Task#thenis deprecated in favor ofTask#bind(@flash-gordon)- Minimal Ruby version is now 2.7 (@flash-gordon)
- Either (old name of Result) was removed (@flash-gordon)
1.4.0 - 2021-07-20
Unitdestructures to an empty array (flash-gordon)- When
.value!called on aFailurevalue the error references to the value (rewritten + flash-gordon)begin Failure("oops").value! rescue => error error.receiver # => Failure("oops") end
Result#alt_mapfor mapping failure values (flash-gordon)Failure("oops").alt_map(&:upcase) # => Failure("OOPS")
Try#recoverrecovers from errors (flash-gordon)error = Try { Hash.new.fetch(:missing) } error.recover(KeyError) { 'default' } # => Try::Value("default")
Maybe#filterruns a predicate against the wrapped value. ReturnsNoneif 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")
- Do notation preserves method visibility (anicholson + flash-gordon)
- Coercing
nilvalues toNonewithSome#fmapis officially deprecated. (flash-gordon) Switch toSome#maybewhen you expectnil. This behavior will be dropped in 2.0 but you can opt out of warnings for the time beingDry::Monads::Maybe.warn_on_implicit_nil_coercion false
- Minimal Ruby version is 2.6
1.3.5 - 2020-01-06
- Smarter keys deconstruction in pattern matching (flash-gordon)
1.3.4 - 2019-12-28
- One more delegation warning happenning in do notation (flash-gordon)
1.3.3 - 2019-12-11
- Incompatibility with Rails. Internal (!) halt exceptions now use mutable backtraces because spring mutates (!) them. For the record, this a bug in Rails (johnmaxwell)
1.3.2 - 2019-11-30
- Pattern matching syntax was improved by implementing
#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
- Performance of do notation was improved for failing cases (1.2x to 1.3x on synthetic benchmarks) (flash-gordon)
- Warnings about keywords from Ruby 2.7 (flash-gordon)
1.3.1 - 2019-09-07
- Added missing
None#maybe😅 (flash-gordon)
1.3.0 - 2019-08-03
-
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.unfoldyields a block returningMaybe<Any>. If the block returnsSome(a)ais appended to the output list. ReturningNonehalts 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! 🎉 (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:
- https://medium.com/@baweaver/ruby-2-7-pattern-matching-destructuring-on-point-90f56aaf7b4e
- https://bugs.ruby-lang.org/issues/14912
Keep in mind this feature is experimental and can be changed by 2.7 release. But it rocks already!
1.2.0 - 2019-01-12
-
Most of constructors now have
callalias 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#collectgathersSomevalues 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
#flattenand#and(falsh-gordon)#flattenremoves one level of monadic structure, it's useful when you're dealing with things likeMaybeofMaybeof 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))
#andis 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.failedis a counterpart ofTask.pure, accepts an exception and returns a failed task immediately (flash-gordon)
1.1.0 - 2018-10-16
-
Success(),Failure(), andSome()now haveUnitas 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 notation was made to work nicely with inheritance. This shouldn't break any existing code but if it does please report (flash-gordon)
1.0.1 - 2018-08-11
to_procwas added to value constructors (flash-gordon)[1, 2, 3].map(&Some) # => [Some(1), Some(2), Some(3)]
- Fixed behavior of
List<Validated>#traversein presence ofValidvalues (flash-gordon + SunnyMagadan)
1.0.0 - 2018-06-26
-
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 callyieldon 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. WithDoyou don't need to chain monadic values withfmap/bindand 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
yieldcan potentially fail and return the failure reason as a result. In other words,yield Noneacts asreturn None. Internally,Douses 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
Taskmonad based onPromisefrom theconcurrent-rubygem.Taskrepresents an asynchronous computation which can be (doesn't have to!) run on a separated thread.Promisealready offers a good API and implemented in a safe manner sodry-monadsjust adds a monad-compatible interface for it. Out of the box,concurrent-rubyhas 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
Taskworks perfectly withDoinclude 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
-
Lazyis a copy ofTaskthat 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.Lazyis run on the same thread asking for the value (flash-gordon) -
Automatic type inference with
.typedfor lists was deprecated. Instead, typed list builders were addedlist = List::Task[Task { get_name }, Task { get_email }] list.traverse # => Task(List['John', 'john@doe.org'])
The code above runs two tasks in parallel and automatically combines their results with
traverse(flash-gordon) -
Trygot 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 withResultbecause when youtraverseaListofResults 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
Validatedvalues 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, andInvalidvalues 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 gotNoneand this tells you nothing about what variable was assigned toNone. It makes sense to useResultinstead ofMaybeand use distinct errors everywhere but it doesn't always look good and forces you to think more. TLDR; call.traceto get the line where a fail-case was constructedFailure(:invalid_name).trace # => app/operations/create_user.rb:43
-
Dry::Monads::Unitwhich 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.discardmethod 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)
-
Either, the former name ofResult, is now deprecated -
Either#valueandMaybe#valuewere both droped, usevalue_ororvalue!when you 💯 sure it's safe -
require 'dry/monads'doesn't load all monads anymore, userequire 'dry/monads/all'instead or cherry pick them withrequire 'dry/monads/maybe'etc (timriley)
0.4.0 - 2017-11-11
Try#or, works asResult#or(flash-gordon)Maybe#success?andMaybe#failure?(aliases for#some?and#none?) (flash-gordon)Either#flipinverts aResultvalue (flash-gordon)List#mapcalled without a block returns anEnumeratorobject (flash-gordon)- Right-biased monads (
Maybe,Result, andTry) now implement the===operator which is used for equality checks in thecasestatement (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
- Direct accessing
valueon 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)
- The
Eithermonad was renamed toResultwhich sounds less nerdy but better reflects the purpose of the type.Either::RightbecameResult::SuccessandEither::LeftbecameResult::Failure. This change is backward-compatible overall but you will see the new names when using oldLeftandRightmethods (citizen428) - Consequently,
Try::SuccessandTry::Failurewere renamed toTry::ValueandTry::Error(flash-gordon)
0.3.1 - 2017-03-18
- Fixed unexpected coercing to
Hashon.bindcall (flash-gordon)
0.3.0 - 2017-03-16
- Added
Either#eitherthat accepts two callbacks, runs the first if it isRightand the second otherwise (nkondratyev) - Added
#fmap2and#fmap3for mapping over nested structures likeList EitherandEither Some(flash-gordon) - Added
Try#value_or(dsounded) - Added the
Listmonad which acts as an immutableArrayand plays nice with other monads. A common example is a list ofEithers (flash-gordon) #bindmade to work with keyword arguments as extra parameters to the block (flash-gordon)- Added
List#traversethat "flips" the list with an embedded monad (flash-gordon + damncabbage) - Added
#teefor all right-biased monads (flash-gordon)
0.2.1 - 2016-11-13
- Added
Either#teethat is similar toObject#tapbut executes the block only forRightinstances (saverio-kantox)
Right(nil).to_maybenow returnsNonewith a warning instead of failing (orisaka)Some#value_ordoesn't require an argument becauseNone#value_ordoesn't require it either if a block was passed (flash-gordon)
0.2.0 - 2016-09-18
- Added
Maybe#to_jsonas an opt-in extension for serialization to JSON (rocknruby) - Added
Maybe#value_orwhich returns you the underlying value with a fallback in a single method call (dsounded)
0.1.1 - 2016-08-25
- Added explicit requires of
dry-equalizer. This allows to safely load only specific monads (artofhuman)
0.1.0 - 2016-08-23
- Support for passing extra arguments to the block in
.bindand.fmap(flash-gordon)
- Dropped MRI 2.0 support (flash-gordon)
0.0.2 - 2016-06-29
- Added
Either#to_eitherso that you can rely on duck-typing when you work with different types of monads (timriley) - Added
Maybe#to_maybefor consistency with#to_either(flash-gordon)
Initial release containing Either, Maybe, and Try monads.