Un vistazo al álgebra de sistemas de tipos
Alex Escalante
Posted on April 4, 2024
Esta nota es una traducción de la nota original que publiqué más temprano. No estoy seguro si hoy en día tiene sentido seguir traduciendo, díganmelo en los comentarios, por favor…
Acabo de ver este video muy interesante sobre cómo escribir programas que inherentemente hacen que el estado no válido sea irrepresentable. Este podría ser un concepto muy importante de entender, especialmente si todavía estás aprendiendo o tu experiencia está basada en lenguajes que no cuentan con un sistema estricto de tipos.
Un "sistema de tipos algebraicos" en programación de computadoras se refiere a un sistema que permite la construcción de tipos complejos a partir de tipos simples mediante operaciones algebraicas. Estas operaciones suelen incluir suma (unión) y producto (combinación) de tipos. Los lenguajes con tipos algebraicos presentan oportunidades únicas para modelar datos imponiendo restricciones e invariantes sobre sus tipos, haciendo así que los programas sean más robustos y más fáciles de entender.
Rust y TypeScript son dos lenguajes que utilizan sistemas de tipos algebraicos de diferentes maneras.
Suma de tipos (union types)
Los union types permiten que un valor sea uno de varios tipos diferentes. Esto es análogo a una operación "or" en álgebra booleana. En programación, esto es útil para modelar situaciones en las que un valor puede provenir de tipos dispares y el programa necesita manejar cada tipo de manera diferente.
-
Rust: Rust usa
enum
para definir tipos de suma. Una enumeración en Rust puede tener variantes y cada variante puede, opcionalmente, contener datos de diferentes tipos. Esto permite a Rust modelar estructuras y patrones de datos complejos, como máquinas de estado o valores opcionales (por ejemplo, el tipoOption<T>
) de forma segura. -
TypeScript: TypeScript tiene tipos de unión que se indican mediante el operador
|
. Esto permite que una variable contenga un valor de uno de varios tipos, lo que permite al programador escribir código flexible manteniendo la seguridad de tipos. Por ejemplo, se podría definir una variable para contener unstring
o unnumber
.
Producto de tipos (structs)
Un “tipos producto” permite la combinación de varios valores en un valor compuesto, donde la parte "producto" surge de la idea de multiplicar las posibilidades de cada tipo de componente para obtener las posibilidades totales del tipo compuesto. Esto es útil para agrupar datos relacionados.
-
Rust: Rust tiene
struct
que se utiliza para crear tipos de datos complejos combinando valores de múltiples tipos. Cada campo de una estructura puede tener un tipo diferente, lo que permite la construcción de estructuras de datos ricas y tipificadas. - TypeScript: TypeScript utiliza interfaces y clases como su medio principal para crear tipos compuestos. Las interfaces en TypeScript se utilizan para definir la forma que deben adoptar los objetos, incluidos los tipos de sus propiedades y métodos. Las clases pueden implementar interfaces y proporcionar implementaciones concretas.
Tipos de datos algebraicos en programación
Los tipos de datos algebraicos (ADT) en lenguajes de programación como Rust y TypeScript mejoran la seguridad y la expresividad de los tipos. Permiten a los desarrolladores modelar datos de una manera que hace que los estados no válidos sean irrepresentables, lo que reduce significativamente los errores de tiempo de ejecución. Por ejemplo, en Rust, el compilador verifica que las expresiones coincidentes sean exhaustivas, asegurando que se manejen todas las variantes posibles de una enumeración. En TypeScript, se pueden utilizar tipos de unión y protecciones de tipos para garantizar que el código maneje correctamente diferentes tipos en tiempo de ejecución.
Tanto Rust como TypeScript aprovechan sus sistemas de tipos para brindar garantías en tiempo de compilación sobre el comportamiento del código, lo que hace que el proceso de desarrollo de software sea más confiable y eficiente. Si bien los detalles de cómo implementan estas características difieren, el principio subyacente de usar tipos para imponer restricciones y modelar datos complejos es una fortaleza compartida de ambos lenguajes.
Veamos un ejemplo que muestra el uso de los tipos de unión de TypeScript junto con las protecciones de tipos para manejar diferentes tipos en tiempo de ejecución de forma segura.
Ejemplo: Distinguiendo entre diferentes figuras
Imagine que está trabajando en una aplicación de gráficos que puede representar diferentes tipos de figuras. Cada figura (como círculo, cuadrado) tiene su propio conjunto de propiedades. Queremos escribir una función que tome una figura e imprima su área, usando union types para las figuras y type guards para distinguirlas en tiempo de ejecución.
Paso 1: Definir tipos de formas con unión
Primero, definimos las figuras que estamos manejando: Círculo
y Cuadrado
. Luego definimos un tipo Shape
que es una unión de estos tipos.
type Circle = {
kind: 'circle';
radius: number;
};
type Square = {
kind: 'square';
sideLength: number;
};
// Union type for any shape
type Shape = Circle | Square;
Paso 2: Implementar type guards
TypeScript nos permite usar “protectores de tipo” definidos por el usuario para verificar el tipo de un union type en tiempo de ejecución. Un type guard es una función que devuelve un valor booleano y tiene un predicado de tipo como tipo de retorno (arg is Type
).
Para nuestras formas, podemos usar la propiedad kind
para distinguirlas:
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}
function isSquare(shape: Shape): shape is Square {
return shape.kind === 'square';
}
Paso 3: Escribir una función para manejar figuras
Ahora, escribimos una función que toma una figura e imprime su área. Usaremos los type guards para acceder de forma segura a las propiedades específicas de cada tipo.
function printArea(shape: Shape) {
if (isCircle(shape)) {
// TypeScript knows `shape` is a Circle here
const area = Math.PI * shape.radius ** 2;
console.log(`Area of the circle: ${area}`);
} else if (isSquare(shape)) {
// TypeScript knows `shape` is a Square here
const area = shape.sideLength ** 2;
console.log(`Area of the square: ${area}`);
}
}
// Example usage
const circle: Circle = { kind: 'circle', radius: 2 };
const square: Square = { kind: 'square', sideLength: 3 };
printArea(circle); // Area of the circle: 12.566370614359172
printArea(square); // Area of the square: 9
Este ejemplo muestra cómo los union types y las type guards de TypeScript se pueden usar para escribir código seguro con verificación de tipos que maneje múltiples tipos en tiempo de ejecución. Al verificar la propiedad kind
, podemos decirle a TypeScript con qué tipo estamos tratando dentro de los bloques if
, lo que nos permite acceder a las propiedades únicas de cada tipo sin correr el riesgo de un error de tiempo de ejecución.
Posted on April 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.