Primeros pasos con Scala

1
26985

Primeros pasos con Scala

Periódicamente tengo una crisis y necesito tocar algo de código para sentir de primera mano algo de tecnología. El verano es una época propicia para ello.

La plataforma Java evoluciona rápidamente y cada día hay más opciones de lenguaje a la hora de programar para ella.

Personalmente creo va a costar algún tiempo que nuestros clientes se animen a integrar otros lenguajes en sus frameworks de desarrollo ya que incluso muchas organizaciones están a medio paso de instruir a su gente y formalizar sus arquitecturas.

Normalmente hay que ser pacientes y amortizar los costes de formación en las organizaciones y no creo que quieran ni deban asumir riesgos innecesariamente. Su negocio habitualmente no suele ser la tecnología sino que la usan como medio.

Otra cosa distinta es que las empresas que proporcionamos servicios y que tendremos que estar al día de los avances y modas y, desde luego, no cerrarnos al cambio. Poco a poco debemos ir viendo donde encajan las piezas, cuando son las tecnologías estables, maduras y confiables y cuando transferirlas.

Me he decidido por cacharrear con Scala porque me resulta más atractivo aprender los detalles de un lenguaje con características funcionales, que puede complementar a Java, que otro posible lenguaje que simplemente presuma de hacernos más fácil programar lo mismo (obviamente con su sutilezas y valor que no dudo que cada uno aporte).

Ya os adelanto que no considero Scala como un lenguaje sencillo pero es que las cosas potentes y versátiles tampoco suelen ser sencillas. Os invito a que leáis un poquito sobre programación funcional porque, de otro modo, algunas cosas que veréis que se puede hacer con las funciones parecerán algo enrevesado y sin sentido: http://es.wikipedia.org/wiki/Programación_funcional

Estoy desarrollando en Mac aunque a todos los efectos creo que da igual el entorno.

Entorno

Voy a partir del eclipse Helios porque aunque ya hay una versión más moderna (Indigo) porque el plugin me ha dado algún problema de instalación en esta última. Para lo que voy a hacer tampoco es algo relevante.

Instalaremos el pluggin de ScalaIDE para Eclipse de http://www.scala-ide.org/.

Scala IDE

Para instalarlo solo nos tenemos que ir al menú de ayuda de nuestro eclipse y pulsar install new software:

Install new software

Deberemos añadir el trayecto donde se encuentran los plugins: http://download.scala-ide.org/sdk/lithium/e44/scala211/stable/site:

Scala URL

Ahora solo tenemos que crear un proyecto de tipo Scala:

New Project

Y luego un objeto Scala:

New Object

Y ya tenemos es esqueleto que para nuestra aplicación.

Solo voy a comentar algunos detalles del lenguaje que ilustraré con alguna captura de pantalla. Serán variaciones de los ejemplos que vienen en la propia documentación o el libro programming Scala de Dean Wampler y Alex Payne (que está disponible gratuitamente vía Web), tratando de simplificarlos. Algunos, aunque los veáis tan cortitos u obvios, me han llevado un rato porque todavía hay poca documentación y se me hace todavía un poco críptica.

Como en lo que más he trabajado es en Java y C++ haré alguna referencia a cosas que me recuerden. Al final, todo se parece en cierta medida a otra cosa.

Una clase se define con class aunque también puede ser definida con object comportándose como un singleton y por lo tanto sus funciones son estáticas.

Un aplicación empieza en un main como en Java:

package es.autentia
object Ejemplo1 {
  def main(args: Array[String]) {
    println("Hola mundo") 
  }
}
Object

Los paquetes se importan con import y para hacer referencia a un grupo de clases en vez de usar * usaremos ‘_’ ya que las funciones también son objetos en Scala y el * es un operador que se puede sobre-escribir.

La importación del paquete java.lang es implícita, por lo que no lo tenemos que importar.

