"Crystallizing" Objects

Hello,

in this post, I’d like to make a proposal to contribute
to Ruby’s fitness for the challenges of parallelization.

The problem with sharing objects between threads
is concurrent write access.
If the objects were frozen,
everything would be OK … but also boring. :slight_smile:

Now, I think it should be possible
to freeze the “already existing” data of an object
and at the same time leave it open for “further growth”.
Especially, I have Hashes and Arrays in mind:
New key/value pairs could be added to Hashes,
objects could be appended to Arrays.

So, I think it would be beneficial to introduce a third state of objects
between frozen and normal, like “growing” or “crystallizing” or
“monotonous” or “incrementally_freezing” or “partially_frozen” …

In this state, all methods that would
modify the already “crystallized” data of the object
should throw an exception.
E.g. for strings, only appending would be allowed.

Perhaps it would help implementing this idea,
if the << method would be made special
and be the only accepted modifying method
in the “growing” state.

Also, it would probably be good to have a means of determining
if a method call is safe to use, in the sense
that its result will always be the same object,
regardless of what other threads do to the receiver.
(Let’s say, it is a “permanent” method call)

Roughly:

  • when the receiving object is frozen, all method calls are “permanent”

  • when the receiver is in the normal (unrestricted) state,
    method calls are “permanent”, iff the receiver is an immediate value.

  • when the object is in the “growing” state, then it’s a bit tricky:

    Just having an array “permanent_methods” would not always work,
    because there are methods that are both,
    “permanent” or not, depending on the arguments.

    For example, we would definitely want to use Array#[],
    but this is only “permanent” for non-negative indices.

    Perhaps, there could be some annotation to those methods,
    (raising an exception if the call would not guarantee a permanent
    result),
    and this would only be switched on if the object is in the
    “crystallizing” state
    (and the general check for “permanentness” were switched on, say
    globally).

OK, let me stop here, for now.
Has a similar idea been discussed before?

Regards,
Sven

Sven S. schrieb:

to freeze the “already existing” data of an object
and at the same time leave it open for “further growth”.

So, I think it would be beneficial to introduce a third state of objects
between frozen and normal, like “growing” or “crystallizing” or
“monotonous” or “incrementally_freezing” or “partially_frozen” …

I’d better give a use case, to make clear what I mean:
Assume, we have a Hash called “users”.

If it were just a normal hash, we would have to lock the object
manually:

if users.has_key(new_nickname)
raise “Nickname already exists”
end

hash could be modified here, if not locked!!!

if another thread would have stored a user with the same nickname

just now, then this data would be overwritten in the next statement.

users[new_nickname] = user_data

If “users” were in this “partially_frozen” state, then we could write:

begin
users[new_nickname] = user_data
rescue CantChangeExistingDataError
raise “Nickname already exists”
end

So, the prime use case is the assignment of hash keys.
Perhaps I was too quick generalizing this approach to all objects.

Also, it would probably be good to have a means of determining
if a method call is safe to use, in the sense
that its result will always be the same object,
regardless of what other threads do to the receiver.
(Let’s say, it is a “permanent” method call)

“reliable” would probably be a better name for that.

And I think that this kind of check is not nessesary for my proposal to
work.

Regards,
Sven

On 5/17/07, Sven S. [email protected] wrote:

Hello,

Interesting idea Sven, I lose you on some points though

and at the same time leave it open for “further growth”.
should throw an exception.
E.g. for strings, only appending would be allowed.

Perhaps it would help implementing this idea,
if the << method would be made special
and be the only accepted modifying method
in the “growing” state.

Hmm maybe that has something to do with your “permanent” methods, of
which I fail to grasp the idea right now.
Assuming that << is the only writing method to a semi-frozen object we
would need to freeze the methods on the object, like pulling them into
the object and disallow method lookup. Maybe it is not the best idea
to control the state at that global level?

Also, it would probably be good to have a means of determining
if a method call is safe to use, in the sense
that its result will always be the same object,
You mean that the receiver will not be modified, right?
regardless of what other threads do to the receiver.
(Let’s say, it is a “permanent” method call)
Non modifiying?

