[RCR proposal] "Map" mixin analogously to "Enumerable"

O

Olaf Klischat

Hi,

yesterday I wanted to implement a hash "facade", i.e. a Hash that's
just a read-only view of some external data. That data may
(synchronously) change anytime, and the view should reflect that
immediately.

This turned out to be surprisingly difficult: You can't just derive
from Hash, override some central methods like [] or each, and then get
all of Hash's functionality. That's because you don't know the
implementation details of Hash (which methods call which other
methods) (this appears to be a manifestation of the "fragile base
class problem"; see, for example,
http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html).

If I had wanted to implement an *array* facade (read-only view) to
some external data, it would've been easy -- just implement "each" and
include Enumerable. One wishes for an equivalent to Enumerable for
"hashable" (or, more precisely, "mappable") things.

So, this is the proposal: Provide a mixin named "Map" that implements
all of Hash's read-only functionality on top of methods
get_mapped_value(key) and each{|key,value|..}. I've included a sample
implementation of such a thing below. This implementation is quite
trivial, but please note (and maybe discuss) the caveat mentioned at
the beginning. I'd also wrap this up in a Rubygem and publish it on
Rubyforge.



Here is the implementation. It can also be downloaded from
http://user.cs.tu-berlin.de/~klischat/map.rb. A unit test is available
at http://user.cs.tu-berlin.de/~klischat/map_test.rb.

-------->snip<--------
# Provides Map functionality (mapping keys to values) in terms of a
# method get_mapped_value(key) and - optionally - each{|k,v|..} in the
# class this module is included in.
#
# get_mapped_value(key) should return the value belonging to key, or
# raise IndexError if there's no such value in the map.
#
# If you want methods like keys, values, each_key, each_value, empty?
# etc. to work, you also have to provide a method "each{|k,v|..}" that
# calls the given block with all key-value pairs of the map
# consecutively. In that case, you may additionally include Enumerable
# (which also relies on "each") to inherit all standard features of an
# enumeration as well.
module Map
# this particular implementation of Map is still (to a lesser
# extent) vulnerable to the fragile base class problem because the
# methods call each other (so the behaviour is
# implementation-dependent if the user decides to override some
# methods). A really correct implementation would probably call only
# get_mapped_value and each from all its methods (or use "internal"
# helper methods that must not be overridden)


# hmm...we need a default value to be compatible with Hash#[]
def default(key=nil)
@map_mixin_default_value
end

def default=(x)
@map_mixin_default_value=x
end


def [](k)
begin
get_mapped_value(k)
rescue IndexError
default
end
end


INTERNAL=Object.new

def fetch(k,dflt=INTERNAL)
begin
get_mapped_value(k)
rescue IndexError
return dflt unless INTERNAL==dflt
return yield(k) if block_given?
raise
end
end

def has_key?(k)
begin
get_mapped_value(k)
return true
rescue IndexError
return false
end
end

alias_method :include?, :has_key?
alias_method :key?, :has_key?
alias_method :member?, :has_key?


def values_at(*keys)
keys.map {|k| self[k] }
end

# two deprecated aliases...
alias_method :indexes, :values_at
alias_method :indices, :values_at



def self.included(mod)
begin
mod.module_eval "alias_method :each_pair, :each"
rescue NameError
# mod doesn't define each. Not an error (but the methods below
# won't work).
end
end

def each_key
each{|(k,v)| yield k}
end

def each_value
each{|(k,v)| yield v}
end

def empty?
each{ return false }
true
end

def has_value?(value)
each{|(k,v)| return true if value==v }
false
end

alias_method :value?, :has_value?

def index(value)
each{|(k,v)| return k if value==v }
nil
end

def keys
result=[]
each{|(k,v)| result << k }
result
end

def values
result=[]
each{|(k,v)| result << v }
result
end

def invert
result={}
each{|(k,v)| result[v]=k }
result
end

def merge(other)
result=to_hash
other.each do |(k,v)|
result[k] = if key? k
if block_given?
yield(k,self[k],v)
else
v
end
else
result[k]=v
end
end
result
end

def reject
result={}
each{|(k,v)| result[k]=v unless yield(k,v) }
result
end

def select
result=[]
each{|(k,v)| result << [k,v] if yield(k,v) }
result
end

def to_hash
result={}
each{|(k,v)| result[k]=v }
result
end

