在 for comprehension 中切换 monad

Switching monad within a for comprehension

我正在尝试使用 for-comprehension 来处理 Options 的实例。但是,作为我的最后一步,我使用的函数不是 return 选项而是 List[Option[Something]](注意,不是 OptionT[List, Something])。如果列表中的所有选项都是 Some 的实例,我只想继续。有没有一种干净的方法可以将其作为 flatMap 调用的一部分进行处理?目前我是这样设置的:

for {
  o1     <- businessLogic1
  o2     <- businessLogic2(o1)
  rawList = businessLogic3(o2)
  list   <- if (rawList.forall(_.isDefined)) 
              Some(rawList.map(_.get)) 
            else 
              None
} { /* processing list */ }

如果有人问这个问题,我很抱歉,但我不知道我正在寻找的术语,所以我无法在这方面有效地搜索网络。

如果我遗漏了重要的理论要点,请提供有关事物我似乎难以理解的文献或其他资源的指针。

这是一种更简洁的测试方式:

for {
  o1 <- businessLogic1
  o2 <- businessLogic2(o1)
  rawList = businessLogic3(o2) if rawList.forall(_.isDefined)
  list = rawList.flatten
} {/* processing list */}

或者您可以先创建扁平化列表,然后比较大小:

for {
  o1 <- businessLogic1
  o2 <- businessLogic2(o1)
  rawList = businessLogic3(o2)
  list = rawList.flatten if list.size == rawList.size
} {/* processing list */}

如果 /* process list */ 只是为了副作用而完成,即没有 yield,最终目标是处理 list 的各个元素,从它们的 [=14] 中解包=]状态,那么你可能会做这样的事情。

for {
  o1 <- businessLogic1
  o2 <- businessLogic2(o1)
  lst = businessLogic3(o2)
  if lst.forall(_.nonEmpty)
  x <- lst.flatten
} {/* process x */}

简短的回答,你不能混用 Monad。请记住 for 只是 flatMap 的糖语法,它具有像 (F[A], A => F[B]) => F[B][=28= 这样的签名]
所以你必须在外面和里面有相同的 Monad。

另外,如果你有一个选项列表并且你想要一个列表选项,你可以使用 sequence (from cats)

def processList(rawList: List[Foo]): List[Bar] = ???

val result: Option[List[Bar]] = for {
  o1      <- businessLogic1
  o2      <- businessLogic2(o1)
  rawList <- businessLogic3(o2).sequence
} yield processList(rawList)

如果您不使用 cats 并且不想包含它,您可以创建自己的 sequence 扩展方法 很容易只是一个选项列表。

implicit class ListOps[A](private val list: List[Option[A]]) extends AnyVal {
  def sequence: Option[List[A]] = {
    @annotation.tailrec
    def loop(remaining: List[Option[A]], acc: List[A]): Option[List[A]] =
      remaining match {
        case Some(a) :: xs => loop(remaining = xs, a :: acc)
        case None :: _     => None
        case Nil           => Some(acc.reverse)
      }
    loop(remaining = list, acc = List.empty)
  }
}