Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: filter null values in map, returning keys #6

Open
glenncoppens opened this issue May 28, 2018 · 9 comments
Open

Question: filter null values in map, returning keys #6

glenncoppens opened this issue May 28, 2018 · 9 comments

Comments

@glenncoppens
Copy link
Contributor

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?

@glenncoppens
Copy link
Contributor Author

glenncoppens commented May 28, 2018

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? :)

@liumiaowilson
Copy link
Contributor

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

@glenncoppens
Copy link
Contributor Author

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)

@liumiaowilson
Copy link
Contributor

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.

@liumiaowilson
Copy link
Contributor

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.

@glenncoppens
Copy link
Contributor Author

glenncoppens commented May 29, 2018

@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?

@liumiaowilson
Copy link
Contributor

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);

@glenncoppens
Copy link
Contributor Author

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!

@liumiaowilson
Copy link
Contributor

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants