Java中的Functor和Monad

​ 随着最近函数式编程(或函数式编程风格)的兴起,monads成为一个广泛讨论的话题。关于他们有很多民间传说:

A monad is a monoid in the category of endofunctors, what’s the problem?

单子是内函子范畴中的一个幺半群,有什么问题吗?

​ — James Iry

The curse of the monad is that once you get the epiphany, once you understand - “oh that’s what it is” - you lose the ability to explain it to anybody.

单子的诅咒是,一旦你获得了顿悟,一旦你明白了——“哦,原来如此”——你就失去了向任何人解释的动力。

​ — Douglas Crockford

​ 绝大多数程序员,尤其是那些没有函数式编程背景的程序员,往往认为 monad是一些神秘的计算机科学概念,过于理论化以至于对编程生涯没有帮助。这种负面观点可归因于一些介绍 monad 的文章和博客过于抽象或过于狭隘。但事实证明,monad 就在我们身边,甚至是标准的 Java 库,尤其是从 Java Development Kit (JDK) 8开始。有趣的是,一旦你理解了 monad,就会对其他的几个概念一通百通。

Monad 概括了各种看似独立的概念,因此学习 monad的变体只需要很少的时间。例如,你不必学习Java 8 中CompletableFuture的工作原理,一旦你意识到它是一个 monad,你就会准确地知道它是如何工作的以及你可以从其语义中得到什么。你或许听说过RxJavaRxJava中的Observable也是一个 monad,所以RxJava也变得没那么神秘。你已经在不知不觉中遇到了许多的 monad示例,因此不要被那些高深的概念吓倒。

Functors

​ 在我们解释 monad是什么之前,让我们探索一个更简单的结构,称为functor(函子)。函子是封装一些值的类型化数据结构。从语法的角度来看,函子是一个具有以下 API 的容器:

import java.util.function.Function;

interface Functor<T> {
    
    <R> Functor<R> map(Function<T, R> f);
    
}

​ 但是仅仅语法合规不足以理解函子是什么。函子提供的唯一操作是map()map()接受一个函数f,这个函数f接收函子中封装的值,转换它并将结果按原样包装到第二个函子中。函子始终是一个不可变的容器,因此map()永远不会改变执行它的原始对象,而是将结果包裹在一个全新的函子中,可能是不同的类型。此外,当应用恒等函数时,函子不应执行任何操作,即map(x -> x)。这种情况下应该总是返回相同的函子或相等的实例。

​ 通常Functor<T>被比作一个盒子,里面装着值T,与T交互的唯一方法是使用map()对其进行转换。一般情况下,没有常用的方法让值T从函子中解脱出来,值始终保持在函子的上下文中。

​ 函子用一个统一的 API 概括了多个常见的习惯用法,如:collections, promises, optionals等。下面介绍几个函子,让你更流畅地使用这个 API:

interface Functor<T,F extends Functor<?,?>> {
    <R> F map(Function<T,R> f);
}

class Identity<T> implements Functor<T,Identity<?>> {

    private final T value;

    Identity(T value) { this.value = value; }

    public <R> Identity<R> map(Function<T,R> f) {
        final R result = f.apply(value);
        return new Identity<>(result);
    }
    
}

​ 上面的例子是个最简单的函子,只是持有一个值,对该值所能做的就是在map方法中对其进行转换,但无法提取它。与函子交互的唯一方法是进行一系列类型安全的转换:

Identity<String> idString = new Identity<>("abc");
Identity<Integer> idInt = idString.map(String::length);

你也可以使用链式写法:

Identity<byte[]> idBytes = new Identity<>(customer)
        .map(Customer::getAddress)
        .map(Address::street)
        .map((String s) -> s.substring(0, 3))
        .map(String::toLowerCase)
        .map(String::getBytes);

从这个角度来看,对函子进行映射与链式调用没有太大区别:

byte[] bytes = customer
        .getAddress()
        .street()
        .substring(0, 3)
        .toLowerCase()
        .getBytes();

