Java企业教程系列
用monad替代嵌套回调
很少有人会去质疑异步计算是酷且有用,整个反应式编程reactive programming 都是可能基于异步计算的,核心思想是让数据和事件流过你的系统,适当的时候对结果进行处理。
看看下面这个异步功能案例:
$("#book").fadeIn("slow", function() { console.log("hurray"); }); |
这段JS代码获得book元素然后逐渐黯淡,当黯淡完成后一个回调函数将被调用, "hurray" 字符串将出现在控制台. 这一切在小系统时工作得很好,但是一旦你的系统发展扩大,你就会发现很多嵌套的回调函数。
回调函数是处理异步或延迟动作的通用方法,但是它们不是最好的选项,回调的问题是他们可能永远嵌套链接下去,回调的回调的回调,知道你发现你自己陷入一个糟糕地步,每次代码的变动都会变得非常痛苦和缓慢。
是否有其他方法来组织异步代码呢?事实上是有的,你所需要做的只是改变一下角度,如果你有一个类型代表异步计算的结果,你的代码就可以像传递值或变量一样传递它,好处是平整,流畅性和可读性。
现在红已经有了monadic类型的Promise,这将使我们的异步代码变得更加精彩,下面谈谈其中原理,源码可见: github repo.
首先,manad单子是函数式编程行家的喜爱,并有成千上万的教程和文章描述的概念。
一个单子monad是一种类型, 这代表一种计算的上下文. 这是什么意思呢?
首先,一个monad并不指定发生了什么,这会在上下文中计算实现的职责,一个monad是指围绕这个计算发生了什么。
Monad有两个属性
1. 它应该围绕什么东西
2. 它接受指令让它如何对围绕的东西做什么
围绕部分在编程语言中容易实现的,也就是获取什么然后返回它,一个构造器或工厂方法会帮助我们实现,假设你有一些Haskell notation知识,这样获取什么然后返回的函数通常如下:
return :: a -> m a |
在Java, 使用构造器来实现, Monad 类如下.
public class Monad<T> { public Monad(T t) { … } } |
事实上,我们已经成功了一半。下面需要完成另外一个属性:Monad接受指令让它如何对围绕的东西做什么。
这样我们就要增加一些接受指令的行为方法,对我们有帮助的是绑定bind函数,能够获取一个动作的一些东西然后返回不同的monad,这个monad包装了这个可以执行的动作,在Haskell.
(>>=) :: m a -> (a -> m b) -> m b |
Bind拿到一个monad通过 a, (m a) 是来自a的一个函数,然后返回一个不同的monad,在Java中实现如下:
public class Monad<T> { public abstract <V> Monad<V> bind(Function<T, Monad<V>> f); } |
这就完成了我们monad通常定义,下面我们来看看如何具体实现。
等等,是不是在Java 中可以有monad呢?
首先,monad有许多不同类型,一个monad更像一个java接口,有一个List monad, 一个Maybe monad, 一个IO monad (语言是非常纯净,不允许自己有正常的IO), 等等。我们可以使用Java8创建指定的monad,因为monad需要lambda支持,Java 8引入了lambda和第一个公民的方法。
现在我们可以有一个monadic类型来代表异步计算,我们所需要的是一个Promise类,它代表异步计算的结果,或者成功或失败。Promise类是在主计算完成时接受回调然后执行,现在这些Promise类可在Akka's Future, Play's promise 等中实现.
这里使用来自 Play Framework中的Promise实例,如下:
public static <V> Promise<V> pure(final V v) { Promise<V> p = new Promise<>(); p.invoke(v); return p; } |
返回Promise 已经被包装,准备提供我们一个计算结果。Bind实现看上去如下:它获取一个函数然后作为一个回调函数加入实例中,回调函数将返回计算结果,然后对其使用指定的函数,无论applicationResult函数返回什么或抛出错误都被用于履行redeem结果Promise:
public <R> Promise<R> bind(final Function<V, Promise<R>> function) { Promise<R> result = new Promise<>();
this.onRedeem(callback -> { try { V v = callback.get(); Promise<R> applicationResult = function.apply(v); applicationResult.onRedeem(applicationCallback -> { try { R r = applicationCallback.get(); result.invoke(r); } catch (Throwable e) { result.invokeWithException(e); } }); } catch (Throwable e) { result.invokeWithException(e); } }); return result; } |
应用指定的函数并从中获得结果被编码进入try-catch代码块, 异常作为Promise一种可能结果被传递到其结果实例中(返回的result是有两种情况正常或失败)。
现在用这两个可以容易串起一个异步计算链条,从而避免进行回调深入循环:.
public static void example1() throws ExecutionException, InterruptedException { Promise<String> promise = Async.submit(() -> { String helloWorld = "hello world"; long n = 500; System.out.println("Sleeping " + n + " ms example1"); Thread.sleep(n); return helloWorld; }); Promise<Integer> promise2 = promise.bind(string -> Promise.pure(Integer.valueOf(string.hashCode()))); System.out.println("Main thread example2"); int hashCode = promise2.get(); System.out.println("HashCode = " + hashCode); } |
这里我们已经实现了基本的monadic类型Promise 用于表示一个异步操作的结果。
Java 8 目前已经有了表示异步计算的monadic类型: CompletableFuture!
其中有几个方法让你绑定一个函数到已经存在的计算结果中,更有甚者,它提供了使用一个函数或使用一个消费者,消费者是一种无返回的函数或普通的Runnable.
- thenApply(Function<? super T,? extends U> fn)
- thenApplyAsync(Function<? super T,? extends U> fn)
- thenAccept(Consumer<? super T> block)
- thenAcceptAsync(Consumer<? super T> block)
- thenRun(Runnable action)
- thenRunAsync(Runnable action)
上面这些结尾以 *Async 的方法将使用ForkJoin 异步执行这个函数,当然你可以定制自己的执行器。