Question: filter null values in map, returning keys
Hi,
Been playing around with the library, but I can't seem to get my head around the following (kind of new to fp):
Map<String, String> = new Map<String, String>{
'Name' => 'Account',
'Description' => null,
'Website' => null
}
Is it possible to only retrieve the keys where the value is null?
It doesn't seem like any of the functions is providing this kind of functionality, so I ended up with this custom FilterByValueFunc which you can pass to the .filter(Func prep) method.
public class FilterByValueFunc extends Func {
public FilterByValueFunc() {
super(3);
}
public override Object exec(Object a, Object b, Object c) {
Object value = (Object)a;
// Object key = (Object)b;
Object testValue = (Object)c;
return value == testValue;
}
}
Or is there a simpler way to achieve this? :)
Thanks for your suggestion. There is indeed a simpler way to achieve this already in R.apex. Please check below:
Map<String, String> m = new Map<String, String>{
'Name' => 'Account',
'Description' => null,
'Website' => null
};
R.of(m)
.filter(R.isNotNull)
.keys()
.debug();
Or you can choose a more functional way like this:
Map<String, String> m = new Map<String, String>{
'Name' => 'Account',
'Description' => null,
'Website' => null
};
Func f = (Func)R.pipe.run(
R.filter.apply(R.isNotNull),
R.keys
);
System.debug(f.run(m));
The thing here is that many functions in R.apex is polymorphic in that they accept multiple types of arguments. Take filter for example ,
filter can take List<Object>, Set<String>, Map<String, Object> and String as arguments.
When filtering on a map, the expecting predicate function in filter is like this:
pred:: (value, key) => Boolean
Aha, that's indeed a cleaner approach! Thanks!
Small question, is there a specific reason why the order of pred arguments is
pred:: (value, key)
and not
pred:: (key, value)
Or is this a standard in all other libraries like Lodash etc.? (I was kind of confused with the "logical inversion" of the args when writing my own predicate function)
Well, that is actually kind of pain in the ass. This difference of usage is commonly seen between lodash and ramdajs.
You can refer to this popular speech to get deeper understanding on this: http://functionaltalks.org/2013/05/27/brian-lonsdorf-hey-underscore-youre-doing-it-wrong/
Simply put, if we want to 'compose' functions in a more functional way, put the main data at last.
Func filterNotNull = R.filter.apply(R.isNotNull);
This composed function waits for the data to be filtered. If we do it in the other way, it would be like this:
Func filterNotNull = R.otherFilter.apply(R.placeholder, R.isNotNull);
Here the otherFilter takes (list, pred), so we need to use a placeholder to keep room for the data. And obviously that takes more code to compose.
As to the predicate function, normally it takes one argument. So the first argument is supposed to be the main data. And in your example, value is more often used in a predicate than key.
@liumiaowilson Thank you for the explanation! Really appreciate it!
Another thing I'm currently trying to do is; filter the fields that do not have a certain value (or a list of possible values).
So I could pass a Map like this to the filter function:
Map<String, List<String>> m = new Map<String, List<String>>{
'Name' => new List<String>{ 'TestAccount', 'TestAccount2' }
};
So if the field value for Name in eg an Account is not equal these specific values, the field should be retained.
Is this still possible with the default filter functionality?
It would be difficult to achieve it with the default filter functionality, I might say.
Well, even if it is possible, I would still suggest that you create a custom function to implement this logic.
It would be a shame if you go too far with functional composition in R.apex and lose the code readability. Do not do functional just for the sake of being functional programming. Just strike a balance between functional style and code readability.
If I were you, I would create a custom function in this case. If it is kind of commonly used, I would put it in a class to reference it like this:
public class CustomFuncs {
public static final Func filter = new CustomFilter();
}
So I can reuse it wherever I want without creating another new instance.
Object result = CustomFuncs.filter.run(m, data);
Thanks!
I came up with this:
private class PropInFunc extends Func {
public PropInFunc() {
super(3);
}
public override Object exec(Object a, Object b, Object c) {
Object value = a;
Object key = b;
Map<String, Object> mMap;
if((Boolean)R.isMapLike.run(c)) {
mMap = (Map<String, Object>)c;
Object mValue = mMap.get((String)key);
if(mValue != null && (Boolean)R.isListLike.run(mValue)) {
return R.of(mValue).contains(value);
}
}
return false;
}
}
and then call it like:
R.filter.apply(new PropInFunc().apply(R.placeholder, R.placeholder, m)),
with m the map with the allowed field values.
I could add it in an MR if you think it's reusable in R.apex?
Anyway, thanks for the help!
@glenncoppens Very good implementation.
If you put the map as the first argument, you can call it like:
R.filter.apply(new PropInFunc().apply(m))
That would be simpler, right?
When you use R.of(mValue).contains(value), it actually returns an instance of R. To get the value out of R, you can try:
R.of(mValue).contains(value).toBoolean()
isMapLike not only checks if the argument is an instance of Map<String, Object>, but checks if it is anything that can be converted to Map<String, Object>, including SObject and R instances.
So you can improve it to:
if((Boolean)R.isMapLike.run(c)) {
mMap = (Map<String, Object>)R.toMap.run(c);
}
toMap is a corresponding function to isMapLike, which converts any map-like objects into maps. So is true with isListLike/toList, isSetLike/toSet, and isStringLike/toString.
Sure you can add it in a PR, and do make sure to include tests and examples of how people are supposed to use this new functions. Good job.