kun-song / Functional-Programming-in-Scala-Specialization

https://www.coursera.org/specializations/scala
0 stars 0 forks source link

1.5 Monad #6

Open kun-song opened 6 years ago

kun-song commented 6 years ago

前面随机值生成器中定义了 flatMapmap 函数,实际上,具备这两个函数的数据结构数不胜数,它们被统称为 Monad

Monad 是纯代数数据结构,在不同编程语言中,定义 Monad 的方式不同,在 Scala 中,Monad 被定义为类型 M[T],且:

在 Scala 中,类型即可定义为 class,也可定义为 trait,因为 Monad 仅仅是类型的描述信息,并无具体实现,因此将其定义为特质:

trait Monad[T] {
  def unit(x: T): Monad[T]
  def flatMap[S](f: T ⇒ Monad[S]): Monad[S]
}

有了 Monad 的定义,很多 Scala 类型都可以对号入座,注意 unit 函数类型为 T => Monad[T]

上面例子中的 List Set 等虽然从名字上看,与 Monad 并无半毛钱关系,但它们都满足 Monad 的定义(类型参数 + 两个函数),所以他们都是 Monad 类型的实例。

注意上面的不同 Monad 实例中,unit 函数的名字各不相同(分别是 List Set Some single),但是 flatMap 函数的名字是完全相同的。

现在 Monad 已经有了 flatMap,但是 for 解析还需要 map,实际上,对所有 Monad 而言,map 都可以用 unit + flatMap 实现:

m map f == m flatMap (x => unit(f(x)))
              == m flatMap (f andThen unit)

注意:这里的 map 实现仅仅是示例,实际中,每个 Monad 实例都有自己与众不同的 unit 实现,因此,map 函数一般作为具体 Monad 实例的 primitive combinator。

Monad 法则

目前 Monad 特质拥有 unitflatMap 函数,这仅仅是形似 Monad,要变为真正的 Monad,这两个函数之间还需要满足 Monad 法则。

结合律

m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)

Left Unit

unit(x) flatMap f = f(x)

Right Unit

m flatMap unit = m

Monad 法则实例

Option 类型是 Monad 实例,它必然满足 Monad 法则:

首先看下 left unit(unit(x) flatMap f = f(x)),在 Option 中,Some 即为 unit,并且将 flatMap 替换为 Option.flatMap 实现,因此证明如下:

     Some(x) flatMap f
== Some(x) match {
       case Some(x) => f(x)
       case None     => None
     }
== f(x)

so:

Some(x) flatMap f = f(x)

Right unit 法则证明如下:

     m flatMap Some
== m match {
       case Some(x) => Some(x)
       case None     => None
     }
== Some(x) or None
== m

交换律证明如下:

     m flatMap f flatMap g
== m match {
       case Some(x) => f(x)
       case None     => None
     } match {
       case Some(x) => g(x)
       case None     => None
     }
== m match {
       case Some(x) => f(x) match { case Some(y) => g(y);  case None => None }
       case None      => None match { case Some(y) => g(y);  case None => None }
     }
== m match {
       case Some(x) => f(x) flatMap g
       case None      => None flatMap g
     }
== m match {
       case Some(x) => f(x) flatMap g
       case None      => None
     }
== m flatMap (x => f(x) flatMap g)

Monad 法则与 for 解析

kun-song commented 6 years ago

TryMonad 与 for 解析

类似 Option,我们可以为可能抛出异常的表达式定义如下容器:

sealed trait Try[+T]

final case class Success[T](v: T) extends Try[T]
final case class Failure(ex: Throwable) extends Try[Nothing]

/**
  * 1. 我们一般这样使用 Try:Try(expression),即将可能抛出异常的表达式用 Try 包裹起来,这样看来,Try 应该类似一个函数,所以在 Try 伴生对象中
  *    实现 apply 方法以支持 Try(expression) 的用法。
  * 2. 注意 apply 参数必须是 lazy,否则 Try(expression) 计算时,会立即计算 expression 表达式,如果 expression 会抛出异常,
  *    则 catch 子句无法捕获它。
  */
object Try {
  def apply[T](f: => T): Try[T] =
    try Success(f)
    catch {
      case NonFatal(ex) ⇒ Failure(ex)
    }
}

我们也想在 for 解析中使用 Try,毕竟 for 解析实在太方便了:

for {
  x <- computeX
  y <- computeY
} yield f(x, y)

使用 for 解析的理想很容易实现,只要为 Try 实现 flatMapmap 函数即可:

sealed trait Try[+T] { self ⇒
  def flatMap[S](f: T ⇒ Try[S]): Try[S] =
    self match {
      case Success(v) ⇒
        try f(v) match {
          case NonFatal(ex) ⇒ Failure(ex)
        }
      case _ ⇒ _
    }

  def map[S](f: T ⇒ S): Try[S] =
    self match {
      case Success(v) ⇒ Try(f(v))
      case _ ⇒ _
    }
}

Monad 法则证明

前面为 Try 实现了 flatMapmap 函数,但还不能说 TryMonad 实例,因为还有 3 个 Monad 法则需要满足。

结合律

   m flatMap f flatMap g
= 

left unit

right unit

kun-song commented 6 years ago

Java 的异常处理机制不够好吗,为什么 Scala 需要引入 Try 机制?

在 Java 中,如果一段代码可能抛出 checked exception,则编译器要求我们必须使用 try-catch 包住这段代码,我们已经很熟悉这种做法了,例如:

public class MainJ {

    private double divide(double m, double n) throws Exception {
        if (n == 0)
            throw new Exception("divider can't be zero!");

        return m / n;
    }

    private void test() {
        double result = 0;

        try {
            result = divide(10, 1);
        } catch (Exception e) {
            // 处理
            e.printStackTrace();
        }

        System.out.println(result);
    }

}

因为 divide 声明会抛出 Exception,而 Exception 属于 checked exception,所以任何使用 divide 方法的地方,都必须使用 try-catch 进行包裹,以捕获可能会抛出的异常。

写过 Java 的同学肯定对此非常熟悉,那么这种异常处理方式有什么问题吗?

目前我就想到这些缺点,但更重要是,在函数式编程中,非常强调引用透明,而 try-catch 恰恰破坏了引用透明,这一点是 Scala 无法忍受的,因为引用透明是是函数组合的基础,也是函数式编程的基础,所以 Scala 以 Try 来取代 try-catch,以恢复引用透明。

如何判断表达式(或函数)是否引用透明呢? 很简单,如果一个表达式,或函数,可以被其计算结果取代,而对其他所有程序无任何应用,则它就是引用透明的。 try-catch 子句,在没有抛出异常时,也符合引用透明定义,但是如果抛出异常,则这段表达式 无任何返回值,并且产生了副作用(比如停止程序等),进而也就不是引用透明的了。