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:
1 + 1
Para ver el resultado de una expresión podés usar println:
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:
valdeclara un valor inmutable: una vez asignado, no se puede cambiar.vardeclara una variable mutable: se le puede asignar otro valor más adelante.
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:
println({
val x = 1 + 1
x + 1
}) // 3
Por eso podés usar un bloque para calcular un valor en varios pasos:
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:
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:
val edad = 30 // Int
val precio = 49.99 // Double
val nombre = "Juan" // String
Los tipos que más vas a usar:
| Tipo | Qué representa | Ejemplo |
|---|---|---|
Int | enteros de 32 bits | 42 |
Long | enteros de 64 bits | 9876543210L |
Double | decimales de doble precisión | 49.99 |
Float | decimales de simple precisión | 3.5f |
Boolean | verdadero o falso | true |
Char | un carácter | 'A' |
String | texto | "Hola" |
Unit | "no hay valor útil", como void en Java | () |
BigDecimal | decimales exactos, ideal para dinero | BigDecimal("1500.25") |
Todos estos tipos forman parte de una jerarquía con una raíz común:
Anyes el supertipo de todos los tipos.AnyValagrupa los tipos de valor:Int,Long,Double,Float,Short,Byte,Char,BooleanyUnit.AnyRefagrupa los tipos de referencia (equivale ajava.lang.Object):String,List,Optiony cualquier clase que definas.Nulles el tipo denully es subtipo de todos losAnyRef.Nothingestá 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.
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:
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:
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:
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:
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:
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á
- 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.
- Usá valores y variables de distintos tipos (
- 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
truesi el consumo es posible ofalsesi excede el límite. - Cada vez que muestres un mensaje, mostrá también la marca de tiempo actual.
Soluciones
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)}")
[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:
requireverifica una condición y, si no se cumple, lanza unaIllegalArgumentExceptioncon 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 unIntni siquiera alcanza para 16 dígitos. - En
f"...$nombre%-20s", el%-20srellena el texto con espacios hasta 20 caracteres, alineado a la izquierda;%32slo 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. limiteAlcanzadose calcula a partir de los datos en vez de escribirlo a mano: así nunca queda desactualizado.
Resumen
- Una expresión produce un valor;
printlnla muestra. vales inmutable yvares 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,StringyUnit, todos bajoAny. - Las funciones se definen con
defy devuelven su última expresión, sinreturn. applypermite usar un objeto como función; los operadores son métodos y podés definir los tuyos.