​ 那为什么还要费心使用这种冗长的包装,不仅不能提供任何附加值,而且不能提取内容的结构呢?事实证明这个原始函子确实没多大用,但是可以使用这个原始函子抽象来模拟其他几个概念,例如:java.util.Optional<T>,这个从 Java 8 开始时带有map()方法的函子。让我们从头开始实现它:

class FOptional<T> implements Functor<T,FOptional<?>> {

    private final T valueOrNull;

    private FOptional(T valueOrNull) {
        this.valueOrNull = valueOrNull;
    }

    public <R> FOptional<R> map(Function<T,R> f) {
        if (valueOrNull == null)
            return empty();
        else
            return of(f.apply(valueOrNull));
    }

    public static <T> FOptional<T> of(T a) {
        return new FOptional<T>(a);
    }

    public static <T> FOptional<T> empty() {
        return new FOptional<T>(null);
    }

}

FOptional<T>函子可能持有一个值,但它也可能是空的。这是一种处理null的类型安全的方式。有两种方式来构造 FOptional,提供值或创建empty()实例。就像 Identity函子一样,FOptional是不可变的,我们只能从内部与值进行交互。不同的是,对 FOptional 来说,如果它为空,则转换函数f不会应用于任何值。这意味着函子不一定只封装一个值T。它也可以包装任意数量的值,就像List…函子一样:

import com.google.common.collect.ImmutableList;

class FList<T> implements Functor<T, FList<?>> {

    private final ImmutableList<T> list;

    FList(Iterable<T> value) {
        this.list = ImmutableList.copyOf(value);
    }

    @Override
    public <R> FList<?> map(Function<T, R> f) {
        ArrayList<R> result = new ArrayList<R>(list.size());
        for (T t : list) {
            result.add(f.apply(t));
        }
        return new FList<>(result);
    }
}

​ API 保持不变(都是使用map()进行转换),但行为却大不相同。我们对FList中的每一项应用转换,声明性地转换整个列表。因此,如果你有一个列表customers并且想要得到他们的street,那么例子如下:

import static java.util.Arrays.asList;

FList<Customer> customers = new FList<>(asList(cust1, cust2));

FList<String> streets = customers
        .map(Customer::getAddress)
        .map(Address::street);

​ 使用customers.getAddress().street()这种链式调用的方式不再能简单的得到结果,你不能对customers集合调用getAddress(),你必须对每个单独的customer调用getAddress(),然后将其放回集合中。顺便说一下,Groovy 发现这种模式非常普遍,以至于它实际上有一个语法糖:customer*.getAddress()*.street()。 这个方法,被称为spread-dot,实际上是一种map的伪装。也许你想知道为什么我在 map 里手动迭代列表,而不是使用Java 8中的 Streamlist.stream().map(f).collect(toList())?如果我告诉你java.util.stream.Stream<T>在 Java 中也是函子呢?

​ 现在你应该看到函子的第一个好处,它们抽象出各种数据结构的内部表示,并提供一致的、易于使用的 API用于操作数据。作为最后一个例子,让我介绍一下promise函子,类似于FuturePromise“承诺”某个值有一天会变得可用,它还没有出现,可能是因为等待后台计算,或者正在等待外部事件,但它会在未来的某个时间出现。

Promise<Customer> customer = //...
Promise<byte[]> bytes = customer
        .map(Customer::getAddress)
        .map(Address::street)
        .map((String s) -> s.substring(0, 3))
        .map(String::toLowerCase)
        .map(String::getBytes);

​ 是不是很熟悉?这就是我想说的!Promise<Customer>现在还没有值,它承诺未来会有值,但是没有关系,就像 FOptionalFList那样,我们依然可以对promise函子进行映射,语法和语义完全相同。customer.map()不会等待底层promise完成,而是生成另一个不同类型的promise,这意味着map是非阻塞的。当上游promise完成时,下游promise会应用传给 map() 的函数,然后将结果传递给更下游。突然之间,我们的函子允许我们以管道的方式进行非阻塞异步计算。但是你不必理解或学习——因为Promise也是一个函子,它遵循函子语法和定律。

