Module: Functional::Protocol

Defined in:
lib/functional/protocol.rb

Overview

Protocols provide a polymorphism and method-dispatch mechanism that exchews stong typing and embraces the dynamic duck typing of Ruby. Rather than interrogate a module, class, or object for its type and ancestry, protocols allow modules, classes, and methods to be interrogated based on their behavior. It is a logical extension of the respond_to? method, but vastly more powerful.

Rationale

Traditional object orientation implements polymorphism inheritance. The Is-A relationship indicates that one object "is a" instance of another object. Implicit in this relationship, however, is the concept of type. Every Ruby object has a type, and that type is the name of its Class or Module. The Ruby runtime provides a number of reflective methods that allow objects to be interrogated for type information. The principal of thses is the is_a? (alias kind_of) method defined in class Object.

Unlike many traditional object oriented languages, Ruby is a dynamically typed language. Types exist but the runtime is free to cast one type into another at any time. Moreover, Ruby is a duck typed. If an object "walks like a duck and quacks like a duck then it must be a duck." When a method needs called on an object Ruby does not check the type of the object, it simply checks to see if the requested function exists with the proper arity and, if it does, dispatches the call. The duck type analogue to is_a? is respond_to?. Thus an object can be interrogated for its behavior rather than its type.

Although Ruby offers several methods for reflecting on the behavior of a module/class/object, such as method, instance_methods, const_defined?, the aforementioned respond_to?, and others, Ruby lacks a convenient way to group collections of methods in any way that does not involve type. Both modules and classes provide mechanisms for combining methods into cohesive abstractions, but they both imply type. This is anathema to Ruby's dynamism and duck typing. What Ruby needs is a way to collect a group of method names and signatures into a cohesive collection that embraces duck typing and dynamic dispatch. This is what protocols do.

Specifying

A "protocol" is a loose collection of method, attribute, and constant names with optional arity values. The protocol definition does very little on its own. The power of protocols is that they provide a way for modules, classes, and objects to be interrogated with respect to common behavior, not common type. At the core a protocol is nothing more than a collection of respond_to? method calls that ask the question "Does this thing behave like this other thing."

Protocols are specified with the Functional::SpecifyProtocol method. It takes one parameter, the name of the protocol, and a block which contains the protocol specification. This registers the protocol specification and makes it available for use later when interrogating ojects for their behavior.

Defining Attributes, Methods, and Constants

A single protocol specification can include definition for attributes, methods, and constants. Methods and attributes can be defined as class/module methods or as instance methods. Within the a protocol specification each item must include the symbolic name of the item being defined.

   Functional::SpecifyProtocol(:KitchenSink) do
     instance_method     :instance_method
     class_method        :class_method
     attr_accessor       :attr_accessor
     attr_reader         :attr_reader
     attr_writer         :attr_writer
     class_attr_accessor :class_attr_accessor
     class_attr_reader   :class_attr_reader
     class_attr_writer   :class_attr_writer
     constant            :CONSTANT
   end

Definitions for accessors are expanded at specification into the apprporiate method(s). Which means that this:

   Functional::SpecifyProtocol(:Name) do
     attr_accessor :first
     attr_accessor :middle
     attr_accessor :last
     attr_accessor :suffix
   end

is the same as:

   Functional::SpecifyProtocol(:Name) do
     instance_method :first
     instance_method :first=
     instance_method :middle
     instance_method :middle=
     instance_method :last
     instance_method :last=
     instance_method :suffix
     instance_method :suffix=
   end

Protocols only care about the methods themselves, not how they were declared.

Arity

In addition to defining which methods exist, the required method arity can indicated. Arity is optional. When no arity is given any arity will be expected. The arity rules follow those defined for the arity method of Ruby's Method class:

  • Methods with a fixed number of arguments have a non-negative arity
  • Methods with optional arguments have an arity -n - 1, where n is the number of required arguments
  • Methods with a variable number of arguments have an arity of -1
   Functional::SpecifyProtocol(:Foo) do
     instance_method :any_args
     instance_method :no_args, 0
     instance_method :three_args, 3
     instance_method :optional_args, -2
     instance_method :variable_args, -1
   end

   class Bar

     def any_args(a, b, c=1, d=2, *args)
     end

     def no_args
     end

     def three_args(a, b, c)
     end

     def optional_args(a, b=1, c=2)
     end

     def variable_args(*args)
     end
   end

Reflection

