function - ¿Qué es "levantar" en Scala?
functional-programming lifting (4)
A veces, cuando leo artículos en el ecosistema de Scala, leo el término "levantar" / "levantar". Desafortunadamente, no se explica lo que eso significa exactamente. Investigué un poco, y parece que levantar tiene algo que ver con valores funcionales o algo así, pero no pude encontrar un texto que explique de qué se trata realmente el levantamiento de una manera amigable para los principiantes.
Existe una confusión adicional a través del marco Lift que tiene elevación en su nombre, pero no ayuda a responder la pregunta.
¿Qué es "levantar" en Scala?
Hay algunos usos:
Función parcial
Recuerde que PartialFunction[A, B]
es una función definida para algún subconjunto del dominio A
(según lo especificado por el método isDefinedAt
). Puede "levantar" una PartialFunction[A, B]
en una Function[A, Option[B]]
. Es decir, una función definida sobre la totalidad de A
pero cuyos valores son de tipo Option[B]
Esto se hace mediante la invocación explícita del método lift
en 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)
Métodos
Puedes "levantar" una invocación de método en una función. Esto se llama expansión eta (gracias a Ben James por esto). Así por ejemplo:
scala> def times2(i: Int) = i * 2
times2: (i: Int)Int
Levantamos un método en una función aplicando el subrayado
scala> val f = times2 _
f: Int => Int = <function1>
scala> f(4)
res0: Int = 8
Note la diferencia fundamental entre métodos y funciones. res0
es una instancia (es decir, es un valor ) del tipo (Int => Int)
función) (Int => Int)
Functores
Un funtor (tal como lo define scalaz ) es un "contenedor" (uso el término de manera muy relajada), F
tal manera que, si tenemos una F[A]
y una función A => B
, entonces podemos obtener una F[B]
(piense, por ejemplo, F = List
y el método de map
)
Podemos codificar esta propiedad de la siguiente manera:
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
Esto es isomorfo para poder "levantar" la función A => B
en el dominio del functor. Es decir:
def lift[F[_]: Functor, A, B](f: A => B): F[A] => F[B]
Es decir, si F
es un functor, y tenemos una función A => B
, tenemos una función F[A] => F[B]
. Puedes intentar implementar el método de lift
, es bastante trivial.
Transformadores de mónada
Como dice hcoopz a continuación (y me acabo de dar cuenta de que esto me habría salvado de escribir una tonelada de código innecesario), el término "levantar" también tiene un significado dentro de Monad Transformers . Recuerde que los transformadores de una mónada son una forma de "apilar" las mónadas una encima de la otra (las mónadas no se componen).
Por ejemplo, supongamos que tiene una función que devuelve un IO[Stream[A]]
. Esto se puede convertir al transformador de mónada StreamT[IO, A]
. Ahora es posible que desee "levantar" algún otro valor un IO[B]
quizás para que también sea un StreamT
. Usted podría escribir esto:
StreamT.fromStream(iob map (b => Stream(b)))
O esto:
iob.liftM[StreamT]
esto plantea la pregunta: ¿por qué quiero convertir un IO[B]
en un StreamT[IO, B]
? . La respuesta sería "aprovechar las posibilidades de composición". Digamos que tienes una función 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]
Otro uso del levantamiento que he encontrado en documentos (no necesariamente relacionados con Scala) es sobrecargar una función de f: A -> B
con f: List[A] -> List[B]
(o conjuntos, conjuntos múltiples, ...). Esto se usa a menudo para simplificar las formalizaciones porque entonces no importa si f
se aplica a un elemento individual o a múltiples elementos.
Este tipo de sobrecarga a menudo se realiza de forma declarativa, por ejemplo,
f: List[A] -> List[B]
f(xs) = f(xs(1)), f(xs(2)), ..., f(xs(n))
o
f: Set[A] -> Set[B]
f(xs) = /bigcup_{i = 1}^n f(xs(i))
o imperativamente, por ejemplo,
f: List[A] -> List[B]
f(xs) = xs map f
También existe el desvío , que es el proceso inverso al levantamiento.
Si el levantamiento se define como
PartialFunction[A, B]
una función parcialPartialFunction[A, B]
en una función totalA => Option[B]
entonces el desentrañar es
convertir una función total
A => Option[B]
en una función parcialPartialFunction[A, B]
La biblioteca estándar de Scala define Function.unlift
como
def unlift[T, R](f: (T) ⇒ Option[R]): PartialFunction[T, R]
Por ejemplo, la biblioteca play-json proporciona unlift para ayudar con la construcción de los serializadores 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))
Tenga en cuenta que cualquier colección que se extienda a PartialFunction[Int, A]
(como lo indica oxbow_lakes) se puede levantar; asi por ejemplo
Seq(1,2,3).lift
Int => Option[Int] = <function1>
que convierte una función parcial en una función total donde los valores no definidos en la colección se asignan a None
,
Seq(1,2,3).lift(2)
Option[Int] = Some(3)
Seq(1,2,3).lift(22)
Option[Int] = None
Además,
Seq(1,2,3).lift(2).getOrElse(-1)
Int = 3
Seq(1,2,3).lift(22).getOrElse(-1)
Int = -1
Esto muestra un enfoque ordenado para evitar excepciones de índices fuera de límites .