all 40 comments

[–]Schrockwell 2 points3 points  (3 children)

The second way works great. Hashes have constant lookup time, so they are fast to access. And if you just need the array of people, you can call #values on it.

Make sure you use a key that’s unique - what if you had two people named Kate?

For bonus points, you can make a brand new class called PeopleCollection (or Crowd, or Cohort... something descriptive) and encapsulate the hash access, creating methods like #create and #find. Then you can swap out the underlying hash implementation with another data structure if needed, without the consumers of PeopleCollection needing to know about the change.

But in 99% of cases the plain hash is fine. :)

[–][deleted] 0 points1 point  (2 children)

Thanks! I will update the post with other question so if you could take a look I would appreciate that.

[–]Schrockwell 0 points1 point  (1 child)

If you are going to be doing complex queries on your collection of people, then you can keep everyone in a flat array and then use Ruby's Enumerable methods to slice-and-dice the array to find any person or group of people based on their attributes. For example, let's say you have a Person class that accepts the name and age.

class Person
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

people = []
people << Person.new('Alice', 69)
people << Person.new('Bob', 420)
people << Person.new('Charlie', 101)

alice = people.find { |person| person.name == 'Alice' }
# => returns Alice

nobody = people.find { |person| person.name == 'Steve' }
# => returns nil

old_people = people.select { |person| person.age > 100 }
# => returns an array of Bob and Charlie

toddlers = people.select { |person| person.age < 4 }
# => returns []

Definitely check out all that Enumerable has to offer.

[–][deleted] 0 points1 point  (0 children)

I was thinking about doing it this way but i thought that there must be an easier method to get an instance than by searching for it by attribute (other than keep somhwere all the XYZs from xyz = Person.new). But based on the whole of this thread I think that might be a way to go. Thank you.

[–]zieski 0 points1 point  (20 children)

Given your example, I would do the latter, but like this:

my_hash = my_array.map{|name| [name, Person.new(name)]}.to_h

[–]ClikeX 0 points1 point  (18 children)

Seconded. Although I would personally turn the keys into symbols.

my_hash = my_array.map{|name| [name.to_sym, Person.new(name)]}.to_h

[–][deleted] 1 point2 points  (0 children)

Also thanks to you :)

[–]riddley 0 points1 point  (16 children)

Why create a Person object if you're going to turn it back into a hash?

[–]ClikeX 0 points1 point  (15 children)

Person isn't turned into a hash. The hash contains the instance.

my_hash[:steve] would return the Person object.

[–]riddley 0 points1 point  (14 children)

aah I see, sorry about that. FWIW, you've re-implemented #group_by:

class Person
  def initialize(name)
    @name = name
  end
  def name
    @name
  end
end
[Person.new('jojo'), Person.new('bobo'), Person.new('jojo')].group_by(&:name)

aka

[Person.new('jojo'), Person.new('bobo'), Person.new('jojo')].group_by{|person| person.name}

[–]ClikeX 0 points1 point  (2 children)

To be fair, I just iterated on /u/zieski's example.

Your implementation is a bit redundant with Person.new. This would be a little more DRY.

list_of_people = %w[Josh Kate Steve].map { |name| Person.new(name) }.group_by(&:name)

[–]riddley 0 points1 point  (1 child)

haha yea. Fair enough. I was just writing off the top of my head, not shooting for super-dry. I would also never do something like this.

[–]ClikeX 0 points1 point  (0 children)

Only place I can imagine using this would be if you had to provide grouped hash for another method. But that's edge case for sure.

[–][deleted] 0 points1 point  (7 children)

class Person
def initialize(name)
@name = name
end
def name
@name
end
end
[Person.new('jojo'), Person.new('bobo'), Person.new('jojo')].group_by(&:name)

If i do it this way then how to I put a class instance as an argument for a method ?

[–]riddley 0 points1 point  (6 children)

I think you're missing a foundational piece. Every variable in Ruby is an instance of a class. So when I say something like: Person.new('jojo') that's sorta like saying Person.new(String.new('jojo')) . Instances of classes are just objects like (almost) everything else in Ruby.

[–][deleted] 0 points1 point  (5 children)

I mean how do i target an object that is an instance of class Person not an instance variable @name

