El libro

Capítulo 9 · Protocolos

Ya sabes agrupar datos en records y tipos suma, y escribir funciones que operan sobre ellos. Falta responder una pregunta concreta: ¿cómo le agregas operaciones a un tipo desde fuera de su declaración?

Por ejemplo: Punto es un record con dos campos. Quieres que se imprima como (3, 4) cuando aparece en una interpolación. ¿Modificas el tipo? ¿Pasas una función al println? ¿Defines una variante de println solo para Punto? Ninguna escala.

La respuesta de kaikai son los protocolos: un contrato con nombre y un puñado de operaciones, que cualquier tipo puede satisfacer. Elegí la palabra “protocolo” y no “trait” ni “interfaz” porque describe mejor lo que pasa: dos partes que acuerdan cómo hablarse. Los protocolos hacen lo que las interfaces de Go, los traits de Rust, los protocols de Clojure y Elixir, y la parte fácil de las typeclasses de Haskell, pero desde un punto preciso del espacio de diseño: single-dispatch explícito, sin propagación de constraints, sin tipos de orden superior. Eso le saca complejidad al sistema de tipos a cambio de algunas cosas que no se pueden expresar. En este capítulo conoces las dos caras.

9.1 Por qué hay protocolos

Los protocolos resuelven tres dolores concretos.

Imprimir tus tipos sin escribir cada vez. Imprimir un record en logs, respuestas o errores no debería costarte una función usuario_a_string por cada tipo. Con Show implementado una vez, la interpolación "#{usuario}" lo usa automáticamente.

Igualdad estructural sin esfuerzo. Cualquier tipo nuevo necesita “esto es igual a aquello” en algún momento. Sin protocolos, escribes eq_punto, eq_cuenta, eq_factura, y vives el resto del proyecto recordando cuál nombre usaste en cuál archivo. Con Eq implementado, la operación se llama eq para todos.

Comparación, hashing, serialización. El mismo argumento, multiplicado por tres operaciones que casi todo tipo necesita tarde o temprano.

Sin protocolos, cada caso se resuelve con convenciones de nombres o con match enormes que enumeran cada tipo posible. Con un solo mecanismo, estos tres y los que vengan después se resuelven en una línea por tipo.

9.2 Declarar un protocol y impl

Un protocolo declara un nombre y una o más operaciones:

protocol Show {
  show(x: Self) : String
}

Self es un nombre reservado que se refiere al tipo que más adelante implemente el protocolo. Cada operación menciona Self al menos una vez: por eso se llama single-dispatch, la operación se decide a partir de un único tipo, el de Self.

Para implementarlo, escribes un impl ... for ...:

type Punto = { x: Int, y: Int }

impl Show for Punto {
  fn show(p: Punto) : String =
    "(#{p.x}, #{p.y})"
}

Desde ahí, show(p) llama a tu implementación cuando p : Punto:

fn main() {
  let p = Punto { x: 3, y: 4 }
  println(show(p))                # imprime "(3, 4)"
  println("p está en #{p}")       # interpolación: usa Show
}

Cuatro detalles que conviene fijar:

  • El cuerpo del impl lista las funciones del protocolo, una por una, con la sintaxis estándar de fn. Cada fn en el bloque tiene que coincidir con la firma declarada en el protocol, sustituyendo Self por el tipo concreto.

  • Una sola implementación por par (protocolo, tipo). Si hay dos impl Show for Punto en la misma compilación, el compilador rechaza con “duplicate impl”. No hay sobreescritura ni resolución contextual.

  • La regla orphan: solo puedes implementar un protocolo P para un tipo T si tu módulo declara P, declara T, o declara el tipo de alguno de los argumentos de P. Esto evita que dos paquetes externos definan implementaciones conflictivas para tipos que ambos importan. Es una limitación práctica, no del sistema de tipos. Y no hay excepción para los builtin: impl Show for Int en tu módulo no compila.

  • El nombre se puede calificar. Cuando dos de tus imports exportan un protocolo con el mismo nombre, dices cuál: impl dibujo.Render for Caja, y lo mismo dentro de un derive, #[derive(protocols.Show)]. Es la regla general del §8.2: el calificador es un desempate, y el nombre pelado está bien mientras nada colisione.

