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 FuturePlay'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.

上面这些结尾以 *Async 的方法将使用ForkJoin  异步执行这个函数,当然你可以定制自己的执行器。

什么是Monad?