​ 还有许多其他函子的例子,例如可以表示值或错误的函子。但现在是了解monad的时候了。

从functors到monads

​ 我假设你已经了解了函子的工作原理以及为什么它们是有用的抽象。但是函子并不像人们想象的那样普遍,如果你的转换函数(作为参数传递给 map()的那个)返回函子实例而不是简单的返回值,会发生什么?好吧,函子也只是一个值,所以没有什么不好的事情发生。返回的任何内容都放回函子中,因此所有行为都一致。然而,想象一下你有这个解析String的方法:


FOptional<Integer> tryParse(String s) {
    try {
        final int i = Integer.parseInt(s);
        return FOptional.of(i);
    } catch (NumberFormatException e) {
        return FOptional.empty();
    }
}

​ 异常是破坏类型系统和函数纯度的副作用。在纯函数式语言中,没有异常,毕竟我们从未在数学课上听说过抛出异常,对吧?取而代之的是,错误使用值和包装器显式表示。例如,tryParse()传入一个String类型的参数,但并不是简单的返回一个int或是抛出异常,而是通过类型系统明确的告诉我们tryParse()可能会成功也可能会失败,但不会有任何异常或错误。这种可选的结果由optional来表示。

​ 试着组合 一下tryParseFOptional<String>

FOptional<String> str = FOptional.of("42");
FOptional<FOptional<Integer>> num = str.map(this::tryParse);

​ 问题出现了,tryParse()会返回一个FOptional<Integer>,但因为map()函数会把FOptional<Integer>当作转换后的值再次包装,变成了FOptional<FOptional<Integer>>,造成了包装两次的尴尬。请仔细回想一下函子的定义,搞清楚为什么这里得到这个双重包装器。除了看起来很糟糕之外,在 functor 中使用 functor 会破坏函子的组合性和流畅的转换链:

FOptional<Integer> num1 = //...
FOptional<FOptional<Integer>> num2 = //...

FOptional<Date> date1 = num1.map(t -> new Date(t));

//doesn't compile!
FOptional<Date> date2 = num2.map(t -> new Date(t));

​ 这里我们尝试通过传入一个 int -> Date 的函数把 FOptional 中的int 转换成Date ,对于num1这很简单。但到了num2情况变得复杂了,num2.map()接收的函数f的输入不再是一个int,而是一个FOptional<Integer>,显然 java.util.Date 没有一个参数类型是FOptional<Integer>的构造器,num2.map()无法通过编译,双重包装破坏了我们的函子。然而,返回函子而不是返回简单值的函数是很常见的需求(如tryParse()),我们不能简单地忽略这样的需求。一种方案是是引入一种特殊的无参数join()方法来“flattens(展平)”嵌套函子:

FOptional<Integer> num3 = num2.join()

​ 由于这种模式非常普遍,因此引入了特殊的方法叫作flatMap()flatMap()非常类似于map(),但它期望接收函数作为参数,返回函子(而不是嵌套函子) ,或者准确地说 — monad

interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> {
    M flatMap(Function<T,M> f);
}

​ 我们简单地得出结论, flatMap 只是一种语法糖,可以实现更好的组合。但是flatMap方法(在 Haskell中通常叫作 bind 或者 >>= )让一切变得不同了,因为它允许以纯函数式的风格组合复杂的转换。如果FOptionalmonad的一个实例,那么之前的转换就能按预期工作:

FOptional<String> num = FOptional.of("42");
FOptional<Integer> answer = num.flatMap(this::tryParse);

