pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • 技术面试题篇

  • 面试准备篇

  • 技术面试题自测篇

  • 练级攻略篇

  • 工作篇

  • 面经篇

  • 笑傲Java面试

    • 2-1 导学-Java编程技巧部分
    • 2-2 IDEA Java配置补充
    • 2-4 Java8 Stream 接口:流和并发计算实例
    • 2-5 和面试官聊聊实现管道和流计算的基石:函数式的Monad
      • [](#引入:流计算的类型设计?)引入:流计算的类型设计?
      • [](#monad)Monad
      • [](#实战场景)实战场景
      • [](#解读定义)解读定义
        • [](#函数的定义)函数的定义
        • [](#函子(fuctor)的实现)函子(fuctor)的实现
        • [](#自函子)自函子
        • [](#幺半群monoid)幺半群(monoid)
      • [](#总结)总结
    • 2-6 Buffer的原理和使用场景+面试题解读
    • 2-7 补充提问:同步和阻塞、异步和非阻塞等不等价?
    • 2-8 阿里面试题:中文乱码处理和大文件计算词频
    • 2-9 实战场景Coding训练:解读反射+代理+AOP 并结合业务逻辑实现
    • 2-10 注解部分答案
    • 2-11 反射-元编程面试题目合集
    • 2-12 面试必备:Java8-11的新特性和理解的误区
    • 2-13 白板篇-Java编程总结(以及面试题)
    • 3-1 算法和数据结构导学
    • 3-2 教你面试时不会忘记的5种手写排序
    • 3-3 手写链表算法
    • 3-4 手写栈和队列面试专项
    • 3-5 课后习题+面试题:用栈和队列实现表达式解析
    • 3-6 迷宫伪代码和8皇后问题源代码
    • 3-7 3-7 树部分源代码
    • 3-8 8皇后问题
    • 3-10 动态规划的课前题目
    • 3-11 总结和课后习题:白板篇-数据结构和算法
    • 4-1 解读:并发编程知识体系
    • 4-2 看看你的基础:Java线程状态之间如何转换?
    • 4-3 CAS和原子操作
    • 4-4 同步器(上篇)——面试官问synchronized本质是什么?
    • 4-5 同步器(中)——AbstractQueuedSynchronizer
    • 4-6 面试官:说6个Java的同步器?
    • 4-7 面试官出难题:并发环境下单例怎么写性能最高
    • 4-8 面试官:LinkedBlockingDeque和SynchronousQueue工作原理一样吗?
    • 4-9 面试要点:volatile的简短补充
    • 4-10 给面试官讲讲无锁编程(Lock-Free Programming)
    • 4-11 高阶并发编程Coding训练:N种优化哲学家就餐问题的方法
    • 4-12 并发基础篇:总结和思考题
    • 4-13 并发部分的通关Boss: 生成、发放大量红包并控制资金流速
  • LeetCode

  • 面试
  • 笑傲Java面试
pursuewind
2021-12-13
目录

2-5 和面试官聊聊实现管道和流计算的基石:函数式的Monad

# 和面试官聊聊实现管道和流计算的基石:函数式的Monad

函数式编程就是用函数写程序,当然这是一句废话。我相信大家都知道什么是用函数写程序。但是大家可能不知道如何通过函数去构造复杂的、大型的、可扩展的、可测试,复用率高,安全性强的、有数学证明的程序。

面向对象编程以现实世界为原型去投影程序的世界,适合对事务进行抽象、组织、分解;函数式编程则研究的是如何更好的进行计算。

计算是计算机最重要的能力。让计算没有副作用、让计算可以并行、让计算可以被数学证明、让计算更安全、让计算最大程度被复用,这些都是函数式编程的目标。

因此作为一个出色的Java工程师,是有必要学习一下函数式编程的。

这节课我是以函数式编程中的一个重要的概念,也是构造流计算的一个重要的概念——Monad。为大家讲解函数式编程。

关联面试题:

  1. Optional的作用?
  2. FunctionalInterface在做什么?
  3. 说说你理解的函数式编程?
  4. 解释下什么是Monad?
  5. 如何实现管道和流?

# 引入:流计算的类型设计?

请你先看一下这个例子。一开始,我们构造了一个Optional的类型。

 @Test
public void test_optional(){
    var result = Optional.of(100)
        .map(x -> x*x)
        .map(x -> x / 100);

}

我们用map进行映射,求了平方,好除以100。请你仔细观察类型的变化,看看有没有什么发现。

image-20210131105039067

上图中是类型的变化,数据源的类型是Optional。中间map函数运算时,进行了拆箱,实际操作的类型是整数到整数的函数。最终的结果,依然是Optional。

那么家来请你思考两个问题:

  1. 为什么中间的运算不用Optional类型?
  2. 为什么两端的数据用Optional类型?

道理其实很简单。在我们进行流计算的这个过程当中,我们期望的是类型的统一、以及类型的安全。Optional类型支持空值,准确说,是empty/undefined类型,因此使用起来更加安全。

空值和空值进行操作,没有抛异常,而是继续返回空值。这让我们无论得到什么值,计算都可以继续。

if(!optionalValue.isPresent()) {
  return Optional.empty();
}
// 计算逻辑

这样的写法不可取。这样会产生大量的判断,这样的写法应该被封装起来。只有这样才能最大程度的减少程序员的心智负担。

看下下面的例子:

@Test
public void test_udef(){
    Optional<Integer> x= Optional.empty();
    var y = x.map(a -> a * a);
    System.out.println(y);
}

在上面的程序当中map内部实现了逻辑,对于empty的值,并不会进入计算逻辑,而是直接以empty的值返回,这样大大节省了用户进行判断的时间。

流计算之所以要设计这些中间的类型,是为了封装错误、避免问题、组织一致性的计算过程、以及惰性的求值过程……

# Monad

上面我们描述的流计算的类型设计,实际上就是Monad模式。Monad有非常多的定义。比如说,数学上的。我们先给一个最简单的定义,是从设计模式上去思考。

一个Monad设计模式有3个显著的特点:

  • 一个泛型的构造函数。比如: Optional
  • 不改变泛型类型的运算操作,内部是非泛型计算:例如:Optional map(T -> R)
  • 泛型类型不变。 比如可以是Optional到Optional,但还是Optional类型。

你可能会说,这样的设计究竟有什么用?这个设计其实非常有价值。比如说你可以思考一下Stream的设计。

@Test
public void test_stream(){
    var result = Stream.of("Hello", "World")
        .map(x -> x.length());
}

泛型类型不变是构造流计算的基石。Stream 是一类事物,这一类事物无论作用在怎样的操作上,最终结果还是Stream。这样map、filter、reduce等操作就能作用在任何类型的流上。

也是Monad的值。Moand设计模式中,除了要设计bind方法,还需要构造函数(或工厂方法)。这个构造函数用于构造Monad类型,比如Stream.of等等。

# 实战场景

管道和流计算是需要Monad的一个非常重要的场景。在我们实现业务逻辑的时候,抽象到非常领域化的逻辑也可以使用Monad。尽管通常这样的设计模式我们会被称之为连贯操作。但是设计的好的连贯操作。应该是Monad,因为Monad的拥有数学的证明,严密可靠。

比如:

SearchResult<Order> result = Search.ofTable(Tables.Order)
    .where(item -> item.amount > 100)
    .where(...)
    .search();


if(result.hasException()) {
    var ex = result.getException();
    // 处理异常
}

if(result.isEmpty()) {
    // 处理空集合
}

// 转换为列表
var list = result.toList();

上面程序中的SearchResult Monad是对搜索结果的一种封装。where 是一个不改变SearchResult泛型类型的操作。上面的程序是惰性的。我们可以根据search 调用之前用户注入进来的过滤函数,生成最优的sql语句,组织缓存。

# 解读定义

最后我们来看看Monad的一些奇怪的定义,例如:Monad是一个自函子范畴上的一个幺半群。

函子接收一个函数,返回提升的版本:

(A -> B) -> (M -> M)

M理解成某种泛型类型。

例如:

Optional.of(100)
    .map(a -> a.toString());

map方法接收一个Integer -> String的方法,然后将Optional(100)转换为Optional("100")。

上面的程序map构成了一个函子。

# 函数的定义

我们重新思考一下一个将类型A映射到类型B的泛型函数,如何描述:

@FunctionalInterface
interface FN<A, B> {
    B apply(A a);
}

# 函子(fuctor)的实现

那么函子是对函数的提升,将FN映射到MA->MB。M可以理解成某个构造函数,比如说Optional的构造函数。

子函子中A,B是相同的类型。

(A->A) -> (MA -> MB),

假设我们有一个封装的类型:Event

class Event<T> {
    T data;    


    static class EventData {
        Integer id;
        String msg;

        public EventData(Integer id, String msg) {
            this.id = id;
            this.msg = msg;
        }
    }
}

里面最重要的数据是data ,我们想要将Integer类型的data,也就是事件编号,映射成为对象类型的数据,也就是EventData对象。我们先定义转换方法:

static class Transforms {
    static EventData transform(Integer id) {
        switch(id) {
            case 0:
                return new EventData(id, "Start");
            case 1:
                return new EventData(id, "Running");
            case 2:
                return new EventData(id, "Done");
            case 3:
                return new EventData(id, "Fail");
            default:
                return new EventData(id, "Error");
        }
    } 
}

我们期望可以这样来使用(架构)程序:

Stream<Event<Integer>> events = 
    Stream.of(new Event(0), new Event(1), new Event(2), new Event(10))
    .map(event -> event.map(Transforms::transform))
    .forEach(e -> {
        System.out.println(e.value);
    });

上面程序可以轻而易举的将整数型的事件流补充额外的信息,并且能将事件轻易的纳入流计算体系。最关键的,我们要实现Event.map方法。

<B> Event<?> map(FN<T, B> f) {
    return new Event<>(f.apply(this.value));
}

这个map方法传入的是一个T->B的函数。返回的是一个需要推导的Event<?>类型。这样也就相当于间接的实现了(A->B) ->(MA->MB)函数的转换。M就是Event,A是Integer, B是EventData。

# 自函子

准确的来说。上面的函子。是一个自函子(Endofuctor)。

函子将(A->B)映射到(C -> D)。自函子要求C、D是一类事物,比如M(Optional, Stream, Event等)。

# 幺半群(monoid)

注意到上面我们的Event类实现,实际上已经是一个Monad模式,可见自函子是实现Monad模式的必要手段。

我们将管道和流式计算引入到了Event类型中。最大的特点,是map方法不改变泛型类型。计算用的函数,比如Transform::transform变成了一种可插拔的组件。或者说是管道节点。

这是Monad的架构神奇的地方。也是以后大家组织流式计算的必经步骤。写的专业的流式计算应该是Monad的。

从数学上或者专业点说。从范畴论上。Monad必须是一个monoid(幺半群),群指的是一类对象,比如说以Optional、Event ,Stream就是群,群可以理解成满足某类关系的对象集合。

群通需要满足封闭性,以Optional为例:

Optional.of(100) * Optional.of(100) = Optional.of(10000)

上面是一个逻辑概念,相当幺半群(集合中)的任意两个元素a(100), b(100),在二元运算符*之后,还是Optional类型(还在群中)。

实际的表达会是这样:

Optional(100) | op1 | op2 | op3 …… | Optional<?>

一个Optional的值,经过无数次操作,还保留Optional的值。

另外,幺半群要求结合律:a*b*c = a*(b*c)

从Java程序上可以这样表达结合律:

Optional.of(100)
    .map(x -> x * 2)
    .map(x -> x + 3);
    
    // 等价于  
Optional.of(100)
  .map(x -> x*2 + 3)

结合率的目标是让计算符合人类的思考方式。人类会像处理步骤一步骤二步骤三这样去看待一次计算。而后面的步骤进行合并。并不会影响最后的结果。看似这样的约定很无聊,而且没有意义,但是如果不这样去约定,那么写出的程序就会和人的思考方式产生非常大的偏差。

最后就是单位元的(unit)的要求。所有的元素。都必须存在一个元素e 使得a * e = e * a =a 。对于Optional的设计,这个单位元就是Optional.empty()。

最后请你回顾一下概念:Monad是一个自函子范畴上的一个幺半群。Moand本身一类数据,类似:泛型类M<?>。Monad需要用函子实现。幺半群(monoid)对Monad定义了详细的规则。

以下是Event的全部程序:

public class Event<T>{

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

    @FunctionalInterface
    interface FN<A, B> {
        B apply(A a);
    }


    static class EventData {
        Integer id;
        String msg;

        public EventData(Integer id, String msg) {
            this.id = id;
            this.msg = msg;
        }

        @Override
        public String toString() {
            return "EventData{" +
                    "id=" + id +
                    ", msg='" + msg + '\'' +
                    '}';
        }
    }

    static class Transforms {
        static EventData transform(Integer id) {
            switch(id) {
                case 0:
                    return new EventData(id, "Start");
                case 1:
                    return new EventData(id, "Running");
                case 2:
                    return new EventData(id, "Done");
                case 3:
                    return new EventData(id, "Fail");
                default:
                    return new EventData(id, "Error");
            }
        }
    }


    <B> Event<?> map(FN<T, B> f) {
        return new Event<B>(f.apply(this.value));
    }



    public static void main(String[] argv) {

        Stream<Event<Integer>> s = Stream.of(new Event(0), new Event(1), new Event(2), new Event(10));
        s.map(event -> event.map(Transforms::transform))
                .forEach(e -> {
                    System.out.println(e.value);
                });
    }

}

# 总结

Java中还有多种Monad的设计,除了Optional和Stream。还有CompletableFuture,封装针对异步运算管道的Monad。还有,Try:针对可能会产生异常操作的Monad。

函数式编程是一片汪洋大海。开始尝试用函数式编程封装自己的业务逻辑。我认为是成为一名领域化高手。第一步。尽管很多面向对象的高手不懂函数式编程。但是经过长期的摸索,往往都会靠近函数式的设计。尽管自己不自知而已。

如果你对函数式编程感兴趣,推荐你一篇翻译的文章。https://zhuanlan.zhihu.com/p/30510749 (opens new window)

《认知科学家写给小白的lambda演算》

希望通过这一篇文章。可以帮助你入门函数式。

Last Updated: 2023/02/14, 18:02:00
2-4 Java8 Stream 接口:流和并发计算实例
2-6 Buffer的原理和使用场景+面试题解读

← 2-4 Java8 Stream 接口:流和并发计算实例 2-6 Buffer的原理和使用场景+面试题解读→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