jdantonio/functional-ruby

View on GitHub
doc/protocol.md

Summary

Maintainability
Test Coverage
###    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](http://en.wikipedia.org/wiki/Data_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](http://en.wikipedia.org/wiki/Dynamic_typingDYNAMIC)
   language. Types exist but the runtime is free to cast one type into another
   at any time. Moreover, Ruby is a [duck typed](http://en.wikipedia.org/wiki/Duck_typing).
   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](http://en.wikipedia.org/wiki/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.

   ```ruby
   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:

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

   is the same as:

   ```ruby
   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](http://www.ruby-doc.org/core-2.1.2/Method.htmlmethod-i-arity):

   * 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`

   ```ruby
   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:

   ```ruby
   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:

   ```ruby
   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:

   * Clojure [protocol](http://clojure.org/protocols)
   * Erlang [behaviours](http://www.erlang.org/doc/design_principles/des_princ.htmlid60128)
   * Objective-C [protocol](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html)
     (and the corresponding Swift [protocol](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html))