​ Monads 不需要实现map,它可以简单的在 flatMap() 之上实现。事实上, flatMap 是打开transformations(变换)领域新世界的基本方法。就像函子一样,语法合规不足以将某个类称为 monad,flatMap()方法必须遵循 monad 定律,它们非常直观,就像 flatMap()和 identity 的结合性。后者要求对于任意持有值 xmonad和任意函数fm(x).flatMap(f)f(x) 相同。我们不会深入研究 monad 理论,我们更关注其实际意义。例如, Promise monad将在未来持有值,你能从类型系统中猜测以下程序中Promise的行为吗?首先,所有可能需要一些时间才能完成的方法都返回一个Promise

import java.time.DayOfWeek;


Promise<Customer> loadCustomer(int id) {
    //...
}

Promise<Basket> readBasket(Customer customer) {
    //...
}

Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) {
    //...
}

Promise<BigDecimal> discount = 
    loadCustomer(42)
        .flatMap(this::readBasket)
        .flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));
	`flatMap()`必须保留 `monadic` 类型,因此所有中间对象都是`Promise`。这不仅保持了类型的顺序,也使得前面的程序完全异步了。`loadCustomer()`返回 一个`Promise`,所以它不会阻塞, `readBasket()` 接收这个 `Promise` 以及它拥有(将拥有)的东西,在应用一个函数后返回另一个 `Promise` ,以此类推。基本上,我们构建了一个异步计算管道,后台完成一个步骤会自动触发下一步。

探索 flatMap()

​ 拥有两个 monad 并将它们包含的值组合在一起是很常见的。然而,函子和 monad 都不允许直接访问它们的内部,我们只能小心地应用转换。想象一下,你有两个 monad 并且想要将它们组合起来:

import java.time.LocalDate;
import java.time.Month;


Monad<Month> month = //...
Monad<Integer> dayOfMonth = //...

Monad<LocalDate> date = month.flatMap((Month m) ->
        dayOfMonth
                .map((int d) -> LocalDate.of(2016, m, d)));

​ 我们有两个独立的 monad,一个包装的类型是 Month,另一个是 Integer。为了构建LocalDate,我们必须构建一个可以访问两个 monad 内部结构的嵌套转换。仔细思考一下,为什么在一个地方使用flatMap()而在另一个地方使用map()。想想如果还有第三个Monad<Year>,你会如何构造这段代码。

​ 这种应用两个参数(在这个例子中是md)的函数是很常见的,在Haskell有一个名为 liftM2 特殊辅助函数,正是在mapflatmap之上实现的用于这种转换的函数。在 Java 伪语法中,就像这样:

Monad<R> liftM2(Monad<T1> t1, Monad<T2> t2, BiFunction<T1, T2, R> fun) {
    return t1.flatMap((T1 tv1) ->
            t2.map((T2 tv2) -> fun.apply(tv1, tv2))
    );
}

​ 你不必为每个 monad 实现这个方法,flatMap()就足够了,而且它对所有 monad 都能一致地工作。当你将其与其他monad一起使用时, liftM2 非常有用。例如: liftM2(list1, list2, function) 将会对 list1list2中列表项的所有组合(笛卡尔积)应用函数function 。另一方面,对于optionals,只有当两个optional都不为空时,它才会应用一个函数。更骚的是,对于 Promise monad,只有当两个Promise 都完成时,才会异步的执行一个函数。这意味着我们发明了同步两个异步操作的简单同步机制(就像fork-join 算法中的join()一样)。

​ 我们可以在 flatMap() 之上构建另一个有用的方法filter(Predicate<T> p),它接受 monad 中的内容T,如果T不满足谓词p,则将其丢弃。在某种程度上,它类似于map,但不是 1-to-1 映射,而是1-to-0-or-1。同样,filter()对于每个 monad具有相同的语义,但根据我们实际使用的 monadfilter()具有amazing的功能。例如,它允许从列表中过滤掉某些元素:

FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);

​ 对于optionals它也同样适用。在这种情况下,如果 optional 的内容不符合某些标准,我们可以将非空 optional 转换为空的,空的 optional保持不变。

从monad的列表到列表的monad

​ 另一个有用的源自 flatMap()的方法是 sequence()。只需查看类型签名,就可以轻松猜出它的作用:

Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)