Roughly:

  • when the receiving object is frozen, all method calls are “permanent”
    Yes I guess I see what you mean with “permanent”

  • when the receiver is in the normal (unrestricted) state,
    method calls are “permanent”, iff the receiver is an immediate value.
    Nope I am losing you, there are still non modifying methods as e.g.
    to_s so I guess I am wrong in my understanding.

  • when the object is in the “growing” state, then it’s a bit tricky:

    Just having an array “permanent_methods” would not always work,
    because there are methods that are both,
    “permanent” or not, depending on the arguments.
    Would you mind to elaborate?

    For example, we would definitely want to use Array#[],
    but this is only “permanent” for non-negative indices.
    You mean because -1 would be different because of growing might occur
    in a different thread. In that case permanent would mean insensible to
    growth?

    Perhaps, there could be some annotation to those methods,
    (raising an exception if the call would not guarantee a permanent result),
    I see, makes sense.
    and this would only be switched on if the object is in the
    “crystallizing” state
    (and the general check for “permanentness” were switched on, say
    globally).

OK, let me stop here, for now.
Has a similar idea been discussed before?
Not to my memories, but I hope this will become a rich one:)

Regards,
Sven

Cheers
Robert

Hi Robert,

Robert D. schrieb:

Perhaps it would help implementing this idea,
if the << method would be made special
and be the only accepted modifying method
in the “growing” state.

Hmm maybe that has something to do with your “permanent” methods, of
which I fail to grasp the idea right now.

No, nothing to do with that.
The idea was meant as a quick solution:
If an object is in “crystallizing” or “semi_frozen” state,
then react to every method call, as if it were frozen, except to
the << method.

I had meant this just as an easy default strategy for new classes,
I still would like to be able to use the normal methods for built-in
objects.
(In particular, I would want to write
hash[key] = value
instead of a newly introduced
hash << [key,value]
)

Assuming that << is the only writing method to a semi-frozen object we
would need to freeze the methods on the object, like pulling them into
the object and disallow method lookup.

Yes, for example. I think, however, that it would be better to raise
a special exception if destructive methods are called,
but I don’t have the picture clear enough to be able to tell.

Maybe it is not the best idea
to control the state at that global level?

Global level? What are you referring to?

Also, it would probably be good to have a means of determining
if a method call is safe to use, in the sense
that its result will always be the same object,

You mean that the receiver will not be modified, right?

Well, not quite – that’s why I was struggeling for a new word for it.
The receiver can very well be modified, but only in a non-destructive
way:
For example
hash[key]
will always return the same object, no matter how many other
key/value-pairs
will be added to hash.
So, I would say Hash#[] is always “permanent” (or “reliable” in my
second post).
On the other hand, Hash#sort and Hash#size are not “permanent” (or
“reliable”).

  • when the receiver is in the normal (unrestricted) state,
    method calls are “permanent”, iff the receiver is an immediate value.

Nope I am losing you, there are still non modifying methods as e.g.
to_s so I guess I am wrong in my understanding.

Well, unless it is an immediate value, there is Object#replace,
and thus the object can be changed completely be another thread.
And thus, to_s will most certainly not be “reliable” or “permanent”.
But we can rely on Symbol#to_s and Fixnum#to_s, because immediate
values
cannot be changed. (It is as if they are always frozen.)

For example, we would definitely want to use Array#[],
but this is only “permanent” for non-negative indices.

You mean because -1 would be different because of growing might occur
in a different thread. In that case permanent would mean insensible to
growth?

Yes, I think you were able to put it better than I.
“permanent” / “reliable” = “insensible to anything that may happen,
including growth”

And, so far we have not said anything about the resulting object itself.
One option would be to demand that the result must also (at least) be
“semi_frozen”.
For example, that a “semi_frozen” hash must only contain frozen and
semi-frozen values.
But then, this restriction might be too strong, sometimes.
I think this is a minor issue at this point.

So long

Sven

On May 17, 2007, at 02:55 , Sven S. wrote:

Now, I think it should be possible
to freeze the “already existing” data of an object
and at the same time leave it open for “further growth”.
Especially, I have Hashes and Arrays in mind:
New key/value pairs could be added to Hashes,
objects could be appended to Arrays.

[…]

To get some clarification and help me understand better, how is this
any different from purely functional data structures?

http://en.wikipedia.org/wiki/Purely_functional_data_structure

On 5/17/07, Sven S. [email protected] wrote:

