dry-monads 1.0 released

Today dry-monads reaches 1.0! It started as a dependency replacement for the Kleisli gem in dry-transaction and dry-types. Later, more common monads were added, as well as support for do notation, which evaporates most of the boilerplate introduced by monads. Since the dry-* gems follow semantic versioning, this means you can consider the dry-monads API to be stable, making the gem more "production-ready". Let us show how monads can be useful in day-to-day ruby code.

Result

Result is the most widely used monad from dry-monads so far. It represents a possibly unsuccessful computation. A trivial example:

require 'dry/monads/result'

class Divide
  include Dry::Monads::Result::Mixin

  def call(x, y)
    if !y.zero?
      Success(x / y)
    else
      Failure(:division_by_zero)
    end
  end
end

Result::Mixin adds two constructors named Success(...) and Failure(...) so that you can separate the happy path from errors.

Suppose we have another math operation, square root:

require 'dry/monads/result'

class Sqrt
  include Dry::Monads::Result::Mixin

  def call(x)
    if !x.negative?
      Success(Math.sqrt(x))
    else
      Failure(:negative_number)
    end
  end
end

Now, as with other monads, we can use bind for composition:

class DivideThenRoot
  def divide
    Divide.new
  end

  def sqrt
    Sqrt.new
  end

  def call(x, y)
    divide.(x, y).bind(sqrt)
  end
end
op = DivideThenRoot.new
op.(1.0, 2.0) # => Success(0.7071067811865476)
op.(1.0, 0.0) # => Failure(:division_by_zero)
op.(-1.0, 2.0) # => Failure(:negative_number)

DivideThenRoot can be composed with other objects or methods returning Results in a similar manner. In the end, you can use dry-matcher for processing the result (or use the Result's API for it).

Real-life code looks the same in general but usually combines more operations together. Here it can become tedious to use bind and fmap directly. This is why we added do notation in the 1.0 release.

Do notation

The name "do" comes from Haskell, where it's a reserved word for a block of code that uses monads to compose results of several operations. We don't have first-class support for it in Ruby, but it's quite possible to emulate it using blocks. Here's a typical piece of code written with do:

require 'dry/monads/do'
require 'dry/monads/result'

class CreateAccount
  include Dry::Monads::Result::Mixin
  include Dry::Monads::Do

  def call(params)
    values = yield validate(params)
    owner = yield create_owner(values[:user])
    account = yield create_account(values[:account])

    yield create_subscription(account, owner)

    Success(account)
  end

  # ...
end

Here it's implied that the validate, create_owner, create_account, and create_subscription methods all return Results. yield takes a Result value and either unwraps it if it's a Success, or interrupts the execution and immediately returns the Failure from call. With do it's extremely easy to combine results of different operations no matter the order in which they're called. This is a major step forward to making monads practically useful in Ruby.

Task

Another highlight from the release is the Task monad. Backed by concurrent-ruby, a battle-tested concurrency gem, Task can be used for composing asynchronous computations. Essentially, it's a Promise with a dry-monads-compatible interface.

require 'dry/monads/task'
require 'dry/monads/do'

class CreateUser
  include Dry::Monads::Task::Mixin
  include Dry::Monads::Do

  def call(email, name)
    # Run two concurrent requests, wait for both to finish using Do
    yield validate_email(email), validate_name(name)

    create_user(email, name)
  end

  def validate_email(email)
    # Ruby 2.5+ syntax
    Task[:io] {
      # async email check, e.g. with an http request
    }
  end

  def validate_name(name)
    Task[:io] {
      # async name check
    }
  end

  def create_user(email, name)
    Task[:io] {
      # async create
    }
  end
end

Still Ruby

Despite using concepts borrowed from other languages, dry-monads stays as much idiomatic to Ruby as possible. We have no plans to turn it into Haskell. Yet.

Maturity

The gem is pretty much complete, and has been used in production for more than two years. We don't expect any major changes to the API, since the scope of monads, in general, is limited. This means any integration code will most likely be somewhere else.

Acknowledgements

dry-monads is a combined effort of more than a dozen people. Thank you all for your work and feedback, it is much appreciated!