【问题标题】:Changing value in a map, the right way改变地图中的价值,正确的方式
【发布时间】:2021-02-21 12:27:28
【问题描述】:

对不起,我认为我写的标题不正确,并且很好地解释了我的要求,但目前我还没有找到更好的标题

我正在尝试将我的方法从 Java 中的命令式编程更改为函数式编程,有时很容易,但在其他情况下并不多……但我不会放弃 :)

我有一个值的映射,应该减少一个函数计算的数量,对于每个输入,都有一个或多个可以应用的函数,前一个函数减少的值对下一个函数,就是这样的

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

在命令式“风格”中,我使用类似的函数解决了这个问题

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

函数式编程的正确方法应该是什么?

提前致谢:)


编辑

我找到的唯一解决方案如下

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

第一步是按输入聚合所有规则,然后对每个输入进行迭代,并使用递归计算器函数创建另一个映射,该映射将为每个输入包含函数应用后的剩余值

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

计算器是一个递归函数,所以遵守不变性规则:)

如果存在其他解决方案,请告诉我!

提前致谢

【问题讨论】:

  • FP 环境通常提供具有“修饰符”的“持久”数据结构,这些“修饰符”不会更改原始数据但返回修改后的副本。对于地图,这意味着拥有newMap = map.updated(key, newValue)。但是,如果您没有支持它的专用数据结构,这可能会非常低效。例如,复制一个完整的 hashmap 并不好。
  • 谢谢 Thilo,你是对的,我目前找到的唯一解决方案是使用收集器重建另一张地图,但我希望这不是唯一的解决方案,因为在这种情况下我不得不反复更新地图,这不是一个好的解决方案恕我直言:)
  • 你为什么不定义第二张地图说resultMap。如果密钥在resultMap -> 计算,如果不是从inputMap 获取密钥-> 计算。无论哪种情况,结果都会发送到resultMap
  • 谢谢卡普兰,或多或少,这是我的第一个解决方案,创建第二张地图,通过收藏家更新,但在这种情况下,我们正在更改一个对象......现在我正在为另一个解决办法,我一会儿贴出答案

标签: java functional-programming


【解决方案1】:

“蛮力”解决方案如下所示:

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}

更短的版本基于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
        )
    );

或者使用containsKey过滤规则,使用takeWhile(Java 9+)然后应用compute输入:

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

更新
不修改外部input 映射而是创建新映射的更好解决方案是基于Collectors.toMapStream::reduce (U identity, BiFunction&lt;U,? super T,U&gt; accumulator, BinaryOperator&lt;U&gt; combiner) 操作:

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}

【讨论】:

  • 谢谢 Alex,但是在这种情况下,您的解决方案修改了输入映射,所以在这种情况下,我们违反了函数式编程的“不变性”规则,对吗?
  • 原来的解决方案也是修改原来的input映射,这就是我提供这种实现的原因。
  • 是的,但在 FP 风格中你不应该这样做;)看看我找到的解决方案
  • @marc0x71,你可以查看更新,它是在reduce操作的帮助下以 FP风格实现的,没有副作用。
  • 感谢 Alex,它可以工作并且以纯函数式风格实现!我更喜欢递归模式,但你的解决方案也很好:)
猜你喜欢
  • 1970-01-01
  • 2013-10-01
  • 2021-03-19
  • 1970-01-01
  • 2014-11-09
  • 2020-01-22
  • 1970-01-01
  • 2020-09-29
  • 1970-01-01
相关资源
最近更新 更多