懒惰但坚持不懈地评估java 8 lambda

时间:2017-06-12 13:50:23

标签: lambda java-8

我目前正致力于在java中创建自己的持久化数组,它使用二叉搜索树来存储值集合。

我想添加一个map方法,该方法以dotted为参数生成一个新数组。除非要求特定值,否则我不想评估函数。这很简单,因为lambdas是惰性评估的。但是,我也只想要一个函数只被评估一次,即使多次请求结果。

我可以创建一个存储供应商的节点,并在评估时更新结果:

Function

...其中class Node<T> { private T value; private Supplier<T> supplier; public T get() { if (null != value) return value; value = supplier.get(); return value; } } 派生自supplier应用于较旧版本的持久数组中的值。

但是,这不再是一种功能性方法,并且有可能在多线程系统中导致错误*。在供应商返回空值**的情况下,它也不会产生优势。

另一种方法是在get get上返回Function的实例:

Node

我喜欢这种保持功能的方法;但是在上传树的所有父节点中只需要一个get调用就需要很多开销。并且在使用应用程序的代码中需要麻烦的拆箱。

有没有人有他们能想到的另一种方法让我的工作方式与我提出的方式相同?感谢。

*这可以使用锁定机制解决,但增加了一层我希望避免的复杂性。

**我已考虑过class Node<T> { private final Optional<T> value; private final Supplier<T> supplier; Node(Supplier<T> supplier, T value) { this.supplier = supplier; this.value = Optional.ofNullable(value); } public Tuple<Node<T>, T> get() { if (null != value) return new Tuple<>(this, value.orElse(null)); T result = supplier.get(); Node<T> newNode = new Node<>(null, result); return new Tuple<>(newNode, result); } } value,其中Optional<T>表示尚未评估,null已经过评估并返回null。但是,这可以解决我的问题,而不是解决它。

对于不熟悉持久化数组的人来说,它是一种数据结构,只要执行更新,它就会创建自己的新实例。这使它纯粹是不可变的。使用二叉树(或更常见的32位树)方法允许更新减少重复数据的速度和内存。

编辑:

可以在github找到该集合的代码。有关使用说明,您可以查看测试文件夹。

3 个答案:

答案 0 :(得分:3)

免责声明:此答案并未直接回答此问题,因为它在Supplier课程中并未直接使用OptionalNode。相反,提出了一种通用的函数式编程技术,可能有助于解决问题。

如果问题只是针对每个输入值仅评估一次函数,那么您就不应该更改树/数组/节点。相反,memoize函数,这是一种纯函数式方法:

  

在计算,memoization或memoisation是一种优化技术,主要用于通过存储昂贵的函数调用的结果来加速计算机程序,并在再次出现相同的输入时返回缓存的结果

这是一种方法,灵感来自Pierre-Yves Saumont编写的this excellent article(请查看它以便对记忆化进行深入介绍):

public static <T, U> Function<T, U> memoize(Function<T, U> function) {
    Map<T, U> cache = new ConcurrentHashMap<>();
    return input -> cache.computeIfAbsent(input, function::apply);
}

假设您有一个执行时间很长的方法。然后,您可以这样使用memoize方法:

// This method takes quite long to execute
Integer longCalculation(Integer x) {
    try {
        Thread.sleep(1_000);
    } catch (InterruptedException ignored) {
    }
    return x * 2;
}

// Our function is a method reference to the method above
Function<Integer, Integer> function = this::longCalculation;

// Now we memoize the function declared above
Function<Integer, Integer> memoized = memoize(function);

现在,如果你打电话:

int result1 = function.apply(1);
int result2 = function.apply(2);
int result3 = function.apply(3);
int result4 = function.apply(2);
int result5 = function.apply(1);

你会注意到这五个电话总共需要5秒钟(每次通话需要1秒钟)。

但是,如果您使用具有相同输入值memoized的{​​{1}}函数:

1 2 3 2 1

你会注意到现在这五个电话总共花了大约3秒钟。这是因为最后两个结果会立即从缓存中返回。

所以,回到你的结构......在你的int memoizedResult1 = memoized.apply(1); int memoizedResult2 = memoized.apply(2); int memoizedResult3 = memoized.apply(3); int memoizedResult4 = memoized.apply(2); // <-- returned from cache int memoizedResult5 = memoized.apply(1); // <-- returned from cache 方法中,你可以只记住给定的函数并使用返回的memoized函数。在内部,这将缓存map中函数的返回值。

由于ConcurrentHashMap方法在内部使用memoize,因此您不必担心并发问题。

注意:这只是一个开始......我在考虑两个可能的改进。一种方法是限制缓存的大小,这样如果给定函数的域太大,它就不会占用整个内存。另一个改进是,只有在事先没有被记忆的情况下才能记住给定的功能。但这些细节留给读者练习......;)

答案 1 :(得分:2)

  

我也只想要一次只评估一次函数,即使多次请求结果。

这个怎么样?

class Node<T> {
    private Supplier<T> supplier;

    Node(T value, Supplier<T> supplier) {
        this.supplier = sync(lazy(value, supplier));
    }

    public T get() {
        return supplier.get();
    }
}

sync方法仅同步Supplier一次,当调用target时,lock将被禁用以用于下一次连续请求:

static <T> Supplier<T> sync(Supplier<T> target) {
    return sync(new ReentrantLock(), target);
}

static <T> Supplier<T> sync(ReentrantLock lock, Supplier<T> target) {
    //     v--- synchronizing for multi-threads once
    return lazy(() -> {
        // the interesting thing is that when more than one threads come in here
        // but target.delegate is only changed once
        lock.lock();
        try {  
            return target.get();
        } finally {
            lock.unlock();
        }
    });
}

lazy方法只调用一次Supplier,如下所示:

static <T> Supplier<T> lazy(T value, Supplier<T> defaults) {
    return lazy(() -> value != null ? value : defaults.get());
}

static <T> Supplier<T> lazy(Supplier<T> target) {
    return new Supplier<T>() {
        private volatile Supplier<T> delegate = () -> {
            T it = target.get();
            //v--- return the evaluated value in turn
            delegate = () -> it;
            return it;
        };

        @Override
        public T get() {
            return delegate.get();
        }
    };
}

IF 您对最终代码的制作方式感兴趣,我已将代码提交到github,您只需复制并使用它即可。你可以发现我已将lazy方法重命名为once,这更具表现力。

答案 2 :(得分:1)

Disclamer:这不是您所要询问的直接解决方案,但是此答案提供了一个在任何地方都没有提及并且值得尝试的解决方案。

您可以使用Google-guava库中的Suppliers#memoize

这将使您免于Supplier返回null时面临的问题,而且它也是线程安全的。

还请注意,Supplier的{​​{1}}方法返回扩展了memoize的{​​{1}},因此您可以使用它将其分配给com.google.base.Supplier,以便您不要强迫您的客户(将使用您的库)依靠Guava库。