(+34) 673 566 782 - (+34) 960 653 052 formacion@imaginagroup.com

Aprende Scala – Tutorial de Uso de las Clases Case y Pattern Matching

En este tutorial sobre Scala vamos a ver las ventajas de declarar una clase con la palabra clave case y las implicaciones que puede tener a nivel de generación de código. Posteriormente, veremos la ventaja de utilizarlo en una estructura match, a través del pattern matching.

Las Clases Case

El primer uso importante es poder utilizar la clase en una estructura match. Para poder entenderlo al completo, veremos la forma en la que el compilador de Scala realiza el pattern matching, que en esencia se basa en la identificación de patrones para facilitar el trabajo de programación y la simplificación del código.

Empecemos por lo básico, cuando declaramos una clase con la palabra clave case, de la siguiente forma:

case class Persona(nombre: String, edad: Int)

El compilador genera el siguiente código:

class Persona(val nombre: String, val edad: Int)
  extends Product with Serializable
{
  def copy(nombre: String = this.nombre, edad: Int = this.edad): Persona =
    new Persona(nombre, edad)

  def productArity: Int = 2

  def productElement(i: Int): Any = i match {
    case 0 => nombre
    case 1 => edad
    case _ => throw new IndexOutOfBoundsException(i.toString)
  }

  def productIterator: Iterator[Any] =
    scala.runtime.ScalaRunTime.typedProductIterator(this)

  def productPrefix: String = "Persona"

  def canEqual(obj: Any): Boolean = obj.isInstanceOf[Persona]

  override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)

  override def equals(obj: Any): Boolean = this.eq(obj) || obj match {
    case that: Persona => this.nombre == that.nombre && this.edad == that.edad
    case _ => false
  }

  override def toString: String =
    scala.runtime.ScalaRunTime._toString(this)
}

Además, genera un objeto acompañante con los siguientes métodos:

object Persona extends AbstractFunction2[String, Int, Persona] with Serializable {
  def apply(nombre: String, edad: Int): Persona = new Persona(nombre, edad)

  def unapply(p: Persona): Option[(String, Int)] =
    if(p == null) None else Some((p.nombre, p.edad))
}

Ciertamente, es mucho el código generado para una palabra tan corta como case, ¿verdad?.

 

Esta es una de las grandes ventajas de Scala. Es un lenguaje que está pensado para hacernos la vida muy fácil. Pero, ¿qué hace una clase case que no haga una clase normal? ¿Qué ganamos con todo este código generado?

Estas son las ventajas:

  • Los objetos que provienen de clases case, se pueden comparar a nivel de valor. Si tenemos p1 y p2, siendo de la clase Persona, p1 == p2, comparará si el nombre y la edad son idénticos, y devolverá true en el caso de que ambos atributos (nombre y edad) devuelvan true a la hora de compararlos. Es como hacer p1.nombre == p2.nombre && p1.edad == p2.edad. Útil, ¿no es así?.
  • Podemos copiar de una manera sencilla un objeto. Para ello, es tan fácil como invocar el método copy sobre un objeto de tipo Persona. Por ejemplo, haciendo val p2 = p1.copy() haremos una copia de p1 en p2. También podemos copiar el objeto pero modificando un atributo. Haciendo val p2 = p1.copy(nombre = “Pepe”), conseguiremos una persona con los mismos atributos que p1 pero de nombre Pepe.
  • Se pueden crear objetos sin utilizar la palabra new. No es que sea el mejor truco del mundo, pero ahí está. Ahora para crear una Persona sólo tienes que hacer val persona = Persona(“Pepe”, 20).
  • VENTAJA NÚMERO 1: A partir de ahora, podremos utilizar cualquier variable de tipo Persona dentro de una estructura match.
persona match {
    case Persona("Pepe", _) => println("Hemos encontrado a Pepe")
    case Persona(_, 40) => println("Alguien tiene 40 años")
    case _ => println("Ni es Pepe ni tiene 40")
}

Si observas con atención, utilizar match sobre un objeto definido por nosotros es muy ventajoso. En el primer caso, cualquier objeto que tenga como nombre “Pepe” hará match y ejecutará el código de la derecha de la flecha. En el segundo caso, buscamos un objeto cuyo nombre no nos importe pero cuya edad sea concretamente 40.

NOTA para los Java Lovers

Si vienes de Java, te parecerá curioso no ver ningún break. Esto es así en Scala, no es necesario que cada línea se finalice con un break. Sólo se ejecutará la línea o bloque de la derecha de la flecha y una vez hecho, el compilador irá a la siguiente línea de después del match.

 

Esta breve introducción a la funcionalidad de las clases case, ya a simple vista, aporta a Scala una versatilidad que es difícil de encontrar en otros lenguajes, como por ejemplo Java. En el siguiente apartado, veremos formas de escribir nuestros bloques match.

 

Pattern Matching

Si has llegado a este punto, seguro que estás diciendo: “Scala mola, pero… ¿cómo comparo los valores de los atributos del objeto?” o “¿Cómo recupero el objeto para modificarlo o utilizarlo después de la flecha?”.

Tienen mucho sentido tus dudas. Veamos por lo tanto las posibilidades que nos ofrece la estructura match.

 

Acceso a atributos

El siguiente código muestra un ejemplo para recuperar el nombre de un objeto Persona:

persona match {
  case Persona("Pepe", _) => println("Se trata de Pepe")
  case Persona(nombre, _) => println(s"Se trata de ${nombre}")
}

Como puedes observar, dándole un nombre al atributo en cuestión, lo podremos utilizar en la parte de la acción del case.

Acceso a los objetos

Veamos el siguiente código:

persona match {
  case Persona("Pepe", _) => println("Se trata de Pepe")
  case p: Persona => println(s"Se trata de ${p.nombre}")
}

Esta es una forma muy típica cuando la variable persona, puede ser de varios tipos (o incluso Any). Gracias al pattern matching, identificamos el tipo y accedemos al objeto a través de p.

Lo lógico es que te hagas la siguiente pregunta: “Pero si ya tengo la variable persona, ¿por qué no accedo directamente a ella?”. Ok, tienes razón. Vamos con un ejemplo más completo:

abstract class Persona {
  def nombre: String
  def edad: Int
}

case class Conductor(val nombre: String, val edad: Int) extends Persona
{
  def conducir() = println(s"${nombre} está conduciendo")
}

case class Viajero (val nombre: String, val edad: Int) extends Persona
{
  def pagarBillete() = println(s"El viajero ${nombre} ha comprado un billete")
}

def realizarAccion(persona: Persona) = {
    persona match {
      case c: Conductor => c.conducir()
      case v: Viajero => v.pagarBillete()
    }
}

val listadoPersonas : List[Persona] = List(Viajero("Antonio", 25), Conductor("Pepe", 40))

listadoPersonas.map(realizarAccion(_))

Y su funcionamiento explicado:

  • Definimos la clase Persona como abstracta, porque no nos interesa que se instancie. Además, ya no necesita que exista un constructor con signatura (anteriormente el constructor era Persona(nombre: String, edad: Int)).
  • Conductor y Viajero heredan de Persona pero cada uno implementa un método diferente.
  • Creamos una lista con 2 personas, un Viajero y un Conductor.
  • Sobre esa lista y utilizando map, ejecutamos sobre cada objeto de la lista la función realizarAccion.
  • Dentro de realizarAccion y según el objeto persona sea de tipo Conductor o Viajero, ejecutaremos el método correspondiente (conducir o pagarBillete).

 

Y este es un gran comienzo si te estás adentrando en el mundo de Scala, porque es la herramienta base para construir programas fáciles de leer, fáciles de mantener y potentes como en Java.

 

Uso de cookies: Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies

ACEPTAR
Aviso de cookies