Что такое «лифтинг» в Scala?

207

Иногда, когда я читал статьи в экосистеме Scala, я читал термин "подъем" / "поднял". К сожалению, не объяснено, что именно это означает. Я сделал некоторые исследования, и кажется, что лифтинг имеет какое-то отношение к функциональным ценностям или что-то в этом роде, но я не смог найти текст, который объясняет, что действительно делает подъем в дружественном для новичков.

Существует дополнительная путаница в рамках Lift, которая имеет отрыв от своего имени, но это не помогает ответить на вопрос.

Что такое "подъем" в Scala?

Теги:
functional-programming
function
lifting

4 ответа

219
Лучший ответ

Существует несколько способов использования:

частично определённая функция

Помните, что PartialFunction[A, B] - это функция, определенная для некоторого подмножества области A (как определено методом isDefinedAt). Вы можете "поднять" PartialFunction[A, B] на Function[A, Option[B]]. То есть функция, определенная во всем A, но значения которой имеют тип Option[B]

Это выполняется явным вызовом метода lift на PartialFunction.

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)

Методы

Вы можете "поднять" вызов метода в функцию. Это называется эта-расширением (благодаря Бен-Джеймсу для этого). Так, например:

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 = List и метод map)

Мы можем кодировать это свойство следующим образом:

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 ниже (и я только что понял, что это спасло бы меня от написания тонны ненужного кода), термин "лифт" также имеет смысл в Monad Transformers. Напомним, что монадные трансформаторы - это способ "укладки" монад друг над другом (монады не сочиняют).

Так, например, предположим, что у вас есть функция, которая возвращает IO[Stream[A]]. Это можно преобразовать в трансформатор монады StreamT[IO, A]. Теперь вы можете захотеть "поднять" другое значение 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]
  • 11
    Возможно, стоит упомянуть, что «поднятие метода в функцию» часто называют eta-расширением .
  • 7
    Погружаясь дальше в скалаз , лифтинг также подходит по отношению к монадным трансформаторам . Если у меня есть экземпляр MonadTrans T для M и экземпляр Monad для N , то T.liftM можно использовать для поднятия значения типа N[A] до значения типа M[N, A] .
Показать ещё 3 комментария
17

Еще одно использование лифтинга, которое я встретил в документах (не обязательно Scala -related), перегружает функцию из 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.
  • 6
    @BenJames Правда, правда. В мою защиту: ответа oxbow_lakes еще не было, когда я начал писать свой.
13

Обратите внимание, что любая коллекция, которая расширяет 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

Это показывает аккуратный подход, чтобы избежать исключений из индекса за пределы.

2

Существует также развязка, которая является обратным процессом подъема.

Если подъем определяется как

превращение частичной функции PartialFunction[A, B] в общую функция A => Option[B]

тогда разблокировка

превращение полной функции A => Option[B] в частичную функцию PartialFunction[A, B]

Scala стандартная библиотека определяет Function.unlift как

def unlift[T, R](f: (T) ⇒ Option[R]): PartialFunction[T, R]

Например, библиотека play-json предоставляет unlift, чтобы помочь при построении JSON serialisers:

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))

Ещё вопросы

Сообщество Overcoder
Наверх
Меню