前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >当我们谈论Monad的时候(一)

当我们谈论Monad的时候(一)

作者头像
KAAAsS
发布2022-01-14 16:41:29
4310
发布2022-01-14 16:41:29
举报
文章被收录于专栏:KAAAsS's Blog

Monad不就是个自函子范畴上的幺半群,这有什么难理解的。 Phillip Wadler

当我们谈论Monad的时候,我们在谈论什么

坊间一直流传着一句话:“一百个学FP的人的心中就有一百个对Monad的理解”。而我相信,他们中的大部分人在看明白后又会写出一篇崭新的Monad文。我也一直很想写一写自己关于Monad的见解,但是一直找不到合适的说明方式。先前我在某群提到,从Optional(也就是Haskell的Maybe)理解Monad会是一个很不错的方式。而直到最近我正好看到了这样一篇文章(Reference 1),与我的想法不谋而合,于是我就借用这篇文章的方式谈一谈我对Monad的理解吧。

Monad作为函数式编程中最著名的几个输出概念之一,困扰了一批又一批想要学习的工程型选手。在我看来,主要的理解障碍有二:

  1. 定义晦涩难懂,一介绍Monad就要从Functor、Applicative一字排开。而大部分语言浅显的文章又“绕着Monad转”,就是不说Monad是什么
  2. 无法直观的看出Monad的用处。废了老大劲看完的文章,也不知道Monad能干嘛,看了几个示范的Monad又仿佛Monad什么都能干

综上,我打算用工程化的方式来解释Monad到底是什么。之后,用Haskell作为过渡,最后在讲讲理论相关的内容。而第一篇作为工程部分,自然用的是大家最喜欢的Java主要是我最喜欢来讲解了。

不过我先打个预防针,本篇文章是站在工程角度的浅显介绍,因此语言可能不甚严谨。

Monad是层数很高的抽象

Runnable一样,Monad是一个功能的抽象。在Java中,我们可以用接口类来描述它。就像你说ThreadRunnable一样,我们也同样可以说XX类Monad。实现了Monad要求的方法,你就可以用一些公用的方法来操作一个类了,就这么简单。

唯一的难点是,Monad要求实现的方法没有特定的功能。这比较像Comparable,而我们知道Comparable比较大小的语义纯粹只是人为增加的而已。只要符合一些规则(自反性、反对称性、传递性),你就可以编写一个靠谱的Comparable。Monad也一样,只不过Monad更加抽象。Monad这波绝对不止第五层

Functor

知道了Monad就是个抽象,下一步就是看看它要求什么接口、能帮我们干什么。于是这就避不开Functor了,因为Functor实际上是Monad的一个“初级形态”。不妨直接来看看Functor的一个Java定义:

代码语言:javascript
复制
import java.util.function.Function;

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

可以看到Functor是一个泛型类,接受一个泛型参数T。此外,Functor接口只需要实现一个map方法。这个map方法接受一个函数,它的参数类型为T,返回值类型为R,写作T -> R。此外,调用时我们还传入了Functor<T>类型的this。最后,函数返回了Functor<R>

既然明白了这个要求,我们就来举个例子看看Functor到底在抽象什么。

代码语言:javascript
复制
import java.util.function.Function;

public class MyFunctor<T> implements Functor<T> {
    private final T value;

    public MyFunctor(T value) {
        this.value = value;
    }

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

简而言之,MyFunctor<T>就是一个T类型的容器,然后map就把参数的函数f应用到自己的身上,得到一个MyFunctor<R>。有什么用呢?嘛,这样你就可以写:

代码语言:javascript
复制
new MyFunctor<>("Functor gives fmap")
    .map((String s) -> s.substring(0, 3))
    .map(String::toLowerCase)
    .map(String::getBytes);

好像根本没有什么锤子用,看起来我们只是把所有值的外边套了一个MyFunctor的娃,然后把一次次调用放在了map函数里。这种什么都不干,套了娃跟没套一样的Functor有一个名字:Identity。

虽然Identity好像没什么用,但是看到这个代码,不觉得有内味了嘛?什么味?这不就是Optional API的调用方法嘛!没错,稍微改一改,我们就能得到一个Optional。

代码语言:javascript
复制
import java.util.function.Function;

public class MyOptional<T> implements Functor<T> {
    private final T valueOrNull;

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

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

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

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

虽然调用的格式还是和Identity一样,只不过现在链式调用是完全空安全的。看到这里,Functor抽象的东西应该就很明显了:一个容器。而map抽象的,则是对容器内部值的操作。

而且由于Functor的抽象层数很高,因此它能抽象Optional这种有两个状态的容器。当然,抽象List这种不限长度的容器也是OK的。

代码语言:javascript
复制
import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
import java.util.function.Function;

public class FList<T> implements Functor<T, FList<?>> {
    private final ImmutableList<T> list;

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

