delete

ScalaSQL: Interfaz sencilla para JDBC desde Scala

Desde hace tiempo que Groovy es mi lenguaje favorito para hacer scripts que a veces necesito para interactuar con bases de datos (algunos reportes o modificar ciertos datos, procesamiento por lotes, etc).

Son principalmente dos razones por las que me gusta tanto usar Groovy para este tipo de scripts:

  1. Tengo a mi disposición todo un arsenal de componentes de Java (simplemente con el JDK tengo un montón de cosas) y además todo lo que me ofrece Groovy como lenguaje, encima del JDK de Java (closures, comprehensión de listas, etc).
  2. El componente groovy.sql.Sql me permite interactuar con bases de datos de manera muy sencilla.

En Scala aún no hay un componente similar. Y aunque lo hubiera, no usaría Scala para scripting (ya he escrito al respecto anteriormente). Pero creo que sí hace falta. Si bien existe el hiperútil componente JdbcTemplate de Spring, para el cual además ya existen unas adiciones para Scala (junto con otras cosas de bean factories) en el proyecto ScalaPrimavera, no siempre es posible o deseable comenzar a depender de Spring.

Así que me propuse desarrollar un componente similar al Sql de Groovy, con algunas cosas adicionales que me han resultado muy útiles de JdbcTemplate, con la finalidad de usarlo en aplicaciones Scala, de modo que debe ser seguro su uso en ambientes multi-hilo, debe tener buen performance, debe manejar bien la transaccionalidad y sobre todo debe ser sencillo de usar.

Primero empecé imaginándome el uso:

//Crearlo con un DataSource para poder usarlo desde varios hilos
//con distintas conexiones
val sql = new ScalaSql(algunDataSource)
//Obtener un solo registro; siendo Scala, esto debería ser un Option
val row = sql.firstRow("SELECT * FROM usuario WHERE clave=?", clave)
//Obtener una lista de registros
val rows = sql.rows("SELECT * FROM usuario WHERE fecha_alta > ?", new Date)
//Pasamos un closure que recibe un renglón y se invoca con cada
//registro obtenido; el renglón debe ser un mapa inmutable
sql.eachRow("SELECT * FROM cuenta WHERE saldo > ?", saldo) { row =>
  //Hacer algo con el mapa
  //Si alguna columna tiene NULL en la base de datos, debemos tener None
  //Si conoces los nombres de columnas, no habrá problema en obtener los valores
  if (row("nombre") == None) { //no trae nombre
  } else { //si trae nombre
  }
  row("saldo") match {
    case s:BigDecimal => //Manejar BigDecimal de Scala, no Java
    case None => No viene saldo
  }
}
//Hacer operaciones dentro de una transaccion
sql.withTransaction { conn => //idealmente no necesitas nada con la conexion
  //Con esto obtenemos llaves autogeneradas
  val nuevaClave = sql.executeInsert("INSERT INTO usuario (nombre,saldo) VALUES (?, ?, ?)", row("nombre"), row("saldo"))
  //Dentro de la transaccion invocamos al mismo componente
  sql.executeUpdate("DELETE FROM usuario WHERE clave=?", row("clave"))
  //Si al final no hubo excepciones, se hace commit
}

En fin, esa es la idea. El código ya está disponible en GitHub y conforme lo utilice, iré agregándole funcionalidad. De entrada no me parece necesario que pueda ser creado o utilizado con una sola conexión, por diversas razones:

  • El esquema de una sola conexión es útil únicamente en scripts y programas de procesamiento por lotes (y a veces ni en esos casos).
  • Se complica bastante el código para poder soportar tanto DataSource como conexión simple en el mismo componente (hay que cerrar conexiones cuando es DataSource pero no cuando es una sola conexión, consideraciones adicionales para transaccionalidad, etc).
  • Para poder usar un solo componente desde varios hilos distintos, es necesario que cada hilo use su propia conexión, por lo tanto se necesita Datasource.

Como lo menciono en la página del proyecto, encontré algo similar ya existente, que incluso sospecho que también está inspirado en el Groovy SQL ya que sí tiene soporte para usarse con una conexión directamente (similar a cuando se crea el Groovy SQL con el puro URL, usuario, password y nombre de la clase del driver JDBC), pero no hay transaccionalidad ni obtención de llaves autogeneradas (aunque esto depende en última instancia de que el driver JDBC lo soporte realmente).

Pero bueno, pues ya hay una opción más para lidiar con bases de datos SQL desde Scala.

delete