​ 通常我们有一堆相同类型的monad,但我们希望有一个该类型列表的单个 monad。这可能听起来很抽象,但它非常有用。想象一下,你想通过 ID 从数据库中同时加载一些客户,因此你对不同的 ID多次使用了loadCustomer(id)方法,每次调用都返回Promise<Customer>。现在你有了一个Promise列表,但你真正想要的是客户列表。sequence()(在 RxJavasequence()被称为concat()或者merge())方法就是为此而生的:


FList<Promise<Customer>> custPromises = FList
    .of(1, 2, 3)
    .map(database::loadCustomer);

Promise<FList<Customer>> customers = custPromises.sequence();

customers.map((FList<Customer> c) -> ...);

​ 对代表客户 ID 列表的FList<Integer>中的每个ID应用database.loadCustomer(id)进行转换,就得到了相当不方便的Promise列表。幸好, sequence() 拯救了它。对于不同种类的 monad,在不同的计算上下文中,sequence()仍然有意义。例如,它可以将 FList<FOptional<T>> 更改为FOptional<FList<T>>


​ 这其实只是flatMap()monads实用性的冰山一角。尽管来自相当晦涩的范畴论,monads依然被证明是非常有用的抽象,即使在面向对象的编程语言(如 Java)中也是如此。能够组合函数返回monad是如此普遍有用,以至于许多不相关的类都遵循 monadic 行为。

​ 此外,一旦你将数据封装在 monad 中,通常很难明确地将其取出。取出这种操作不是 monad 行为的一部分,并且通常会破坏monad的哲学。例如,技术上 ,Promise<T> 中的Promise.get()可以返回T,但只能通过阻塞的方式获取,而所有基于的flatMap()操作都是非阻塞的。另一个例子是FOptional.get()FOptional.get()可能会失败,因为FOptional可能是空的。甚至是调用FList.get(idx)从列表中查看特定元素,也是不推荐的,因为通常你可以使用map()来替换for循环。

​ 现在,我希望你明白了为什么 monad 现在如此流行。即使在像 Java 这样的面向对象语言中,它们也是非常有用的抽象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
函数式编程的 `Maybe`, `Either`, `Functor` 和 `Monad` 是常见的一些概念。 `Maybe` 表示一个可能为空的值,它可以用来避免程序的空指针异常。在 Haskell ,`Maybe` 是一个数据类型,它有两个值:`Just a` 表示存在一个值,值为 `a`;`Nothing` 表示不存在值。在其他语言,可以通过自定义一个 `Maybe` 类型来实现类似的功能。 `Either` 表示一个值可能是两种类型的一种。在 Haskell ,`Either` 是一个数据类型,它有两个参数:`Either a b` 表示值可以是类型 `a` 或 `b` 的一种。在其他语言,可以通过自定义一个 `Either` 类型来实现类似的功能。 `Functor` 是一个抽象概念,它表示一个能够被映射(map)的数据结构。在 Haskell ,`Functor` 是一个类型类,它要求实现一个 `fmap` 函数,它可以将一个函数作用于一个 Functor 上。在其他语言,可以通过实现一个 `map` 函数来实现类似的功能。 `Monad` 是一个用于处理副作用的抽象概念。在函数式编程,副作用是指函数执行过程对程序状态进行修改,比如 I/O 操作、异常处理等。为了避免副作用对程序的影响,函数式编程引入了 Monad 的概念。Monad 要求实现一个 `bind` 函数,它可以将一个 Monad 的值传递给一个函数,并返回一个新的 Monad。在 Haskell ,`Monad` 是一个类型类,它要求实现 `return` 和 `>>=` 函数。在其他语言,可以通过自定义一个 Monad 类型来实现类似的功能。 综上所述,`Maybe`, `Either`, `Functor` 和 `Monad` 都是函数式编程常见的一些概念,它们可以帮助程序员编写更加健壮、可维护的代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值