Con los builtin, la regla orphan aparece antes de lo que esperas: tarde o temprano quieres que un Int o un String se comporten distinto en tu dominio. El compilador te dice exactamente dónde estás parado:

error: orphan rule: `impl Show for Int` is in module `a`, but `Show` is declared in
`protocols` and `Int` is builtin; an impl must live in the module of its protocol, its
type, or a protocol argument's type

La salida es envolver el builtin en un tipo tuyo y darle el impl a ese:

type Puntaje = Puntaje(Int)

impl Show for Puntaje {
  fn show(self: Puntaje) : String = match self { Puntaje(n) -> "#{n} puntos" }
}

Ahora el impl vive en el módulo de su propio tipo y la regla se cumple sola. De paso ganas lo que un Int desnudo no te daba: Puntaje y Edad dejan de ser el mismo tipo, y el compilador deja de aceptar que los sumes.

Un impl escrito a mano, y la interpolación que lo empieza a usar sin que se lo pidas:

# ejemplos/cap09/01_protocolo_basico.kai
type Punto = { x: Int, y: Int }

# Implementación manual: damos al record un Show que produzca
# el formato cartesiano "(x, y)".
impl Show for Punto {
  fn show(p: Punto) : String =
    "(#{p.x}, #{p.y})"
}

fn main() {
  let origen = Punto { x: 0, y: 0 }
  let p = Punto { x: 3, y: 4 }
  println(show(origen))
  println(show(p))

  # Como Show está implementado, la interpolación lo usa
  # automáticamente:
  println("p está en #{p}")
}
$ kai run ejemplos/cap09/01_protocolo_basico.kai
(0, 0)
(3, 4)
p está en (3, 4)

9.3 Los cinco protocolos del stdlib

kaikai trae en stdlib/protocols.kai cinco protocolos que vas a usar todo el tiempo:

ProtocoloOperacionesPara qué
Showshow(x: Self) : StringConvertir a string para imprimir
Eqeq(a: Self, b: Self) : BoolIgualdad
Ordcmp(a: Self, b: Self) : Int, min, maxOrden total
Hashhash(x: Self) : IntPara tablas hash y conjuntos
Serializeto_string, from_stringConversión texto ↔ valor

Los tipos primitivos (Int, Real, Bool, String, Char) ya implementan los cinco. Para un tipo nuevo, eliges cuáles vale la pena implementar, con una excepción: la igualdad no se pide. == sobre un tipo de datos compara estructuralmente, campo a campo y constructor a constructor, sin que declares nada. impl Eq es para cuando quieres otra igualdad, no para tener una. Lo único sin igualdad posible es un valor que guarda una función: comparar dos funciones no significa nada, y el compilador lo dice así.

Ord merece una nota: tiene tres operaciones, no una. cmp(a, b) : Int devuelve un entero negativo si a < b, cero si son iguales, positivo si a > b. Las otras dos, min(a, b) y max(a, b), devuelven uno de los dos argumentos según el orden. Las tres se implementan juntas en el mismo bloque:

impl Ord for Cuenta {
  fn cmp(a: Cuenta, b: Cuenta) : Int =
    if a.saldo < b.saldo { -1 }
    else if a.saldo > b.saldo { 1 }
    else { 0 }

  fn min(a: Cuenta, b: Cuenta) : Cuenta =
    if a.saldo < b.saldo { a } else { b }

  fn max(a: Cuenta, b: Cuenta) : Cuenta =
    if a.saldo > b.saldo { a } else { b }
}

Si te da pereza implementar tres operaciones, espera a §9.4: en muchos casos el compilador las deriva por ti.

