不能使用 flatMap 作为自写 monad 实例的扩展方法 问题 1. 您正在使用非参数类型的非不透明类型别名解决方案 1. 使用不透明类型别名问题 2这种类型不是 monad

问题描述

我尝试在 WriterT 上使用 flatMap 并且成功了。

所以问题可能出在我的类型上,但我找不到它有什么问题。

import cats.Monad
import cats.Syntax.flatMap._

object Main extends App {
    type Optional[A] = A | Null
    
    val maybeInt1: Optional[Int] = 1
    val maybeInt2: Optional[Int] = null
    
    given Monad[Optional] with {
        def pure[A](x: A): Optional[A] = x
        def flatMap[A,B](fa: Optional[A])(f: A => Optional[B]): Optional[B] = {
            fa match {
                case null => null
                case a: A => f(a)
            }
        }
        def tailRecM[A,B](a: A)(f: A => Optional[Either[A,B]]): Optional[B] = {
            f(a) match {
                case null     => null
                case Left(a1) => tailRecM(a1)(f)
                case Right(b) => b
            }
        }
    }

    def f[F[_]: Monad,A,B](a: F[A],b: F[B]) = a.flatMap(_ => b)
    
    println(Monad[Optional].flatMap(maybeInt1)(_ => maybeInt2)) //OK: null
    println(f[Optional,Int,Int](maybeInt1,maybeInt2)) // OK: null
    println(maybeInt1.flatMap(_ => maybeInt2)) // Compilation Error
}

错误是:

value flatMap 不是 Main.Optional[Int] 的成员。
尝试了一个扩展方法,但无法完全构造:
cat.Syntax.flatMap.toFlatMapOps([A] =>> Any),A(given_Monad_Optional)

解决方法

你的定义有几个问题。

问题 1. 您正在使用非参数类型的非不透明类型别名

type Optional[A] = A | Null 是一个类型表达式,会尽快扩展。 当您使用它作为结果类型时,您实际得到的是

val maybeInt1: Int | Null = 1
val maybeInt2: Int | Null = null

所以当编译器有类似的东西

implicit def toFlatMapOps[F[_],A](fa: F[A])(implicit F: Monad[F]): MonadOps[F,A]

从 Scala 2 库或 Scala 3 中的等效扩展导入, 终于到了maybeOption.flatMap
然后尝试应用以前的扩展方法,
它无法对表达式进行类型检查 toFlatMapOps(maybeInt1).flatMap(_ => maybeInt2)

所以现在你有Int | Null作为参数,因为Optional已经展开,需要计算对应的F[_]A,它有很多解,比如>

  1. F[X] = Int | X,A = Null
  2. F[X] = X | Null,A = Int
  3. F[X] = A | Null,A = Nothing
  4. F[X] = [X] =>> X,A = Int | Null

所以 Scala 自然不会尝试猜测。

尽管scala 3编译器在这里可以使用隐式\上下文值等附加信息,但是这里匹配Monad的优先级最高的隐式值是

    given Monad[Optional]

现在可以尝试申请 toFlatMapOps[F = Maybe](maybeInt1 : Int | Null) 然后有了 F[X] = X | Null,您需要计算 A,知道 F[A] = Null | A 并且它也有许多合理的解决方案

  1. A = Int
  2. A = Int | Null

所以即使 Scala 不会在第一步失败,它也会卡在这里

解决方案 1. 使用不透明类型别名

scalacOptions += "-Yexplicit-nulls" 添加到您的 sbt 配置并尝试此代码

import cats.Monad
import cats.syntax.flatMap.given

object Optional:
  opaque type Optional[+A] >: A | Null = A | Null

  extension [A] (oa: Optional[A]) def value : A | Null = oa

  given Monad[Optional] with 
    def pure[A](x: A): Optional[A] = x
    def flatMap[A,B](fa: A | Null)(f: A => B | Null) = 
      if fa == null then null else f(fa)       
    def tailRecM[A,B](a: A)(f: A => Optional[Either[A,B]]): Optional[B] = 
        f(a) match 
            case null     => null
            case Left(a1) => tailRecM(a1)(f)
            case Right(b) => b

type Optional[+A] = Optional.Optional[A]   

@main def run =    
    val maybeInt1: Optional[Int] = 1
    val maybeInt2: Optional[Int] = null   

    def f[F[_]: Monad,A,B](a: F[A],b: F[B]) = a.flatMap(_ => b)
    
    println(Monad[Optional].flatMap(maybeInt1)(_ => maybeInt2)) //OK: null
    println(f(maybeInt1,maybeInt2)) // OK: null
    println(maybeInt1.flatMap(_ => maybeInt2)) // Compilation Error

问题 2。这种类型不是 monad

即使在这个固定版本中,Optional[A] 也无法满足基本的一元法则 考虑这个代码

def orElse[F[_],A](fa: F[Optional[A]])(default: => F[A])(using F: Monad[F]): F[A] = 
    fa.map(_.value).flatMap(fb => if fb == null then default else F.pure(fb : A))   

def filterOne(x: Int): Optional[Int] = if x == 1 then null else x - 1

println(orElse(maybeInt1.map(filterOne))(3)) 

第一种方法尝试使用给定的计算出的 monadic 值解决缺失值,第二种方法只是过滤掉那些。 那么,当评估这样的事情时,我们期望看到什么?

orElse(maybeInt1.map(filterOne))(3)

我们可能取非空,然后用缺失的位置替换 1,然后立即使用提供的 3 修复它。因此,我希望看到 3,但实际上,我们获得 null 作为结果,因为 null 在包装值内考虑为 flatMap 期间外部 Optional 的缺失分支。 这是因为这种天真的定义类型违反了 left-identity law

更新 关于comment by @n-pronouns-m 这个定义如何违反左身份法。 左身份声明

pure(a).flatMap(f) == f(a) 
for all types A,B,and values a: A,f: A => Optional[B]

所以让我们取 A = Optional[Int],B = Int,A = null,f(a) = if a == null then 3 else 2

pure(a) 仍然为 null,flatMap 为第一个参数中的每一个返回 null,所以 pure(a).flatMap(f) == nullf(a) == 3

,

Odersky 的 response 主题

如果您将 Null 设为单独的类,或者您使用 -Yexplicit-nulls。

事物为 Null 的方式是底部类型。所以每个类的实例 Optional[C] 实际上是 C。

我尝试将 Optional 的定义更改为

type Optional[A] = A

然后也找不到隐式。所以 问题看起来根本不是联合类型的问题。如果 Optional[A] 定义了一个真正有效的联合。它看起来很像一个 HK 类型推断的局限性在于它不能推断身份。 我认为这实际上是预期的。