Qu'est-ce que «soulever» à Scala?

253

Parfois, lorsque je lis des articles dans l'écosystème Scala, je lis le terme «levage» / «levé». Malheureusement, il n'est pas expliqué ce que cela signifie exactement. J'ai fait des recherches, et il semble que le levage a quelque chose à voir avec des valeurs fonctionnelles ou quelque chose comme ça, mais je n'ai pas pu trouver de texte qui explique en quoi consiste le levage d'une manière conviviale pour les débutants.

Il y a une confusion supplémentaire à travers le framework Lift qui a levage dans son nom, mais cela n'aide pas à répondre à la question.

Qu'est-ce que «soulever» à Scala?

user573215
la source

Réponses:

290

Il y a quelques utilisations:

Fonction partielle

N'oubliez pas que a PartialFunction[A, B]est une fonction définie pour un sous-ensemble du domaine A(comme spécifié par la isDefinedAtméthode). Vous pouvez "soulever" un PartialFunction[A, B]dans un Function[A, Option[B]]. Autrement dit, une fonction définie sur l' ensemble de Amais dont les valeurs sont de typeOption[B]

Cela se fait par l'invocation explicite de la méthode liftsur 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)

Les méthodes

Vous pouvez "soulever" une invocation de méthode dans une fonction. C'est ce qu'on appelle eta-expansion (merci à Ben James pour cela). Ainsi, par exemple:

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

Nous élevons une méthode dans une fonction en appliquant le trait de soulignement

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

scala> f(4)
res0: Int = 8

Notez la différence fondamentale entre les méthodes et les fonctions. res0est une instance (c'est-à-dire une valeur ) de type (fonction)(Int => Int)

Foncteurs

Un foncteur (tel que défini par scalaz ) est un "conteneur" (j'utilise le terme de façon très vague), de Fsorte que, si nous avons une F[A]fonction et une A => B, nous pouvons mettre la main sur unF[B] (pensez, par exemple, F = Listet la mapméthode )

Nous pouvons encoder cette propriété comme suit:

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

Ceci est isomorphe à la possibilité de "lever" la fonction A => B dans le domaine du foncteur. C'est:

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

Autrement dit, si Fest un foncteur, et nous avons une fonction A => B, nous avons une fonctionF[A] => F[B] . Vous pourriez essayer d'implémenter la liftméthode - c'est assez trivial.

Transformateurs de monade

Comme le dit hcoopz ci-dessous (et je viens de réaliser que cela m'aurait évité d'écrire une tonne de code inutile), le terme "lift" a également une signification dans Monad Transformers . Rappelons qu'un transformateur de monade est un moyen «d'empiler» les monades les unes sur les autres (les monades ne composent pas).

Par exemple, supposons que vous ayez une fonction qui renvoie un IO[Stream[A]]. Celui-ci peut être converti en transformateur monade StreamT[IO, A]. Maintenant, vous voudrez peut-être "soulever" une autre valeur et IO[B]peut-être que c'est aussi un StreamT. Vous pouvez soit écrire ceci:

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

Ou ca:

iob.liftM[StreamT]

cela soulève la question: pourquoi est-ce que je veux convertir un IO[B]en un StreamT[IO, B]? . La réponse serait "de profiter des possibilités de composition". Disons que vous avez une fonctionf: (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
la source
12
Il peut être utile de mentionner que «élever une méthode à une fonction» est souvent appelé eta-expansion .
Ben James
7
En approfondissant le scalaz , le levage se pose également en relation avec les transformateurs monades . Si j'ai une MonadTransinstance Tpour Met une Monadinstance pour N, alors T.liftMpeut être utilisé pour soulever une valeur de type N[A]en une valeur de type M[N, A].
846846846
Merci Ben, hcoopz. J'ai modifié la réponse
oxbow_lakes
Parfait! Juste une raison de plus pour dire: Scala - le meilleur. Ce qui pourrait être porté à Martin Odersky & Co - le meilleur. Je voudrais même l'utiliser liftMpour cela, mais je n'ai pas réussi à comprendre comment le faire correctement. Les gars, vous êtes rock!
Dmitry Bespalov
3
Dans la section Méthodes ... res0 est une instance (c'est-à-dire une valeur) de type (fonction) (Int => Int) ... Ne devrait pas fêtre une instance, non res0?
srzhio
21

Une autre utilisation de la levée que j'ai rencontrée dans des articles (pas nécessairement liés à Scala) est la surcharge d'une fonction f: A -> Bavec f: List[A] -> List[B](ou ensembles, multisets, ...). Ceci est souvent utilisé pour simplifier les formalisations car il n'a alors pas d'importance s'il fest appliqué à un élément individuel ou à plusieurs éléments.

Ce type de surcharge est souvent effectué de manière déclarative, par exemple,

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

ou

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

ou impérativement, par exemple,

f: List[A] -> List[B]
f(xs) = xs map f
Malte Schwerhoff
la source
5
Il s'agit du «soulèvement dans un foncteur» décrit par oxbow_lakes.
Ben James
7
@BenJames True en effet. Pour ma défense: la réponse d'oxbow_lakes n'était pas encore là quand j'ai commencé à écrire la mienne.
Malte Schwerhoff
20

Notez que toute collection qui s'étend PartialFunction[Int, A](comme indiqué par oxbow_lakes) peut être levée; ainsi par exemple

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

qui transforme une fonction partielle en fonction totale dans laquelle des valeurs non définies dans la collection sont mappées None,

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

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

De plus,

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

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

Cela montre une approche soignée pour éviter les exceptions d' index hors limites .

orme
la source
6

Il y a aussi le déliftage , qui est le processus inverse de la levée.

Si le levage est défini comme

transformer une fonction partielle en fonction PartialFunction[A, B]totaleA => Option[B]

alors le décollage est

transformer une fonction totale en fonction A => Option[B]partielle PartialFunction[A, B]

La bibliothèque standard Scala se définit Function.unliftcomme

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

Par exemple, la bibliothèque play-json fournit unlift pour faciliter la construction des sérialiseurs 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
la source