Skip to content
This repository has been archived by the owner on Jan 1, 2023. It is now read-only.

RobertDober/Forwarder2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Forwarder2

This implementation is for, and needs, Ruby 2. For Ruby 1.9 please see https://github.com/RobertDober/Forwarder19. For Ruby 1.8.7 please see https://github.com/RobertDober/Forwarder.

Abstract

Ruby's core Forwardable gets the job done(barely), but produces most unreadable code.

This is a nonintrusive (as is Forwardable) module that allows to delegate methods to instance variables, objects returned by instance_methods, other methods of the same receiver, the receiver itself, a chain of messages or an arbitrary object. Paramters can be provided in the forwarding definition (parially or totally.

It also defines after and before filters. and some more sophisticated use cases}

License

This software is released under the very liberal MIT license as indicated in the attached file LICENSE. If you do not have the LICENSE file delivered the terms of the license are referred to here: http://www.opensource.org/licenses/mit-license.html

Performance

Execution time is that of 85~95% of Forwardable by evalling strings whenever possible.

Simple Delegation As In Forwardable

forward <a_message>, to: <target>
forward <a_message>, to: <target>, as: <translation>

These two forms of the forward method, (and only these two forms) are directly implemented with def_delegator method of Forwardable, as follows:

def_delegator <target> <a_message>
def_delegator <translation> <a_message>

Furthermore the forward_all method is translated to the def_delegators method in the following form, thusly

forward_all msg1, msg2, msg3, ..., to: target

is implemented as

def_delegators target, msg1, msg2, msg3, ...

Additional Features

  • Parameters (partial or total application)
  • Custom And Chained Targets
  • AOP Filters
  • Helpers

Parameters

Passing One Parameter

Assuming a class ArrayWrapper and that their instances wrap the array object via the instance variable @ary the Smalltalk method second can be implemented as follows.

require 'forwarder'
class ArrayWrapper
  extend Forwarder
  forward :second, to: :@ary, as: :[], with: 1
  ...
end

The with keyword paramter is thus used to provide the first slice of arguments that will be provided to the forwarded invocation. This slice will be extended by the actual parameters of the invocation of the proxy method (e.g. the instance method defined by the forward method itself).

Passing More Parameters

If with: is passed an array, it is splatted into the invocation, thus allowing us to pass more than one parameter. This becomes clearer with an example.

   forward :add_whitespace_to_punctuation,
           to:   :name,
           as:   :gsub!,
           with: [ /[,.]\b/, '\& ' ]

A Useful Shorthand

As I found myself using the following idioms all the time

  forward :some_method, to: :@some_hash, as: :[], with: :some_method
  forward :other_method, to: :hash, as: :[], with: :other_key

I conceived the to_hash shortcut for these. Strictly spoken (and not striktly spoken too) this is a gross generalisation of the usecase as if they target had to be a Hash all the time. This is not the case of course, we are just forwarding a message with a parameter...

Here is how the above idioms can be expressed by means of the to_hash target:

   forward :some_method, to_hash: :@some_hash
   forward :other_method, to_hash: :hash, as: :other_key

Concerning jargon we are doing something a little bit confusing here. In all cases we have an implicit translation (which is :[] of course). In the second case we have an explicit translation (being :other_key) too. The explicit translation is transformed into the first, and only argument, as we do not allow explicit arguments for to_hash: targets.

However you still can use the forward_all version and a to_hash: chain target, here is an example:

   class Params
     extend Forwarder
     forward_all :count, :limit, to_hash: [:@params, :mandatory]
     forward :pretend?, to_hash: [:@params, :optional], as: :dry_run
   end

AOP is not supported for to_hash: targets in this version, this might change in the future as use cases are imaginable (e.g. an after filter for the :pretend? method, applying !! to the result).

Partial Application

This example gives us the oppurtunity to look at a use case for partial applications. Let us assume that we do not always use whitespaces, than we can leave the second paramter to be provided by the invocation of the defined forwarder proxy.

   forward :add_something_to_punctuation,
           to:   :name,
           as:   :gsub!,
           with: /([,.])\b/