    @Override
    public <R> FList<R> 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);
    }
}

这样,我们就可以方便的对列表元素进行链式操作了。

Monad

但是Functor还是有一个问题,它没法解决嵌套。比如,如果我们希望计算两个MyOptional<Integer>的和,得到一个MyOptional<Integer>,那要怎么编码呢?

代码语言:javascript
复制
MyOptional<Integer> optA = MyOptional.of(1);
MyOptional<Integer> optB = MyOptional.of(2);
var result = optA.map(a -> optB.map(b -> a + b));

我们虽然不能确定optAoptB内部的值(它们可能是null),但是通过map,我们可以变相的得到他们的真实值。这么写倒是没什么问题,但是,你猜猜result的类型是什么?没错,是:MyOptional<MyOptional<Integer>>。而且随着你需要的参数变多(这里是加法,故只需要两个),结果的套娃也会一层一层变多。这太丑陋了!

而且你细品这个娃品谁的娃?。你会发现内层MyOptional实际上是因为我们无法确定optB有无值而引入的。但是实际上我们希望达到的效果是,只要optAoptB有一个是empty的就返回MyOptional.empty(),否则就计算加法。因此,这个娃套的可以说完全没有必要。

Monad就是对这种没必要的套娃的抽象。Monad引入了一个join函数,把两层嵌套简化为一层:

代码语言:javascript
复制
import java.util.function.Function;

public abstract class Monad<T> implements Functor<T> {
    public abstract <R> Monad<R> map(Function<T, R> f);

    public abstract <R> Monad<R> join(Monad<Monad<R>> m);
}

这里注意,join本来的意图是将Monad<Monad<R>>变成Monad<R>,因此理论上它应该是个静态方法。但是Java不允许声明抽象静态方法,只能变成这样了。

很明显,对于Optional实现join,我们只要检查内部值是不是null就可以安心返回内层Optional了。也就是:

代码语言:javascript
复制
@Override
public <R> MyMonadOptional<R> join(Monad<Monad<R>> m) {
    var opt = (MyMonadOptional<Monad<R>>) m;
    if (opt.valueOrNull == null)
        return empty();
    else
        return (MyMonadOptional<R>) opt.valueOrNull;
}

这样我们只需要在调用的最后增加一个join,就可以消除这层没必要的嵌套了。

不光如此,有了join函数,我们还可以很简单的构造出flatMap函数。

代码语言:javascript
复制
public <R> Monad<R> flatMap(Function<T, Monad<R>> f) {
    Monad<Monad<R>> result = this.map(f);
    return this.join(result);
}

在Optional的情况下,flatMap是用来实现返回值本身可能是null的函数,比如:

代码语言:javascript
复制
MyMonadOptional<Integer> tryParse(String s) {
    try {
        final int i = Integer.parseInt(s);
        return MyMonadOptional.of(i);
    } catch (NumberFormatException e) {
        return MyMonadOptional.empty();
    }
}

使用flatMap,我们同样可以避免map会产生的嵌套问题。还记得开始的时候我们举得例子嘛?我们现在可以改写成:

代码语言:javascript
复制
var mOptA = MyMonadOptional.of(1);
var mOptB = MyMonadOptional.of(2);
var ret = mOptA.flatMap(a -> mOptB.map(b -> a + b));
// ret is MyMonadOptional<Integer> !!

更有意思的一件事情是,使用flatMap也可以实现join函数。也就是说,我们也能定义出Monad!

代码语言:javascript
复制
import java.util.function.Function;

public abstract class Monad<T> implements Functor<T> {
    public abstract <R> Monad<R> map(Function<T, R> f);

    public abstract <R> Monad<R> flatMap(Function<T, Monad<R>> f);

    public <R> Monad<R> join(Monad<Monad<R>> m) {
        return m.flatMap(Function.identity());
    }
}

而这个定义,就是大多数编程语言(比如Scala、Haskell)对Monad的定义。

Program in Monad

通过了刚才的介绍,你应该能找出不少现有的Monad。像一直举例用的Java的Optional API,Java的Stream API,还有ES6的Promise,它们本质上其实都是Monad。相信这些Monad你应该已经不难看出了,所以我想介绍一个略微特别的Monad——列表。

由于我们之前已经实现过列表的Functor了,因此我们只需要考虑它的join,也就是要设计一个把嵌套的列表变成不嵌套的函数。嘛,直接把他们连起来就可以了。比如:join([ [ 1, 2 ], [ 3, 4 ] ]) = [ 1, 2, 3, 4 ]

代码语言:javascript
复制
import com.google.common.collect.ImmutableList;

import java.util.ArrayList;
import java.util.function.Function;

public class MonadList<T> extends Monad<T> {
    private final ImmutableList<T> list;

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

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

    @Override
    public <R> MonadList<R> join(Monad<Monad<R>> m) {
        var lists = (MonadList<Monad<R>>) m;
        var result = new ArrayList<R>(list.size());
        for (var list : lists.list) {
            result.addAll(((MonadList<R>) list).list);
        }
        return new MonadList<>(result);
    }
}

接下来,我们看看对Monad开发的通用函数用在List身上是什么效果。还记得我们之前计算加法的模式嘛?稍微抽象下,我们就能得到一个对所有Monad都适用的函数:

代码语言:javascript
复制
import java.util.function.BiFunction;

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

那么问题来了。它用在List上是什么效果呢?

代码语言:javascript
复制
var lstA = new MonadList<>(Arrays.asList(1, 2));
var lstB = new MonadList<>(Arrays.asList(4, 5));
var result = Functional.liftM2(lstA, lstB, Integer::sum);
// [ 5, 6, 6, 7 ]

没错,它返回了4个数字,而这正是两个列表内容的所有可能组合进行运算的结果(1+4、1+5、2+4、2+5)!liftM2作用于List的效果就是一个笛卡尔积。而且你细品,这不就是列表推导式嘛。

根据这个例子,不难看出:由于高度的抽象,基于Monad编写的函数(如liftM2)本身没有“明确的用途”。根据Monad的不同,它实际表现出来的作用很可能相当不同。我觉得代码复用的最高层次也莫过于此。

不确定性之盒

说了那么多,你应该能了解到Monad只是一个抽象结构而已。不过这么说确实还是太抽象了,所以我打算用一个经典的例子再次描述Monad——纸箱。

由于需要一个类型参数T,Monad几乎必然持有一个T类型的值(你确实可以写一个完全不持有的Monad,但是它什么都做不了)。但是这个T类型的值存在的“形式”是不确定的。对于Optional来说,它有可能存储一个T,也有可能是空的。对List来说,它储存的T元素数目是不确定的。所以Monad实际上是一个存储不确定性的纸箱

对于Monad,如果我们不“打开”它,我们就永远无法得知其中的内容,因为我们根本无法确定纸箱里面内容的具体形式。Monad的创意是,它用map来变相帮助我们读取它的内容!也就是说,Monad把处理数据的操作也变得不确定了。如果纸箱里有东西,我们就把它取出来处理,如没有东西就原封不动。操作的执行与否和纸箱里面的东西存在与否息息相关!而且Monad还允许我们引入新的不确定性。如果一个操作的结果就是一个纸箱,我们就不必再重复套纸箱了。

Monad的灵魂就在于不拆开纸箱。对于Optional,我们尽可能晚的打开纸箱(也就是get等等消去Optional的方法),这样我们就不用担心处理过程中的不确定性会影响整个流程了。而对于Promise,我们根本没办法打开纸箱。回调成了我们读取数据的唯一办法,而这就是Monad能抽象的部分。

下一个话题

到这里,我关于Monad工程角度的介绍就结束了。我个人认为,只是理解Monad的用途是没有必要,也没有意义去看Monad背后的数学定义的。

不过只从工程角度理解Monad是远远不够的。文中没有提及flatMap需要遵守的规则,对Monad的定义也不太完备(缺少了return),也没有细究join和flatMap的互相实现。要真正理解Monad,理论上的内容同样是不可避免的。

下一篇文章,我将简单介绍Haskell中的Monad实现与一些有趣的Monad,作为过渡。再下一篇,我将从理论角度(主要是范畴论)介绍Monad。

Reference

  1. Functional Programming in Pure Java: Functor and Monad Examples(https://dzone.com/articles/functor-and-monad-examples-in-plain-java
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 当我们谈论Monad的时候,我们在谈论什么
  • Monad是层数很高的抽象
  • Functor
  • Monad
  • Program in Monad
  • 不确定性之盒
  • 下一个话题
  • Reference
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档