which I fail to grasp the idea right now.
(In particular, I would want to write
Yes, for example. I think, however, that it would be better to raise
a special exception if destructive methods are called,
but I don’t have the picture clear enough to be able to tell.

Maybe it is not the best idea
to control the state at that global level?

Global level? What are you referring to?
Exactly what you described above, raise an Exception as soon as an
illegal operation on the object is performed; not depending on the
method.
Global was a bad name.
The receiver can very well be modified, but only in a non-destructive way:
Thats what I should have said, the concept is new and I find it
difficult to find the correct wordings.
For example
hash[key]
will always return the same object, no matter how many other
key/value-pairs
will be added to hash.
Agreed.
So, I would say Hash#[] is always “permanent” (or “reliable” in my
second post).
Ok, I see clearer now maybe constant might be a better word, but that
might be confusing for C++ folks.
On the other hand, Hash#sort and Hash#size are not “permanent” (or
“reliable”).
Clear now.
And thus, to_s will most certainly not be “reliable” or “permanent”.
But we can rely on Symbol#to_s and Fixnum#to_s, because immediate values
cannot be changed. (It is as if they are always frozen.)
Aha sorry I think talking about the return value is not relevant or do
I still misunderstand?
“permanent” / “reliable” = “insensible to anything that may happen,
including growth”
Great, I really see what is permanent now.

And, so far we have not said anything about the resulting object itself.
One option would be to demand that the result must also (at least) be
“semi_frozen”.
Hmmm gotta think I see no reason so far for that!
For example, that a “semi_frozen” hash must only contain frozen and
semi-frozen values.
Sure but that is containment not return values?
But then, this restriction might be too strong, sometimes.
I think this is a minor issue at this point.
All this is open to investigation, you just introduced a concept…

So long

Sven

Cheers
Robert

Sven S. [email protected] writes:

Sven S. schrieb:

If it were just a normal hash, we would have to lock the object manually:

Start locking from this point, before you do a check.

if users.has_key(new_nickname)
raise “Nickname already exists”
end

hash could be modified here, if not locked!!!

if another thread would have stored a user with the same nickname

just now, then this data would be overwritten in the next

statement.

If you start locking at this point, then in between the check and the
insertion another check and insertion could have occurred and the
following insertion effectively undoes that insertion.

users[new_nickname] = user_data

If “users” were in this “partially_frozen” state, then we could write:

begin
users[new_nickname] = user_data
rescue CantChangeExistingDataError
raise “Nickname already exists”
end

For Hash in official ruby implementation, since #= is implemented in C
without any yielding, #= is effectively an atomic operation. But since
you want to throw an exception if there is already existing entry, you’d
still need a lock to do the check-and-insert atomically.

In another thread, Ryan D. brought up purely functional data
structure. However, that won’t lift the necessity of locking in your
example case because the users list would have to be a global, shared
resource and you are trying to do something akin to the P() operation
of sempahore. Unless Hash already have an atomic check-before-set
operation, you can’t get around it the need of having a lock. [And no,
Hash has no such thing yet. Set#add? is similar to what you’re looking
for but it’s not atomic].

YS.

On 5/17/07, Yohanes S. [email protected]
wrote:

raise “Nickname already exists”
users[new_nickname] = user_data
For Hash in official ruby implementation, since #= is implemented in C
without any yielding, #= is effectively an atomic operation. But since
you want to throw an exception if there is already existing entry, you’d
still need a lock to do the check-and-insert atomically.

I see this point now, I totally forgot what OP wanted this for, easy
synchronization.
I doubt that this is possible, R/W locks would do the same and of course
<<
would need a Write lock even if it “only” creates a new key.

FWIAC I thought the concept of demi-frozen object state avoiding any
information loss was interesting on a theoretical base, not for
synchronization.

> > YS. > > R.

On 17.05.2007 11:55, Sven S. wrote:

and at the same time leave it open for “further growth”.
Especially, I have Hashes and Arrays in mind:
New key/value pairs could be added to Hashes,
objects could be appended to Arrays.

Do you envision this added state to be shared by threads or thread
local?

So, I think it would be beneficial to introduce a third state of objects
between frozen and normal, like “growing” or “crystallizing” or
“monotonous” or “incrementally_freezing” or “partially_frozen” …

In this state, all methods that would
modify the already “crystallized” data of the object
should throw an exception.
E.g. for strings, only appending would be allowed.

