/zx-monads

Monads for Ruby

Primary LanguageRubyMIT LicenseMIT

🔃 Zx::Monads

FP Monads for Ruby

Gem Build Status

Motivation

Because in sometimes, we need to handling a safe value for our objects. This gem simplify this work.

Documentation

Version Documentation
unreleased https://github.com/thadeu/zx-monads/blob/main/README.md

Table of Contents

Compatibility

kind branch ruby
unreleased main >= 2.7.6, <= 3.2.x

Installation

Use bundle

bundle add zx-monads

or add this line to your application's Gemfile.

gem 'zx-monads'

and then, require module

require 'zx'

Configuration

Without configuration, because we use only Ruby. ❤️

Usage

How to use in my codebase?

class Order
  include Zx # include all Zx library.
end

class Order
  include Zx::Maybeable
end

class ProcessOrder < Zx::Steps
  # include Zx::Maybeable included now!
end

Available public methods

#type -> Returns maybe type
#some? -> Returns boolean 
#none? -> Returns boolean 
#unwrap -> Returns value unwrapped
#or(value) -> Returns unwrap or value
#>>(other) -> Forward to another Maybe
#fmap -> Create an step and wrap new value
#map(:key) -> Same the fmap, but receive another parameters
#map(&:method) -> Same the map, but respond to method
#map {} -> Same the the map, but receive an block
#map!{} -> Same the map, but change to new value
#apply!{} -> Same the map! but more legible
#dig(keys) -> Get values using keys like Hash#dig
#dig!(keys) -> Same them dig, but return unwrap
#match(some:, none:) -> Receive callables and associate them
#on_success{} -> Only when Some
#on_failure{} -> Only then None
#on(:success|:failure){}

ZX::Maybe

result = Zx::Maybe[1] # or Maybe.of(1)
result = Zx::Maybe[nil] # or Maybe.of(nil)
result = Zx::Maybe[1].map{ _1 + 2}
# -> Some(3)
result = Zx::Maybe[nil].map{ _1 + 2}
# -> None
result = Zx::Maybe.of(1).or(2)
result.or(2) # 1
result = Zx::Maybe.of(' ').or(2)
result.or(2) # 2
result = Zx::Maybe.of(' ').or(2)
result.or(2) # 2
order = {
  shopping: {
    banana: {
      price: 10.0
    }
  }
}

price =  Zx::Maybe[order]
  .map { _1[:shopping] }
  .map { _1[:banana] }
  .map { _1[:price] }

# -> Some(10.0)

# or using #dig

price =  Zx::Maybe[order].dig(:shopping, :banana, :price)
# -> Some(10.0)

price_none =  Zx::Maybe[order].dig(:shopping, :banana, :price_non_exists)
# -> None

price_or =  Zx::Maybe[order].dig(:shopping, :banana, :price_non_exists).or(10.0)
# -> Some(10.0)
class Response
  attr_reader :body

  def initialize(new_body)
    @body = Zx::Maybe[new_body]
  end

  def change(new_body)
    @body = Zx::Maybe[new_body]
  end
end

response = Response.new(nil)
expect(response.body).to be_none

response.change({ status: 200 })
expect(response.body).to be_some

response_status = response.body.match(
  some: ->(body) { Zx::Maybe[body].map { _1.fetch(:status) }.unwrap },
  none: -> {}
)

Use case, when use to parse response stringify json

dump = JSON.dump({ status: { code: '300' } })

response = Response.new(dump) # It's receive an JSON stringified

module StatusCodeUnwrapModule
  def self.call(body)
    Zx::Maybe[body]
      .map{ JSON(_1, symbolize_names: true) }
      .dig(:status, :code)
      .apply(&:to_i)
      .unwrap
  end
end

response_status = response.body.match(
  some: StatusCodeUnwrapModule,
  none: -> { 400 }
)
  
expect(response_status).to eq(300)

You can use >> to compose many callables, like this.

sum = ->(x) { Zx::Maybe::Some[x + 1] }

subtract = ->(x) { Zx::Maybe::Some[x - 1] }

result = Zx::Maybe[1] >> \
  sum >> \
  subtract

expect(result.unwrap).to eq(1)

If handle None, no worries.

sum = ->(x) { Zx::Maybe::Some[x + 1] }

subtract = ->(_) { Zx::Maybe::None.new }

result = Zx::Maybe[1] \
  >> sum \
  >> subtract

expect(result.unwrap).to be_nil
class Order
  def self.sum(x)
    Zx::Maybe[{ number: x + 1 }]
  end
end

result = Order.sum(1)
  .dig(:number)
  .apply(&:to_i)

expect(result.unwrap).to be(2)

Zx::Maybe::Some

class Order
  include Zx::Maybeable

  def self.sum(x)
    new.sum(x)
  end

  def sum(x)
    Some[{ number: x + 1 }]
  end
end

result = Order.sum(1)
  .dig(:number)
  .apply(&:to_i)

expect(result.unwrap).to be(2)

Zx::Maybe::None

class Order
  include Zx::Maybeable

  def self.sum(x)
    new.sum(x)
  end

  def sum(x)
    Try {{ number: x + ' ' }}
  end
end

result = Order.sum(1)
number = result.dig(:number).apply(&:to_i)

expect(result).to be_none
expect(result).to be_a(Maybe::None)
expect(number.unwrap).to be(0) # nil.to_i == 0

Zx::Maybe::Try

Only included or inherited!

class Order
  include Zx::Maybeable

  def self.sum(x)
    new.sum(x)
  end

  def sum(x)
    Try {{ number: x + 1 }}
  end
end

result = Order.sum(1)
  .dig(:number)
  .apply(&:to_i)

expect(result.unwrap).to be(2)

With default value, in None case.

class Order
  include Zx::Maybeable

  def self.sum(x)
    new.sum(x)
  end

  def sum(x)
    Try(2) {{ number: x + ' ' }}
  end
end

result = Order.sum(1)
  .dig(:number)
  .apply(&:to_i)

expect(result.unwrap).to be(2)
class Order
  include Zx::Maybeable

  def self.sum(x)
    new.sum(x)
  end

  def sum(x)
    Try(or: 1000) {{ number: x + ' ' }}
  end
end

result = Order.sum(1).dig(:number).apply(&:to_i)

expect(result.unwrap).to be(1000)

Zx::Steps

class OrderStep < Zx::Steps
  def initialize(x = nil)
    @x = x
  end

  step :positive?
  step :apply_tax
  step :divide

  def positive?
    return None unless total.is_a?(Integer) || total.is_a?(Float)
    return None if total <= 0

    Some total
  end

  def apply_tax
    Try { @x -= (@x * 0.1) }
  end

  def divide
    Try { @x /= 2 }
  end

  def total
    @x
  end
end
order = OrderStep.new(20)

order.call
  .map { |n| n + 1 }
  .on_success { |some| expect(some.unwrap).to eq(10) }
  .on_failure { |none| expect(none.or(0)).to eq(0) }
order = OrderStep.new(20)

order.call
  .map { |n| n + 1 }
  .on(:success) { |some| expect(some.unwrap).to eq(10) }
  .on(:failure) { |none| expect(none.or(0)).to eq(0) }
order = OrderStep.new(-1)

order.call
  .on_success { raise }
  .on_failure { |none| expect(none.or(0)).to eq(0) }

⬆️  Back to Top

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/thadeu/zx-monads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.