rubyduck-typing

Shorthand for converting ruby procs to duck type / functional interface


I am given a "duck type" of a single method (a "functional interface") and I have a whole flock of adapters as methods and blocks.

For example, the protocol defined by the library looks like this:

class Grouper
  # group together an array of things into
  # an array of arrays of things
  # (that is, an array of _groups_ of things)
  def group_together(things); end
end

The contract for group_together is simple enough that I can define a bunch of them as simple methods.

module CommonGroupers
  def one_per_group(things)
    things.map {|x| x}
  end

  def one_group(things)
    [things.dup]
  end

  def geometric(things)
    result = []
    things.each_with_index do |thing, i|
      result << [] if i & (i + 1) == 0
      result[-1] << thing
    end
    result
  end

  def sorted_by(delegate, &block)
    lambda do |things|
      delegate.group_together(things.sort_by(&block))
    end
  end

  def partition_by(delegate, &block)
    lambda do |things|
      ...
    end
  end

  ...
end

Is there a shorthand way of making these methods adhere to the Grouper protocol?

Here are two options that I have so far. Is there is something built-in or more idiomatic?

class ProcGrouper
  def initialize(&block)
    @proc = block
  end

  def group_together(things)
    @proc.call(things)
  end
end

module CommonGroupers
  def geometric
    ProcGrouper.new do |things|
      result = []
      things.each_with_index do |thing, i|
        result << [] if i & (i + 1) == 0
        result[-1] << thing
      end
      result
    end
  end
end

CommonGroupers.geometric.group_together 1..10

or

def grouper(&block)
  class << block
    alias_method :group_together, :call
  end
  block
end

module CommonGroupers
  def geometric
    grouper do |things|
      result = []
      things.each_with_index do |thing, i|
        result << [] if i & (i + 1) == 0
        result[-1] << thing
      end
      result
    end
  end
end

CommonGroupers.geometric.group_together 1..10

Solution

  • I am still quite unclear on the implementation or use case suggested here is but as a general suggestion I guess you could go with something like

    module Functions
      def self.grouper_method(name,&body) 
         body.define_singleton_method(:group_together) {|things| call(things) } 
         define_singleton_method(name) { body } 
      end 
      
      grouper_method(:one_group) {|things| [things.dup] } 
    
      grouper_method(:geometric) do |things|
        things.each_with_index.with_object([]) do |(thing, i), result|
          result << [] if i & (i + 1) == 0
          result.last << thing
        end
      end
    
      # ETC
    end 
    
    Functions.geometric.group_together 1..10
    #=> [[1], [2, 3], [4, 5, 6, 7], [8, 9, 10]]
    

    If you really wanted to implement these as functionesque you could pass a target specification as well such that these methods could be defined in a different scope than the module that defines make_function, for instance a global Object scope if desired.(not recommended)

    module Functions
      def self.grouper_method(target,name,&body) 
         body.define_singleton_method(:group_together) {|things| call(things) } 
         target.define_singleton_method(name) { body } 
      end 
    end 
    
    Functions.grouper_method(self, :geometric)  do |things|
      result = []
      things.each_with_index do |thing, i|
        result << [] if i & (i + 1) == 0
        result[-1] << thing
      end
      result
    end
    
    geometric.group_together 1..10
    #=> [[1], [2, 3], [4, 5, 6, 7], [8, 9, 10]]