Add a callback function to a Ruby array to do something when an element is added
The following code only invokes the size_changed
hook when the array size has changed and it is passed the new size of the array:
a = []
class << a
Array.instance_methods(false).each do |meth|
old = instance_method(meth)
define_method(meth) do |*args, &block|
old_size = size
old.bind(self).call(*args, &block)
size_changed(size) if old_size != size
end if meth != :size
end
end
def a.size_changed(a)
puts "size change to: #{a}"
end
a.push(:a) #=> size change to 1
a.push(:b) #=> size change to 2
a.length
a.sort!
a.delete(:a) #=> size change to 1
Add key value pair to all objects in array
The map() function is a best choice for this case
tl;dr - Do this:
const newArr = [
{name: 'eve'},
{name: 'john'},
{name: 'jane'}
].map(v => ({...v, isActive: true}))
The map() function won't modify the initial array, but creates a new one.
This is also a good practice to keep initial array unmodified.
Alternatives:
const initialArr = [
{name: 'eve'},
{name: 'john'},
{name: 'jane'}
]
const newArr1 = initialArr.map(v => ({...v, isActive: true}))
const newArr2 = initialArr.map(v => Object.assign(v, {isActive: true}))
// Results of newArr1 and newArr2 are the same
Add a key value pair conditionally
const arr = [{value: 1}, {value: 1}, {value: 2}]
const newArr1 = arr.map(v => ({...v, isActive: v.value > 1}))
What if I don't want to add new field at all if the condition is false?
const arr = [{value: 1}, {value: 1}, {value: 2}]
const newArr = arr.map(v => {
return v.value > 1 ? {...v, isActive: true} : v
})
Adding WITH modification of the initial array
const initialArr = [{a: 1}, {b: 2}]
initialArr.forEach(v => {v.isActive = true;});
This is probably not a best idea, but in a real life sometimes it's the only way.
Questions
- Should I use a spread operator(
...
), orObject.assign
and what's the difference?
Personally I prefer to use spread operator, because I think it uses much wider in modern web community (especially react's developers love it). But you can check the difference yourself: link(a bit opinionated and old, but still)
- Can I use
function
keyword instead of=>
?
Sure you can. The fat arrow (=>
) functions play a bit different with this
, but it's not so important for this particular case. But fat arrows function shorter and sometimes plays better as a callbacks. Therefore the usage of fat arrow functions is more modern approach.
- What Actually happens inside map function:
.map(v => ({...v, isActive: true})
?
Map function iterates by array's elements and apply callback function for each of them. That callback function should return something that will become an element of a new array.
We tell to the .map()
function following: take current value(v
which is an object), take all key-value pairs away from v
andput it inside a new object({...v}
), but also add property isActive
and set it to true ({...v, isActive: true}
) and then return the result. Btw, if original object contains isActive
filed it will be overwritten. Object.assign
works in a similar way.
- Can I add more then one field at a time
Yes.
[{value: 1}, {value: 1}, {value: 2}].map(v => ({...v, isActive: true, howAreYou: 'good'}))
- What I should not do inside
.map()
method
You shouldn't do any side effects[link 1, link 2], but apparently you can.
Also be noticed that map()
iterates over each element of the array and apply function for each of them. So if you do some heavy stuff inside, you might be slow. This (a bit hacky) solution might be more productive in some cases (but I don't think you should apply it more then once in a lifetime).
- Can I extract map's callback to a separate function?
Sure you can.
const arr = [{value: 1}, {value: 1}, {value: 2}]
const newArr = arr.map(addIsActive)
function addIsActive(v) {
return {...v, isActive: true}
}
- What's wrong with old good for loop?
Nothing is wrong with for
, you can still use it, it's just an old-school approach which is more verbose, less safe and mutate the initial array. But you can try:
const arr = [{a: 1}, {b: 2}]
for (let i = 0; i < arr.length; i++) {
arr[i].isActive = true
}
- What also i should learn
It would be smart to learn well following methods map(), filter(), reduce(), forEach(), and find(). These methods can solve 80% of what you usually want to do with arrays.
In .filter() how do I make a callback function call another function?
Yes, you can certainly call any other functions inside a filter
callback.
The only thing you're missing is that the callback needs to return a value. Your callback doesn't have a return
statement. It calls the filterCreatures()
function, but then it discards the value returned by that function and by default returns undefined
.
Compare with the original version of your code:
creatures.filter(filterCreatures);
In this code, filter()
calls the filterCreatures
function directly and then uses its return value to decide whether to include the element. You don't see this explicitly in the code, but filter()
is looking for that return value.
Here's an exercise to help clarify this. Take your broken code and move the inline filter callback out to a named function. We'll call it filterCreaturesWrapper
. So instead of this:
filterCreatures = function(v) {
return v.species === 'Zombie';
}
zombieCreatures = creatures.filter(function(v) {
filterCreatures(v);
});
we have this:
filterCreatures = function(v) {
return v.species === 'Zombie';
}
filterCreaturesWrapper = function(v) {
filterCreatures(v);
}
zombieCreatures = creatures.filter(filterCreaturesWrapper);
If you study that, you'll see it's exactly the same code as before, we just moved it around a little. filterCreaturesWrapper
is the same function that was inline inside the .filter()
call. And now the problem should jump out at us: filterCreatures
has a return
statement and filterCreaturesWrapper
doesn't.
Going back to the original broken code, it's as if we had written:
zombieCreatures = creatures.filter(function(v) {
filterCreatures(v);
return undefined; // .filter treats this as 'false'
});
So just add a return
in your callback and it works:
'use strict';
var creatures = [], zombieCreatures = [];
var filterCreatures;
creatures = [ {species: 'Zombie', hitPoints: 90}, {species: 'Orc', hitPoints: 40}, {species: 'Skeleton', hitPoints: 15}, {species: 'Zombie', hitPoints: 85}]; filterCreatures = function(v) { return v.species === 'Zombie';} zombieCreatures = creatures.filter(function(v) { return filterCreatures(v); // <-- added "return"});
console.log(zombieCreatures);
Update if exists or add new element to array of objects - elegant way in javascript + lodash
You can use an object instead of an array:
var hash = {
'1': {uid: 1, name: "bla", description: "cucu"},
'2': {uid: 2, name: "smth else", description: "cucarecu"}
};
The keys are the uids. Now your function addOrReplace
is simple like this:
function addOrReplace(hash, object) {
hash[object.uid] = object;
}
UPDATE
It's also possible to use an object as an index in addition to the array.
This way you've got fast lookups and also a working array:
var arr = [],
arrIndex = {};
addOrReplace({uid: 1, name: "bla", description: "cucu"});
addOrReplace({uid: 2, name: "smth else", description: "cucarecu"});
addOrReplace({uid: 1, name: "bli", description: "cici"});
function addOrReplace(object) {
var index = arrIndex[object.uid];
if(index === undefined) {
index = arr.length;
arrIndex[object.uid] = index;
}
arr[index] = object;
}
Take a look at the jsfiddle-demo (an object-oriented solution you'll find here)
Is there a ruby equivalent to javascript's Array.prototype.every method?
I would use all?.
my_array.all? { |element| element >= 10 }
You pass in a block
of code which is functionally equivalent to passing your function in JavaScript.
Arrays: conditionally mapping elements
There is no such native function because you can easily implement it yourself:
const mapWhen = (p, f) => xs => xs.map(x => p(x) ? f(x) : x);
const users = [ {id: 1, removed: true}, {id: 2, removed: true}, {id: 3, removed: true}];
console.log( mapWhen( ({id}) => id === 2, user => Object.assign({}, user, {removed: false}) ) (users));
Magic First and Last Indicator in a Loop in Ruby/Rails?
You could grab the first and last elements and process them differently, if you like.
first = array.shift
last = array.pop
process_first_one
array.each { |x| process_middle_bits }
process_last_one
Related Topics
Why Doesn't Module.Method_Defined(:Method) Work Correctly
How to Install a Gem Globally Without Sudo Using Rbenv
Pretty Print to a File in Ruby
In Ruby What Is the Meaning of Colon After Identifier in a Hash
Using Activerecord Interface for Models Backed by External API in Ruby on Rails
Sinatra and Session Variables Which Are Not Being Set
Write a Migration with Reference to a Model Twice
Rvm VS Native Installation of Ruby
Comma Separated Array with a Text_Field in Rails
Is There a Function Like String#Scan, But Returning Array of Matchdatas
Examples of 'Things' That Are Not Objects in Ruby