El poder de Either

Hace tiempo escribí acerca del poder de usar Option en Scala, cuando se puede manejar un valor que podría ser nulo.

Esto es muy útil por ejemplo para un método de login: pasamos usuario y password, y obtenemos un Usuario, siempre y cuando exista el usuario con ese nombre y su password sea correcto. Entonces podemos implementar el método de estar forma:

def login(username:String, password:String):Option[Usuario]={
  val user = //Buscamos el usuario en la base de datos
  if (user != null && passwordEsValido) Some(user)
  else None
}

Entonces cuando invocamos el método login, ya no tenemos que validar contra null en un if para proceder de una forma, y presentar un error en el else; en vez de eso podemos hacer distintas funciones:

def continuar(user:Usuario)=//esto lo invocaremos si el login estuvo OK
def error()=//Esto lo invocaremos si el login falla

//Aquí viene lo bonito de usar Option:
//Esta línea de código expresa muy claramente lo que hay que hacer
login("username", "password") map continuar orElse error

Casos más sofisticados

Supongamos que queremos manejar errores de manera más específica. Internamente, el login pudo fallar por distintas razones: el usuario no existe, o el password es inválido, o el status del usuario es inactivo, ha sido bloqueado por alguna razón, etc.

Tal vez hacia afuera, en una aplicación web por ejemplo, solamente queremos presentar condiciones de éxito o error, y es suficiente con un Option, pero si internamente queremos tomar distintas acciones dependiendo del error que haya ocurrido, entonces usar un Option ya no es suficiente. Afortunadamente, tenemos otra clase similar, llamada Either, la cual puede contener dos objetos completamente distintos. Entonces supongamos que implementamos una clase ErrorLogin que indica la razón por la que falló, y reimplementamos nuestro método así:

def login(username:String, password:String):Either[Usuario,LoginError]={
  val user = //Buscamos el usuario en la base de datos
  if (user == null) {
    Right(LoginError(UsuarioNoExiste))
  } else if (!passwordEsValido) {
    Right(LoginError(PasswordInvalido))
  } else if (user.status != 1) {
    Right(LoginError(StatusInvalido, "Status es " + user.status))
  } else {
    Left(user)
  }
}

Either es una clase abstracta, igual que Option. Y así como Option tiene dos encarnaciones, Some y None, Either tiene también dos versiones concretas: Left y Right.

Todo esto sirve para expresar, en este caso, que el método login puede devolver un Usuario, o un LoginError. Y entonces podemos actuar de distintas formas dependiendo del resultado del login. Una forma muy simple sería así:

//Podemos reimplementar el método error para que ahora reciba el error
//El método continuar no sufre cambios
def error(err:LoginError)=//Manejar el error

def login = login("usuario", "password")
if (login.isLeft) {
  //El login salió OK
  continuar(login.left.get)
} else error(login.right.get)

Pero ese código no aprovecha realmente las ventajas de tener un Either. Así como con Option podemos usar map, orElse, getOrElse, etc, podemos también aprovechar algunas funciones que implementa Either para expresar de manera más clara nuestro propósito. En este caso, hay un método llamado fold que recibe dos funciones: la primera función debe recibir un parámetro del tipo especificado para el Left y la segunda función debe recibir un parámetro del tipo especificado para el Right. Solamente una de las funciones será invocada, dependiendo del contenido real del Either. La única restricción es que ambas funciones deben tener el mismo tipo de retorno. En este caso, nuestroas métodos continuar y error podrían devolver un URL por ejemplo, para saber a dónde redirigir al usuario según el resultado del login:

def continuar(user:Usuario):URL=//blabla
def error(err:LoginError)=//blabla

val login = login("usuario", "password")
val url = login.fold(continuar, error)

Aquí estamos aprovechando la característica call-by-name que nos permite simplemente pasar el nombre de un método o función como parámetro; dado que el método continuar recibe un Usurio y el método error recibe un LoginError, podemos usar únicamente sus nombres como parámetros; es importante recordar que en esa última línea no estamos invocando a ninguno de los dos métodos, sino que se los pasamos a fold y ahí se ejecutará uno de ellos nada más.

Una vez que se comprende bien Either, se puede utilizar en este tipo de métodos que pueden devolver dos valores distintos dependiendo del éxito o fracaso de una operación, en donde un Option no es lo suficientemente expresivo porque solamente puede contener un valor o no contener nada.

delete

ScalaQuery: Un DSL para acceso a base de datos en Scala

