trubidata
Lección 2 de 14·16 min

Las bases

En esta guía (10)

En esta lección vas a conocer los ladrillos con los que se construye cualquier programa en Scala: expresiones, valores y variables, bloques, los tipos más usados y las funciones. Al final vas a ver dos ideas muy propias de Scala: el método apply, que permite usar un objeto como si fuera una función, y la posibilidad de definir tus propios operadores.

Todo lo de esta lección se prueba mejor en el intérprete: abrí una terminal, escribí scala, y ejecutá cada ejemplo línea por línea (para salir, :q). Si no instalaste nada todavía, usá Scastie.

Expresiones

Una expresión es cualquier cosa que se puede evaluar y produce un valor:

scala
1 + 1

Para ver el resultado de una expresión podés usar println:

scala
println(1)                   // 1
println(1 + 1)               // 2
println("Hola!")             // Hola!
println("Hola," + " mundo!") // Hola, mundo!

Valores y variables

En Scala hay dos formas de darle nombre a un resultado:

  • val declara un valor inmutable: una vez asignado, no se puede cambiar.
  • var declara una variable mutable: se le puede asignar otro valor más adelante.
scala
val x = 10   // valor inmutable
// x = 20    // no compila: error: reassignment to val

var y = 5    // variable mutable
y = 7        // se puede reasignar
println(y)   // 7

La regla práctica: usá val siempre, salvo que tengas un buen motivo para var. Un valor que no cambia es más fácil de razonar: si ves val total = ..., sabés que total vale eso en todo el programa. Esta preferencia por la inmutabilidad es una de las ideas centrales de la programación funcional, y la vas a ver en todo el curso.

Bloques

Podés combinar varias expresiones encerrándolas entre llaves {}. Eso es un bloque. El resultado de la última expresión del bloque es el resultado del bloque completo:

scala
println({
  val x = 1 + 1
  x + 1
}) // 3

Por eso podés usar un bloque para calcular un valor en varios pasos:

scala
val saldoFinal = {
  val saldoInicial = 1000.0
  val depositos = 250.0
  val retiros = 400.0
  saldoInicial + depositos - retiros
}
println(saldoFinal) // 850.0

Los valores intermedios (saldoInicial, depositos, retiros) solo existen dentro del bloque. Afuera queda únicamente saldoFinal.

Tipos de uso común

Scala es un lenguaje estáticamente tipado: cada valor tiene un tipo que el compilador conoce antes de ejecutar. Podés escribirlo explícitamente después del nombre, con dos puntos:

scala
val edad: Int = 30
val precio: Double = 49.99
val nombre: String = "Juan"
val esEstudiante: Boolean = true

O dejar que el compilador lo infiera a partir del valor:

scala
val edad = 30          // Int
val precio = 49.99     // Double
val nombre = "Juan"    // String

Los tipos que más vas a usar:

TipoQué representaEjemplo
Intenteros de 32 bits42
Longenteros de 64 bits9876543210L
Doubledecimales de doble precisión49.99
Floatdecimales de simple precisión3.5f
Booleanverdadero o falsotrue
Charun carácter'A'
Stringtexto"Hola"
Unit"no hay valor útil", como void en Java()
BigDecimaldecimales exactos, ideal para dineroBigDecimal("1500.25")

Todos estos tipos forman parte de una jerarquía con una raíz común:

  • Any es el supertipo de todos los tipos.
  • AnyVal agrupa los tipos de valor: Int, Long, Double, Float, Short, Byte, Char, Boolean y Unit.
  • AnyRef agrupa los tipos de referencia (equivale a java.lang.Object): String, List, Option y cualquier clase que definas.
  • Null es el tipo de null y es subtipo de todos los AnyRef.
  • Nothing está abajo de todo: es subtipo de todos los tipos y no tiene valores. Es, por ejemplo, el tipo de una expresión que lanza una excepción.

Algunos tipos numéricos se convierten solos a otros más "anchos" cuando hace falta: un Int se puede usar donde se espera un Long o un Double, pero no al revés.