I do not believe that it would be beneficial and here’s why: on an
abstract level the state of the object still changes, so you need proper
synchronization mechanisms in place. Now, if you can identify the
subset of the state that does not change, you can put it into another
instance (probably of another class). Now you have a clean separation
of constant state and mutable state - even better: you do not have any
locking issues at all if you use the mutable instance only in one thread
and share the constant only. Implementation complexity (of the
interpreter) and efficiency wise this is much better than having a
single instance with split state where read write synchronization has to
take place. You will have to define the constant and mutable part for
any class individually anyway so you can as well build concrete
collaborating classes in Ruby that will handle this.

Concrete: if you want Arrays with common immutable prefixes, just create
a class SharedImmutablePrefixArray (possibly using Delegator or such)
that refers a shared constant Array and a local mutable Array and
implements methods like #[], #[]=, #each, #size etc. accordingly. You
could even create this class in a way that the mutable state is stored
via Thread#[] (i.e. thread local storage) so a single instance can
actually be shared by multiple threads.

Other advantages of this approach: it is explicit, i.e. in code you
create an instance of SharedImmutablePrefixArray per thread and it
becomes apparent what’s going on.

Also, with a single instance that would combine mutable and immutable
state for multiple threads internally garbage collection will be much
harder, since you will not have an object reference for the per thread
state. This again will make the implementation of GC much more complex
and likely less efficient.

Has a similar idea been discussed before?

I am not aware of something like this. But it’s good to discuss
suggestions like this from time to time. :slight_smile:

Kind regards

robert

On 5/18/07, Robert K. [email protected] wrote:

Has a similar idea been discussed before?

I am not aware of something like this. But it’s good to discuss
suggestions like this from time to time. :slight_smile:

I feel that the idea is worth discussing out of the synchronization
issue. I do not think it is fit for that purpose.
However the idea of an object state that allows to add information but
forbids the loss/overwriting of information is just great.

Maybe an example is better

class History < Array # for shortness of code, I’d probably use
deleagtion
def initialize
no_loss # --> that is the semi frozen state
end
end
h= History.new
h << “Just lost my SVN Server”
h << “My dog eat my flash disk, the small one (dog not disk)”
puts h ==> “2007-05-16 14:13:12 Just lost my SVN Server\n…”
h.pop ==> TypeError: cannot remove/modify information in a no_loss
object <…>

I just feel that would be nice to have.

Cheers
Robert

On May 18, 2007, at 01:55 , Robert D. wrote:

However the idea of an object state that allows to add information but
forbids the loss/overwriting of information is just great.

and not the least bit new:

http://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/
0521663504

Sven S. [email protected] writes:

Look at the ease with which you can write concurrent programs in Erlang.
No need to think about locking there – the language takes care of
that.

That’s because there is no mutable data structure there, so it
sidesteps the issue entirely. That is not possible in languages like
Ruby where such things are abound and shared. Immutable data structure
is an exception, not the rule, in ruby.

Furthermore, you still need to think about locking in Erlang when
several processes are sharing external resources.

I think that logic variables would not fit well into current Ruby.
(But maybe I’m wrong… well, I’d be delighted to have logic variables
in Ruby… :slight_smile: )

What a coincidence. See Austin Z.'s code in rubytalk:252018 if
you want the single-assignment property of logic variable.

But I think that these “partially frozen” or “crystallizing” objects
could be a step towards bringing some of that ease of parallel programming
into Ruby.

That would be difficult considering ruby is full of global, shared,
mutable objects. Having said that, fortunately there are approaches
that are more suitable for non-functional languages, where the need
for explicit locking is not as jarring as it is now.

YS.

On 5/18/07, Ryan D. [email protected] wrote:

On May 18, 2007, at 01:55 , Robert D. wrote:

However the idea of an object state that allows to add information but
forbids the loss/overwriting of information is just great.

and not the least bit new:

    http://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/

0521663504

That does not help very much.
It is definitely quite new to this list. I would like to learn more
about it but these pointers are quit boring…

Robert

You see things; and you say Why?
But I dream things that never were; and I say Why not?
– George Bernard Shaw

Hi,

Yohanes S. schrieb:

Unless Hash already have an atomic check-before-set
operation, you can’t get around it the need of having a lock. [And no,
Hash has no such thing yet. Set#add? is similar to what you’re looking
for but it’s not atomic].

Exactly.
I want this atomic check+set.

But this is not the only thing that I was trying to achieve by my
proposal.

I am also very much in favor of multithreadedness
to be deeply integrated in the core of Ruby.

Look at the ease with which you can write concurrent programs in Erlang.
No need to think about locking there – the language takes care of that.

Erlang is based on logic variables, they make this easy.
I think that logic variables would not fit well into current Ruby.
(But maybe I’m wrong… well, I’d be delighted to have logic variables
in Ruby… :slight_smile: )

But I think that these “partially frozen” or “crystallizing” objects
could be a step towards bringing some of that ease of parallel
programming
into Ruby.

On the whole, I’m very pleased how much attention Erlang gets
from the Ruby community currently.
I think there’s really something that can be learned.

Yohanes S. schrieb:

insertion another check and insertion could have occurred and the
following insertion effectively undoes that insertion.

users[new_nickname] = user_data

Yes, that’s what I wanted to say, thanks for elaborating.

For Hash in official ruby implementation, since #= is implemented in C
without any yielding, #= is effectively an atomic operation. But since
you want to throw an exception if there is already existing entry, you’d
still need a lock to do the check-and-insert atomically.

Yes.

Regards,
Sven

Yohanes S. schrieb:

Ruby where such things are abound and shared.

Yes, and in such a language it may still be possible to establish areas
where the immutable paradigm rules.

Immutable data structure is an exception, not the rule, in ruby.

Of course. And I don’t think that this could change dramatically.
But immutable data structures may become less exotic,
when the need for them increases.

Furthermore, you still need to think about locking in Erlang when
several processes are sharing external resources.

Granted.

What a coincidence. See Austin Z.'s code in rubytalk:252018 if
you want the single-assignment property of logic variable.

Thanks for pointing me to it!

OK. What do you have in mind?

So long,
Sven

On May 18, 2007, at 12:49 , Robert D. wrote:

That does not help very much.
It is definitely quite new to this list. I would like to learn more
about it but these pointers are quit boring…

I’m sorry, but reading is pretty much mandatory in our field, even if
“boring”. I’m reasonably sure you can use google for “functional data
structures fun” on your own.

Hello Robert,

thank you for your mail,
however, I’m afraid that I haven’t been able to grasp
what you were up to.

Robert K. schrieb:

On 17.05.2007 11:55, Sven S. wrote:

Now, I think it should be possible
to freeze the “already existing” data of an object
and at the same time leave it open for “further growth”.
Especially, I have Hashes and Arrays in mind:

Do you envision this added state to be shared by threads or thread local?

This state is a property of the object, and thus shared by all threads
using this object.

So, I think it would be beneficial to introduce a third state of objects
between frozen and normal, like “growing” or “crystallizing” or
“monotonous” or “incrementally_freezing” or “partially_frozen” …

I do not believe that it would be beneficial and here’s why:

I have read your explanation (several times) and I just cannot see your
point.
How does it fit to my use case
in my post [email protected] of May-17, 12:01 GMT:

If “users” were in this “partially_frozen” state, then we could write:
begin
users[new_nickname] = user_data
rescue CantChangeExistingDataError
raise “Nickname already exists”
end

Robert K. wrote:

on an abstract level the state of the object still changes,
so you need proper synchronization mechanisms in place.

??
What do you mean?
The synchronization mechanism is the thing that I am addressing:
atomic “check&set” – such that manual locking is no longer needed.

Now, if you can identify the subset of the state that does not change,
you can put it into another instance (probably of another class).

OK, let’s suppose, we would have several worker threads
that would all be allowed to add data to the “users”-Hash.
And assuming that the “users”-Hash were initially empty,
what would be the subset that does not change, just {} ?

Now you have a clean separation of constant state and mutable state -
even better: you do not have any locking issues at all if you use
the mutable instance only in one thread and share the constant only.

OK, if I read you correctly, then you would propose to have
an object that internally keeps this Hash (“users”) and
externally presents a thread-safe method check_add_user(name, data).
But then you would have to do the locking inside this method,
or have I overlooked anything?

Implementation complexity (of the interpreter) and efficiency wise
this is much better than having a single instance with split state
where read write synchronization has to take place.

Really?
I cannot really tell, but I suggested this idea
because I believe that this “partially frozen” state can be implemented
without considerable loss of performance.

You will have to define the constant and mutable part
for any class individually anyway

??
What do you mean by that?

so you can as well
build concrete collaborating classes in Ruby that will handle this.

Concrete: if you want Arrays with common immutable prefixes,
just create a class SharedImmutablePrefixArray (possibly using
Delegator or such)
that refers a shared constant Array and a local mutable Array
and implements methods like #[], #[]=, #each, #size etc. accordingly.

But I do not want a local mutable Array,
I want the mutable part shared across threads, too!!
Or do you mean that to say that SharedImmutablePrefixArray
refers a mutable Array for each thread?? (And then the #each
method summarizes over the immutable array and all the mutable arrays?)

You could even create this class in a way that the mutable state
is stored via Thread#[] (i.e. thread local storage)
so a single instance can actually be shared by multiple threads.

OK. So this is the way you plan to share the mutable part across
threads.
Do you mean that one thread is special and carries the mutable part
for all threads? Then why don’t you just use a global variable?
Or do you mean that each thread can have its own mutable part?
Then how are you going to organize this emerging chaos? ( :slight_smile: )

Sorry, I just don’t get you here.

Other advantages of this approach: it is explicit, i.e. in code you
create an instance of SharedImmutablePrefixArray per thread
and it becomes apparent what’s going on.

Again, I don’t understand.
If the object is shared, one object for all the relevant threads would
be enough, what can be the use of one object per thread?

So long
Sven

On 18.05.2007 22:34, Sven S. wrote:

Hello Robert,

thank you for your mail,
however, I’m afraid that I haven’t been able to grasp
what you were up to.

Reading this posting and rereading your original posting it occurs to me
that I probably misread your intentions and got on a completely
different track: I believed you wanted a thread safe data structure that
is initialized with a portion that is constant and shared by multiple
threads which can extend it locally. Apparently this is not what you
want as I gather now. I am sorry for the confusion.

Back to the original topic. One bit that took me astray was your
mentioning of “a third state of objects between frozen and normal” that
should allow for growth only. IMHO that concept is best described as a
behavioral requirement, i.e. Hash#store and Hash#[]= must behave
differently than normal as an example. I believe this is best served by
dedicated classes because you won’t likely change the “mode” of
operation of a single instance; also, if implemented as mode the
implementation will be more complex than necessary. Also, your “mode”
differs quite a lot from frozen and not frozen because behavior will
change depending on data structure while freeze just prevents instance
variable assignment - and this is the same for all classes.

Your second requirement is to have the class thread safe, which can be
achieved in different ways. I believe most standard Ruby classes do not
have built in thread safety on purpose: most of the time (i.e. when
using it in a single thread) synchronization is just plain overhead.
Another good reason not to build this into standard classes is that it’s
often not sufficient to synchronize individual methods (as you are
probably aware because of the test and set scenarios needed for your
growing data structures). (You can observer a similar learning process
in Java: the original collections Vector and Hashmap were synchronized
while later collection classes did not have built in synchronization,
instead there are wrapper classes that do the synchronization; even
later on collections with specific synchronization properties were
created).

Having said that, I believe the solution to your problem is not another
mode between normal and frozen but a set of new classes (GrowingArray,
GrowingHash etc., see attachment). It’s simpler to do (no change in the
core of the interpreter necessary), more modularized and can be better
adjusted to varying needs. You can synchronize them as needed or even
build synchronized variants.

Kind regards

robert

end

alias []= store

def set_if_unset(k,v=nil)
store(k, block_given? ? yield : v) unless key? k
end
end

Is this intended to be thread-safe? Am I missing something with the
lack of locking in the store method…?

What stops two threads doing a store of the same key sometimes letting
both stores go through?

For example, the timeline:
Thread1 Thread2


raise ArgumentError, “Key already set: #{k.inspect}” if key? k
raise ArgumentError, “Key already set: #{k.inspect}” if
key? k
super
super

On 20.05.2007 18:12, fud wrote:

end

alias []= store

def set_if_unset(k,v=nil)
store(k, block_given? ? yield : v) unless key? k
end
end

Is this intended to be thread-safe?

Of course not. (see my posting)

Kind regards

robert

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs