lib/functional/tuple.rb
require 'functional/synchronization'
module Functional
# A tuple is a pure functional data strcture that is similar to an array but is
# immutable and of fixed length. Tuples support many of the same operations as
# array/list/vector.
#
# @note The current implementation uses simple Ruby arrays. This is likely to be
# very inefficient for all but the smallest tuples. The more items the tuple
# contains, the less efficient it will become. A future version will use a fast,
# immutable, persistent data structure such as a finger tree or a trie.
#
# @!macro thread_safe_immutable_object
#
# @see http://en.wikipedia.org/wiki/Tuple
# @see http://msdn.microsoft.com/en-us/library/system.tuple.aspx
# @see http://www.tutorialspoint.com/python/python_tuples.htm
# @see http://en.cppreference.com/w/cpp/utility/tuple
# @see http://docs.oracle.com/javaee/6/api/javax/persistence/Tuple.html
# @see http://www.erlang.org/doc/reference_manual/data_types.html
# @see http://www.erlang.org/doc/man/erlang.html#make_tuple-2
# @see http://en.wikibooks.org/wiki/Haskell/Lists_and_tuples#Tuples
class Tuple < Synchronization::Object
# Create a new tuple with the given data items in the given order.
#
# @param [Array] data the data items to insert into the new tuple
# @raise [ArgumentError] if data is not an array or does not implement `to_a`
def initialize(data = [])
raise ArgumentError.new('data is not an array') unless data.respond_to?(:to_a)
super
@data = data.to_a.dup.freeze
self.freeze
ensure_ivar_visibility!
end
# Retrieve the item at the given index. Indices begin at zero and increment
# up, just like Ruby arrays. Negative indicies begin at -1, which represents the
# last item in the tuple, and decrement toward the first item. If the
# given index is out of range then `nil` is returned.
#
# @param [Fixnum] index the index of the item to be retrieved
# @return [Object] the item at the given index or nil when index is out of bounds
def at(index)
@data[index]
end
alias_method :nth, :at
alias_method :[], :at
# Retrieve the item at the given index or return the given default value if the
# index is out of bounds. The behavior of indicies follows the rules for the
# `at` method.
#
# @param [Fixnum] index the index of the item to be retrieved
# @param [Object] default the value to return when given an out of bounds index
# @return [Object] the item at the given index or default when index is out of bounds
#
# @see Functional::Tuple#at
def fetch(index, default)
if index >= length || -index > length
default
else
at(index)
end
end
# The number of items in the tuple.
#
# @return [Fixnum] the number of items in the tuple
def length
@data.length
end
alias_method :size, :length
# Returns a new tuple containing elements common to the two tuples, excluding any
# duplicates. The order is preserved from the original tuple.
#
# @!macro [attach] tuple_method_param_other_return_tuple
# @param [Array] other the tuple or array-like object (responds to `to_a`) to operate on
# @return [Functional::Tuple] a new tuple with the appropriate items
def intersect(other)
Tuple.new(@data & other.to_a)
end
alias_method :&, :intersect
# Returns a new tuple by joining self with other, excluding any duplicates and
# preserving the order from the original tuple.
#
# @!macro tuple_method_param_other_return_tuple
def union(other)
Tuple.new(@data | other.to_a)
end
alias_method :|, :union
# Returns a new tuple built by concatenating the two tuples
# together to produce a third tuple.
#
# @!macro tuple_method_param_other_return_tuple
def concat(other)
Tuple.new(@data + other.to_a)
end
alias_method :+, :concat
# Returns a new tuple that is a copy of the original tuple, removing any items that
# also appear in other. The order is preserved from the original tuple.
#
# @!macro tuple_method_param_other_return_tuple
def diff(other)
Tuple.new(@data - other.to_a)
end
alias_method :-, :diff
# Returns a new tuple built by concatenating the given number of copies of self.
# Returns an empty tuple when the multiple is zero.
#
# @param [Fixnum] multiple the number of times to concatenate self
# @return [Functional::Tuple] a new tuple with the appropriate items
# @raise [ArgumentError] when multiple is a negative number
def repeat(multiple)
multiple = multiple.to_i
raise ArgumentError.new('negative argument') if multiple < 0
Tuple.new(@data * multiple)
end
alias_method :*, :repeat
# Returns a new tuple by removing duplicate values in self.
#
# @return [Functional::Tuple] the new tuple with only unique items
def uniq
Tuple.new(@data.uniq)
end
# Calls the given block once for each element in self, passing that element as a parameter.
# An Enumerator is returned if no block is given.
#
# @yieldparam [Object] item the current item
# @return [Enumerable] when no block is given
def each
return enum_for(:each) unless block_given?
@data.each do |item|
yield(item)
end
end
# Calls the given block once for each element in self, passing that element
# and the current index as parameters. An Enumerator is returned if no block is given.
#
# @yieldparam [Object] item the current item
# @yieldparam [Fixnum] index the index of the current item
# @return [Enumerable] when no block is given
def each_with_index
return enum_for(:each_with_index) unless block_given?
@data.each_with_index do |item, index|
yield(item, index)
end
end
# Calls the given block once for each element in self, passing that element
# and a tuple with all the remaining items in the tuple. When the last item
# is reached ab empty tuple is passed as the second parameter. This is the
# classic functional programming `head|tail` list processing idiom.
# An Enumerator is returned if no block is given.
#
# @yieldparam [Object] head the current item for this iteration
# @yieldparam [Tuple] tail the remaining items (tail) or an empty tuple when
# processing the last item
# @return [Enumerable] when no block is given
def sequence
return enum_for(:sequence) unless block_given?
@data.length.times do |index|
last = @data.length - 1
if index == last
yield(@data[index], Tuple.new)
else
yield(@data[index], Tuple.new(@data.slice(index+1..last)))
end
end
end
# Compares this object and other for equality. A tuple is `eql?` to
# other when other is a tuple or an array-like object (any object that
# responds to `to_a`) and the two objects have identical values in the
# same foxed order.
#
# @param [Object] other the other tuple to compare for equality
# @return [Boolean] true when equal else false
def eql?(other)
@data == other.to_a
end
alias_method :==, :eql?
# Returns true if self contains no items.
#
# @return [Boolean] true when empty else false
def empty?
@data.empty?
end
# Returns the first element of the tuple or nil when empty.
#
# @return [Object] the first element or nil
def first
@data.first
end
alias_method :head, :first
# Returns a tuple containing all the items in self after the first
# item. Returns an empty tuple when empty or there is only one item.
#
# @return [Functional::Tuple] the tail of the tuple
def rest
if @data.length <= 1
Tuple.new
else
Tuple.new(@data.slice(1..@data.length-1))
end
end
alias_method :tail, :rest
# Create a standard Ruby mutable array containing the tuple items
# in the same order.
#
# @return [Array] the new array created from the tuple
def to_a
@data.dup
end
alias_method :to_ary, :to_a
# Describe the contents of this object in a string.
#
# @return [String] the string representation of this object
#
# @!visibility private
def inspect
"#<#{self.class}: #{@data.to_s}>"
end
# Describe the contents of this object in a string that exactly
# matches the string that would be created from an identical array.
#
# @return [String] the string representation of this object
#
# @!visibility private
def to_s
@data.to_s
end
end
end