String representing relative file name, adding (meta) data?


#1

Hello,

just getting started into Ruby, I’m working with relative filenames as
strings (e.g. “foo/bar/baz.bla”) which I’ve to pass around a lot.

Sometimes I’ve those strings in simply arrays, sometimes I use them in a
Hash as keys, sometimes I’ve a Set to avoid duplicates.

I can generate those filenames from various sources and now I like to
attach a kind of meta data to that string: where it originated from (in
terms of another string information, actually another relative filename
but that’s not obligation). Actually it will be just for the users
convenience for error reporting.

I’m trying to get away without rewriting everything (which may not work
anyway) but I’m unsure how to proceed. For my given use cases above,
would could be suggested to do?

Should I create a new class? But could that fit into my “don’t want to
rewrite everything” approach?

Can I, when I receive a string the first time from a source, “just add”
some meta data to it, which I can later retrieve? I’m thinking about
JavaScript somehow, because there I can just add properties to an
existing object.I tried that but it raised “undefined method
`source_file=’” exceptions.

Rewrite the filename handling stuff?

Here’s an example method:

def find_shaders
shaders = Set.new
@map.entities.each { |entity|
entity.brushes.each { |brush|
brush.sides.each { |side|
side.shader ?
shaders.add(side.shader) :
nil
}
}
}
shaders
end

side.shader - string, like “texture/foo/bar”

The @map has a .source_file property which I would like to attach to the
“side.shader” value somehow, without removing the behavior of the
entries in the shaders-Set and without loosing the information.

Outside that method, it’s like that that at some point the Set gets
merged with another one, gets converted to an array, etc.

thanks,

  • Markus

#2

Markus F. wrote:

Can I, when I receive a string the first time from a source, “just add”
some meta data to it, which I can later retrieve?

Sure. String and Array are both objects like any other Object.

(1) Instance variables

irb(main):001:0> s1 = “hello”
=> “hello”
irb(main):002:0> s1.instance_eval { @src = “terminal” }
=> “terminal”
irb(main):003:0> s1.instance_variables
=> ["@src"]
irb(main):004:0> s1.instance_variable_get(:@src)
=> “terminal”

Taking this further: you could make a subclass of String which has
accessor methods.

(2) Singleton methods

irb(main):005:0> s2 = “world”
=> “world”
irb(main):006:0> def s2.source; “irb”; end
=> nil
irb(main):007:0> s2.source
=> “irb”

The only downside here is that an object with a singleton class cannot
be serialized using Marshal.

(3) Delegation

This is the most flexible, and my preferred option. You have a wrapper
object which contains your String, plus any other metadata object(s),
and you forward requests to whichever object(s) makes sense for each
action.

You can either do this using explicit forwarding, and/or method_missing,
or libraries to handle this for you (look at delegate.rb in the standard
library)

irb(main):001:0> require ‘delegate’
=> true
irb(main):002:0> class MyStr < SimpleDelegator; attr_accessor :src; end
=> nil
irb(main):003:0> s3 = MyStr.new(“hello”)
=> “hello”
irb(main):004:0> s3.src = “irb”
=> “irb”
irb(main):005:0> s3
=> “hello”
irb(main):006:0> s3.src
=> “irb”

HTH,

Brian.


#3

Hi,

many things, I surely learned a lot from your post!

The downside is that “Set” failed me (or I failed, POV :). I always lost
my “added” information (I tested with simple instance_eval) until I
figured out the following:

  • Set uses Hash internall
  • Hash.store says the following about Strings:
    “a String passed as a key will be duplicated and frozen”

I don’t know how to verify 100%, but it seems that’s way I’m “loosing”
my data:

$ cat meta.rb
require ‘pp’
require ‘set’

set = Set.new
set.add(‘foo’)
s = ‘bar’
s.instance_eval { @src = ‘source’ }
set.add(s)

set.each { |e|
pp e.instance_eval { @src }
}

$ ruby meta.rb
nil
nil

The second ‘nil’ should have been “source”.

Seems I need to go for non-String object in my case :frowning:

  • Markus

#4

Markus F. wrote:

I figured out the following:

  • Set uses Hash internall
  • Hash.store says the following about Strings:
    “a String passed as a key will be duplicated and frozen”

Ah yes, that’s right. It’s a general problem with Hash that if you
mutate an object, it will still be sitting on the old (wrong) hash
chain, and so won’t be found by value.

Strings are very common as hash keys, so Ruby decided to special-case
this to minimise foot-shooting. However the problem still remains for
other mutable objects:

irb(main):001:0> a = [1,2,3]
=> [1, 2, 3]
irb(main):002:0> h = {a => 99}
=> {[1, 2, 3]=>99}
irb(main):003:0> h[[1,2,3]]
=> 99
irb(main):004:0> a.push(4)
=> [1, 2, 3, 4]
irb(main):005:0> h.keys
=> [[1, 2, 3, 4]]
irb(main):006:0> h[[1,2,3,4]]
=> nil
irb(main):007:0> h.rehash
=> {[1, 2, 3, 4]=>99}
irb(main):008:0> h[[1,2,3,4]]
=> 99

And you’re also right that if you ‘dup’ an object which has singleton
methods, those are lost in the copy (otherwise, it wouldn’t be a
singleton any more :slight_smile:

Regards,

Brian.


#5

Hi myself,

further testing revealed that delegation works (singleton methods
didn’t):

-----------------8<---------------------------
$ cat meta.rb
require ‘pp’
require ‘set’

require ‘delegate’
class MyStr < SimpleDelegator
attr_accessor :src
end

def test_delegation

 set = Set.new
 set.add('foo')
 s = MyStr.new('bar')
 s.src = 'source'
 set.add(s)

 set.each { |e|
     if e.respond_to?(:src)
         pp e.src
     end
 }

end

test_delegation
-----------------8<---------------------------
$ ruby meta.rb
“source”
-----------------8<---------------------------

It didn’t worked in my “App” so I initially didn’t care about writing a
smaller test -> was I wrong.

I’ll dig further, thank you (Brian, not me) :slight_smile:

  • Markus

#6

Again, for my own record:

There were actually still problems with the prior approach: my test was
flawed as I did not test adding two MyStr classes with the same string
value. It didn’t work.

Thanks to the great and friendly guys over at IRC I was able to craft
this solution: use a clean custom class, but implement custom hash, eql?
and <=> method so it works they way I need it within the Set (which
internally uses Hash) and allows sorting, too.

class Resourcename
attr_accessor :source_file
def initialize(name, source_file = nil)
@name = name.to_s
@source_file = source_file
end
def to_s
@name
end
def hash
@name.hash
end
def eql? other
@name.eql? other.instance_eval { @name }
end
def <=> other
@name.<=> other.instance_eval { @name }
end
end

The downsize is, of course, it’s not a string. So whenever I need a
string operation, I need to explicitly call .to_s, but I can live with
that. Would be nice if it could automatically work in string context,
like concatentation, too.

  • Markus

#7

Markus F. wrote:

The downsize is, of course, it’s not a string. So whenever I need a
string operation, I need to explicitly call .to_s, but I can live with
that. Would be nice if it could automatically work in string context,
like concatentation, too.

Some Ruby methods call to_str implicitly when they expect a String, so
try that.

irb(main):005:0> class Foo; def initialize(s) @s=s; end; def to_str; @s;
end; end
=> nil
irb(main):006:0> str1 = “foo”
=> “foo”
irb(main):007:0> str2 = Foo.new(“bar”)
=> #<Foo:0xb7bdb8cc @s=“bar”>
irb(main):008:0> str1 << str2
=> “foobar”

Also, you may wish to delegate other string-like methods to @name inside
class Resourcename, e.g.

def <<(other)
@name.<<(other)
end
def
@name[*args]
end
… etc

This lets you pick and choose the ones which you wish to expose.
Otherwise, you can delegate all unknown methods like this:

def method_missing(m, *args, &blk)
@name.send(m, *args, &blk)
end


#8

Brian C. wrote:

And you’re also right that if you ‘dup’ an object which has singleton
methods, those are lost in the copy (otherwise, it wouldn’t be a
singleton any more :slight_smile:

LOL! So obvious, you’re right …

thanks!

  • Markus

#9

Markus F. wrote:

def method_missing(m, *args, &blk)
@name.send(m, *args, &blk)
end

That’s wonderful, I integrated this and now the rest of my code is less
cluttered and suddenly the class works in cases I haven’t thought of.
Very nice, thank you!

For more hidden goodness, you may also want to override respond_to?

alias :orig_respond_to? :respond_to?
def respond_to?(*m)
  orig_respond_to?(*m) || @name.respond_to?(*m)
end

#10

Brian C. wrote:

Some Ruby methods call to_str implicitly when they expect a String, so
try that.

O_O ! That a surprising thing for a beginner. With that information and
searching for “ruby to_s vs to_str” reveals some useful background. One
just has to grasp this.

This lets you pick and choose the ones which you wish to expose.
Otherwise, you can delegate all unknown methods like this:

def method_missing(m, *args, &blk)
@name.send(m, *args, &blk)
end

That’s wonderful, I integrated this and now the rest of my code is less
cluttered and suddenly the class works in cases I haven’t thought of.
Very nice, thank you!

  • Markus