Co to jest „podnoszenie” w Scali?

252

Czasami, gdy czytam artykuły w ekosystemie Scala, czytam termin „podnoszenie” / „podniesienie”. Niestety nie wyjaśniono, co to dokładnie znaczy. Przeprowadziłem pewne badania i wydaje się, że podnoszenie ma coś wspólnego z wartościami funkcjonalnymi lub coś w tym rodzaju, ale nie byłem w stanie znaleźć tekstu, który wyjaśnia, czym właściwie jest podnoszenie w przyjazny sposób dla początkujących.

Istnieje dodatkowe zamieszanie w strukturze Lift , która ma w nazwie nazwę lift, ale nie pomaga odpowiedzieć na pytanie.

Co to jest „podnoszenie” w Scali?

użytkownik573215
źródło

Odpowiedzi:

290

Istnieje kilka zastosowań:

Częściowa funkcja

Pamiętaj, że a PartialFunction[A, B]jest funkcją zdefiniowaną dla niektórych podzbiorów domeny A(określonych przez isDefinedAtmetodę). Możesz „podnieść” a PartialFunction[A, B]do Function[A, Option[B]]. Oznacza to, że funkcja zdefiniowana nad cała of Aale których wartości są typuOption[B]

Odbywa się to poprzez jawne wywołanie metody liftna 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)

Metody

Możesz „podnieść” wywołanie metody do funkcji. Nazywa się to eta-ekspansją (dzięki za to Benowi Jamesowi). Na przykład:

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

Podnosimy metodę do funkcji, stosując znak podkreślenia

scala> val f = times2 _
f: Int => Int = <function1>

scala> f(4)
res0: Int = 8

Zwróć uwagę na zasadniczą różnicę między metodami i funkcjami. res0jest instancją (tj. wartością ) typu (funkcji)(Int => Int)

Functors

Funktor (zgodnie z definicją scalaz ) jest jakiś „pojemnik” (używam tego terminu bardzo luźno), Ftak, że jeśli mamy F[A]i funkcję A => B, to możemy dostać w swoje ręce F[B](myślę, na przykład, F = Lista mapmetoda )

Możemy zakodować tę właściwość w następujący sposób:

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

Jest to izomorficzne w stosunku do możliwości „podniesienia” funkcji A => Bdo dziedziny funktora. To jest:

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

To znaczy, jeśli Fjest funktorem, a my mamy funkcję A => B, mamy funkcję F[A] => F[B]. Możesz spróbować zaimplementować tę liftmetodę - jest to dość trywialne.

Transformatory Monad

Jak mówi poniżej hcoopz (i właśnie zdałem sobie sprawę, że uratowałoby mnie to od napisania mnóstwa niepotrzebnego kodu), termin „lift” ma również znaczenie w Monad Transformers . Przypomnij sobie, że transformatory monadowe są sposobem „układania” monad jeden na drugim (monady się nie komponują).

Załóżmy na przykład, że masz funkcję, która zwraca an IO[Stream[A]]. Można to przekonwertować na transformator monadowy StreamT[IO, A]. Teraz możesz chcieć „podnieść” jakąś inną wartość i IO[B]być może dlatego, że jest to również StreamT. Możesz albo napisać to:

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

Albo to:

iob.liftM[StreamT]

rodzi się pytanie: dlaczego chcę przekonwertować plik IO[B]na StreamT[IO, B]? . Odpowiedź brzmiałaby: „skorzystać z możliwości kompozycji”. Powiedzmy, że masz funkcję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]
oxbow_lakes
źródło
12
Warto wspomnieć, że „podniesienie metody do funkcji” jest często nazywane rozszerzeniem eta .
Ben James
7
Zagłębiając się w skala , podnoszenie pojawia się również w odniesieniu do transformatorów monadowych . Jeśli mam MonadTransinstancję Tdla Mi Monadinstancję dla N, to T.liftMmożna go użyć do podniesienia wartości typu N[A]do wartości typu M[N, A].
846846846
Dzięki Ben, hcoopz. Zmodyfikowałem odpowiedź
oxbow_lakes
Idealny! Jeszcze jeden powód, aby powiedzieć: Scala - najlepszy. Który można przenieść do Martina Odersky & Co - najlepszy. Chciałbym nawet do tego wykorzystać liftM, ale nie udało mi się zrozumieć, jak to zrobić poprawnie. Chłopaki, jesteście rockowi!
Dmitrij Bespalow,
3
W sekcji Metody ... res0 jest instancją (tj. Wartością) typu (funkcji) (Int => Int) ... Nie powinna to fbyć instancja, prawda res0?
srzhio
21

Innym zastosowaniem podnoszenia , na które natknąłem się w papierach (niekoniecznie związanych ze Scalą), jest przeciążanie funkcji za f: A -> Bpomocą f: List[A] -> List[B](lub zestawów, multisetów, ...). Jest to często używane w celu uproszczenia formalizacji, ponieważ wtedy nie ma znaczenia, czy fzostanie zastosowane do pojedynczego elementu czy do wielu elementów.

Tego rodzaju przeciążenie często dokonuje się deklaratywnie, np.

f: List[A] -> List[B]
f(xs) = f(xs(1)), f(xs(2)), ..., f(xs(n))

lub

f: Set[A] -> Set[B]
f(xs) = \bigcup_{i = 1}^n f(xs(i))

lub bezwzględnie, np.

f: List[A] -> List[B]
f(xs) = xs map f
Malte Schwerhoff
źródło
5
Jest to „podnoszenie na funktor”, które opisuje oxbow_lakes.
Ben James
6
@BenJames True rzeczywiście. W mojej obronie: odpowiedzi oxbow_lakes jeszcze nie było, kiedy zacząłem pisać moje.
Malte Schwerhoff
20

Zauważ, że każda kolekcja, która się rozszerza PartialFunction[Int, A](jak wskazano w oxbow_lakes) może zostać zniesiona; tak na przykład

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

co zamienia funkcję częściową w funkcję całkowitą, na której odwzorowywane są wartości niezdefiniowane w kolekcji None,

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

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

Ponadto,

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

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

To pokazuje zgrabne podejście do unikania indeksu poza wyjątkami.

wiąz
źródło
6

Istnieje również unlifting , który jest odwrotnym procesem do podnoszenia.

Jeśli podnoszenie jest zdefiniowane jako

zamieniając funkcję częściową w funkcję PartialFunction[A, B]całkowitąA => Option[B]

wtedy unlifting jest

zamieniając funkcję całkowitą w funkcję A => Option[B]częściową PartialFunction[A, B]

Standardowa biblioteka Scala definiuje Function.unliftjako

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

Na przykład biblioteka play-json zapewnia unlift, aby pomóc w budowie serializatorów 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))
Mario Galic
źródło