在Scala中,“lifting”是什么?

287
有时候我读Scala生态系统的文章时会看到术语"lifting" / "lifted"。不幸的是,它并没有解释那到底是什么意思。我进行了一些研究,似乎lifting与函数值或类似的东西有关,但我没能找到一篇初学者友好地解释lifting到底是什么的文章。
此外,Lift框架也让问题更加混乱,因为它的名字中包含lifting,但它并不能回答这个问题。
在Scala中,“lifting”是什么?
4个回答

321

这里有几个用途:

PartialFunction

记住,PartialFunction[A, B] 是为域 A 的一些子集(由 isDefinedAt 方法指定)定义的函数。您可以将 PartialFunction[A, B]“提升”到 Function[A, Option[B]]。也就是说,虽然该函数在整个域 A 上有定义,但其值的类型为 Option[B]

这通过在 PartialFunction 上显式调用方法 lift 来完成。

scala> val pf: PartialFunction[Int, Boolean] = { case i if i > 0 => i % 2 == 0}
pf: PartialFunction[Int,Boolean] = <function1>

scala> pf.lift
res1: Int => Option[Boolean] = <function1>

scala> res1(-1)
res2: Option[Boolean] = None

scala> res1(1)
res3: Option[Boolean] = Some(false)

方法

您可以将方法调用“提升”为函数。这被称为eta扩展(感谢@Ben James)。例如:

scala> def times2(i: Int) = i * 2
times2: (i: Int)Int

我们通过应用下划线将一个方法提升为函数。
scala> val f = times2 _
f: Int => Int = <function1>

scala> f(4)
res0: Int = 8

请注意方法和函数之间的根本区别。 res0 是(函数)类型 (Int => Int)实例(即它是一个

函子

scalaz定义的函子是一些“容器”(我使用术语非常宽泛),F,使得如果我们有一个F[A]和一个函数A => B,那么我们就可以得到一个F[B](例如,考虑F = Listmap方法)

我们可以将此属性编码如下:

trait Functor[F[_]] { 
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

这类似于能够将函数 A => B 提升到函子的域中。也就是说:

def lift[F[_]: Functor, A, B](f: A => B): F[A] => F[B]

也就是说,如果F是一个函子,并且我们有一个函数A => B,那么我们就有一个函数F[A] => F[B]。你可以尝试实现lift方法 - 这很简单。

Monad变换器

正如*hcoopz*在下面所说的(我刚意识到这可以节省我大量不必要的代码),术语“lift”在**Monad变换器**中也有一种含义。回想一下,Monad变换器是一种将单子“堆叠”在一起的方法(单子不能组合)。

例如,假设您有一个返回IO [Stream[A]]的函数。这可以转换为单子变换器StreamT[IO, A]。现在,您可能希望将某个其他值提升为IO[B],以便它也是一个StreamT。你可以写成这样:

StreamT.fromStream(iob map (b => Stream(b)))

或者这样:

iob.liftM[StreamT]

这引出了一个问题:为什么我想要将IO[B]转换为StreamT[IO, B]答案是“利用组合的可能性”。假设你有一个函数f: (A, B) => C
lazy val f: (A, B) => C = ???
val cs = 
  for {
    a <- as                //as is a StreamT[IO, A]
    b <- bs.liftM[StreamT] //bs was just an IO[B]
  }
  yield f(a, b)

cs.toStream //is a Stream[IO[C]], cs was a StreamT[IO, C]

13
值得一提的是,“将方法提升为函数”的操作通常被称为 eta扩展 - Ben James
7
进一步探究 scalaz,在与 monad transformers 相关的内容中也出现了 _lifting_。如果我有一个 MonadTrans 实例 T 用于 M 以及一个 Monad 实例用于 N,那么可以使用 T.liftM 将类型为 N[A] 的值提升为类型为 M[N, A] 的值。 - 846846846
谢谢Ben和hcoopz。我已经修改了答案。 - oxbow_lakes
太完美了!这又是一个说Scala是最好的理由。这也可以推广到Martin Odersky和他的团队——他们是最棒的。我甚至想用liftM来表达,但是我没有弄明白如何正确地使用它。你们太棒了! - Dmitry Bespalov
3
方法部分,_...res0是(Int => Int)类型的实例(即它是一个值)..._。f 不应该是实例吗?而不是 res0 吗? - srzhio

27

请注意,任何扩展 PartialFunction[Int, A](如 oxbow_lakes 所指出的)的集合都可以被提升;因此例如:

Seq(1,2,3).lift
Int => Option[Int] = <function1>

将部分函数转化为完全函数,其中未在集合中定义的值被映射到None

Seq(1,2,3).lift(2)
Option[Int] = Some(3)

Seq(1,2,3).lift(22)
Option[Int] = None

此外,

Seq(1,2,3).lift(2).getOrElse(-1)
Int = 3

Seq(1,2,3).lift(22).getOrElse(-1)
Int = -1

这展示了一种避免数组越界异常的简洁方法。


21

我在论文中(不一定与Scala有关)遇到的 lifting 另一个用法是用一个参数为 f: A -> B 的函数重载一个参数为 f: List [A] -> List [B](或集合、多重集等)。这种方法通常用于简化规范,因为它不管是将 f 应用于单个元素还是多个元素都无所谓。

这种重载通常以声明方式执行,例如:

f: List[A] -> List[B]
f(xs) = f(xs(1)), f(xs(2)), ..., f(xs(n))
或者
f: Set[A] -> Set[B]
f(xs) = \bigcup_{i = 1}^n f(xs(i))

或者是命令式的,例如,

f: List[A] -> List[B]
f(xs) = xs map f

5
这是 oxbow_lakes 所描述的"提升为函子"。 - Ben James
7
@BenJames 确实如此。为了辩护自己:当我开始写我的回答时,oxbow_lakes的回答还没有出现。 - Malte Schwerhoff

6

还有一种叫做unlifting的过程,它是lifting的逆过程。

如果将lifting定义为将部分函数PartialFunction[A, B]转换为总函数A => Option[B],那么unlifting就是将总函数A => Option[B]转换为部分函数PartialFunction[A, B]

Scala标准库将Function.unlift定义为:

def unlift[T, R](f: (T) ⇒ Option[R]): PartialFunction[T, R]
例如,play-json库提供unlift来帮助构建JSON序列化器
import play.api.libs.json._
import play.api.libs.functional.syntax._

case class Location(lat: Double, long: Double)

implicit val locationWrites: Writes[Location] = (
  (JsPath \ "lat").write[Double] and
  (JsPath \ "long").write[Double]
)(unlift(Location.unapply))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接