Composición de funciones en Scala

La idea principal en programación funcional es llevar a cabo el proceso mediante la aplicación de funciones sin guardar estado (sin modificar los valores originalmente proveídos). Por ello, y al igual que en álgebra, es posible crear una función compuesta, que no es nada más que la aplicación sucesiva de dos funciones. Obviamente, hay que tener cuidado en que el valor de retorno de la primera función sea del tipo del argumento que la segunda función espera.

Wikipedia muestra una imagen que ilustra claramente el concepto de composición de funciones.

Compfun

La composición de funciones se define con el signo ・, que indica que la primera función se aplica al resultado de aplicar la segunda función al parámetro recibido. Usando el ejemplo de la imagen y definiendo h = g ・f, tenemos que h(a) = @, puesto que h se define aplicando g(f(a)).

El mismo concepto se aplica en programación funcional. Supongamos que tenemos las siguientes funciones:

 def toInt(s: String) = s.toInt
 def addOne(i: Int) = i + 1
 def by4(i: Int) = i * 4

Scala tiene 2 operadores para componer funciones: compose y andThen. La diferencia es que mientras f compose g es f(g(x)), f andThen g es g(f(x)). Por tanto, si queremos definir una función que primero convierta la cadena recibida en entero y después le sume uno a ese entero, podemos hacerlo de dos formas:

  /*
    With compose:
    composed1 = addOne(toInt(x))
   */
  val composed1 = addOne _ compose toInt

  /*
    With andThen
    composed2 = addOne(toInt(x))
   */
  val composed2 = toInt _ andThen addOne

Es entonces fácil ver que estas 2 funciones compuestas (composed3 y composed4) no son lo mismo:

val composed3 = addOne _ compose by4
val composed4 = addOne _ andThen by4

Aplicando las funciones compuestas definidas:

val strFunctions = List(composed1, composed2)
val intFunctions = List(composed3, composed4)

strFunctions foreach (f => println(f("3")))
intFunctions foreach (f => println(f(4)))

Y los resultados son:

4
4
17
20

El concepto de composición de funciones es realmente muy simple, y facilita mucho la creación de nuevas funciones basadas en algunas que ya tengamos definidas.

Como nota adicional, en Haskell es mucho más fácil crear una función compuesta, ya que solo es necesario usar “.”:

import Control.Monad

main = do
 f ← liftM(composed1) $ getLine
 print f
 where composed1 = addOne . toInt
 
addOne ∷ Int → Int
addOne = (+1)

toInt ∷ String → Int
toInt x = read x ∷ Int

El “.” en Haskell funciona como el compose en Scala; además, hay diferentes formas de hacer un programa como el de arriba, pero en este caso escogí usar liftM.

Cabe mencionar que en Scala también es posible componer 2 funciones monádicas en una usando algo llamado composición Kleisli, pero hay que usar Scalaz para tener acceso a ella. Y no es necesario entender teoría de categorías para usarlas; simplemente hay que cuidar los parámetros y los tipos de datos que regresan las funciones.  Esto es tema de otro post, pero lo pongo aquí por si alguien se interesa pueda ir investigando por su cuenta.