类型构造器是Monad还是拥有Monad?多实例及Monoid、Applicative相关问题
咱先直接给结论:完全可以! 像Scala/Scalaz、Haskell这类函数式语言/库的类型系统,并没有限制为同一个类型构造器只能定义一个Monad实例——只要每个实例都严格遵守Monad的三条核心定律(左单位元、右单位元、结合律),就都是合法的。
有实际意义的示例:Left-biased 的 Either Monad
最常见的实际场景就是Either类型构造器。Scala标准库自带的Either Monad是Right-biased的:Right代表成功值,Left代表错误,flatMap会自动短路到第一个Left。但在某些场景下,我们可能需要反过来——用Left代表成功,Right代表错误,这时候一个Left-biased的Monad实例就会非常顺手。
比如处理某个第三方API,它返回的Either[SuccessData, ErrorInfo]里,Left才是正常结果,Right是错误。这时候用自定义的Left-biased Monad,就能用for推导式流畅处理,不用每次都手动模式匹配。
Scala代码示例(用Cats库实现):
import cats.Monad import cats.syntax.all._ // 自定义Left-biased的Either Monad实例 implicit def leftBiasedEitherMonad[E]: Monad[Either[*, E]] = new Monad[Either[*, E]] { override def pure[A](x: A): Either[A, E] = Left(x) override def flatMap[A, B](fa: Either[A, E])(f: A => Either[B, E]): Either[B, E] = fa match { case Left(success) => f(success) // 正常流转成功值 case Right(error) => Right(error) // 遇到错误直接短路 } override def tailRecM[A, B](a: A)(f: A => Either[Either[A, E], B]): Either[B, E] = f(a) match { case Left(Left(nextSuccess)) => tailRecM(nextSuccess)(f) case Left(Right(error)) => Right(error) case Right(result) => Left(result) } } // 模拟数据类 case class User(name: String, age: Int) case class UserProfile(name: String, desc: String) // 用for推导式处理Left为成功的场景 val successCase = for { userData <- Left(User("Alice", 30)): Either[User, String] processed <- Left(UserProfile(userData.name, s"${userData.age}岁")): Either[UserProfile, String] } yield processed // 结果:Left(UserProfile("Alice", "30岁")) // 遇到Right错误时自动短路 val errorCase = for { userData <- Right("用户不存在"): Either[User, String] processed <- Left(UserProfile("Bob", "25岁")): Either[UserProfile, String] } yield processed // 结果:Right("用户不存在")
人工构造的示例:只处理第一个元素的List Monad
为了更直观地展示“同一类型构造器可以有多个Monad”,咱可以人工构造一个和标准List Monad行为完全不同的实例:这个Monad的flatMap只会处理列表的第一个元素,忽略后续所有元素,同时严格遵守Monad定律。
Scala代码示例:
import cats.Monad import cats.syntax.all._ // 自定义List Monad:只处理第一个元素 implicit def firstElementListMonad: Monad[List] = new Monad[List] { override def pure[A](x: A): List[A] = List(x) override def flatMap[A, B](fa: List[A])(f: A => List[B]): List[B] = fa.headOption match { case Some(first) => f(first) // 仅处理第一个元素 case None => Nil // 空列表直接返回空 } override def tailRecM[A, B](a: A)(f: A => List[Either[A, B]]): List[B] = f(a).headOption match { case Some(Left(nextA)) => tailRecM(nextA)(f) case Some(Right(result)) => List(result) case None => Nil } } // 使用这个自定义Monad val demo = for { num <- List(1, 2, 3, 4) doubled <- List(num * 2, num * 3) } yield doubled // 结果:List(2, 3) —— 只处理了第一个元素1,忽略了2、3、4
验证一下定律:
- 左单位元:
flatMap(pure(x))(f) = f(x),符合,因为pure(x)是List(x),head就是x,flatMap返回f(x) - 右单位元:
flatMap(fa)(pure) = pure(fa.head),符合该Monad“只保留第一个元素结果”的语义 - 结合律:
flatMap(flatMap(fa)(f))(g) = flatMap(fa)(a => flatMap(f(a))(g)),所有操作仅针对第一个元素,自然成立
其他代数结构的类似情况
不止Monad,像Monoid、Applicative这类代数结构,也普遍存在“同一类型/类型构造器有多个合法实例”的情况:
Monoid的例子
Monoid是带单位元的结合二元运算,同一类型可以有多种完全不同的Monoid:
Int可以有加法Monoid(单位元0,运算_+_)、乘法Monoid(单位元1,运算_*_)、最大值Monoid(单位元Int.MinValue,运算math.max)等。
import cats.Monoid // 加法Monoid val addMonoid: Monoid[Int] = Monoid.instance(0, _ + _) println(addMonoid.combine(3, 5)) // 8 // 乘法Monoid val multiplyMonoid: Monoid[Int] = Monoid.instance(1, _ * _) println(multiplyMonoid.combine(3, 5)) // 15 // 最大值Monoid val maxMonoid: Monoid[Int] = Monoid.instance(Int.MinValue, math.max) println(maxMonoid.combine(3, 5)) //5
Applicative的例子
Applicative是Monad的弱化版本,同样可以有多个实例。比如List类型构造器:
- 标准Applicative:
ap操作会把每个函数应用到所有元素,结果是所有可能的组合(和标准Monad的flatMap语义一致) - ZipList Applicative:
ap操作会把函数列表和元素列表按位置拉链,每个函数只应用到对应位置的元素
import cats.Applicative import cats.data.ZipList import cats.syntax.all._ // 标准List Applicative val standardApp = Applicative[List].pure(2) |@| Applicative[List].pure(3) map (_ + _) // 结果:List(5) // ZipList Applicative val zipApp = ZipList(List(1, 2, 3)) |@| ZipList(List(10, 20, 30)) map (_ + _) // 结果:ZipList(List(11, 22, 33))
内容的提问来源于stack exchange,提问作者Sebastien Lorber