Eq por id y Ord por saldo sobre el mismo tipo, que es exactamente el caso donde derivar no sirve:

# ejemplos/cap09/02_eq_ord.kai
type Cuenta = {
  id: Int,
  saldo: Int
}

impl Eq for Cuenta {
  fn eq(a: Cuenta, b: Cuenta) : Bool = a.id == b.id
}

# Ordenamos por saldo. Las tres ops del protocolo Ord viven
# juntas en la misma `impl`.
impl Ord for Cuenta {
  fn cmp(a: Cuenta, b: Cuenta) : Int =
    if a.saldo < b.saldo {
      -1
    } else if a.saldo > b.saldo {
      1
    } else {
      0
    }
  fn min(a: Cuenta, b: Cuenta) : Cuenta =
    if a.saldo < b.saldo { a } else { b }
  fn max(a: Cuenta, b: Cuenta) : Cuenta =
    if a.saldo > b.saldo { a } else { b }
}

fn main() {
  let c1 = Cuenta { id: 1, saldo: 100 }
  let c2 = Cuenta { id: 1, saldo: 999 }  # mismo id, distinto saldo
  let c3 = Cuenta { id: 2, saldo: 50 }

  let mismo_id_12 = eq(c1, c2)
  let mismo_id_13 = eq(c1, c3)
  println("c1 == c2: #{mismo_id_12}")  # true: el id manda
  println("c1 == c3: #{mismo_id_13}")  # false

  let cmp12 = cmp(c1, c2)
  let cmp13 = cmp(c1, c3)
  println("cmp(c1, c2): #{cmp12}")  # negativo: 100 < 999
  println("cmp(c1, c3): #{cmp13}")  # positivo: 100 > 50

  let menor = min(c1, c3)
  println("menor saldo: #{menor.saldo}")  # 50
}
$ kai run ejemplos/cap09/02_eq_ord.kai
c1 == c2: true
c1 == c3: false
cmp(c1, c2): -1
cmp(c1, c3): 1
menor saldo: 50

9.4 #[derive(...)] y cuándo usarlo

Cuando basta la implementación obvia, la que delega en cada campo, kaikai te ofrece un atajo: la directiva #[derive(...)] antes de la declaración de tipo.

#[derive(Show)]
type Persona = {
  nombre: String,
  edad: Int,
}

#[derive(Show, Ord)]
type Punto = {
  x: Int,
  y: Int,
}

#[derive(Show)] le dice al compilador “genérame un Show para este record, recorriendo los campos y delegando en el Show de cada tipo de campo”. El resultado para Persona:

$ kai run ejemplo.kai
Persona { nombre: Ada, edad: 30 }

Ese formato canónico, TipoNombre { campo: valor, ... }, sirve para debugging y logs. Si quieres otro formato (por ejemplo, el clásico (3, 4) para un punto), escribes el impl a mano, como en §9.2.

#[derive(...)] funciona para los cinco protocolos del stdlib, siempre que cada campo del record también implemente el protocolo que estás derivando. Si tu record tiene un campo cuyo tipo no tiene Show, #[derive(Show)] falla en compilación con un mensaje que apunta al campo problemático.

La regla práctica:

  • Empieza con #[derive(...)]: es la forma más rápida y casi siempre correcta para records.
  • Cambia a impl manual cuando la implementación derivada no te sirve: formato distinto, igualdad solo por algunos campos, comparación según un campo específico (no el orden natural).

Ya usaste #[derive(Show)] sobre Punto en el tour (§1.7); ahora sabes también cuándo conviene el impl manual.

Dos tipos derivados, uno con Show y otro con Show y Ord:

# ejemplos/cap09/03_derive.kai
#[derive(Show)]
type Persona = {
  nombre: String,
  edad: Int
}

#[derive(Show, Ord)]
type Punto = {
  x: Int,
  y: Int
}

fn main() {
  let ada = Persona { nombre: "Ada", edad: 30 }
  println(show(ada))

  # Show automático en interpolación.
  println("registro: #{ada}")

  let p = Punto { x: 3, y: 4 }
  let q = Punto { x: 3, y: 4 }
  let r = Punto { x: 5, y: 5 }

  # La igualdad no se deriva: `==` compara campo a campo.
  println("p == q: #{p == q}")
  println("p == r: #{p == r}")

  # El orden sí, y queda lexicográfico por campos.
  println("p < r: #{p < r}")
  println("mayor: #{max(p, r)}")
}
$ kai run ejemplos/cap09/03_derive.kai
Persona { nombre: Ada, edad: 30 }
registro: Persona { nombre: Ada, edad: 30 }
p == q: true
p == r: false
p < r: true
mayor: Punto { x: 5, y: 5 }

Derives que no son protocolos: Json

No todo lo que deriva un record es uno de los cinco protocolos. Hay dos derives más, de otra naturaleza: Layout, que fija el orden de bytes de un record para serialización binaria (lo encuentras en el cap. 19), y Json, que lleva el record al formato JSON.

#[derive(Json)] genera dos cosas: to_json, que convierte tu record a un valor JSON, y un shim <tipo>_of_json, que lo reconstruye desde uno. Es la frontera de datos con el mundo: APIs, archivos de configuración, mensajes.

El código generado usa el modelo de JSON que vive en encoding.json_bind, así que el archivo que deriva tiene que importarlo.

# Listado 9.6: ejemplos/cap09/06_json.kai
import encoding.json.{json_decode, json_encode}
import encoding.json_bind

#[derive(Json)]
type Nota = { titulo: String, prioridad: Int, etiqueta: Option[String] }

fn main() : Unit / Stdout {
  let n = Nota { titulo: "comprar pan", prioridad: 1, etiqueta: None }
  println(json_encode(to_json(n)))

  match json_decode("{\"titulo\":\"llamar\",\"prioridad\":2}") {
    None    -> println("json inválido")
    Some(v) -> match nota_of_json(v, "") {
      Ok(m)  -> println("#{m.titulo} (p#{m.prioridad})")
      Err(e) -> println(json_error_show(e))
    }
  }
}
$ kai run ejemplos/cap09/06_json.kai
{"titulo":"comprar pan","prioridad":1,"etiqueta":null}
llamar (p2)

Las reglas del mapeo son las esperables, y el compilador las aplica sin preguntar. Los nombres de campo van al JSON tal cual, sin conversión de mayúsculas. Un campo Option[T] acepta un null explícito y una clave ausente por igual (ambos decodifican a None); cualquier otro campo es obligatorio. Las claves que el record no declara se ignoran. Y si algo falla al decodificar, el error trae la ruta JSON al nodo culpable (address.boxes[2].zip: expected String, got Number), no un “parse error” pelado.

Cuando el mundo externo no usa tus nombres, hay tres ajustes por campo, cada uno en su propio atributo:

#[derive(Json)]
type Config = {
  #[json(rename = "user_name")] usuario: String,
  #[json(default = 3)]          reintentos: Int,
  #[json(skip)] #[json(default = false)] cacheado: Bool,
}

rename cambia el nombre en el JSON; default = <expr> da un valor cuando la clave falta (y vuelve opcional ese campo); skip saca el campo del JSON por completo, y como al decodificar no hay nada que leer, exige un default o que el campo sea Option. Un campo que necesita dos ajustes (saltado y con default) se escribe con dos atributos, como arriba.

Una limitación que conviene saber: #[derive(Json)] funciona sobre records, no sobre tipos suma. Codificar una unión etiquetada es una convención (¿el tag va en una clave "type", en un envoltorio, adyacente?) y el derive no elige por ti. Para eso, to_json/of_json a mano.

9.5 Protocolos propios

Nada te impide declarar tus propios protocolos, con la misma sintaxis que usa el stdlib:

protocol Drawable {
  dibujar(x: Self) : String
}

type Circulo = { radio: Int }
type Cuadrado = { lado: Int }
type Triangulo = { base: Int, altura: Int }

impl Drawable for Circulo {
  fn dibujar(c: Circulo) : String =
    "Círculo de radio #{c.radio}"
}

impl Drawable for Cuadrado {
  fn dibujar(c: Cuadrado) : String =
    "Cuadrado de lado #{c.lado}"
}

impl Drawable for Triangulo {
  fn dibujar(t: Triangulo) : String =
    "Triángulo #{t.base}x#{t.altura}"
}

Ahora tres tipos distintos comparten la operación dibujar, que se resuelve estáticamente: el compilador sabe en cada llamada qué impl usar a partir del tipo del argumento.

Drawable es un ejemplo de juguete; los casos reales que verás en código kaikai incluyen Encodable para distintos formatos, Loggable para que el sistema de logging sepa representar tu tipo, Validable para reglas de validación, etc. Cualquier “comportamiento que comparten varios tipos” es candidato.

Un protocolo propio con tres implementaciones, una por tipo:

# ejemplos/cap09/04_protocolo_propio.kai
protocol Drawable {
  dibujar(x: Self) : String
}

# Tres tipos distintos, tres implementaciones.

type Circulo = { radio: Int }

type Cuadrado = { lado: Int }

type Triangulo = { base: Int, altura: Int }

impl Drawable for Circulo {
  fn dibujar(c: Circulo) : String =
    "Círculo de radio #{c.radio}"
}

impl Drawable for Cuadrado {
  fn dibujar(c: Cuadrado) : String =
    "Cuadrado de lado #{c.lado}"
}

impl Drawable for Triangulo {
  fn dibujar(t: Triangulo) : String =
    "Triángulo #{t.base}x#{t.altura}"
}

fn main() {
  let c = Circulo { radio: 5 }
  let s = Cuadrado { lado: 3 }
  let t = Triangulo { base: 4, altura: 6 }

  println(dibujar(c))
  println(dibujar(s))
  println(dibujar(t))
}
$ kai run ejemplos/cap09/04_protocolo_propio.kai
Círculo de radio 5
Cuadrado de lado 3
Triángulo 4x6

9.6 Por qué no hay typeclasses al estilo Haskell

Los protocolos de kaikai se inspiran en las typeclasses de Haskell, pero son deliberadamente más simples. Hay tres cosas que Haskell hace y kaikai no:

Sin constraints en firmas de funciones. Esta es la diferencia más visible. En Haskell, una función que ordena una lista declara que el tipo del elemento tiene que tener Ord:

sort :: Ord a => [a] -> [a]

El Ord a => es la constraint. Cuando llamas a sort xs, el compilador busca por su cuenta el Ord para el tipo de xs y se lo “inyecta” a la función sin que tú escribas nada. La constraint viaja escondida, y se propaga: si sort llama a otra función que también pide Ord, Haskell encadena la búsqueda solo.

kaikai te deja escribir una cota en la firma (lo verás en §9.7), pero esa cota es una declaración de intención, no un resolver que viaja escondido. La diferencia se ve cuando ordenas una lista. En kaikai la función puede recibir el comparador como un argumento explícito:

fn sort_by[T](xs: [T], cmp: (T, T) -> Int) : [T] = ...

Y el call site nombra el comparador. Si Transaccion implementa Ord, su cmp está disponible como una función ordinaria, y la pasas:

list.sort_by(transacciones, cmp)   # cmp viene de impl Ord for Transaccion

Cuesta poco escribirla, pero la distinción conceptual pesa: lo que en Haskell viaja implícito, en kaikai lo escribes tú. sort_by no “exige” que T tenga Ord, solo que alguien le pase una función de comparación. Que esa función venga de un impl Ord for T es decisión del que llama, no de la firma de sort_by.

Sin tipos de orden superior (HKT). protocol Functor[F[_]] no parsea. Los parámetros de tipo son siempre de primer orden. Esto descarta una familia de abstracciones (Functor, Monad, Applicative, etc.) que en Haskell son centrales y que en kaikai se resuelven con efectos algebraicos (cap. 12) y combinadores explícitos.

Sin propagación de constraints. Una función polimórfica no “lleva” el Ord consigo a las funciones que llama. La cota de §9.7 documenta qué pide esa función; no se propaga sola a las que ella invoque.

Estas restricciones te dan tres cosas:

  • Compilación rápida. La inferencia de tipos sigue siendo Hindley-Milner extendido con efectos, sin el costo del resolver de constraints de Haskell.
  • Errores claros. Si una función necesita Ord y no lo recibe, el error apunta al sitio donde te falta pasar el comparador. No hay cadenas de “no instance for Ord (Maybe a) because of Ord a”.
  • El call site dice qué hace. Cuando ves list.sort_by(xs, cmp), sabes que se ordena con cmp. Cuando ves sort xs en Haskell, tienes que mirar la firma de sort para saber qué Ord se está usando.

A cambio se pierden algunas abstracciones que en Haskell son elegantes, particularmente todo lo que vive sobre Functor y amigos. Ese trade-off lo tomé con los ojos abiertos: las abstracciones que quise priorizar viven en el sistema de efectos (cap. 12) y no en el de tipos.

9.7 Cotas de protocolo en funciones genéricas

Hasta aquí los protocolos aparecían en el impl que los implementa y en el call site que llama la operación. Falta un tercer lugar: una función genérica sobre un T cualquiera puede exigir que ese T implemente un protocolo, y escribirlo en la firma:

fn mostrar_dos[T: Show](a: T, b: T) : String =
  "#{show(a)} y #{show(b)}"

La cota [T: Show] se lee “para cualquier T que implemente Show”. Dentro del cuerpo, las operaciones del protocolo están disponibles: show(a) despacha al impl Show del tipo concreto que llegue en cada llamada.

fn main() : Unit / Stdout {
  Stdout.print(mostrar_dos(1, 2))      # 1 y 2
  Stdout.print(mostrar_dos("x", "y"))  # x y y
}

Si vienes de otro lenguaje con genéricos, esto te suena. En Rust es where T: Trait; en Java, un genérico acotado <T extends Comparable>. La idea es la misma (un parámetro de tipo que no es “cualquier cosa” sino “cualquier cosa que cumpla este contrato”), solo que en kaikai el contrato es un protocolo.

Las cotas se apilan con + cuando el cuerpo usa más de un protocolo, y cada parámetro de tipo lleva la suya:

fn etiquetar[T: Show + Eq](a: T, b: T) : String =
  if eq(a, b) { "iguales: #{show(a)}" }
  else { "#{show(a)} y #{show(b)}" }

fn par[T: Show, U: Show](a: T, b: U) : String =
  "#{show(a)}, #{show(b)}"

Funciona con cualquier protocolo, del stdlib o propio. Una función que toma el mayor de dos valores pide Ord; una que renderiza pide tu Dibujable:

fn mayor[T: Ord](a: T, b: T) : T = max(a, b)
fn render[T: Dibujable](x: T) : String = "[" ++ dibujar(x) ++ "]"

Lee con cuidado, porque aquí kaikai se separa de Haskell aunque la sintaxis se parezca. La cota es una declaración de intención, no la constraint con propagación de §9.6: el compilador no resuelve un diccionario ni lo inyecta. Cada llamada monomorfiza la función al tipo concreto, y ahí show, eq o cmp se resuelven al impl que corresponde, el mismo despacho estático de siempre. La cota documenta el contrato en la firma y hace que el error, cuando un tipo no implementa el protocolo, sea claro: no impl of 'Ord' for type 'X'.

Es el paso natural desde un genérico sobre un T cualquiera: de “esta función sirve para cualquier tipo” a “esta función sirve para cualquier tipo que sepa hacer show”. Sin el resolver implícito de Haskell ni la propagación, la cota deja el contrato escrito donde se lee, que casi siempre es lo que querías.

Las tres formas de cota en un programa: una del stdlib, una propia, y una que exige dos protocolos a la vez.

# ejemplos/cap09/05_cotas.kai
protocol Dibujable {
  dibujar(x: Self) : String
}

type Circulo = { radio: Int }

impl Dibujable for Circulo {
  fn dibujar(c: Circulo) : String = "círculo de radio #{show(c.radio)}"
}

fn mayor[T: Ord](a: T, b: T) : T = max(a, b)

fn render[T: Dibujable](x: T) : String = "[" ++ dibujar(x) ++ "]"

fn etiquetar[T: Show + Eq](a: T, b: T) : String =
  if eq(a, b) {
    "iguales: #{show(a)}"
  } else {
    "#{show(a)} y #{show(b)}"
  }

fn main() : Unit / Stdout {
  Stdout.print(show(mayor(3, 7)))
  Stdout.print(show(mayor("a", "b")))
  Stdout.print(render(Circulo { radio: 5 }))
  Stdout.print(etiquetar(1, 1))
  Stdout.print(etiquetar(2, 9))
}
$ kai run ejemplos/cap09/05_cotas.kai
7
b
[círculo de radio 5]
iguales: 1
2 y 9

9.8 Operadores: +, ==, < como protocolos

Los operadores estándar son protocolos. == es Eq.eq, < es comparación basada en Ord.cmp, + es Add.add. Cuando declaras impl Eq for Cuenta, automáticamente c1 == c2 (con c1, c2 : Cuenta) llama a tu implementación.

type Punto = { x: Int, y: Int }

fn main() {
  let p = Punto { x: 3, y: 4 }
  let q = Punto { x: 3, y: 4 }
  if p == q { println("iguales") }   # Eq.eq estructural
}

Esto unifica la sintaxis: + para Int, Real, vectores, matrices, monedas, todos los que tengan impl Add for .... == para todo lo que tenga Eq. Esa uniformidad es el primer beneficio de tener un mecanismo único para “operaciones dispatched por tipo”.

Los operadores que kaikai trata como protocolos:

OperadorProtocoloOp
==, !=Eqeq
<, <=, >, >=Ordcmp
+, -, *, /Add, Sub, Mul, Divadd, sub, mul, div
++Concatconcat

Los tipos primitivos los implementan todos; los tuyos, cuando escribes el impl. Si un operador no tiene sentido para un tipo, no lo implementas y el compilador rechaza la expresión.

Ejercicios

9.1. Define type Distancia = { metros: Int } y dale un impl Show para que show(d) produzca "42 m". Luego prueba println("la distancia es #{d}") y comprueba que la interpolación toma tu impl Show automáticamente.

9.2. Define type Carta = { palo: String, valor: Int } y dale impl Ord que ordene por valor. Verifica con tres cartas que cmp(c1, c2) devuelve los números esperados.

9.3. Toma cualquier record que hayas escrito en capítulos anteriores y agrégale #[derive(Show, Ord)] arriba de la declaración. Verifica que show y cmp funcionan sin que hayas escrito nada más, y que == ya funcionaba antes del derive.

9.4. Declara un protocolo propio Validable con una operación validar(x: Self) : Result[Self, String] que devuelva Ok(x) si el valor es válido o Err("razón") si no. Implementa Validable para type Edad = { años: Int } de manera que rechace edades negativas o mayores a 130.

9.5. Lee el código del stdlib en stdlib/protocols.kai. ¿Cuántas operaciones tiene Hash? ¿Cómo se usaría para implementar una tabla de hash de Cuenta? Diseña la firma de la función que harías para “buscar una cuenta por id” en una tabla de hash hipotética: ¿qué tendría que estar implementado en Cuenta para que funcionara?

← Volver al índice