Curry, por favor…

Uno de los aportes de Scala de los que no podemos dejar pasar la ocasión de hablar acerca de ellos es el currying.

4252082-curry

La teoría

Si tenemos una función (T,U) => V, currificar la función supone descomponer la función en otra más sencilla que permite construir el resultado de manera incremental. En este caso pasaríamos a tener una función T => (U => V), es decir, a partir de un T obtenemos una función que solo necesita un U para generar un V. ¿Lioso? Veámoslo mejor con el siguiente ejemplo.

Supongamos que tenemos una case class que modela un estudiante:

case class Student(
  name: String,
  age: Int,
  enrolled: Boolean)

Podríamos tener adicionalmente un método que nos instanciara un estudiante como, por ejemplo, el método apply que se ha generado automáticamente para la case class:

//Auto generated code below
object Student {

  def apply(
    name: String, 
    age: Int, 
    enrolled: Boolean): Student =
    new Student(name, age, enrolled)

}

Utilizando dicho método, podemos construir un estudiante como sigue:

Student("john", 18, enrolled=true)

Hasta aquí fácil. Ahora supongamos la siguiente situación:

En nuestro proceso de admisión de alumnos, el candidato tiene que pasar por una serie de ventanillas para aportar su documentación poco a poco (en la ventanilla A indicaría el nombre, en la ventanilla B indicaría la edad; y en la ventanilla C le daríamos el visto bueno, o no, para formar parte de la escuela).

Primera aproximación: Hacer clases es gratis

Podemos definir nuestras ventanillas como alias de funciones transformadoras. Es decir:

type WindowA = String => NotAStudientYet
type WindowB = (NotAStudentYet, Int) => AlmostAStudent
type WindowC = (AlmostAStudent, Boolean) => Student

case class NotAStudentYet(name: String)
case class AlmostAStudent(name: String, age: Int)

Fijaros que, por una parte, las ventanillas se representan mediante funciones.
La primera ventanilla es una función que, a partir de un nombre, genera algo “que aún no es estudiante”.
La segunda ventanilla, teniendo algo “que aún no es estudiante” y recibiendo una edad, devuelve algo que “casi es un estudiante”.
Y la última ventanilla recibe algo “que casi es un estudiante” y una aprobación de admisión (aprobada o denegada) y genera un estudiante.

Para ello, en esta primera aproximación, hemos generado dos case classes nuevas, que van a servir de acumuladores, para finalmente crear un estudiante.

La implementación sería algo del estilo:

val windowA: WindowA = 
  (name) => 
    NotAStudentYet(name)

val windowB: WindowB = 
  (notStudent, age) => 
    AlmostStudent(notStudent.name, age)

val windowC: WindowC = 
  (almost, enrolled) => 
    Student(almost.name, almost.age, enrolled)

…sinceramente, no es posible que para hacer tal cosa tengamos que definirnos dos clases adicionales. Optemos por dar otro enfoque.

Segunda aproximación: Funciones, funciones everywhere …

Probemos a definir funciones que devuelvan otras funciones (funciones de orden superior):

type WindowA = String => WindowB
type WindowB = Int => WindowC
type WindowC = Boolean => Student

val windowA: WindowA = 
  (name: String) => {
    val windowB: WindowB =
      (age: Int) => {
        val windowC: WindowC =
          (enrolled: Boolean) =>
            Student(name, age, enrolled)
        windowC
      }
    windowB
  }

Fijaros que a partir de pequeñas funciones, vamos dando valores a los parámetros que construirán nuestro estudiante. Es más fácil si intentamos leerlo desde la función más interior a la mas exterior(primero windowC, después windowB y finalmente windowA). Para invocar nuestra función basta con ejecutar:

val student = windowA("john")(18)(true)

Tercera aproximación: ¿Seguro que no existe nada que haga esto?

Por supuesto que lo hay. Dentro del companion de Function en Scala, se encuentra el método curried, cuyo cometido es descomponer una función que recibe N argumentos en N funciones concatenadas, como veíamos al principio del post, y en el último ejemplo.

Para aplicar esta maravilla al ejemplo expuesto bastaría con escribir:

val f = (Sudent.apply _).curried
//f: String => (Int => (Boolean => Student))

f("john")(18)(true)
//Student("john", 18, true)

Existe además la función inversa uncurried, que dadas N funciones encadenadas, por ejemplo, Int => (String => (Boolean => Double))) devuelve una única función que recibe N argumentos: (Int, String, Boolean) => Double:

val myApply = Function.uncurried(f)
//myApply: (String, Int, Boolean) => Student

myApply("john",18,true)
//Student("john",18,true)

Fácil, sencillo y para toda la familia.
Agur de limón 🙂

Anuncios

2 thoughts on “Curry, por favor…

  1. Umm! Imaginemos que en alguna de las “ventanillas” se produjera un error (pe: que la edad estuviera limitada a menores de 25 años). El control de errores haría que la currificación perdiera la elegancia que le ves.

    El problema que planteas lo veo más un caso para usar “monads” que para currificación, o mejor para para un “Future[Student]”.

    Me gusta

    • En efecto, en ningún momento se ha tenido en cuenta la validación en los ejemplos.
      Tuvimos dudas sobre enlazar el tema con la mónada Reader, de la cual habló David en otro post, pero para ser un tema que hemos etiquetado como “beginner”, nos parecía un poco heavy enlazar ambos conceptos.

      Gracias por tu comentario 🙂

      Me gusta

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s