La manera más básica o primitiva de interactuar con una base de datos en Java es por medio de JDBC. Esto por supuesto se puede hacer también en Scala, Groovy o cualquier otro lenguaje para la JVM, pero al usar JDBC se tiene que programar en estilo Java por la manera en que fue diseñado.

En Scala existe una alternativa interesante: ScalaQuery. Lo que esta biblioteca nos permite hacer es realizar queries con código Scala, con un margen de error mucho menor, ya que se aprovecha el tipado estático del lenguaje de una forma que incluso en Java no se hace con JDBC.

La mejor manera de ilustrar su uso es con un ejemplo sencillo. Supongamos que tenemos una tabla, definida así en SQL:

CREATE TABLE ejemplo(
  clave     SERIAL PRIMARY KEY, --autoincrementada
  nombre    VARCHAR(80) NOT NULL,
  apellidos VARCHAR(80) NULL,
  fecha_nac DATE NOT NULL,
  ultimo_acceso TIMESTAMP NULL,
  entero    INT NULL,
  saldo     NUMERIC(10,4) NOT NULL
)

Esta tabla tiene columnas con los tipos más comunes, algunas aceptan nulos, otras no. En ScalaQuery lo primero que hay que hacer para poder realizar operaciones sobre esta tabla, es definirla, como un objeto. Y tenemos que ponerle los tipos de cada columna. Si están familiarizados con Hibernate, verán que esto tiene cierta similitud, pero a la vez es muy distinto:

//El objeto usa generics
//Al final se pone el nombre externo de la tabla
object Ejemplo extends Table[(Int, String, String, Date, Option[Timestamp], Option[Int], Double)]("ejemplo") {
  //Cada columna va como un método que llama a column con sus opciones
  def clave        = column[Int]("clave", O PrimaryKey)
  def nombre       = column[String]("nombre")
  def apellidos    = column[String]("apellidos")
  def fechaNac     = column[Date]("fecha_nac")
  def ultimoAcceso = column[Option[Timestamp]]("ultimo_acceso")
  def entero       = column[Option[Int]]("entero")
  def saldo        = column[Double]("saldo")
  //Y al final tenemos que definir una proyección de todas las columnas
  def * = clave ~ nombre ~ apellidos ~ fechaNac ~ ultimoAcceso ~ entero ~ saldo
}

Algunos puntos importantes de esta definición:

  • Por la manera en que la clase Table está definida, el compilador arroja un error si el método * no incluye todas las columnas que se definieron en la declaración de la tabla (los tipos de la tupla al principio de la definición).
  • Del mismo modo, si definimos una columna con un tipo distinto que en la declaración de la tabla, la proyección de * no tendrá los mismos tipos y el compilador arroja una error.
  • Las columnas que aceptan nulos son de tipo Option[tipo] en vez de ser directamente del tipo de la columna. De esta manera se pueden manejar los valores nulos de manera más idiomática en el código.
  • La columna que es llave primaria lo tiene indicado con una opción, esto le sirve a ScalaQuery cuando queremos hacer un UPDATE.

Cabe mencionar que ScalaQuery no es un ORM, al menos no del tipo de Hibernate. Cuando hagamos un query, obtendremos tuplas; no podemos indicar que queremos instancias de cierta clase.

Queries compilados

Una vez que tenemos la definición de la tabla, es donde viene la parte tan útil de ScalaQuery: Las consultas que hagamos en código, si compilan, podemos estar seguros que funcionarán, siempre y cuando la definición de la tabla en Scala corresponda con la tabla en la base de datos. Esto es muy bueno porque si bien al principio podemos tardarnos más de lo normal en hacer una consulta, por la curva de aprendizaje, el beneficio a mediano y largo plazo es que tendremos menos problemas en tiempo de ejecución por tipos de datos mal mapeados (ClassCastException), manejo de nulos (NullPointerException), consultas desactualizadas, etc.

La consulta más simple, SELECT * FROM ejemplo, se puede hacer de varias formas en Scala:

val q1 = Query(Ejemplo)
val q2 = for { r <- Ejemplo } yield r.*
//y esto es SELECT count(*) FROM ejemplo
val c1 = Query(Ejemplo.count)

Hasta aquí, en q1, q2 y c1 tenemos los queries ya compilados como objetos listos para usarse, pero aún no se ejecuta nada. Podemos pedirles el SQL que van a generar, invocando selectStatement, pero para ejecutar uno de estos queries, necesitamos tener una sesión o una transacción abierta, y ejecutarlos ahí dentro. Para ello utilizamos un objeto Database, al cual le pedimos una sesión y le pasamos una función que recibe la sesión como parámetro y ejecutamos nuestras consultas. Esto es muy fácil con un closure. El Database lo podemos crear a partir de un DataSource de JDBC.