def to_a
result=[]
each{|(k,v)| result << [k,v] }
result
end

def to_s
self.to_a.join
end

def sort(&block)
to_a.sort(&block)
end

def length
result=0
each{|(k,v)| result += 1 }
result
end

alias_method :size, :length


def ==(other)
unless other.kind_of?(Hash) or other.kind_of?(Map) # better: include Map in Hash
other = other.to_hash # better (...): to_map
end
# return false unless self.default==other.default
l=0
begin
self.each do |(k,v)|
return false unless other.fetch(k)==v
l+=1
end
rescue IndexError
return false
end
l==other.length
end

end
-------->snap<--------

Sample usage:

class MyMap
def get_mapped_value(key)
case key
when "age"; 52
when "iq"; 196
when "answer"; 42
else raise IndexError, "no such key: #{key}"
end
end

def each
yield("age", 52)
yield("iq", 196)
yield("answer",42)
end

include Map
end

m=MyMap.new
m["iq"] # => 196
m.index(196) # => "iq"
m["foo"] # => nil
m.keys # => ["age", "iq", "answer"]
m.values # => [52, 196, 42]
m.to_a # => [["age", 52], ["iq", 196], ["answer", 42]]
m.reject{|k,v| v>100} # => {"answer"=>42, "age"=>52}

# etc.


For better integration into Ruby, Hash should include Map (in addition
to Enumerable). So should other "map-like" thinks like
ENV. Additionally, "to_hash" may be accompanied by "to_map" where
feasible. That's why I thought of writing up a RCR. However, you could
always include Map yourself where needed, so this may not be so
important.

What do you think?

Olaf
 
G

gabriele renzi

Olaf Klischat ha scritto:
Hi,

yesterday I wanted to implement a hash "facade", i.e. a Hash that's
just a read-only view of some external data. That data may
(synchronously) change anytime, and the view should reflect that
immediately.

This turned out to be surprisingly difficult: You can't just derive
from Hash, override some central methods like [] or each, and then get
all of Hash's functionality. That's because you don't know the
implementation details of Hash (which methods call which other
methods) (this appears to be a manifestation of the "fragile base
class problem"; see, for example,
http://www.javaworld.com/javaworld/jw-08-2003/jw-0801-toolbox.html).

If I had wanted to implement an *array* facade (read-only view) to
some external data, it would've been easy -- just implement "each" and
include Enumerable. One wishes for an equivalent to Enumerable for
"hashable" (or, more precisely, "mappable") things.

So, this is the proposal: Provide a mixin named "Map" that implements
all of Hash's read-only functionality on top of methods
get_mapped_value(key) and each{|key,value|..}. I've included a sample
implementation of such a thing below. This implementation is quite
trivial, but please note (and maybe discuss) the caveat mentioned at
the beginning. I'd also wrap this up in a Rubygem and publish it on
Rubyforge.

see the existing RCR, #277 IIRC. Maybe I should add your code (if you
allow me to do it), but I'm out of time ATM :/
Anyway, I guess a C patch to current ruby would have more chance to be
accepted :)
Oh, and I definitely agree with you a Map mixin would be great :)
 
D

Daniel Berger

gabriele renzi wrote:
see the existing RCR, #277 IIRC. Maybe I should add your code (if you
allow me to do it), but I'm out of time ATM :/
Anyway, I guess a C patch to current ruby would have more chance to be
accepted :)
Oh, and I definitely agree with you a Map mixin would be great :)

I think you meant RCR #278, Hash#collect_to_hash. RCR #277 is 'Make
def return something useful', which you should support, because I wrote
it. ;)

I've always wondered if it would have been better to have a List
superclass from which classes like Array and Hash would be subclasses.
But perhaps that's neither here nor there.

Regards,

Dan
 
G

gabriele renzi

Daniel Berger ha scritto:
gabriele renzi wrote:



I think you meant RCR #278, Hash#collect_to_hash. RCR #277 is 'Make
def return something useful', which you should support, because I wrote
it. ;)

actually, RCR261. But I definitely support your RCR :)
I've always wondered if it would have been better to have a List
superclass from which classes like Array and Hash would be subclasses.

in this case I disagree, since it would be an almost empty class, I think.
I like the idea of having easy to use mixin much more than the one about
a reasonable class hierarchy :)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top