Project

zx-monads

0.0
The project is in a healthy, maintained state
Use Maybe to handle errors in your code
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 1.14
>= 10.0
>= 3.0
>= 0.70
 Project Readme

🔃 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

  • Installation
  • Usage

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.