Once a protocol has been defined, any class, method, or object may be interrogated for adherence to one or more protocol specifications. The methods of the Functional::Protocol classes provide this capability. The Satisfy? method takes a module/class/object as the first parameter and one or more protocol names as the second and subsequent parameters. It returns a boolean value indicating whether the given object satisfies the protocol requirements:

   Functional::SpecifyProtocol(:Queue) do
     instance_method :push, 1
     instance_method :pop, 0
     instance_method :length, 0
   end

   Functional::SpecifyProtocol(:List) do
     instance_method :[]=, 2
     instance_method :[], 1
     instance_method :each, 0
     instance_method :length, 0
   end

   Functional::Protocol::Satisfy?(Queue, :Queue)        => true
   Functional::Protocol::Satisfy?(Queue, :List)         => false

   list = [1, 2, 3]
   Functional::Protocol::Satisfy?(Array, :List, :Queue) => true
   Functional::Protocol::Satisfy?(list, :List, :Queue)  => true

   Functional::Protocol::Satisfy?(Hash, :Queue)         => false

   Functional::Protocol::Satisfy?('foo bar baz', :List) => false

The Satisfy! method performs the exact same check but instead raises an exception when the protocol is not satisfied:

   2.1.2 :021 > Functional::Protocol::Satisfy!(Queue, :List)
   Functional::ProtocolError: Value (Class) 'Thread::Queue' does not behave as all of: :List.
    from /Projects/functional-ruby/lib/functional/protocol.rb:67:in `error'
    from /Projects/functional-ruby/lib/functional/protocol.rb:36:in `Satisfy!'
    from (irb):21
     ...

The Functional::Protocol module can be included within other classes to eliminate the namespace requirement when calling:

   class MessageFormatter
     include Functional::Protocol

     def format(message)
       if Satisfy?(message, :Internal)
         format_internal_message(message)
       elsif Satisfy?(message, :Error)
         format_error_message(message)
       else
         format_generic_message(message)
       end
     end

     private

     def format_internal_message(message)
        format the message...
     end

     def format_error_message(message)
        format the message...
     end

     def format_generic_message(message)
        format the message...
     end

Inspiration

Protocols and similar functionality exist in several other programming languages. A few languages that provided inspiration for this inplementation are:

Constant Summary

@@info =

The global registry of specified protocols.

{}

Class Method Summary (collapse)

Class Method Details

+ (Symbol) Satisfy!(target, *protocols)

Does the given module/class/object fully satisfy the given protocol(s)? Raises a Functional::ProtocolError on failure.

Parameters:

  • target (Object)

    the method/class/object to interrogate

  • protocols (Symbol)

    one or more protocols to check against the target

Returns:

  • (Symbol)

    the target

Raises:



87
88
89
90
91
# File 'lib/functional/protocol.rb', line 87

def Satisfy!(target, *protocols)
  Protocol::Satisfy?(target, *protocols) or
    Protocol.error(target, 'does not', *protocols)
  target
end

+ (Boolean) Satisfy?(target, *protocols)

Does the given module/class/object fully satisfy the given protocol(s)?

Parameters:

  • target (Object)

    the method/class/object to interrogate

  • protocols (Symbol)

    one or more protocols to check against the target

Returns:

  • (Boolean)

    true if the target satisfies all given protocols else false

Raises:

  • (ArgumentError)

    when no protocols given



72
73
74
75
# File 'lib/functional/protocol.rb', line 72

def Satisfy?(target, *protocols)
  raise ArgumentError.new('no protocols given') if protocols.empty?
  protocols.all?{|protocol| Protocol.satisfies?(target, protocol.to_sym) }
end

+ (Boolean) Specified!(*protocols)

Have the given protocols been specified? Raises a Functional::ProtocolError on failure.

Parameters:

  • protocols (Symbol)

    the list of protocols to check

Returns:

  • (Boolean)

    true if all given protocols have been specified

Raises:

  • (Functional::ProtocolError)

    if one or more of the given protocols have not been specified

  • (ArgumentError)

    when no protocols are given



115
116
117
118
119
# File 'lib/functional/protocol.rb', line 115

def Specified!(*protocols)
  raise ArgumentError.new('no protocols given') if protocols.empty?
  (unspecified = Protocol.unspecified(*protocols)).empty? or
    raise ProtocolError.new("The following protocols are unspecified: :#{unspecified.join('; :')}.")
end

+ (Boolean) Specified?(*protocols)

Have the given protocols been specified?

Parameters:

  • protocols (Symbol)

    the list of protocols to check

Returns:

  • (Boolean)

    true if all given protocols have been specified else false

Raises:

  • (ArgumentError)

    when no protocols are given



100
101
102
103
# File 'lib/functional/protocol.rb', line 100

def Specified?(*protocols)
  raise ArgumentError.new('no protocols given') if protocols.empty?
  Protocol.unspecified(*protocols).empty?
end