Hay varias maneras de ejecutar un query, dependiendo de lo que queremos hacer con los resultados.

val datasource = //puede venir de JDBC, incluso ser un pool de conexiones
val db = Database.forDataSource(datasource)
db withSession { session:Session =>
  //Aquí ya podemos ejecutar los queries
  println("Tabla tiene " + c1.first()(session) + " registros")
  //Así podemos imprimir cada tupla
  asterisco1.foreach { println(_) }(session)
}
//Y así podemos quedarnos con la lista para usarla después con la sesión ya cerrada
//el "db withSession" devuelve lo que devuelva el closure
val lista = db withSession { session:Session => asterisco1.list()(session) }

A fin de cuentas, estamos haciendo acceso a base de datos con programación funcional.

Queries parametrizados

Al no estar escribiendo SQL y concatenándolo a mano, nos evitamos problemas como inyecciones de SQL. Las consultas con condiciones se definen de manera idiomática con el for de Scala, donde podemos poner al final las columnas que queremos devolver:

val qp = for { rowId <- Parameters[Int]
  r <- Ejemplo if r.clave === rowId
} yield r.nombre ~ r.apellidos ~ r.fechaNac

Nótese que estamos usando un operador especial ===, en vez de simplemente ==. Este operador está definido en las columnas de la tabla, para definir la equivalencia pero no realizarla en el momento en que se define sino hasta después cuando se ejecute el query. Lo anterior sería equivalente a usar un PreparedStatement con el SQL SELECT nombre, apellidos, fecha_nac FROM ejemplo WHERE clave=?. Como estamos buscando sobre la llave primaria, sabemos que podemos obtener un solo registro, como máximo; puede que no haya registro con la clave solicitada. Por lo tanto lo mejor será usar el método firstOption del query; hay un first pero si no se encuentra al menos un registro, arroja una excepción, mientras que firstOption devuelve un Option con la tupla si la encuentra:

db withSession { session =>
  qp.firstOption(100)(session) match {
    case Some((nombre, apellidos, nacimiento)) =>
      //Código que se ejecuta si se encuentra la tupla
    case None =>
      //Código que se ejecuta si no se encuentra la tupla
  }
}

Así que podemos utilizar el pattern matching de Scala de manera normal.

Modificar datos

ScalaQuery no solamente sirve para hacer consultas. Los datos que se obtienen también se pueden actualizar, utilizando el mismo query. Si quisiéramos convertir a mayúsculas los datos de la tupla que encontramos con este último query, podemos poner este código dentro del caso donde sí la encontramos (en el Some):

  qp.mutate(100){
    _.row = (nombre.toUpperCase, apellidos.toUpperCase, nacimiento)
  }(session)

Para las inserciones, se invoca directamente el objeto que representa la tabla, pasando los datos a insertar. Hay una variante que permite insertar varias tuplas al mismo tiempo, usando el batch insert de JDBC.

Ejemplo.insert(100, "Enrique", "Zamudio", new Date(), None, Some(5), 5.4)
//O para insertar varios
Ejemplo.insertAll(
  (501, "Juan", "Pérez", new Date(), new Timestamp(), None, 1.2),
  (502, "Pablo", "González", new Date(), None, Some(1), 3.5),
  (5034, "Pedro", "Perez", new Date(), new Timestamp(), Some(2), 4.5)
)

Transacciones

Para cuando se requieren hacer varias operaciones en una sola transacción de base de datos, simplemente hay que utilizar withTransaction en vez de withSession:

db withTransaction { session =>
  //Todo lo que se haga aquí será ejecutado dentro de una transacción de base de datos
  //Si este código arroja una excepción, se da rollback automáticamente.
  //También se puede dar rollback manual
  session.rollback()
}

//También se pueden manejar transacciones dentro de sesiones abiertas
db withSession { session =>
  //El código que se ejecuta aquí no está en una transacción
  session.withTransaction {
    //Esto va dentro de una transacción
  }
  //Esto tampoco se ejecuta en transacción
  //ya se hizo commit para cuando se ejecuta esto
}

Funcionalidad adicional

Esto es apenas una probadita de lo que puede hacer ScalaQuery. Además de todo esto, hay sintaxis para joins, para crear las tablas directamente desde ScalaQuery (para lo cual hay que definir varias cosas adicionales en cada columna, como su longitud, default, etc), etc.

Esta biblioteca sigue evolucionando; al momento de escribir esto, la versión es 0.9.5. El mayor problema que he tenido para utilizarla es que la documentación es algo escueta y no está muy actualizada, por ejemplo el API publicado es para la versión 0.9.4, no hay de la 0.9.5; las guías de uso llevan bastante tiempo sin actualizarse y algunas cosas ya no son como dice la documentación porque ha habido cambios de sintaxis y cosas así.

En fin, esta es una opción más para manejar el acceso a bases de datos en Scala. Otra opción es Squeryl, que tiene una filosofía similar de aprovechar el tipado estático para construir queries que se validan mayormente en la fase de compilación, sin embargo parece ser un ORM más en forma, lo cual tiene sus propias ventajas y desventajas.

delete

Interesante presentación de Scala para Javeros

Esta presentación de Scala, que tiene casi tantos títulos alternos como slides, está bastante interesante porque está diseñada para hacerla a programadores Java, con la intención de interesarlos en Scala. Muchas de las bondades del lenguaje se muestran haciendo comparación directa de distintas tareas en Java y Scala. Adivinen cuál tiene siempre menos código y además tiene mejor legibilidad…

http://prezi.com/07yqjyfcotn6/top-10-reasons-java-programs-envy-scala/

delete

Un caso interesante de Scala vs Java

Hace mucho tiempo, implementé un codificador para Base 64 en Java. El algoritmo es relativamente simple: consiste en codificar 3 bytes como 4 caracteres de texto, tomados de un alfabeto de 64 posibles caracteres (de ahí el nombre). El alfabeto está compuesto de las letras A a la Z mayúsculas y luego minúsculas, los dígitos y los signos + y /. Existe además un símbolo especial = para indicar la ausencia de datos, cuando se codifica un bloque de menos de 3 bytes.

Cuando lo implementé en Java, lo primero que hice fue definir el alfabeto, como un arreglo de char Para ello, dentro de la clase que iba a contener los dos métodos (codificar y decodificar), hice esto:

public class Base64 {
  private static char[] HEX_CHARS = new char[]{
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
  };
  /** El arreglo de 64 caracteres que componen el 'alfabeto' de base 64. */
  static final char[] B64_CHARS = new char[64];

  static {
    for (int i = 0; i < 26; i++) {
      B64_CHARS[i] = (char)(i + 65);
    }
    for (int i = 26; i < 52; i++) {
      B64_CHARS[i] = (char)(i + 71);
    }
    for (int i = 52; i < 63; i++) {
      B64_CHARS[i] = (char)(i - 4);
    }
    B64_CHARS[62] = '+';
    B64_CHARS[63] = '/';
  }
}

Es decir, en el static de la clase, que se ejecuta cuando se carga la misma, es donde lleno el arreglo de 64 caracteres. Estoy consciente de que bien podría simplemente tener la cadena “ABCDEF…xyz0123…9+/”, pero no lo tengo por dos razones: Primero, qué flojera teclearla y segundo, tendría que hacer pruebas más rigurosas para verificar que no me equivoqué al teclearla. O revisarla manualmente o pedirle a alguien más que lo haga. Qué padre, tener que revisar que una cadena vaya de la A a la Z, etc. Qué bueno que tenemos computadoras que puedan generar esos datos por nosotros.

Ahora, para codificar un arreglo de bytes como texto en base64, lo que hacemos es codificar bloques de 3 bytes, cada uno de los cuales resulta en 4 caracteres, porque se separan los 3 bytes, que son 3 grupos de 8 bits, para un total 24 bits, en 4 bloques de 6 bits; con 6 bits se pueden representar números del 0 al 63, y los representaremos con un caracter del alfabeto que ya definimos. El último bloque puede contener menos de 3 bytes (puede estar vacío, en cuyo caso no se hace nada, o tener 1 ó 2 bytes), por lo que se trata de manera especial:

  • Un bloque vacío (0 bytes) se representa con una cadena vacía
  • Un bloque de un byte requiere 2 caracteres (6 bits en un caracter y 2 bits en el siguiente
  • Un bloque de dos bytes requiere 3 caracteres (6 bits en el primer caracter, 6 en el segundo, y los últimos 4 bits van al tercer caracter)

En los últimos dos casos, faltan caracteres para tener un bloque de 4; lo que se hace es rellenarlo con =. De modo que un bloque de un byte tendrá terminación == y un bloque de dos bytes tendrá un = al final.

Pues bien, podemos hacer la implementación en Java usando un StringBuilder en donde vamos a poner el texto. El método para codificar debe recibir un arreglo de bytes, y podemos recibir también el índice inicial donde debemos empezar a codificar y el número de bytes que debemos codificar. Esto nos permite codificar un segmento de un arreglo y no solamente arreglos enteros.

ADVERTENCIA: El siguiente código contiene operaciones directas sobre bits, tales como bitwise-and, bitwise-or, y bitshifts, por lo que se recomienda verlo acompañado de un programador Senior.

  public static String base64Encode(byte[] input, int start, int len) {
    //Le ponemos capacidad inicial
    StringBuilder buf = new StringBuilder((input.length * 4) / 3);

    //Leemos de tres en tres, escribimos de cuatro en cuatro
    int end = start + len;
    for (int i = start; i < end; i += 3) {
      int indice = -1;
      int uno = 0, dos = 0, tres = 0;
      //Los bytes van del -128 al 127, pero hay que convertirlos a valores del 0 al 255
      uno = input[i] & 0xff;

      //tomar los primeros seis bits del primer byte
      buf.append(B64_CHARS[uno >> 2]);

      //Los ultimos dos bits van al siguiente caracter
      indice = (uno & 0x03) << 4;
      if (i + 1 < len) {
        //Leer el segundo byte y pasarlo a positivo
        dos = input[i + 1] & 0xff;
        //Contar tambien los primeros 4 bits del segundo byte
        indice |= (dos & 0xf0) >> 4;
      }
      buf.append(B64_CHARS[indice]);

      //Leer el tercer byte y pasarlo a positivo
      if (i + 2 < len) {
        tres = input[i + 2] & 0xff;
        //Los ultimos 4 del segundo y los primeros 2 del tercero
        indice = ((dos & 0x0f) << 2) | ((tres & 0xC0) >> 6);
        buf.append(B64_CHARS[indice]);
        //los ultimos 6 del tercero
        indice = tres & 0x3f;
        buf.append(B64_CHARS[indice]);
      } else if (i+2 == len) {
        indice = (dos & 0x0f) << 2;
        buf.append(B64_CHARS[indice]);
        buf.append("=");
      }
      if (i + 1 >= len) {
        buf.append("==");
      } else if (i >= len) {
        buf.append("=");
      }
    }
    return buf.toString();
  }

A ver… ¿no dije que era un algoritmo relativamente sencillo? ¿Por qué 27 líneas de código entonces? Este es uno de los problemas de la programación en estilo imperativo… estamos expresando exactamente no sólo lo que se tiene que hacer, sino cómo se tiene que hacer. Y cada condición especial (lo del último bloque, que si mide un byte, o dos, o tres) hay que considerarla y manejarla de manera especial.

Muy probablemente este código se pueda simplificar. Lo que no me gusta mucho, es que aunque yo mismo lo escribí, lo veo y ya no quiero ni moverle; no es obvio si es optimizable o simplificable, dónde puedo quitar código, cómo refactorizarlo, etc.

El decodificador de plano ya mejor ni lo pongo, es todavía más código, y son puras líneas muy cortas. Incluso tiene breaks. No estoy orgulloso de esas 35 líneas (efectivas) de código. Pero al igual que el método para codificar, tampoco encuentro la manera de simplificarlo.

Eso sí, este código es muy rápido. Se tarda entre 280 y 320 milisegundos en codificar 10MB de datos. No lo hice para codificar bloques tan largos de código, de hecho este código se usa en la práctica para codificar bloques de menos de 2KB, pero potencialmente muchos bloques cortos, por lo que sí es importante el performance. Este es uno de esos raros casos en que la manera en que manipulamos los bytes directamente nos puede dar un mejor performance.

Ahora en Scala

A manera de ejercicio, decidí implementar ahora el codificar Base 64 en Scala. Algo que me gustó desde el principio fue la manera en que puedo definir el alfabeto:

object Base64 {
  val chars = ('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') :+ '+' :+ '/'
}

Es decir, el rango de caracteres A-Z, concatenado con el rango de caracteres a-z, con los dígitos y luego con los dos símbolos adicionales. El operador ++ es un método de las listas de Scala, para concatenar dos listas, y el método :+ es para crear una nueva lista que trae el nuevo elemento al final.

En Scala, todo es un objeto. Por lo tanto, los arreglos de bytes son objetos, y tienen muchos de los métodos de las colecciones de Scala, por ejemplo para particionar o para obtener un sub-arreglo. Por lo tanto, el método para codificar no necesita que le indiquemos índice inicial y final, porque simplemente podemos obtener primero el sub-arreglo que queremos codificar y pasar eso al método. Y otra cosa: aprovechemos los beneficios de la programación funcional, y el estilo declarativo. Simplemente tengo que indicar a grandes rasgos, lo que quiero hacer, y luego voy implementando las partes más pequeñas. El algoritmo expresado de manera lineal es:

Tomar bloques de 3 bytes, y generar bloques de 4 caracteres, para formar una cadena al final.

Esto expresado en Scala puede ser así (aunque no tengamos todo lo necesario ahorita):

bytes grouped 3 map encodeBlock flatten

Lo anterior puede funcionar, si bytes es un arreglo o lista o secuencia o lo que sea de bytes, porque a las colecciones en Scala les podemos pedir que nos den un iterador que tome grupos de N elementos. Entonces bytes grouped 3 nos dará un iterador para poder procesar grupos de 3 bytes. Si al final hay menos de 3 bytes, nos devuelve lo que queda (el último grupo puede ser más corto). Los iteradores también tienen la función map, a la que podemos pasarle una función que reciba un elemento y devuelva otra cosa, y entonces el iterador termina devolviendo los elementos que resultaron de cada invocación a la función. En este caso me inventé una función llamada encodeBlock, que me imagino que debe recibir un bloque de 3 bytes y nos debe devolver ya sea una cadena, o una lista o algo con 4 caracteres. Por lo tanto al final tendremos una lista de listas, y entonces el método flatten nos sirve para convertirla en una lista plana; lo que hace flatten cuando tienes una lista de listas por ejemplo, es que extrae los elementos de las listas internas y devuelve una lista ya plana, con los elementos de cada una; es decir:

List(List(1,2,3), List(4,5), List(6)).flatten == List(1,2,3,4,5,6)

Y bueno, al final hay que hacer una cadena con esa colección de caracteres, pero lo más importante es que hay que implementar ese encodeBlock, el método que codifica un bloque de 1 a 3 bytes. La manera en que lo implementé fue como una función dentro del método encode, porque no le sirve a nadie fuera de ese método:

  def encode(a:Seq[Byte]):String = {
    def encodeBlock(seg:Seq[Byte]) = {
      //El primer caracter son los primeros 6 bits del primer byte
      val c0 = (seg(0) & 0xff) >> 2
      //El segundo caracter son los últimos 2 bits del primer byte y los 4 primeros bits del segundo byte (si existe)
      val c1 = ((seg(0) & 3) << 4) | (if (seg.size > 1) {
        (seg(1) & 0xf0) >> 4
      } else 0)
      //El tercer caracter son los últimos 4 bits del segundo byte y los primeros 2 bits del tercer byte (si existe)
      val c2 = (if (seg.size > 1) {
        (seg(1) & 0xf) << 2
      } else -1) | (if (seg.size > 2) {
        (seg(2) & 0xf0) >> 6
      } else if (seg.size > 1) 0 else -1)
      //Y el cuarto caracter son los últimos 6 bits del tercer byte (si existe)
      val c3 = if (seg.size > 2) (seg(2) & 0x3f) else -1
      //Al final devolvemos una lista de caracteres
      //Los primeros dos siempre vienen,
      List[Char](chars(c0), chars(c1),
        //Pero el tercero y cuarto pueden ser "=" si no hay más bytes
        if (c2 >=0) chars(c2) else '=', if (c3 >= 0) chars(c3) else '=')
    }
    //Al final hacemos lo que ya había explicado, y a la lista resultante le invocamos mkString que nos da una cadena con los elementos (que son caracteres)
    (a grouped 3 map encodeBlock flatten) mkString
  }

14 líneas de código. La mitad del equivalente en Java. ¿Dónde estuvo el ahorro? Pues una buena parte es debido a la facilidad que tienen las colecciones para procesar datos; eso de grouped 3 nos ahorra tener un ciclo donde tomemos los bytes de 3 en 3. Y luego el map encodeBlock es para pasarle cada grupo de 3 (o menos) bytes a la función encodeBlock; luego aplanamos esas listas internas para tener una sola y construimos una cadena con eso.

Dentro de la función encodeBlock, seguimos teniendo varios if’s, pero la ventaja es que en Scala el if nos devuelve un valor; if (x) a else b es una función que nos va a devolver a si se cumple la condición x o b si no se cumple. Por eso es que están alrevés del código en Java (en Java primero tengo if y dentro de los bloques hago la asignación a variables; en Scala tengo los if dentro de la asignación a los valores).

Muy bonito, pero… esta versión en Scala procesa los mismos 10MB en 6 segundos. Se tarda 30 veces más que la versión en Java.

Primera optimización: Podemos hacer que encodeBlock devuelva una cadena, y entonces no tenemos que aplanar la colección final, solamente concatenar todas las cadenas. Entonces en vez de List[Char](...), la última línea de encodeBlock es Array[Char](...) mkString. Y la llamada se hace a grouped 3 map encodeBlock mkString (quitamos el flatten). Con esto bajamos el tiempo a 3.7 segundos (unas 12 veces más que la versión en Java; me parece que sigue siendo inaceptable pero ya no está tan grave como los 6 segundos anteriores).

Y de hecho, si en vez de mkString, construimos un String al final de encodeBlock: new String(Array[Char](...)), el tiempo baja a 3 segundos. De modo que tal vez la lentitud está en la conversión de datos…

Analizando el código con ayuda del IDE y unos cuantos println, puedo ver que el alfabeto es un Vector de Scala. Si lo convierto a un Array, el tiempo de ejecución baja entre 50 y 100 milisegundos, que no es significativo. Pero si lo convierto a String y utilizo directamente charAt para obtener los caracteres que necesito, el tiempo baja casi 300 milisegundos, quedando en 2.7 segundos.

Después de estar experimentando con varias opciones, ese tiempo fue el menor que pude lograr. La versión de Scala tarda 10 veces el tiempo que la de Java. Como mencioné anteriormente, este es uno de esos algoritmos en los que sí se tiene un beneficio muy notorio al poder manejar los tipos nativos de Java y hacer las operaciones de bits directamente. No es el caso típico de una aplicación; es de hecho el caso marginal en el que conviene tener el código en Java, por el performance.

Ya solamente para completar el código, dejo el método para decodificar:

  def decode(s:String):Seq[Byte]={
    val s0 = s filter { Character.isWhitespace(_) }
    require(s0.length % 4 == 0, "Longitud inválida, debe ser múltiplo de 4")
    def decodeBlock(seg:String)={
      val (s1, s2, s3) = (seg(1), seg(2), seg(3))
      val b0 = (chars.indexOf(seg(0)) << 2) | (chars.indexOf(s1) >> 4)
      if (s2 == '=') {
        Seq(b0.toByte)
      } else {
        val b1 = (chars.indexOf(s1) << 4) | (chars.indexOf(s2) >> 2)
        if (s3 == '=') {
          Seq(b0.toByte, b1.toByte)
        } else {
          val b2 = (chars.indexOf(s2) << 6) | chars.indexOf(s3)
          Seq(b0.toByte, b1.toByte, b2.toByte)
        }
      }
    }
    s0 grouped 4 flatMap decodeBlock toSeq
  }

15 líneas de código. Ya no hice benchmark, pero seguramente será también órdenes de magnitud más lento que la versión en Java.

Conclusión

Con todo eso, no trato de decir que Java sea mejor, o que Scala sea mejor o peor, o que sea lento en general, etc. La JVM me parece una excelente plataforma, y dentro de ella, me parece que Java es un buen lenguaje para implementar algoritmos intensivos donde el desempeño es muy importante y depende directamente del lenguaje. Pero cuando desarrollamos aplicaciones, la mayor parte del código no es más rápido o lento por estar haciendo cosas como este ejemplo; de hecho, por las abstracciones que ya es necesario manejar para poder administrar la complejidad de una aplicación común hoy día, no habrá mucha diferencia entre manejarlo en uno u otro lenguaje, en lo que a performance se refiere; pero sí hay mucha diferencia en la legibilidad del código, que depende mucho de la expresividad, sintaxis, simpleza, etc del lenguaje que se utilice.

Es por ello que creo que Java es una buena opción para implementar este tipo de algoritmos, pero Scala y Groovy, JRuby, etc son mucho mejores opciones para desarrollo general de aplicaciones. Tal vez en un futuro no muy lejano, Java se convierta en el lenguaje de infraestructura de la JVM, y solamente se utilice para este tipo de cosas, así como en otras plataformas se utiliza todavía C para este tipo de algoritmos y se le pone una interfaz para un lenguaje de más alto nivel.