We can achieve the same as above with the following invocation

   o = Name.new( "the,quick, fox." )
   o.add_something_to_punctuation( '\1 ' )
  # name: "the, quick, fox." )

but we can also add a hyphen after interpunctations with this invocation

   o = Name.new( "the,quick, fox." )
   o.add_something_to_punctuation( '\1- ' )
  # name: "the,- quick,- fox." )

But more importantely we can forward to the partial application, thus using the partial application as a mean of composition

   forward :add_ws_to_punctuation,
           to_object: :self,
           as: :add_something_to_punctuation,
           with: '\1 '

   forward :add_hypen_to_punctuation,
           to_object: self,
           as: :add_something_to_punctuation,
           with_block: ->(*grps){ "#{grps.first}- " }
 

Passing One Array

If a real array shall be passed in as one parameter it can be wrapped into an array of one element, or the with_ary: keyword parameter can be used.

Example:

  forward :append_suffix, to: :@ary, as: :concat, with: [%w{ my suffix }]
  forward :append_suffix, to: :@ary, as: :concat, with_ary: %w{ my suffix }

Passing A Block

In case of the necessity to provide a block to the forwarded invocation, it can be specified as the block parameter of the forward invocation itself.

The following example uses inject to compute a sum of elements

  forward :sum, to: :elements, as: :inject do |s,e| s+e end

Please note however that common patterns like this one can benefit of the provided helpers, in our case it is Integer.sum.

require 'forwarder/helpers/integer/sum'
...
  forward :sum, to: :elements, as: :inject &Integer.sum
# or
  forward :sum, to: :elements, as: :inject, with_block: Integer.sum
...

Accounting for different tastes a block can be provided as a block parameter or as a lambda to the with_block: keyword parameter. The later is taking preference over the former, which no defined usage of the block in this case (at least for the time being).

Selective Helpers

As we do not want to be intrusive the helpers have to be requested explicitly.

This can be done in three levels of granularity:

  • Per helper

require 'forwarder/helpers/integer/sum'

  • All helpers

require 'forwarder/helpers'

  • Per monkey patched class

require 'forwarder/helpers/integer'

Custom And Chained Targets

So far the to: keyword was followed by a symbol or string denoting a symbolic receiver, that is an instance_variable or method with the denoted name. Custom and Chain Targets are implementing a different story.

Chain Targets

Chain Targets are also expressed with the to: keyword parameter, but by passing an array of symbolic receivers. This array will resolve to the final target by sending each message to the result of the preceding message. The following example should make this clearer:

  forward :size, to: %w{@content children}

which could have been implemented by hand as follows:

  def size
    @content.children.size
  end

Custom Targets

Allow the user to define a target that cannot be expressed as a symbolic receiver.

Custom targets are expressed by the means of the to_object: keyword parameter.

I want to give two examples here, the first using self, which evaluates to the module in which forward is invoked of course, and might thus be used to forward to class instance methods, as in the following example:

class Callback
  def self.instances; @__instances__ ||= [] end
    
  def self.register an_instance
    instances << an_instance
  end

  extend Forwarder
  forward :register, to_object: self

  def initialize
    register self
  end
end

But when looking closely one can see that the self.register method is just another delegation, thus the whole code can be rewritten even more concesily as:

class Callback
  class << self
    extend Forwarder
    forward :<<, to: :instances
  
    def instances; @__instances__ ||= [] end
  end

  extend Forwarder
  forward :register, to_object: self, as: :<<
end

The second example is a forward to the instance itself, for that purpose the symbol :self can be used. The followin is, again, an implementation of Smalltalk's second method. But here we are defining it on Array itself, not a wrapper.

class Array
  extend Forwarder
  forward :second, to_object: :self, as: :[], with: 1

However the same could be accomplished by using the object/identity helper and the default target implementation.

require 'forwarder/helpers/object/identity'
class Array
  extend Forwarder
  forward :second, to: :identity, as: :[], with: 1

