miércoles, 28 de junio de 2017

Parámetro implícito en Scala

"Este es el primer paso para implementar la abstracción contextual en Scala". ¿Qué quiero decir con esto?

Abstracción: La capacidad de nombrar un concepto y usar sólo el nombre después.

Contextual: Un fragmento de un programa produce resultados o resultados en algún contexto. Nuestros lenguajes de programación son muy buenos para describir y abstraer lo que se producen. Pero casi no hay nada disponible para abstraer sobre las entradas que los programas obtienen de su contexto. Un ejemplo para resolver esto es la inyección de dependencias.

Los tipos de funciones implícitas son una forma sorprendentemente sencilla y general de hacer abstractables los patrones de codificación que resuelven estas tareas, reduciendo el código clásico y aumentando la aplicabilidad. Para entender esto debemos entender los parámetro implícito

En un lenguaje funcional, las entradas a un cálculo se expresan más naturalmente como parámetros. Uno podría simplemente aumentar las funciones para tomar parámetros adicionales que representan configuraciones, capacidades, diccionarios, o cualquier otro dato contextual que las funciones necesiten. El único inconveniente de esto es que a menudo hay una gran distancia en el gráfico de llamadas entre la definición de un elemento contextual y el sitio donde se utiliza. En consecuencia, se hace tedioso definir todos esos parámetros intermedios y pasarlos a lo largo de donde finalmente se consumen.

Los parámetros implícitos solucionan la mitad del problema. Ellos no tienen que ser propagados usando código de repetitivo; el compilador se encarga de eso. Esto los hace prácticos en muchos escenarios donde los parámetros simples serían demasiado engorrosos. Los parámetros implícitos también son muy útiles como un mecanismo general de paso del contexto.

Digamos que queremos escribir algún código que está diseñado para ejecutarse en una transacción. En aras de la ilustración, aquí hay una clase de transacción simple:

class Transaction {
  private val log = new ListBuffer[String]
 
  def println(s: String): Unit = log += s
 
  private var aborted = false
 
  private var committed = false

  def abort(): Unit = { aborted = true }

  def isAborted = aborted

  def commit(): Unit =
    if (!aborted && !committed) {
      Console.println("******* log ********")
      log.foreach(Console.println)
      committed = true
    }

}

La transacción encapsula un registro, al que se pueden imprimir mensajes. Puede estar en uno de los tres estados: run, commit o abort. Si la transacción se confirma, imprime el registro almacenado en la consola.

El método de transacción permite ejecutar un determinado código dentro de una transacción recién creada:

 def transaction[T](op: Transaction => T) = {
    val trans: Transaction = new Transaction
    op(trans)
    trans.commit()
  }

La transacción actual se debe pasar a lo largo de una cadena de llamadas a todos los lugares que necesitan para acceder a ella. Para ilustrar esto, aquí están tres funciones f1, f2 y f3 que se llaman entre sí, y también acceder a la transacción actual. La manera más conveniente de lograr esto es pasar la transacción actual como un parámetro implícito.

 def f1(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"first step: $x")
    f2(x + 1)
  }
  def f2(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"second step: $x")
    f3(x * x)
  }
  def f3(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"third step: $x")
    if (x % 2 != 0) thisTransaction.abort()
    x
  }

El programa principal llama a f1 en un nuevo contexto de transacción e imprime su resultado:

  def main(args: Array[String]) = {
    transaction {
      implicit thisTransaction =>
        val res = f1(args.length)
        println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
    }
  }