0

Sorry, I don't think the title I wrote is correct and explains well what I'm asking, but at the moment I have not found better

I'm trying to change my approach from imperative to functional-programming in Java, sometime is easy but in other case no much... But I'm not giving up :)

I have a map of values that should be decreased of an amount calculated by a function, and for each input the are one or more functions that could be applied, what has been decreased by the previous function is "visible" to the next function, so something like that

    Map<String, Double> input = new HashMap<>() {{
        put("IN1", 100.0);
        put("IN2", 10.0);
    }};

    Map<String, List<Function<Double, Double>>> rules = Map.of(
            "IN1", List.of(value -> value / 10.0, value -> 80.0, value -> 10.0),
            "IN2", List.of(value -> 2.0),
            "IN3", List.of(value -> value / 5.0));

In imperative "style", I solve this using a function like that

    for (Map.Entry<String, List<Function<Double, Double>>> entry : rules.entrySet()) {
        String inName = entry.getKey();
        Double inValue = input.get(inName);
        if (inValue==null) continue;
        for (Function<Double, Double> function : entry.getValue()) {
            inValue -= function.apply(inValue);
            if (inValue <= 0F) break;
        }
        input.put(inName, inValue);
    }

What should be the right way in functional programming?

Thanks in advance :)


EDIT

The only solution I found is the following

    Map<String, Double> result = 
       input.entrySet().stream()
            .map(e -> calculator(e, rules.get(e.getKey())))
            .collect(Collectors.toMap(Pair::getKey, Pair::getValue));

First step is aggregate all rules by input and then iterate for each input and, using a recursive calculator function, create another map that will contain, for each input, the remaining value after function application

private static Pair<String, Double> calculator(Map.Entry<String, Double> input, List<Function<Double, Double>> entries) {
    double value = input.getValue();
    double result = calculator(value, entries.iterator());
    return Pair.of(input.getKey(), result);
}

private static double calculator(double value, Iterator<Function<Double, Double>> iterator) {
    if (!iterator.hasNext() || value<=0F) return value;
    Function<Double, Double> function = iterator.next();
    double cost = function.apply(value);
    return calculator(value-cost, iterator);
}

calculator is a recursive function, so the immutability rule is respected :)

Let me know if exists another solution!

Thanks in advance

marc0x71
  • 43
  • 6
  • FP environments usually offer "persistent" datastructures that have "modifiers" that don't change the original data but return a modified copy. For a Map, this would mean having `newMap = map.updated(key, newValue)`. This can be horribly inefficient, though, if you don't have specialized data structures that support it. Copying a complete hashmap for example wouldn't be great. – Thilo Feb 21 '21 at 12:49
  • Maybe interesting: https://stackoverflow.com/q/8575723/14955 – Thilo Feb 21 '21 at 12:51
  • Thanks Thilo, you are right, the only solution I found at moment is rebuild another map using collectors, but I hope is not the only solution, because in this case I had to update a map repeatedly, and this is not a good solution IMHO :) – marc0x71 Feb 21 '21 at 14:46
  • Why don't You define a second map say `resultMap`. If the key is in the `resultMap` -> calculate, if not take the key from the `inputMap` -> calculate. In either case the result goes to the `resultMap`. – Kaplan Feb 21 '21 at 15:28
  • Thanks Kaplan, more o less, this was my first solution, creating a second map, updated via collectors, but also in this case we are changing an object... Now I'm working to another solution, I'll post the answer in a while – marc0x71 Feb 21 '21 at 15:59
  • Question updated :) – marc0x71 Feb 21 '21 at 16:14
  • @marc0x71, Why do you create a copy of `rules` by re-collecting it into `functions` map? – Nowhere Man Feb 21 '21 at 19:33
  • Just to have a list of function to be applied for every input, this simplify the next step ;) – marc0x71 Feb 21 '21 at 19:36
  • No you are right, is not useful! :D I'll remove it, thanks! – marc0x71 Feb 21 '21 at 19:39

1 Answers1

0

A "brute-force" solution can look as follows:

rules.entrySet().stream()
    .filter(e -> input.containsKey(e.getKey())) // skip keys missing in the input
    .forEach(e -> e.getValue().stream()
        .filter(f -> input.get(e.getKey()) > 0) // check the limit in input
        .forEach(
            f -> input.put(e.getKey(), input.get(e.getKey()) - f.apply(input.get(e.getKey())))
        )
    );

System.out.println(input); // {IN2=8.0, IN1=0.0}

A shorter version is based on Map::computeIfPresent:

rules.entrySet().stream()
    .forEach(e -> e.getValue().stream()
        .forEach(
            f -> input.computeIfPresent( // if key is present in input
                e.getKey(), 
                (k, v) -> v > 0 ? v - f.apply(v) : v) // check the limit
        )
    );

Or filter the rules using containsKey, use takeWhile (Java 9+) and then apply compute to input:

rules.entrySet().stream()
    .filter(e -> input.containsKey(e.getKey()))
    .forEach(e -> e.getValue().stream()
        .takeWhile(f -> input.get(e.getKey()) > 0)
        .forEach(
            f -> input.compute(e.getKey(), (k, v) -> v - f.apply(v))
        )
    );
 

Update
A better solution which does not modify the outside input map but creates a new map instead is based on Collectors.toMap and Stream::reduce (U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner) operations:

Map<String, Double> result = input.entrySet()
        .stream()
        .collect(Collectors.toMap(
            Map.Entry::getKey, 
            e -> rules.getOrDefault(e.getKey(), Collections.emptyList())
                .stream()
                .reduce(
                    e.getValue(), // initial value from input
                    (res, fun) -> res > 0 ? res - fun.apply(res) : res, // accumulate
                    (res1, res2) -> res2 // combine
                )
        ));
System.out.println(result);  // {IN2=8.0, IN1=0.0}
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
  • 2
    Thanks Alex, but in this case your solution modifies the input map, so in this case we are violating the "immutability" rule of functional programming, is it right? – marc0x71 Feb 21 '21 at 14:22
  • Original solution was also modifying the original `input` map, that's the reason I provided such implementation. – Nowhere Man Feb 21 '21 at 17:38
  • Yes it's true but in FP style you shouldn't ;) Take a look at the solution I found – marc0x71 Feb 21 '21 at 17:41
  • @marc0x71, you may check the update, it is implemented in _pure_ FP style without side effect with the help of `reduce` operation. – Nowhere Man Feb 21 '21 at 18:24
  • Thanks Alex, it works and is implemented in pure functional style! I prefer the recursive mode, but is good also you solution :) – marc0x71 Feb 21 '21 at 19:19