Custom Targets And Closures

Another application of custom targets would be to hide a enclosed object, but as in the first example above, such an object cannot be defined on instance level, but only on class level. Assuming that the class itself does not need access to the object enclosed by the closure, one could easily implement an instance count for a class as follows:

  container = []
  forward :register, to_object: container, as: :<<, with: :sentinel
  forward :instance_count, to_object: container, as: :size

AOP Filters

Before and After filters are implemented in this version.

The respective before: and after: keyword parameters expect lambdas as paramters, but by specifying the :use_block value the block parameter of the forward method can be abused for this purpose.

The following examples all operate on a class wrapping a hash instance via the hash attribute reader. Our first goal is to implement a max_value method, that will return the maxium value of all values for given keys.

After Filter

The lambda provided by after: is applied to the return value of the forwarded invocation. The following three examples all implement the max_value method correctly.

  forward :max_value, to: :hash, as: :values_at, after: lambda{ |x| x.max }
  forward :max_value, to: :hash, as: :values_at, after: :use_block do | x | 
    x.max 
  end
  require 'forwarder/helpers/kernel/sendmsg'
  forward :max_value, to: :hash, as: :values_at, after: sendmsg( :max )

N.B. The Kernel#sendmsg method is my reply to the hated - by me that is at least - Symbol#to_proc kludge and its limitations, I will talk about it more in the Helpers section.

Before Filter.

Our next goal is to implement a method value_of_max that returns the value for the greatest of all provided keys.

For this we will use a before filter, its lambda is applied to the arguments of the implemented forwarder and the result will be passed in to the forwarded invocation. The pass in will use a splash if appropriate.

  forward :value_of_max, to: :hash, as: :[], before: lambda{ |*args| args.max }
  require 'forwarder/helpers/kernel/sendmsg'
  forward :value_of_max, to: :hash, as: :[], before: sendmsg( :max )

Helpers

N.B. These are no longer part of Forwarder19, but have been moved into the gemdependency lab419_core.

Helpers define two type of methods. Firstly methods that return lambdas for frequently used block patterns, e.g. Integer.sum. Secondly methods that are convenient to use inside forward invocations, but not necessarily only there, e.g. Kernel#sendmsg or Object#identity.

Functional Helpers

I see this second group, as small as it is, as an important enhancement for the functional programming style. The possibilty to nullify a block that is necessarily used in a chain of functional calls by passing in {|x| x.identity}, sendmsg(:identity) or even the hated &:identity is a recurring pattern.

Warning: I will become evangelic now.

I do not like the Symbol#to_proc kludge, and that for two reasons. The first is pragamatic. You cannot pass parameters, and that sucks. Why can I express map(&:succ) but not map(&:+, 2). Well the answer is clear, Ruby's syntax does not support it.

The second reason is on philosophical grounds. It feels wrong that Symbol shall be responsable of transforming itself into a lambda.

Thus I created a helper in Kernel that takes the responsability, and doing so with a clear name, expressing intent. This helper is Kernel#sendmsg.

  map do |ele|
   ele.hello "World"
  end

is the same as

  map( &sendmsg( :hello, "World") )

Furthermore it might be usuful to keep the returned lambda around, please compare

  adder = sendmsg( :+ )

versus

  adder = :+.to_proc

Mapping with a Symbol might not only be conveniently expressed as sending a message to each element, sometimes a different meaning might be appropriate as in the example below:

  map do | ele |
    some_method ele
  end

A different helper can do this job without any ambiguity:

  map( &applying( :some_method ) )

Commonly Used Pattern Helpers

This group of helpers is just to avoid to rewrite lambdas you/one/whoever/I have written zillions of times. Here is a short list of examples the API doc should give you enough information if you look for something specific.

Integer.sum

  class Integer
    def self.sum
      ->(a, b){ a + b }
    end
  end

Integer#inc

  class Integer
    alias_method :inc, :succ # should have used forward ;)
  end

About

Porting Forwarder19 to Ruby2

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages