Module: Flexibility

Defined in:
lib/flexibility.rb

Overview

Flexibility is a mix-in for ruby classes that allows you to easily #define methods that can take a mixture of positional and keyword arguments.

For example, suppose we define

class Banner
  include Flexibility

  define( :show,
    message: [
      required,
      validate { |s| String === s },
      transform { |s| s.upcase }
    ],
    width:   [
      default { @width },
      validate { |n| 0 <= n }
    ],
    symbol:  default('*')
  ) do |message,width,symbol,unused_opts|
    width = [ width, message.length + 4 ].max
    puts "#{symbol * width}"
    puts "#{symbol} #{message.ljust(width - 4)} #{symbol}"
    puts "#{symbol * width}"
  end

  def initialize
    @width = 40
  end
end

Popping over to IRB, we could use Banner#show with keyword arguments,

irb> banner = Banner.new
irb> banner.show( message: "HELLO", width: 10, symbol: '*' )
**********
* HELLO  *
**********
 => nil

positional arguments

irb> banner.show( "HELLO WORLD!", 20, '#' )
####################
# HELLO WORLD!     #
####################
 => nil

or a mix

irb> banner.show( "A-HA", symbol: '-', width: 15 )
---------------
- A-HA        -
---------------
 => nil

The keyword arguments are taken from the last argument, if it is a Hash, while the preceeding positional arguments are matched up to the keyword in the same position in the argument description.

Flexibility also allows the user to run zero or more callbacks on each argument, and includes a number of callback generators to specify a #default value, mark a given argument as #required, #validate an argument, or #transform an argument into a more acceptable form.

Continuing our prior example, this means Banner#show only requires one argument, which it automatically upper-cases:

irb> banner.show( "celery?" )
****************************************
* CELERY?                              *
****************************************

And it will raise an error if the message is missing or not a String, or if the width argument is negative:

irb> banner.show
!> ArgumentError: Required argument :message not given
irb> banner.show 8675309
!> ArgumentError: Invalid value 8675309 given for argument :message
irb> banner.show "hello", -9
!> ArgumentError: Invalid value -9 given for argument :width

Just as Flexibility#define allows the method caller to determine whether to pass the method arguments positionally, with keywords, or in a mixture of the two, it also allows method authors to determine whether the method receives arguments in a Hash or positionally:

class Banner
  opts_desc = { a: [], b: [], c: [], d: [], e: [] }
  define :all_positional, opts_desc do |a,b,c,d,e,opts|
    [ a, b, c, d, e, opts ]
  end
  define :all_keyword, opts_desc do |opts|
    [ opts ]
  end
  define :mixture, opts_desc do |a,b,c,opts|
    [ a, b, c, opts ]
  end
end
irb> banner.all_positional(1,2,3,4,5)
=> [ 1, 2, 3, 4, 5, {} ]
irb> banner.all_positional(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ 1, 2, 3, 4, 5, {f:6} ]
irb> banner.all_keyword(1,2,3,4,5)
=> [ { a:1, b:2, c:3, d:4, e:5 } ]
irb> banner.all_keyword(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ { a:1, b:2, c:3, d:4, e:5, f:6 } ]
irb> banner.mixture(1,2,3,4,5)
=> [ 1, 2, 3, { d:4, e:5 } ]
irb> banner.mixture(a:1, b:2, c:3, d:4, e:5, f:6)
=> [ 1, 2, 3, { d:4, e:5, f:6 } ]

Author:

Argument Callback Generators (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Object) append_features(target)

When included, Flexibility adds all its instance methods as private class methods of the including class:

irb> c = Class.new
irb> before = c.private_methods
irb> c.class_eval { include Flexibility }
irb> c.private_methods - before
=> [ :default, :required, :validate, :transform, :define ]

Parameters:

  • target (Module)

    the class or module that included Flexibility

See Also:

  • Module#include


960
961
962
963
964
965
966
967
# File 'lib/flexibility.rb', line 960

def self.append_features(target)
  class<<target
    Flexibility.instance_methods.each do |name|
      define_method(name, Flexibility.instance_method(name))
      private name
    end
  end
end

+ (UnboundMethod) create_unbound_method(klass, &body)

helper for creating UnboundMethods

irb> inject = Array.instance_method(:inject)
irb> Flexibility.run_unbound_method(inject, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
=> "(((xa)b)c)"
irb> inject_r = Flexibility.create_unbound_method( Array ) { |*args,&blk| reverse.inject(*args, &blk) }
irb> Flexibility.run_unbound_method(inject_r, %w{ a b c }, "x") { |l,r| "(#{l}#{r})" }
=> "(((xc)b)a)"

in a less civilized time, I might have just monkey-patched this as UnboundMethod::create


Parameters:

  • klass (Class)

    class to associate the method with

  • body (Proc)

    proc to use for the method body

Returns:

  • (UnboundMethod)


23
24
25
26
27
28
29
30
31
# File 'lib/flexibility.rb', line 23

def self.create_unbound_method(klass, &body)
  name = body.inspect
  klass.class_eval do
    define_method(name, &body)
    um = instance_method(name)
    remove_method(name)
    um
  end
end

+ (res) run_unbound_method(um, instance, *args, &blk)

helper to call UnboundMethods with proper number of args, and avoid ArgumentError: wrong number of arguments.

irb> each = Array.instance_method(:each)
irb> each.bind( [ 1, 2, 3] ).call( 4, 5, 6 ) { |x| puts x }
!> ArgumentError: wrong number of arguments (3 for 0)
irb> Flexibility.run_unbound_method(each, [ 1, 2, 3], 4, 5, 6 ) { |x| puts x }
1
2
3
=> [1,2,3]

in a less civilized time, I might have just monkey-patched this as UnboundMethod#run


Parameters:

  • um (UnboundMethod(*args,blk) => res)

    UnboundMethod to run

  • instance (Object)

    object to bind um to, must be a instance of um.owner

  • args (Array)

    arguments to pass to invocation of um

  • blk (Proc)

    block to bind to invocation of um

Returns:

  • (res)


56
57
58
59
# File 'lib/flexibility.rb', line 56

def self.run_unbound_method(um, instance, *args, &blk)
  args = args.take(um.arity) if 0 <= um.arity && um.arity < args.length
  um.bind(instance).call(*args,&blk)
end

Instance Method Details

- (UnboundMethod(val,key,opts,initial,&blk)) default(default_val = nil) {|key, opts, initial, &blk, self| ... }

#default allows you to specify a default value for an argument.

You can pass #default either

  • an argument containing a constant value

  • a block to be bound to the instance and run as needed

With the block form, you also have access to

  • self and the instance variables of the bound instance

  • the keyword associated with the argument

  • the hash of options defined thus far

  • the original argument value (useful if an earlier transformation nil'ed it out)

  • the block bound to the method invocation

For example, given the method dimensions:

class Banner
  include Flexibility

  define( :dimensions,
    depth:    default( 1 ),
    width:    default { @width },
    height:   default { |_key,opts| opts[:width] } ,
    duration: default { |&blk| blk[] if blk }
  ) do |opts|
    opts
  end

  def initialize
    @width = 40
  end
end

We can specify (or not) any of the arguments to see the defaults in action

irb> banner = Banner.new
irb> banner.dimensions
=> { depth: 1, width: 40, height: 40 }
irb> banner.dimensions( depth: 2, width: 10, height: 5, duration: 7 )
=> { depth: 2, width: 10, height: 5, duration: 7 }
irb> banner.dimensions( width: 10 ) { puts "getting duration" ; 12 }
getting duration
=> { depth: 1, width: 10, height: 10, duration: 12 }

Note that the yield keyword inside the block bound to default won't be able to access the block bound to the method invocation, as yield is lexically scoped (like a local variable).

module YieldExample
  def self.create
    Class.new do
      include Flexibility
      define( :run,
        using_yield:  default { yield },
        using_block:  default { |&blk| blk[] }
      ) { |opts| opts }
    end.new
  end
end
irb> YieldExample.create { :class_creation }.run { :method_invocation }
=> { using_yield: :class_creation, using_block: :method_invocation }

Parameters:

  • default_val (defaults to: nil)

    if the returned UnboundMethod is called with nil as its first parameter, it returns default_val (unless #default is called with a block)

Yields:

  • if the returned UnboundMethod is called with nil as its first parameter, it returns the result of yield (unless #default is called with an argument).

    The block bound to #default receives the following parameters when called by a method created with #define:

Yield Parameters:

  • key (Symbol)

    the key of the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

Raises:

  • (ArgumentError)

    unless called with a block and no args, or called with no block and one arg

See Also:



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/flexibility.rb', line 155

def default(*args,&cb)
  if args.length != (cb ? 0 : 1)
    raise(ArgumentError, "Wrong number of arguments to `default` (expects 0 with a block, or 1 without)", caller)
  elsif cb
    um = Flexibility::create_unbound_method(self, &cb)
    Flexibility::create_unbound_method(self) do |*args, &blk|
      val = args.shift
      unless val.nil?
        val
      else
        Flexibility::run_unbound_method(um,self,*args,&blk)
      end
    end
  else
    default = args.first
    Flexibility::create_unbound_method(self) { |*args| val = args.shift; val.nil? ? default : val }
  end
end

- (Object) define(method_name, expected = {}) { ... }

#define lets you define methods that can be called with either

  • positional arguments

  • keyword arguments

  • a mix of positional and keyword arguments

It takes a method_name, an Hash using the argument keywords as keys, and a block defining the method body.

For example

class Example
  include Flexibility

  define( :run,
    a: [],
    b: [],
    c: []
  ) do |opts|
    opts.each { |k,v| puts "#{k}: #{v.inspect}" }
  end

end
irb> ex = Example.new
irb> ex.run( 1, 2, 3 )  # all positional arguments
a: 1
b: 2
c: 3
irb> ex.run( c:1, a:2, b:3, d: 0 ) # all keyword arguments
a: 2
b: 3
c: 1
d: 0
irb> ex.run( 7, 9, d: 18, c:11 ) # mixed keyword and positional arguments
a: 7
b: 9
c: 11
d: 18

Positional arguments will override keyword arguments if both are given

irb> ex.run( 10, 20, 30, a: 1, b: 2, c: 3 )
a: 10
b: 20
c: 30

By default, nil or unspecified values won't appear in the options hash given to the method body.

irb> ex.run( nil, a: 2, c: 3 )
c: 3

You can use as many keyword arguments as you like, but calling the method with extra positional arguments will cause the method to raise an exception

irb> ex.run( 1, 2, 3, 4 )
!> ArgumentError: Got 4 arguments, but only know how to handle 3

#define also lets you decide whether the method body receives the arguments

  • in a Hash

  • as a mix of positional arguments and a trailing hash

It does this by inspecting the arity of the block that defines the method body. A block that takes N+1 arguments will be provided with N positional arguments. The final argument to the block is always a hash of options.

For example:

class Example
  include Flexibility

  define( :run,
    a: [],
    b: [],
    c: []
  ) do |a,b,opts|
    puts "a    = #{a.inspect}"
    puts "b    = #{b.inspect}"
    puts "opts = #{opts.inspect}"
    opts.length
  end
end
irb> ex.run( 1, 2, 3 )
a    = 1
b    = 2
opts = {:c=>3}
irb> ex.run( a:1, b:2, c:3, d:4 )
a    = 1
b    = 2
opts = {:c=>3, :d=>4}

If the method body takes too many arguments (more than the number of keywords plus one for the options hash), then #define will raise an error instead of creating the method, since it lacks keywords to use to refer to those extra arguments

irb> Class.new { include Flexibility ; define(:ex) { |a,b,c,opts| } }
!> ArgumentError: More positional arguments in method body than specified in expected arguments

Currently, it's also an error to give #define a method body that uses a splat (*) to capture a variable number of arguments:

irb> Class.new { include Flexibility ; define(:ex) { |*args,opts| } }
!> NotImplementedError: Flexibility doesn't support splats in method definitions yet, sorry!

#define also lets you specify, along with each keyword, a sequence of UnboundMethod callbacks to be run on the argument given for that keyword on each run of the generated method.

When run, these callbacks will be passed:

  • the current value of the given argument

  • the keyword associated with the given argument

  • the hash of options generated thus far

  • the original value of the given argument

  • any block passed to this invocation of the generated method

The callback will also have its value of self bound to the same instance running the generated method.

class IntParser
  include Flexibility

  def initialize base
    @base = base
  end

  def parse arg
    arg.to_i(@base)
  end

  define(:parse_both,
    a: [ instance_method(:parse) ],
    b: [ instance_method(:parse) ]
  ) do |opts|
    opts
  end
end
irb> p16 = IntParser.new(16)
irb> p32 = IntParser.new(32)
irb> p16.parse_both *%w{ ff 11 }
=> { a: 255, b: 17 }
irb> p32.parse_both *%w{ ff 11 }
=> { a: 495, b: 33 }

If you pass multiple callbacks, they are executed in sequence, with the result of one callback being fed to the next:

class IntParser
  #...
  def increment num
    num + 1
  end

  def decrement num
    num - 1
  end

  def format arg
    arg.to_s(@base)
  end

  define(:parse_change_and_format_both,
    a: [ instance_method(:parse), instance_method(:increment), instance_method(:format) ],
    b: [ instance_method(:parse), instance_method(:decrement), instance_method(:format) ],
  ) do |opts|
    opts
  end
end
irb> p16.parse_change_and_format_both *%w{ ff 11 }
=> { a: "100", b: "10" }
irb> p32.parse_change_and_format_both *%w{ ff 11 }
=> { a: "fg", b: "10" }

Rather than defining one-off instance methods like IntParser#increment and IntParser#decrement, you can use the #default, #required, #transform, and #validate methods provided by Flexibility to construct UnboundMethod callbacks:

class IntParser
  #...
  parse = instance_method(:parse)
  format = instance_method(:format)

  parsable = validate do |s|
    _0 = '0'.ord
    _9 = _0 + [@base, 10].min - 1
    _a = 'a'.ord
    _z = _a + [@base - 10, 26].min - 1
    _A = 'A'.ord
    _Z = _A + [@base - 10, 26].min - 1
    s.chars.all? do |c|
      n = c.ord
      [ _0 <= n && n <= _9,
        _a <= n && n <= _z,
        _A <= n && n <= _Z,
      ].any?
    end
  end

  define(:parse_change_and_format_both,
    a: [ parsable, parse, transform { |i| i + 1 }, format ],
    b: [ parsable, parse, transform { |i| i - 1 }, format ],
  ) do |opts|
    opts
  end
end
irb> p16.parse_change_and_format_both *%w{ ff 11 }
=> { a: "100", b: "10" }
irb> p16.parse_change_and_format_both *%w{ gg 11 }
!> ArgumentError: Invalid value "gg" given for argument :a
irb> p32.parse_change_and_format_both *%w{ gg 11 }
=> { a: "gh", b: "10" }

To make it even simpler, you can also use a Proc, Symbol or anything else that responds to #to_proc for a callback as well.

class Item
  def initialize foo, bar
    @foo, @bar = foo, bar
  end
  def foo(*args)
    puts "running foo! with #{args.inspect}"
    @foo
  end
  def bar(*args)
    puts "running bar! with #{args.inspect}"
    @bar
  end
  def inspect
    "#<Item @foo=#@foo @bar=#@bar>"
  end
end

class Example
  include Flexibility
  def initialize tag
    @tag = tag
  end

  define(:run,
    a: [ :foo, proc { |n,&blk| blk[ @tag, n ] } ],
    b: [ :bar, proc { |n,&blk| blk[ @tag, n ] } ]
  ) do |opts|
    opts
  end
end
irb> item = Item.new( "left", "right" )
irb> ex   = Example.new( "popcorn" )
irb> ex.run( a: item, b: item ) { |tag, val| puts "running block with tag=#{tag} val=#{val}" ; tag + val }
running foo! with [:a, {}, #<Item @foo=left @bar=right>]
running block with tag=popcorn val=left
running bar! with [:b, {:a=>"popcornleft"}, #<Item @foo=left @bar=right>]
running block with tag=popcorn val=right
=> { a: "popcornleft", b: "popcornright" }

Note how, as mentioned earler, we can access the bound block and prior options within the callback.

In addition, if you only need a single callback for an argument, you don't have to wrap it in an array:

class Example

  def initialize(min)
    @min = min
  end

  define(:run,
    foo:  required,
    bar:  validate { |bar| bar >= @min },
    baz:  default { |_,opts| opts[:bar] },
    quux: transform { |val,key| val[key] }
  ) do |opts|
    opts
  end
end
irb> ex = Example.new(10)
irb> ex.run
!> ArgumentError: Required argument :foo not given
irb> ex.run 100, 0
!> ArgumentError: Invalid value 0 given for argument :bar
irb> ex.run 100, 17, quux: { quux: 5 }
=> { foo: 100, bar: 17, baz: 17, quux: 5 }

The method body given to #define can receive the block bound to the method call at runtime using the standard & prefix:

class AmpersandExample
  include Flexibility

  define(:run) do |&blk|
    (1..4).each(&blk)
  end
end
irb> AmpersandExample.new.run { |i| puts i }
1
2
3
4
=> 1..4

Note, however, that the yield keyword inside the method body won't be able to access the block bound to the method invocation, as yield is lexically scoped (like a local variable).

module YieldExample
  def self.create
    Class.new do
      include Flexibility
      define( :run ) do |&blk|
        blk.call :using_block
        yield :using_yield
      end
    end
  end
end
irb> klass = YieldExample.create { |x| puts "class creation block got #{x}" }
irb> instance = klass.new
irb> instance.run { |x| puts "method invocation block got #{x}" }
method invocation block got using_block
class creation block got using_yield

Parameters:

  • method_name (Symbol)

    the name of the method to create

  • expected ({ Symbol => [ UnboundMethod(val,key,opts,initial,&blk) ] }) (defaults to: {})

    an ordered Hash of keywords for each argument, associated with an Array of UnboundMethod callbacks to call on each argument value when the defined method is run.

    In addition to UnboundMethod, qnything that responds to #to_proc may be used for a callback, and a single callback can be used in place of an Array of one callback.

Yields:

  • The result of running all the callbacks on each parameter for a given call to the defined method.

    If the block bound to #define takes N+1 parameters, then the first N will be bound to the values of the first N keywords. The last parameter given to the block will contain a Hash mapping the remaining keywords to their values.

Raises:

  • (ArgumentError)

    If the method body takes N+1 arguments, but fewer than N keywords are given in the expected parameter, then #define does not define the method, and instead raises an error.

  • (NotImplementedError)

    If the method body uses a splat (*) to capture a variable number of arguments, #define raises an error, as Flexibility has not determined how best to handle that case yet. Sorry. Bother the developer if you want that changed.

See Also:



872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
# File 'lib/flexibility.rb', line 872

def define method_name, expected={}, &method_body
  if method_body.arity < 0
    raise(NotImplementedError, "Flexibility doesn't support splats in method definitions yet, sorry!", caller)
  elsif method_body.arity > expected.length + 1
    raise(ArgumentError, "More positional arguments in method body than specified in expected arguments", caller)
  end

  # create an UnboundMethod from method_body so we can
  # 1. set `self`
  # 2. pass it arguments
  # 3. pass it a block
  #
  # `instance_eval` only allows us to do (1), whereas `instance_exec` only
  # allows (1) and (2), and `call` only allows (2) and (3).
  method_um = Flexibility::create_unbound_method(self, &method_body)

  # similarly, create UnboundMethods from the callbacks
  expected_ums = {}

  expected.each do |key, cbs|
    # normalize a single callback to a collection
    cbs = [cbs] unless cbs.respond_to? :inject

    expected_ums[key] = cbs.map.with_index do |cb, index|
      if UnboundMethod === cb
        cb
      elsif cb.respond_to? :to_proc
        Flexibility::create_unbound_method(self, &cb)
      else
        raise(ArgumentError, "Unrecognized expectation #{cb.inspect} for #{key.inspect}, expecting an UnboundMethod or something that responds to #to_proc", caller)
      end
    end
  end

  # assume all but the last block argument should capture positional
  # arguments
  keys = expected_ums.keys[ 0 ... method_um.arity - 1]

  # interpret user arguments using #options, then pass them to the method
  # body
  define_method(method_name) do |*given, &blk|

    # let the caller bundle arguments in a trailing Hash
    trailing_opts = Hash === given.last ? given.pop : {}
    unless expected_ums.length >= given.length
      raise(ArgumentError, "Got #{given.length} arguments, but only know how to handle #{expected_ums.length}", caller)
    end

    opts = {}
    expected_ums.each.with_index do |(key, ums), i|
      # check positional argument for value first, then default to trailing options
      initial = i < given.length ? given[i] : trailing_opts[key]

      # run every callback, threading the results through each
      final = ums.inject(initial) do |val, um|
        Flexibility::run_unbound_method(um, self, val, key, opts, initial, &blk)
      end

      opts[key] = final unless final.nil?
    end

    # copy remaining options
    (trailing_opts.keys - expected_ums.keys).each do |key|
      opts[key] = trailing_opts[key]
    end

    Flexibility::run_unbound_method(
      method_um,
      self,
      *keys.map { |key| opts.delete key }.push( opts ).take( method_um.arity ),
      &blk
    )
  end
end

- (UnboundMethod(val,key,opts,initial,&blk)) required

#required allows you to throw an exception if an argument is not given.

#required returns an UnboundMethod that simply checks that its first parameter is non-nil:

  • if the parameter is nil, it raises an ArgumentError

  • if the parameter is not nil, it returns it.

For example,

class Banner
  include Flexibility

  define( :area,
    width: required,
    height: required
  ) do |width,height,_|
    width * height
  end
end

We can specify (or not) any of the arguments to see the checking in action

irb> banner = Banner.new
irb> banner.area
!> ArgumentError: Required argument :width not given
irb> banner.area :width => 5
!> ArgumentError: Required argument :height not given
irb> banner.area :height => 5
!> ArgumentError: Required argument :width not given
irb> banner.area :width => 6, :height => 5
=> 30

Note that #required specifically checks that the argument is non-nil, not unspecified, so explicitly given nil arguments will still raise an error:

irb> banner.area :width => nil, :height => 5
!> ArgumentError: Required argument :width not given

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    UnboundMethod which returns first parameter given if non-nil, otherwise raises ArgumentError

See Also:



222
223
224
225
226
227
228
229
230
# File 'lib/flexibility.rb', line 222

def required
  Flexibility::create_unbound_method(self) do |*args|
    val, key = *args
    if val.nil?
      raise(ArgumentError, "Required argument #{key.inspect} not given", caller)
    end
    val
  end
end

- (UnboundMethod(val,key,opts,initial,&blk)) transform {|val, key, opts, initial, &blk, self| ... }

#transform allows you to lift an arbitrary code block into an UnboundMethod.

You pass #transform a block which will be invoked each time the returned UnboundMethod is called. Within the block, you have access to

  • self and the instance variables of the bound instance

  • the keyword associated with the argument

  • the hash of options defined thus far

  • the original argument value (useful if an earlier transformation nil'ed it out)

  • the block bound to the method invocation

The return value of the UnboundMethod will be completely determined by the return value of the block bound to the call of #transform.

require 'date'
class Timer
  include Flexibility

  to_epoch = transform do |t|
    case t
    when String   ; DateTime.parse(t).to_time.to_i
    when DateTime ; t.to_time.to_i
    else          ; t.to_i if t.respond_to? :to_i
    end
  end

  define( :elapsed,
    start: to_epoch,
    stop:  to_epoch
  ) do |start, stop, _|
    stop - start
  end
end
irb> timer = Timer.new
irb> timer.elapsed "1984-06-07", "1989-06-16"
=> 158544000
irb> (timer.elapsed DateTime.now, (DateTime.now + 365)) / 60
=> 525600

And just to show how you can access instance variables, earlier parameters, and the bound block with #transform

class Silly
  include Flexibility

  def initialize base
    @base = base
  end

  define( :tag_with_base,
    fst:  transform { |x,&blk|   [x, blk[@base] ]      },
    snd:  transform { |x,_,opts| [x, opts[:fst].last] }
  ) { |opts| opts }
end
irb> silly = Silly.new( "base value" )
irb> silly.tag_with_base( fst: 3, snd: "hi" ) { |msg| puts msg ; msg.length }
base value
=> { fst: [ 3, 10 ], snd: [ "hi", 10 ] }

Note that the yield keyword inside the block bound to #transform won't be able to access the block bound to the method invocation, as yield is lexically scoped (like a local variable).

module YieldExample
  def self.create
    Class.new do
      include Flexibility
      define( :run,
        using_yield:  transform { |val|      yield(val) },
        using_block:  transform { |val,&blk| blk[val] }
      ) { |opts| opts }
    end.new
  end
end
irb> YieldExample.create { |val| [:class_creation, val] }.run(1,2) { |val| [ :method_invocation, val] }
=> { using_yield: [:class_creation, 1], using_block: [:method_invocation,2] }

Yields:

  • The block bound to #transform receives the following parameters when called by a method created with #define:

Yield Parameters:

  • val (Object)

    the value of the option currently being processed

  • key (Symbol)

    the key for the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Yield Returns:

  • value for returned UnboundMethod to return

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    UnboundMethod created from block bound to #transform

See Also:



472
473
474
# File 'lib/flexibility.rb', line 472

def transform(&blk)
  Flexibility::create_unbound_method(self, &blk)
end

- (UnboundMethod(val,key,opts,initial,&blk)) validate {|val, key, opts, initial, &blk, self| ... }

#validate allows you to throw an exception if the given block returns falsy.

You pass #validate a block which will be invoked each time the returned UnboundMethod is called.

  • if the block returns true, the UnboundMethod will return the first parameter

  • if the block returns false, the UnboundMethod will raise an ArgumentError

Within the block, you have access to

  • self and the instance variables of the bound instance

  • the keyword associated with the argument

  • the hash of options defined thus far

  • the original argument value (useful if an earlier transformation nil'ed it out)

  • the block bound to the method invocation

For example, given the method “:

class Converter
  include Flexibility

  define( :polar_to_cartesian,
    radius: validate { |r| 0 <= r },
    theta:  validate { |t| 0 <= t && t < Math::PI },
    phi:    validate { |p| 0 <= p && p < 2*Math::PI }
  ) do |r,t,p,_|
    { x: r * Math.sin(t) * Math.cos(p),
      y: r * Math.sin(t) * Math.sin(p),
      z: r * Math.cos(t)
    }
  end
end
irb> conv = Converter.new
irb> conv.polar_to_cartesian -1, 0, 0
!> ArgumentError: Invalid value -1 given for argument :radius
irb> conv.polar_to_cartesian 0, -1, 0
!> ArgumentError: Invalid value -1 given for argument :theta
irb> conv.polar_to_cartesian 0, 0, -1
!> ArgumentError: Invalid value -1 given for argument :phi
irb> conv.polar_to_cartesian 0, 0, 0
=> { x: 0, y: 0, z: 0 }

And just to show how you can access instance variables, earlier parameters, and the bound block with #validate

class Silly
  include Flexibility

  def initialize(min,max)
    @min,@max = min,max
  end

  in_range = validate { |x,&blk| @min <= blk[x] && blk[x] <= @max }

  define( :check,
    lo:     in_range,
    hi:     [
      in_range,
      validate { |x,key,opts,&blk| blk[opts[:lo]] <= blk[x] }
    ],
  ) { |opts| opts }
end
irb> silly = Silly.new(3,5)
irb> silly.check("hi", "salutations") { |s| s.length }
!> ArgumentError: Invalid value "hi" given for argument :lo
irb> silly.check("hey", "salutations") { |s| s.length }
!> ArgumentError: Invalid value "salutations" given for argument :hi
irb> silly.check("hello", "hey") { |s| s.length }
!> ArgumentError: Invalid value "hey" given for argument :hi
irb> silly.check("hey", "hello") { |s| s.length }
=> { lo: "hey", hi: "hello" }

Note that the yield keyword inside the block bound to #validate won't be able to access the block bound to the method invocation, as yield is lexically scoped (like a local variable).

module YieldExample
  def self.create
    Class.new do
      include Flexibility
      define( :run,
        using_yield:  validate { |val,key|      puts [key, yield].inspect ; true },
        using_block:  validate { |val,key,&blk| puts [key, blk[]].inspect ; true }
      ) { |opts| opts }
    end.new
  end
end
irb> YieldExample.create { :class_creation }.run(1,2) { :method_invocation }
[:using_yield, :class_creation]
[:using_block, :method_invocation]
=> { using_yield: 1, using_block: 2 }

Yields:

  • The block bound to #validate receives the following parameters when called by a method created with #define:

Yield Parameters:

  • val (Object)

    the value of the option currently being processed

  • key (Symbol)

    the key for the option currently being processed

  • opts (Hash)

    the options hash thus far

  • initial (Object)

    the original value passed to the method for this option

  • &blk (Proc)

    the block passed to the method

  • self (keyword)

    bound to the same instance that the method is invoked on

Yield Returns:

  • (Boolean)

    indicates whether the returned UnboundMethod should return the first parameter or raise an ArgumentError.

Returns:

  • (UnboundMethod(val,key,opts,initial,&blk))

    UnboundMethod which returns first parameter given if block bound to #validate returns truthy on arguments/block given , raises ArgumentError otherwise.

See Also:



356
357
358
359
360
361
362
363
364
365
# File 'lib/flexibility.rb', line 356

def validate(&cb)
  um = Flexibility::create_unbound_method(self, &cb)
  Flexibility::create_unbound_method(self) do |*args,&blk|
    val, key, _opts, orig = *args
    unless Flexibility::run_unbound_method(um,self,*args,&blk)
      raise(ArgumentError, "Invalid value #{orig.inspect} given for argument #{key.inspect}", caller)
    end
    val
  end
end