No hay diferencia entre tipos nativos y objetos, todo es un objeto en Scala.
Utilizamos var para declarar los objetos variables y val para solo lectura (por lo tanto tienen que ser inicializados al ser declarados). Ojo que es el mismo concepto que en C++. Una cosa es que el puntero sea invariable a que apunte a una zona de memoria invariable. Es decir, si declaro como val un array, esto significa que con esa variable no puedo apuntar a otro array, no que no pueda cambiar el contenido de los elementos del array 😉

El valor de retorno por defecto de una función es la última expresión ejecutada en su cuerpo:

package es.autentia
object Ejemplo1 {
  def depura(mensaje: String) {
    println(mensaje)
  }
  def mayusculas (cadena:String) : String = { cadena.toUpperCase() }
  def main(args: Array[String]) {
    depura (mayusculas ("ejemplo") );
  } // def main
}
Object2

El constructor principal es el propio cuerpo de la clase. Para acceder a los miembros podemos omitir el operador punto:

package es.autentia
class mensaje(texto:String){
	val mensaje = texto
	println("Estamos construyendo nuestra clase con " + mensaje)
	def vuelca() = { println (mensaje) }
}
object Ejemplo1 {
  def main(args: Array[String]) {
	var x = new mensaje("Hola mundo")
	x vuelca()
  } // def main
}
Object3

Las funciones tienen argumentos por defecto. Así no hay que sobrecargarlas (redefinirlas)
innecesariamente delegando una sobre otra (como teníamos que hacer en Java para simular el mismo comportamiento):

Object4

Se pueden pasar los nombres de los parámetros y así, sabemos cual es el parámetro por defecto que queremos usar e incluso no vernos forzados por el orden de su declaración. Tiene sentido:

package es.autentia
import java.util._
object Ejemplo1 {
  def depura( cadena: String = "Punto de control",  hora: Date) {
    println(cadena + " " + hora)
  }
  def main(args: Array[String]) {
    depura(hora = new Date , cadena = "Hola parametros con nombre")
  }
}

Se pueden retornar más de un valor a la vez con el concepto de tupla (para evitar utilizar objetos forzados que no forman parte del modelo real .. aunque ojo con los modelos anémicos). Hay reconocimiento de patrones, que es algo diferente al instanceof y es una característica importante de los lenguajes funcionales.

package es.autentia
import java.util._
object Ejemplo1 {
  def depura(cadena: String) {
    println(cadena)
  }
  def calcula(x: Int, y: Int): (Int, Int, Int) = {
    depura("Haciendo el cálculo")
    return (x, y, x * y)
  }
  def main(args: Array[String]) {
    val res = calcula(2, 3)
    res match {
      case t: Tuple3[Int, Int, Int] => println("Primer valor: " + t._1 + " segundo valor: " + t._2 + " tercer valor: " + t._3)
    }
    res match {
      case (a, b, c) => println("Primer valor: " + a + " segundo valor: " + b + " tercer valor: " + c)
    }
    res match { // son equivalentes
      case Tuple3(a, b, c) => println("Primer valor: " + a + " segundo valor: " + b + " tercer valor: " + c)
    }
  }
}

Se puede crear funciones anidadas con ocultación de ámbito. Es decir, crear una función en el cuerpo de otra. Esto es muy útil y es recurrentemente utilizado con funciones anónimas.

Las funciones se pueden pasar como parámetros (me recuerda a punteros a funciones):