scala
val entero: Int = 10
val ancho: Double = entero   // 10.0, conversión automática
// val angosto: Int = 3.7    // no compila: type mismatch
val truncado: Int = 3.7.toInt // 3, conversión explícita

Además de estos tipos, podés definir los tuyos con clases y case classes, que vas a ver en el módulo 3.

Funciones y llamadas a métodos

Las funciones se definen con la palabra clave def. Llevan parámetros con su tipo y, opcionalmente, el tipo de retorno:

scala
def suma(a: Int, b: Int): Int = {
  a + b
}

val resultado = suma(5, 3) // llamada a la función
println(resultado)         // 8

No hace falta escribir return: una función devuelve el valor de la última expresión de su cuerpo, igual que un bloque. Si el cuerpo es una sola expresión, podés omitir las llaves:

scala
def resta(a: Int, b: Int): Int = a - b

Además de tus funciones, vas a llamar todo el tiempo a métodos de objetos que ya existen, con la notación de punto:

scala
val texto = "scala"
println(texto.toUpperCase)  // SCALA
println(texto.length)       // 5
println(texto.reverse)      // alacs
println(10.max(25))         // 25

Fijate el último: 10 es un objeto y max es un método de Int. En Scala, todo es un objeto.

El método apply

En Scala, cualquier objeto puede tener un método especial llamado apply. Cuando "llamás" al objeto como si fuera una función, Scala invoca a apply automáticamente:

scala
class Sumador(valor: Int) {
  def apply(x: Int): Int = valor + x
}

val sumador = new Sumador(10)
val resultado = sumador(5) // equivale a sumador.apply(5)
println(resultado)         // 15

Vas a encontrar apply en todos lados. Cuando escribís List(1, 2, 3) o texto(0) para obtener el primer carácter de un string, por detrás se está llamando a un apply. También se usa mucho para crear objetos sin escribir new, algo que vas a ver con los companion objects en la lección 8.

Sobrecarga de operadores

En Scala, los operadores son métodos comunes con nombres simbólicos. Eso significa que podés definir tus propios +, - o * para tus clases. Un ejemplo con números complejos:

scala
class NumeroComplejo(val real: Double, val imaginario: Double) {
  def +(otro: NumeroComplejo): NumeroComplejo =
    new NumeroComplejo(real + otro.real, imaginario + otro.imaginario)

  override def toString: String = s"$real + ${imaginario}i"
}

val num1 = new NumeroComplejo(1.0, 2.0)
val num2 = new NumeroComplejo(3.0, 4.0)
val suma = num1 + num2  // equivale a num1.+(num2)

println(suma) // 4.0 + 6.0i

Un detalle importante: los parámetros de la clase llevan val. Sin val, real e imaginario serían visibles solo dentro de cada instancia, y la expresión otro.real no compilaría porque no podrías leer los campos de otro número. Vas a ver esto en detalle en la lección 8.

println(suma) muestra el texto que devuelve toString, que redefinimos (override) para que se vea prolijo.

Practicá

  1. Escribí un script que guarde y muestre la información de la tarjeta de crédito de un cliente bancario.
    • Usá valores y variables de distintos tipos (Int, String, Boolean, Double).
    • Incluí al menos un bloque.
    • Guardá estos campos: nombre impreso en la tarjeta, fecha de vencimiento, código de seguridad, número de la tarjeta, tipo de tarjeta (Visa, Mastercard, American Express), límite disponible, si la tarjeta está activa y si alcanzó su límite.
    • Escribí una función que, dado el monto total usado y el límite disponible, devuelva el monto que todavía se puede usar.
    • Escribí una función que, dada la información de una tarjeta, la muestre de forma legible con println.
  2. Para sumar complejidad:
    • Una función que, dado el número de una tarjeta, muestre únicamente los últimos 4 dígitos.
    • Una función que, dados el monto de un consumo, el monto del resumen actual y el límite disponible, devuelva true si el consumo es posible o false si excede el límite.
    • Cada vez que muestres un mensaje, mostrá también la marca de tiempo actual.

Soluciones

