# Exploring Dry-RB With Dry Matcher And Dry Monads

> See how we can use the Dry RB gems dry-matcher and dry-monads to clean up our Ruby code.

- Canonical: https://dennisokeeffe.com/blog/2022-02-21-exploring-dry-rb-with-dry-matcher-and-dry-monads
- Published: 2022-02-21
- Updated: 2022-02-21
- Tags: ruby

This post will give a short introduction to the [`dry-monads`](https://dry-rb.org/gems/dry-monads/1.3/) and [`dry-matcher`](https://dry-rb.org/gems/dry-matcher/0.8/) gems and walk through how we can use these gems to clean up our Ruby code to create a nicer developer experience.

Our project today will be to create a contrived `Calculator` class that will make use of the `dry-monads` gem and a `CrunchNumbers` class that operates as an imperative shell calling the calculator instance and handling the responses.

Source code can be found [here](https://github.com/okeeffed/demo-dry-monads-matcher)

## Prerequisites

1. Basic familiarity with [Bundler](https://bundler.io/).
2. Familiarity with reading tests written in [RSpec](https://rspec.info/).
3. Familiarity with the [Pry Gem](https://www.rubydoc.info/gems/pry/0.10.3/Pry).

## Getting started

We will create the project directory `demo-dry-monads-matcher` and use Bundler to initialize the project:

```s
$ mkdir demo-dry-monads-matcher
$ cd demo-dry-monads-matcher

# initialise Bundler project
$ bundler init
# RSpec added for testing
$ bundler add rspec --group "development,test"
# Pry added for debugging
$ bundler add pry --group "development,test"
# Add the other required dry-rb gems
$ bundler add dry-monads
$ bundler add dry-matcher

# Create some folders and files that we will use
$ mkdir lib spec
$ touch lib/calculator.rb lib/crunch_numbers.rb spec/crunch_numbers_spec.rb
```

We will be creating both a `Calculator` class that features a `call` method to call some contrived private methods and a `CrunchNumbers` class that will rely on dependency injection of a calculator instance, use that instance to method `call` and then handle the responses.

## Creating the Calculator class.

Inside of `lib/calculator.rb`, add the following:

```rb
require 'dry/monads'

class Calculator
  include Dry::Monads[:result, :do]

  def call(input:)
    value = Integer(input)

    value_add_3 = yield add_3_if_gt_1(value)
    value_mult_2 = yield mult_2_if_even(value_add_3)

    Success(value_mult_2)
  end

  private

  def add_3_if_gt_1(value)
    if value > 1
      Success(value + 3)
    else
      Failure(:less_than_one)
    end
  end

  def mult_2_if_even(value)
    if value.even?
      Success(value * 2)
    else
      Failure(:not_even)
    end
  end
end
```

In our contrived class, we are writing two private methods that will return either a `Success` or `Failure` monad based on the input.

The `call` method that invokes both of our private methods makes use of the `yield` keyword to assign the result of invoking a private method to a variable, otherwise it will return the `Failure` monad to the caller.

For example, if we call `calculator.call(3)`, the return value will be `Success(12)`, however if we pass invalid values we could possibly have a return value of `Failure(:less_than_one)` or `Failure(:not_even)`.

This will be demonstrated in our RSpec tests that we will write later.

## Creating the CrunchNumbers class

Our `CrunchNumbers` class will be a class that requires a calculator instance to be injected into it, as well as an input value (that we are inferring to be an integer). The instance will then have a `crunch` method that will call the `call` method on the calculator instance.

We will use `dry-matcher` to match on the result in order to demonstrate the value of the gem:

```rb
require 'calculator'
require 'dry/matcher/result_matcher'

class CrunchNumbers
	include Dry::Monads[:result]

  def crunch(calculator:, input:)
    Dry::Matcher::ResultMatcher.call(calculator.call(input: input)) do |m|
      m.success do |response|
        "Yay! Value is #{response}"
      end

      m.failure :less_than_one do
        'Nay less than one'
      end

      m.failure :not_even do
        'Nay not even'
      end
    end
  end
end
```

With `m`, we can check if the response was successful or whether it was a particular failure so that we can handle the response accordingly.

In order to test this, let's write our RSpec tests.

## Writing our specs

Inside of `spec/crunch_numbers_spec.rb`, add the following:

```rb
require 'crunch_numbers'
require 'calculator'

RSpec.describe CrunchNumbers do
  let(:calculator) { Calculator.new }

  describe '#crunch' do
    context 'valid input' do
      it 'returns "Yay! Value is <expect_value>" when the return value is greater than 1 and even' do
        expect(subject.crunch(calculator: calculator, input: 3)).to eq('Yay! Value is 12')
      end
    end

    context 'invalid input' do
      it 'returns "Nay less than one" when the add_3 fails > 1 validation' do
        expect(subject.crunch(calculator: calculator, input: 1)).to eq('Nay less than one')
      end

      it 'returns "Nay not even" when the return value is odd' do
        expect(subject.crunch(calculator: calculator, input: 2)).to eq('Nay not even')
      end
    end
  end
end
```

In our tests, we are checking that the `crunch` method returns the expected value when the input is valid and when the input is invalid.

We are also creating a `calculator` variable to use between each test.

Our `context` blocks help us to visually see the tests in a more readable format based on valid or invalid `input` values.

If we run `bundle exec rspec`, we can see that our tests pass as expect and `dry-matcher` is working as expected.

```s
$ bundle exec rspec
...

Finished in 0.00593 seconds (files took 0.16121 seconds to load)
3 examples, 0 failures
```

Awesome! We have successfully written our `CrunchNumbers` class to handle all expected outcomes from our `Calculator` class.

## Pattern matching with Dry-Monads

Although the `dry-matcher` gem has proven useful, an alternative way to implement our code would be to match use of pattern matching in Ruby (for Ruby 2.7+).

In our `lib/crunch_numbers.rb` file, update the code to the following:

```rb
require 'calculator'
require 'dry/matcher/result_matcher'

class CrunchNumbers
  include Dry::Monads[:result]

  def crunch(calculator:, input:)
    case calculator.call(input: input)
    	in Success(Integer => response) then "Yay! Value is #{response}"
    	in Failure(:less_than_one) then 'Nay less than one'
    	in Failure(:not_even) then 'Nay not even'
    end
  end
end
```

If we run RSpec again, we can see that we are still successful.

```s
bundle exec rspec
/Users/dennisokeeffe/code/blog-projects/demo-dry-monads-matcher/lib/crunch_numbers.rb:22: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
...

Finished in 0.00407 seconds (files took 0.19054 seconds to load)
3 examples, 0 failures
```

It is up to you which method you prefer, but I must admit that I find the pattern matching much more succinct and readable (as well as one less gem).

## Handling errors

When pattern matching, we can handle specific errors by rescuing them and returning them as a failure.

> Note: if you `rescue StandardError`, it may cause unexpected behavior for your other failure monads.

For example, our current `Calculator` class can still failure if we do not pass a valid integer. To handle this argument error, we could update our `Calculator` class as follows:

```rb
require 'dry/monads'

class Calculator
  include Dry::Monads[:result, :do]

  def call(input:)
    value = Integer(input)

    value_add_3 = yield add_3_if_gt_1(value)
    value_mult_2 = yield mult_2_if_even(value_add_3)

    Success(value_mult_2)
  rescue ArgumentError => e
    Failure(e)
  end

  # ... rest omitted
end
```

Our `CrunchNumbers` class could now look like so:

```rb
require 'calculator'
require 'dry/matcher/result_matcher'

class CrunchNumbers
  include Dry::Monads[:result]

  def crunch(calculator:, input:)
    case calculator.call(input: input)
    	in Success(Integer => response) then "Yay! Value is #{response}"
    	in Failure(:less_than_one) then 'Nay less than one'
    	in Failure(:not_even) then 'Nay not even'
      # Handling the ArgumentError and assigning the error to a value
      in Failure(ArgumentError => e) then "Nay #{e.message}"
    end
  end
end
```

Finally, we could add one last test to our spec to check this works as expected:

```rb
require 'crunch_numbers'
require 'calculator'

RSpec.describe CrunchNumbers do
  let(:calculator) { Calculator.new }

  describe '#crunch' do
    # ... valid context tests omitted

    context 'invalid input' do
      # ... previous tests omitted

      it 'returns `Nay invalid value for Integer(): "Hello"` when the return value is not a number' do
        expect(subject.crunch(calculator: calculator, input: 'Hello')).to eq('Nay invalid value for Integer(): "Hello"')
      end
    end
  end
end
```

Finally, we can check everything works as expected by running our tests one more time:

```rb
bundle exec rspec
/Users/dennisokeeffe/code/blog-projects/demo-dry-monads-matcher/lib/crunch_numbers.rb:22: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
....

Finished in 0.00423 seconds (files took 0.16233 seconds to load)
4 examples, 0 failures
```

Returning our errors as a failure monad enables us to continue following the concept of "function core, imperative shell" and our `CrunchNumbers` "shell" could then handle that failure to capture the error to our error logging system.

## Summary

Today's post demonstrated how to use both the `dry-matcher` and `dry-monads` gems.

While the `dry-matcher` gem has its place, I am personally more attached to the elegance of using `dry-monads` along with pattern matching.

I will be visiting other Dry RB gems in upcoming posts to try out their features and see if their pros can be used in future Ruby code.

## Resources and further reading

- [Bundler](https://bundler.io/)
- [RSpec](https://rspec.info/)
- [Pry Gem](https://www.rubydoc.info/gems/pry/0.10.3/Pry)
- [Source code](https://github.com/okeeffed/demo-dry-monads-matcher)
- [`dry-monads`](https://dry-rb.org/gems/dry-monads/1.3/)
- [`dry-matcher`](https://dry-rb.org/gems/dry-matcher/0.8/)

_Photo credit: [`rgaleria`](https://unsplash.com/@rgaleria)_
