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.
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:
-
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
-
Erlang behaviours