package es.autentia
object Ejemplo1 {
  def temporizador (funcionPasada: (Int) => Unit) {
    var contador = 0 
    while (contador 
Object5

Incluso podemos tener funciones parciales. Es decir, declarar funciones que representen una versión incompleta. En el siguiente ejemplo podemos ver como la función puntero representa una llamada la la función multiplica donde el primer parámetro siempre es 2.

 package es.autentia
	object Ejemplo1 {
		def depura(mensaje: String) {
		println(mensaje)
		}
		def multiplica (a: Int, b: Int) : Int = {
			return a * b
		}
	def main(args: Array[String]) {
		val puntero = multiplica(2, _: Int)
		depura( "Invocando función parcial" + puntero(5))
	}
}
 

Estas funciones parciales puedes combinarse para proporcionar combinados. Podemos ver que hay una función test que se ejecutará con unas sentencia booleana y, a partir de este parámetro, se ejecutará el código de una función parcial u otro. Revisadlo bien porque tiene su importancia y complejidad.

package es.autentia
object Ejemplo1 {
  def depura(mensaje: String) {
    println(mensaje)
  }
  def conectaServidor : Boolean = { return true }
  def limpiaEspacio : Boolean = { return false }
  def main(args: Array[String]) {
	  val traza : PartialFunction[Boolean, String] ={ case true => "a trazar"}
	  val error : PartialFunction[Boolean, String] ={ case false => "a levantar a alguien"}
	  val test = traza orElse error 
	  println( test (conectaServidor) )
	  println( test (limpiaEspacio) )
  }
}
Object6

Podemos hasta hacerlo más divertido condicionando la utilización de funciones parciales por el resultado de una expresión u otra función, como en el siguiente ejemplo. Fijaos que podríamos decir que es una forma de polimorfismo encubierta. El mismo código «test = traza orElse elige(hora)» ejecuta en base a una variable dos porciones distintas de código, correspondiente a dos funciones parciales.

Esto puede ser muy potente pero creo que puede hacer los programas muy difícil de seguir para gente con un pensamiento tradicional donde siempre que invoca a la misma función, con los mismos valores, espera obtener el mismo resultado.

package es.autentia
object Ejemplo1 {
  def depura(mensaje: String) {
    println(mensaje)
  }
  def conectaServidor: Boolean = { return true }
  def limpiaEspacio: Boolean = { return false }
  def main(args: Array[String]) {
    var hora = 10;
    val traza: PartialFunction[Boolean, String] = { case true => "a trazar" }
    val error: PartialFunction[Boolean, String] = { case false => "a levantar a alguien ya" }
    val errorDeDia: PartialFunction[Boolean, String] = { case false => "se encargara un operador " }
    def elige(hora: Int): PartialFunction[Boolean, String] = {
      if (hora == 10) {
        return errorDeDia
      } else
        return error
    }
    var test = traza orElse elige(hora)
    println(test(conectaServidor))
    println(test(limpiaEspacio))
    hora = 12;
    test = traza orElse elige(hora)
    println(test(conectaServidor))
    println(test(limpiaEspacio))
  } // def main
}
Object7

Existe otro concepto de funciones Currying que consisten en convertir funciones que tienen varios parámetros en encadenamiento de funciones con un solo parámetro.

package es.autentia
object Ejemplo1 {
  def main(args: Array[String]) {
	  // Función currying
	  def multiplica (a:Int) (b:Int) = a * b;
	  println( multiplica (5)(5) )
	  // funcion parcial sobre función currying
	  val por2 =  multiplica (2)(_)
	  println( por2 (5) )	  
  } // def main
}

Las funciones pueden declarar atributos implícitos. Esto puede despistar ya que es como si fuera un parámetro por defecto pero parcialmente, desde que se le da el valor hasta final del bloque actual.

package es.autentia
object Ejemplo1 {
  def main(args: Array[String]) {
	  // Función currying
	  def multiplica (a:Int) (implicit b:Int) = a * b;
	  println( multiplica (5)(5) )
	  implicit val b = 10
	  println( multiplica (5) )
  } // def main
}

Algo sorprendente es la capacidad de llamar a funciones por nombre (y no por valor). Esto consiste en que invocamos una función con una expresión y, en vez de ser evaluada como pasaría con cualquier parámetro, la propia expresión se evalua como código dentro de la función invocada.

Mirad detenidamente el ejemplo porque el resultado obtenido puede desconcertar un poco:

package es.autentia 
object Ejemplo1 {
  def main(args: Array[String]) {
    //función invocada por nombre
	  def dameNombre (parametroPorNombre: => Boolean) (cuerpo: => Unit) 	  {
	    if( parametroPorNombre ) {
		    cuerpo
		} else {
		    println("retornamos otra cosa")
		}
	  }
	  dameNombre (3*4/5 == 4) {
		  println("es un resultado 2");
	  }	  
  } // def main
}
Object8

Los bucles sobre listas son sencillos y potentes:

package es.autentia

object Ejemplo1 {

  def depura(cadena: String) {
    println(cadena)
  }

  def main(args: Array[String]) {

    var lista = List("Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", 
        "Julio", "agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre")

    for (mes 

Es muy sencillo hacer búsquedas por patrón en las listas. Podréis ver lo fácil que es discriminar en una lista de meses aquellas entradas que no tengan una ‘r’:

Object9

Es todavía puede ser mucho más útil con el uso de patrones basados en expresiones regulares. Las comillas dobles 3 veces permiten comentarios de múltiples líneas con caracteres de escape ignorados (útil para nuestras expresiones regulares)

package es.autentia
object Ejemplo1 {
  def depura(cadena: String) {
    println(cadena)
  }
  def main(args: Array[String]) {
    val telefono = """([^,]+) ([^,]+) ([^,]+) ([0-9]+)""".r
    var lista = List("Luis Perez Jimenez 1234567",
      "Pedro Garcia Gomez 1111111",
      "Roberto Canales Mora rcanales@autentia.com",
      "Paco Pepez Perez")
    for (contacto  depura("Llamar a " + n + " al " + tel)
        case entry => depura("No reconozco patrón")
      } // match
    } // for
  } //def
}

Podemos actuar sobre todos los elementos de una lista de un modo sencillo. Con el uso de map, iteramos sobre todos los elementos. El carácter ‘_’ nos vale como sustituto del elemento a iterar:

package es.autentia
object Ejemplo1 {
  def depura( mensaje : String) {
    println(mensaje)
  }
  def main(args: Array[String]) {
     var serie = List (1,2,3,4,5,6) map (_ * 2)  
     serie foreach { numero => depura("El número es " + numero) }
  }
}
Object10

Trabajar con mapas es igual de sencillo que trabajar con listas. La única diferencia es que el valor sobre el que iteramos es una tupla (que ya hemos visto anteriormente):

package es.autentia
object Ejemplo1 {
  def depura( mensaje : String) {
    println(mensaje)
  }
  def main(args: Array[String]) {
     var mapa = Map (
         "Luis" 		-> "Programador",
         "Angel" 	-> "Analista",
         "Pedro" 	-> "Programador",
         "Juan" 		-> "Diseñador" )  
     mapa foreach { pareja => depura("la clave es " + pareja._1 + " y el valor " + pareja._2 ) }
     val mapaMayusculas = mapa map { pareja => (pareja._1.toUpperCase, pareja._2.toUpperCase) }
     mapaMayusculas foreach { pareja => depura("la clave es " + pareja._1 + " y el valor " + pareja._2 ) }    
  }
}
Object11

Se pueden utilizar closures. Definir funciones que utilizan variables definidas en otros lugares. En cierto modo me recuerda al ligado de variables de JavaFx https://adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=javafx.
Los closures combinados con funciones parciales condicionadas nos darán muchísima potencia.

package es.autentia
object Ejemplo1 {
  def depura(cadena: String = "------") {
    println(cadena)
  }
  def depura(cadena: Int) {
    println(cadena)
  }
  def main(args: Array[String]) {
    var a = 10
    var otra = (x: Int) => x + a
    depura(a);     depura()
    depura(otra(5))
    depura(a);     depura()
    a = a + 1
    depura(otra(5))
  } //def
}
Object12

El método equals compara, al igual que ==, si los objetos son iguales. Eq comprueba si las dos referencias apuntan al mismo objeto.

El método copy clona un objeto.

Hay inferencia automática de tipos en la asignación. Es decir, la variable se convierte automáticamente al tipo del que es inicializada.

Existen los trails que son una especie de interfaces tipo java con implementación opcional. Muy útil cuando hay comportamientos similares a sobrecargar en distintas clases derivadas o que implementan un interfaz (en caso de java) y que normalmente se resuelven con una delegación o (malamente resuelto) copiando y pegando código:

package es.autentia
class Objeto{
	var x:Int = 0; 	var y:Int = 0 ; var ancho:Int = 0 ; var alto:Int = 0
}
trait ObjetoGrafico{
	def pinta () {
		println("Pintamos objeto") 	
	}
    def muevete() 
}
class Pato extends Objeto with ObjetoGrafico{
   override def muevete() {
		println("Movemos pato") 	
   }
}
class Nube extends Objeto with ObjetoGrafico{
	override def muevete() { 
		println("Movemos nube") 	
	}
	override def pinta(){
		println("pintamos nube") 	
	}
}
object Ejemplo1 {
  def depura(mensaje: String) {
    println(mensaje)
  }
  def main(args: Array[String]) {
	  val lista = List( new Pato(), new Nube() )
	  for (objeto 

Se pueden crear clases con tipos parametrizables, como en java, aunque se cambian las flechas por corchetes:

package es.autentia
class almacen[param] {
  var array: List[param] = Nil
  def add(elemento:param) { array = elemento :: array }
  def lista() {
    array foreach { elemento => println(elemento.toString()) }
  }
}
object Ejemplo1 {
  def main(args: Array[String]) {
    var x = new almacen[Int]
    x.add(1)
    x.add(2)    
    x.lista()
  } // def main
}
Object13

No existe un valor de retorno NULL sino que es un objeto o None o Some que deriva de Option.

Existe el concepto de Actores basado en el modelo de Erlang. A través de mensajes podemos evitar problemas de concurrencia.

Existen muchas más cosas de las que no he hablado como los valores lazy (que se inicializan solo la primera vez que se usan), la implementación de DSLs, los tipos de datos, la ejecución tanto en plataforma .Net como java, las herramientas, etc. pero ya sería enrollarme demasiado para un primer vistazo.

Conclusión:

Francamente me ha sorprendido Scala y me ha hecho que me interese por la programación funcional. Ya sabes, cuando abres una puerta, normalmente te encuentras con otras que también te apetece abrir…

Como aspecto negativo (que podría ser positivo) es que creo que requiere dominar mucho el lenguaje y los conceptos subyacentes para sacarle partido y que, en muchos casos, depurar y mantener el código de otro puede resultar tremendamente complejo: pequeños cambios en la declaración de las funciones tienen significados y comportamientos muy distintos.

Esto no es para aficionados … por eso decía que tal vez sea también positivo.

Seguiré jugando un poco con Scala y espero animarme pronto a dar continuidad a este tutorial.

Recursos interesantes disponibles:

1 COMENTARIO

  1. Este tipo de lenguajes empiezan a despertar mi interés, junto con otros como Groovy & Grails, de los cuales he oido muy buenas cosas pero todavía no he tenido la oportunidad de estudiar. Por otra parte, estoy de acuerdo en que el grado de resistencia del lenguaje Java es todavía alto y así se mantendrá. Tenemos un precedente histórico en informática en Cobol. Y sólo hay que ver la resistencia de los clientes a nuevos frameworks y lenguajes de desarrollo, especialmente por el interés que mantienen estos en tener casi siempre a una persona de dentro que no puede competir en este sentido contra los externos y poder mantener el control, algo que desde un punto de vista comprensivo es completamente lógico.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad