Functo

Functo is a dynamic module for composable method objects in ruby.

It turns this:

class AddsTwo
  def self.call(*args)
    new(*args).add
  end

  attr_reader :number
  protected :number

  def initialize(number)
    @number = number
  end

  def add
    number + 2
  end
end

in to this:

class AddsTwo
  include Functo.call :add, :number

  def add
    number + 2
  end
end

Installation

Add this line to your application's Gemfile:

gem 'functo'

And then execute:

$ bundle

Or install it yourself as:

$ gem install functo

Usage

Functo objects can take up to three arguments.

class Multiply
  include Functo.call :multiply, :first, :second, :third

  def multiply
    first * second * third
  end
end

Multiply.call(10, 20, 30)
# => 6000

class Divide
  include Functo.call :multiply, :first, :second, :third, :fourth

  def divide
    first / second / third / fourth
  end
end
# => ArgumentError: given 4 arguments when only 3 are allowed

If you find yourself needing more you should consider composing method objects or encapsulating some of your arguments in another object.

You can use square brackets to call Functo objects:

AddsTwo[3]
# => 5

and they can be used in blocks:

[1, 2, 3].map(&AddsTwo)
# => [3, 4, 5]

Composition

Functo objects can be composed using compose or the turbo operator >>:

AddMulti = AddsTwo.compose(MultipliesThree)

AddMulti.call(3)
# => 15

MultiAdd = MultipliesThree >> AddsTwo

MultiAdd[3]
# => 11

The difference between the two is that the turbo operator will splat arrays passed between the composed objects but compose will not.

class SplitDigits
  include Functo.call :split, :number

  def split
    number.to_s.split(//).map(&:to_i)
  end
end

class Sum
  include Functo.call :sum, :first, :second, :third

  def sum
    first + second + third
  end
end

SumDigits = SplitDigits >> Sum

SumDigits[123]
# => 6

SumDigits2 = SplitDigits.compose(Sum)

SumDigits2[123]
# => ArgumentError: wrong number of arguments (given 1, expected 3)

Filters

Filters can be passed to the Functo constructor, for example to implement types or coercion. A filter can be anything which responds to [].

class ValidatesNumeric
  include Functo.call :validate, :number

  ValidationError = Class.new(StandardError)

  def validate
    raise ValidationError unless number.is_a?(Numeric)

    number
  end
end

class AddsThree
  include Functo.call :add, number: ValidatesNumeric

  def add
    number + 2
  end
end

AddsThree[10]
# => 13

AddsThree['10']
# => ValidationError

You could use the dry-types gem for example.

require 'dry-types'

module Types
  include Dry::Types.module
end

class AddsFour
  include Functo.call :add, number: Types::Strict::Int

  def add
    number + 4
  end
end

AddsFour[4]
# => 4

AddsFour['4']
# => Dry::Types::ConstraintError

If you have multiple arguments and only want one of them to have a filter, you can use Functo.pass for a filter that just returns its input.

Acknowledgements

Functo was inspired by these gems:

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sbscully/functo.

License

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