scala
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// Ejercicio 1: los datos de la tarjeta
val nombreTarjeta = {
  val nombre = "Lucas E"
  val apellido = "Trubiano"
  nombre + " " + apellido
}
val fechaVencimiento = "12/25"
val codigoSeguridad = 123              // nunca se muestra: se imprime como "***"
val numeroTarjeta = "1234567812345678"
val tipoTarjeta = "VISA"
val limiteDisponible = 1550.10
var montoUsado = 300.0
val estaActiva = true
val limiteAlcanzado = montoUsado >= limiteDisponible

def saldoDisponible(montoUsado: Double, limiteDisponible: Double): Double = {
  require(montoUsado >= 0, "El monto usado no puede ser menor a cero.")
  require(montoUsado <= limiteDisponible, "El monto usado no puede ser mayor que el límite.")
  limiteDisponible - montoUsado
}

// Ejercicio 2
def ocultarDigitos(numeroTarjeta: String): String =
  "**** **** **** " + numeroTarjeta.takeRight(4)

def esConsumoPosible(montoConsumo: Double, montoResumen: Double, limiteDisponible: Double): Boolean =
  montoConsumo <= (limiteDisponible - montoResumen)

def timestamp(): String =
  LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

def log(mensaje: String): Unit = println(s"[${timestamp()}] $mensaje")

// Ejercicio 1: mostrar la tarjeta
def mostrarTarjeta(numero: String, nombre: String, vencimiento: String, tipo: String): Unit = {
  println("======================================")
  println("|                                    |")
  println(f"|   ${ocultarDigitos(numero)}%-33s|")
  println("|                                    |")
  println(f"|   $nombre%-20s $vencimiento%-12s|")
  println(f"|   $tipo%32s |")
  println("======================================")
}

log("Datos de la tarjeta")
mostrarTarjeta(numeroTarjeta, nombreTarjeta, fechaVencimiento, tipoTarjeta)
log(s"Activa: $estaActiva - Límite alcanzado: $limiteAlcanzado")
log(s"Disponible: ${saldoDisponible(montoUsado, limiteDisponible)}")
log(s"¿Puedo consumir 1000? ${esConsumoPosible(1000, montoUsado, limiteDisponible)}")
log(s"¿Puedo consumir 1500? ${esConsumoPosible(1500, montoUsado, limiteDisponible)}")
text
[2023-10-16 10:15:30] Datos de la tarjeta
======================================
|                                    |
|   **** **** **** 5678              |
|                                    |
|   Lucas E Trubiano     12/25       |
|                               VISA |
======================================
[2023-10-16 10:15:30] Activa: true - Límite alcanzado: false
[2023-10-16 10:15:30] Disponible: 1250.1
[2023-10-16 10:15:30] ¿Puedo consumir 1000? true
[2023-10-16 10:15:30] ¿Puedo consumir 1500? false

La marca de tiempo que veas va a ser, claro, la de tu ejecución. Algunos detalles de la solución:

  • require verifica una condición y, si no se cumple, lanza una IllegalArgumentException con el mensaje que le pasás. Es la forma más simple de validar los argumentos de una función; vas a ver las excepciones en la lección 5.
  • El número de tarjeta se guarda como String, no como número: no vas a hacer cuentas con él y un Int ni siquiera alcanza para 16 dígitos.
  • En f"...$nombre%-20s", el %-20s rellena el texto con espacios hasta 20 caracteres, alineado a la izquierda; %32s lo alinea a la derecha. Así el recuadro queda prolijo sin contar espacios a mano. El formateo de strings lo vas a ver en detalle en la lección 10.
  • limiteAlcanzado se calcula a partir de los datos en vez de escribirlo a mano: así nunca queda desactualizado.

Resumen

  • Una expresión produce un valor; println la muestra.
  • val es inmutable y var es mutable. Preferí val.
  • Un bloque { ... } vale lo que vale su última expresión.
  • Scala es estáticamente tipado e infiere los tipos; los más comunes son Int, Long, Double, Boolean, Char, String y Unit, todos bajo Any.
  • Las funciones se definen con def y devuelven su última expresión, sin return.
  • apply permite usar un objeto como función; los operadores son métodos y podés definir los tuyos.

← Volver al temario