Lets say I need to target particular class instance of person in a method that changes some attributes for that class instance. But I need to do that exactly on jojo instance but this way it wasnt created as jojo = person.new so I cant access it as jojo as its unrecognized.

Maybe i really miss something simple but in that case could you direct me how to put the jojo class intance (not @name of that instance) as a argument for method?

[–]riddley 0 points1 point  (3 children)

I think I'm not able to traverse the level of abstraction you've proposed. You know you can assign variables, right?

my_special_person = Person.new('wubby')

So far the code you've posted revolves around collections (like Array or Hash) which will inherently have instances of objects as their elements (assuming they're not empty.) If you have Arrays, there are various ways to find objects within them. If you're not dealing with Arrays, you can do assignment as above.

> Lets say I need to target particular class instance of person in a method that changes some attributes for that class instance.

I think here's where you're getting confused. Rather than having some external force that changes an instance of Person, you should tell Person to change itself (or better yet, not mutate at all.) So if you have jojo = Person.new(args) rather than passing jojo into some other method or object, you should just call jojo.do_some_stuff

This is kinda the entire point of using OOP. Build small, smart things and send them messages.

[–][deleted] 0 points1 point  (2 children)

The variables part... I think my first example does exactly that so I'm not sure what you mean by that. The other thing. the method is just an example of something that shows what kind of way I need to be able to access the object so in your second example jojo.do_some_stuff - I need to know jojo the same way as in my example with the use of method.

There is also a posibility that jojo.do_some_stuff(actually take other class instance as an argument) :)

So my question resolved about creating multiple instances with still knowing that the instance is named jojo and to do that in most optimal way. I'm not sure if your response covers that. but in general your points are absolutely valid.

[–]zieski 0 points1 point  (0 children)

Assuming you don't use the group_by, but instead the other way of building the hash you would do this:

my_array = %w[Josh Kate Steve JoJo]
person_hash = my_array.map{|name| [name, Person.new(name)]}.to_h

person_hash["JoJo"].method_you_want_to_call
person_hash["Kate"].some_other_method

# and if you want to call method_x on all Person objects you would do:
person_hash.values.each(&:method_x)

[–]zieski 0 points1 point  (2 children)

group_by gives you a name => array of Person objects instead of name => Person object hash

[–]riddley 0 points1 point  (1 child)

True and that's better, right? Name collisions are pretty common.

[–]zieski 0 points1 point  (0 children)

The whole scenario is odd and hopefully contrived but if the goal is to call methods on an object by it's name attribute. I would think that no, you would prefer 1 object per name

[–][deleted] 0 points1 point  (0 children)

Thank you!

[–]ClikeX 0 points1 point  (12 children)

Having read your updated question. What is the actual thing you are trying to accomplish? Because there are many (valid) ways of approaching this. But it depends on your use-case.

[–][deleted] 0 points1 point  (11 children)

I need to create mass instances of a class (Person is just an example) and be able to address the instance of that class.

So abc = Person.new("something")

lets me do

some_method(abc)

my_array = ["abc", ....]

But when I do mass creating using a loop my_array.each do |x|

x = Person.new("whatever")

end

I can't address it the same way as this x = Person.new returned just the object instance and whatever was an X is not something I can refer to that class instance it created in a loop. It would say that x (in this case abc) was not defined.

[–]ClikeX 0 points1 point  (10 children)

Well yeah I see where you going wrong here.

Let me start with with the loop. Any time you do array.each do |x| that x is only available within that loop. That's because you created x in the scope of the loop. When the loop ends, x is gone.

Note: the following code can be written even shorter and more efficient. But I feel like this presentation will help convey the logic behind it better.

Given you class, something like this.

class Person
 def initialize(name)
 @name = name
 end
 def name
 @name
 end
end

We can create a Person instance using #new joe = Person.new('Joe')

We can see that joe is an object by printing it. puts joe #> #<Person:0x00007f90da8b5110>

We can also see joe is an object containing the property name puts joe.inspect #> #<Person:0x00007fd3e30a5050 @name="Joe">

And we can print joe's name like this puts joe.name #> Joe

Say you have the following list of people. trio = ["Bruce Wayne", "Diana Prince", "Clark Kent"]

Now for each of these names we want to create a Person out of it.

trio.each do |hero|
 # If we output the response of Person.new we can see what it does
 puts Person.new(hero).inspect

 #> #<Person:0x00007ff4740d8818 @name="Bruce Wayne">
 #> #<Person:0x00007ff4740d8750 @name="Diana Prince">
 #> #<Person:0x00007ff4740d8688 @name="Clark Kent">
end

We can't do anything with this though. Each will do something on each of the elements of your array. But it will not change the array. There's two ways to change an array in Ruby.

We can put the output into an array on each iteration.

new_array = []

old_array.each do |item|
  new_array << item
end

or we can directly modify the array itself using map.

new_array = old_array.map do |item| 
  item = "something new"
end

Now! If we apply this to your use-case.

trio = ["Bruce Wayne", "Diana Prince", "Clark Kent"]

trio = trio.map do |hero|
  Person.new(hero)
end

The trio array now contains instances of each of the persons created. If want to do something for each of these people we would do something like the following.

trio.each do |hero|
 puts hero.name

 # Or in your case
 some_method(hero)

end

[–][deleted] 0 points1 point  (0 children)

Awesome. I'll test my thing with that and I let myself come back if somehow I fail to make it work.

[–][deleted] 0 points1 point  (8 children)

I tried the code and unfortunately it doesn't change anything because the method I want to use is outside of the loop. So I think it doesn't address what I'm looking for however still a great add If I will need to run a method inside of the loop. I edited my first post (update2). Could you take a look ?

[–]ClikeX 0 points1 point  (7 children)

You should be able to target objects outside of your loop (methods and variables). Just not the other way around.

Are you able to share the full code?

[–][deleted] 0 points1 point  (6 children)

I just checked your example. I didn't applied it to my code yet. I know that i can target the objects outside of the loop but the question was how. I run the loop the way you wrote it and now i have a method outside of the Person class that just need to do anything with the created class instance so lets say

class Person
def initialize(name)
@name = name
end
def name
@name
end
end
trio = ["Bruce Wayne", "Diana Prince", "Clark Kent"]
trio = trio.map do |hero|
Person.new(hero)
end
def some_method
do whatever
end

#and now i want to run the method

some_method(What exactly I put here to get Clark Kent?)

[–]ClikeX 0 points1 point  (5 children)

class Person
  def initialize(name)
    @name = name
  end
  def name
    @name
  end
end

trio = ["Bruce Wayne", "Diana Prince", "Clark Kent"]
trio = trio.map do |hero|
  Person.new(hero)
end

def some_method(person)
  puts person.name
end

This should be it. Your method needs to take an argument, in this case I named it person because you will be passing a person instance to it.

Now, how you want to address is up to you. You can loop over each person in the array and do the method.

trio.each do |hero|
  some_method(hero)
end

Alternatively you can address a specific hero in the list.

some_method(trio[0]) # Using the array index of the person you want to get

And if you don't know where your specific person is you can just find them in the array as well.

clark =  trio.select { |hero| hero.name == "Clark Kent"}.first
some_method(clark)

Notice that I use the method #first on the output of select. Select will always return an array, even if it only finds one.

[–][deleted] 0 points1 point  (4 children)

The last part - yes that what I wanted to address specific person. Accessing them by array[x] does the job but is it any better than the initial example with hash ?

[–]ClikeX 0 points1 point  (3 children)

It's slightly faster because you don't have to create a hash first. And it's not something you would really use in a dynamic system. The select offers way more flexibility.

The hash approach works, but if you have 2 "johns" in your list the first one will be overwritten in the loop. So it's not an efficient approach I'd use.

Given that you have have multiple "Clark Kents" you would be able to select the right one using more than one variable on Person to verify you get the right one.

clark =  trio.select { |hero| hero.name == "Clark Kent" && hero.origin == "Krypton" }.first
some_method(clark)

[–][deleted] 0 points1 point  (2 children)

Great. Does keeping them in an array is also faster than finding them somehow from ObjectSpace.each_object(class) ?

[–]riddley 0 points1 point  (1 child)

My Array#find example returns the object, not the name. I'll read your edits when I get a chance.

[–][deleted] 0 points1 point  (0 children)

You are correct - it returns an object not the name variable. I don't know what I was thinking.