Menu

Genéricos en Zero: parámetros de tipo en funciones y shapes

Cómo funcionan los genéricos en Zero: declarar parámetros de tipo en funciones y shapes, llamar a funciones genéricas y el patrón de alias de tipo que convierte parametrizaciones largas en nombres limpios.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Por qué genéricos

Enseguida te topas con tipos que deberían funcionar para más de un tipo de elemento. Un "par" de dos valores no se preocupa por si los valores son enteros, cadenas o algún shape definido por el usuario. Los genéricos te dejan escribir el tipo una vez e instanciarlo con cualesquiera tipos de elemento que necesite el llamador.

La alternativa — escribir IntPair, StringPair, BytePair, etc. — se vuelve aburrida rápido y no compone. Los genéricos en Zero son la herramienta estándar para esto.

Funciones genéricas

Declara parámetros de tipo entre paréntesis angulares entre el nombre de la función y la lista de parámetros:

fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
    return Pair { left: left, right: right }
}

T y U son tipos marcadores — el llamador decide qué son.

Las llamadas normalmente no necesitan deletrearlos; el compilador infiere a partir de los tipos de los argumentos:

let pair = makePair(40, 2_u8)

Aquí T se infiere como i32 (lo que viene por defecto para un literal entero sin sufijo) y U como u8 (por el sufijo _u8). La ligadura resultante pair tiene tipo Pair<i32, u8>.

Si la inferencia escogería tipos equivocados — por ejemplo, porque los literales son ambiguos — puedes fijar los parámetros en el sitio de la llamada:

let pair = makePair<u8, u8>(1, 2)

(Si la sintaxis de llamada con paréntesis angulares es exactamente la mostrada puede variar entre versiones de Zero; consulta la documentación actual para la forma precisa. El comportamiento "inferencia primero" es la parte estable.)

Shapes genéricos

Los shapes llevan parámetros de tipo de la misma forma:

shape Pair<T, U> {
    left: T,
    right: U,
}

El tipo de cada campo puede mencionar los parámetros. Las instancias los fijan:

let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words:    Pair<String, String> = Pair { left: "hi", right: "there" }

Un ejemplo trabajado que combina un shape genérico y una función genérica — pulsa Ejecutar para ver la inferencia en acción:

Una declaración de shape genérico, una función genérica, un sitio de llamada fuertemente tipado. Sin boilerplate del estilo IntBytePair.

Alias de tipo

Cuando el mismo tipo parametrizado aparece una y otra vez, dale un nombre con type:

type BytePair = Pair<u8, u8>

Ahora BytePair es intercambiable con Pair<u8, u8> allá donde puedas escribir un tipo:

Un alias es solo una característica de nombrado: no crea un tipo distinto. Una función que recibe un BytePair aceptará sin problemas un valor de tipo Pair<u8, u8> (y viceversa).

Genéricos en la biblioteca estándar

El mismo mecanismo alimenta buena parte de la biblioteca estándar. Algunos que verás en código real de Zero:

  • Maybe<T> — un valor opcional, que contiene o bien un T o bien nada.
  • Span<T> — una porción prestada sobre valores T. Span<u8> es la vista canónica sobre un buffer de bytes.
  • ref<T> y mutref<T> — tipos de referencia explícitos para casos donde necesitas compartir datos sin copiar.

No tienes que aprenderlos todos de golpe. El sentido de los genéricos es que el mismo shape sirve para cualquier tipo de elemento que tengas a mano.

Cuándo compensan los genéricos (y cuándo no)

Recurre a los genéricos cuando te encuentres escribiendo la misma función o shape dos veces con distintos tipos de elemento. Recurre a un tipo concreto cuando:

  • La lógica de la función solo tiene sentido para un tipo específico (un parseador de Strings, por ejemplo).
  • Quieres que el tipo aparezca en los mensajes de error para facilitar la depuración.
  • Las características de rendimiento dependen de un tamaño en memoria específico.

El coste de los genéricos es real: binarios mayores (cada instanciación genera código nuevo) y tiempos de compilación algo más lentos. Para la mayoría del código de aplicación ese coste es despreciable, pero conviene saberlo cuando construyes código compacto, estilo embebido, donde el tamaño del binario importa.

Una nota sobre restricciones

Algunos sistemas de genéricos te permiten restringir un parámetro de tipo ("T debe soportar ==", "T debe implementar Iterator"). La historia de restricciones en Zero pre-1.0 sigue evolucionando — los ejemplos del repositorio oficial usan genéricos en su forma simple, sin restricciones elaboradas. A medida que el lenguaje se asiente, espera que aterrice una sintaxis de restricciones en una forma pequeña y regular, coherente con el resto del lenguaje. Por ahora, escribe genéricos que funcionen para cualquier T que de verdad les pases y deja que el compilador te avise cuando una operación no esté soportada.

Lo siguiente: enums

Los genéricos te dejan parametrizar sobre tipos. La próxima pieza está en el otro extremo del espectro: los enums, el tipo de enumeración simple de Zero para casos donde las variantes no llevan datos extra.

Preguntas frecuentes

¿Cómo funcionan los genéricos en Zero?

Declara parámetros de tipo entre paréntesis angulares después del nombre de la función o shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> o shape Pair<T, U> { left: T, right: U }. Quien llama fija los parámetros explícitamente (Pair<i32, u8>) o deja que el compilador los infiera a partir de los argumentos.

¿Pueden ser genéricos los shapes en Zero?

Sí. Un shape puede llevar parámetros de tipo con la misma sintaxis de paréntesis angulares usada en funciones: shape Pair<T, U> { left: T, right: U }. Cada campo puede usar los parámetros en su tipo. Las instancias se forman escribiendo el tipo parametrizado — por ejemplo Pair<i32, u8>.

¿Hay que especificar los parámetros de tipo al llamar a una función genérica?

Normalmente no. El compilador los infiere a partir de los tipos de los argumentos. Llamar a makePair(40, 2_u8) basta — T pasa a ser i32 y U pasa a ser u8. Puedes fijar los parámetros explícitamente cuando la inferencia escogería el tipo equivocado o cuando quieras que el sitio de llamada los documente.

¿Qué es un alias de tipo en Zero?

Un alias de tipo es un nombre corto para una expresión de tipo más larga. type BytePair = Pair<u8, u8> te deja escribir BytePair allá donde escribirías Pair<u8, u8>. El alias es una pura comodidad de nombrado: no introduce un tipo nuevo, solo una forma más corta de referirse a uno existente.

¿Dónde aparecen los genéricos en la biblioteca estándar de Zero?

Por todas partes — allá donde un tipo necesite contener o operar sobre un tipo de elemento arbitrario. Maybe<T> para valores opcionales, Span<u8> para porciones de bytes, tipos contenedor parametrizados por su tipo de elemento. El mismo mecanismo genérico maneja los tipos definidos por usuario y los de la biblioteca estándar.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR