TypeScript y programación funcional
Introducción
Desde hace varios años, la programación funcional ha vuelto a estar en primer plano. Popular entre los desarrolladores web, este paradigma a menudo se ha limitado al ámbito científico y académico. Ahora, este estilo de programación se utiliza para el desarrollo de aplicaciones web a gran escala y ha sido destacado especialmente por Facebook, que es conocido por sus proyectos que utilizan programación funcional (en particular, React, Immutable.js, ReScript...).
TypeScript, por su parte, es un lenguaje llamado multiparadigma. Los desarrolladores pueden elegir el estilo de programación que quieren utilizar para desarrollar sus proyectos. La programación orientada a objetos (consulte el capítulo Programación orientada a objetos) es uno de los estilos de programación más populares en TypeScript, pero el lenguaje también tiene capacidades funcionales que lo convierten en una opción interesante cuando se desea desarrollar un proyecto con este paradigma. Sin embargo, TypeScript se considera un lenguaje parcialmente funcional porque no implementa ciertas capacidades específicas del mundo funcional, pero, por otro lado, su naturaleza multiparadigma permite mezclar estilos de programación.
La programación funcional tiene fama de ser difícil de aprender, pero en realidad no es así. Muchos desarrolladores han recibido formación...
Función pura e impura
Lo primero que hay que aprender al iniciarse en la programación funcional es la noción de pureza. Se dice que una función es pura si:
-
No causa ningún side effect (efecto secundario en español).
-
Devuelve el mismo valor para los mismos parámetros.
Estas dos reglas permiten aumentar la fiabilidad de un programa. Una función pura no causa impactos incontrolados durante su ejecución porque garantiza que su funcionamiento no pueda alterar el de otra función.
Para evitar causar efectos secundarios, en ningún caso una función debe cambiar el valor de una variable. Por tanto, se dice que una función es impure si muta una variable global.
Ejemplo:
let value = 1295;
const add = (number: number)=> {
value += number;
};
add(42);
// Log: 1337
console.log(value);
En este ejemplo, la función add incrementa la variable de valor global con el parámetro number que se le pasa. Este incremento provoca un efecto secundario global y, por lo tanto, puede tener un impacto en el funcionamiento de otra función que utilice esta variable. Este tipo de impacto puede ser la fuente de un error en una aplicación y debe considerarse peligroso.
Preste atención a las funciones que no tienen tipo de retorno. Su ejecución presenta...
Inmutabilidad
En la sección anterior, la definición de una función pura dependía principalmente de cómo se implementaba. Sin embargo, es posible tratar los efectos secundarios como errores al compilar con TypeScript. Para ello es necesario declarar las variables de forma inmutable. La noción de mutación ya se ha abordado anteriormente con el uso de las palabras clave var, let y const (consulte el capítulo Tipos e instrucciones básicas). Usar const para declarar una variable evita que se reasigne más adelante. Por tanto, la referencia de esta variable se vuelve inmutable y su valor ya no se puede modificar.
En programación funcional, es preferible utilizar variables inmutables, ya que evitan la creación de efectos secundarios y ayudan a los desarrolladores a implementar funciones puras. Además, el uso de la inmutabilidad facilita el análisis de variables durante las sesiones de depuración. En efecto: si los valores no se pueden modificar, las mutaciones se asignarán a nuevas variables. Entonces resulta fácil comparar la variable original y la versión modificada que ha devuelto una función.
Como recordatorio, he aquí un ejemplo de cómo declarar una variable inmutable con la palabra clave const:
const firstName: string = "Evelyn";
// Compilation Error TS2588:
// Cannot assign to 'firstName' because it is a constant.
firstName = "John";
La palabra clave const plantea un problema porque, cuando se usa para declarar una variable, solo la referencia es inmutable. Esto significa que, en el caso de un objeto, sus propiedades son mutables. En consecuencia, es posible modificar los valores contenidos en un objeto.
Ejemplo:
const employe = {
prenom: "Martin",
nom: "Dupont",
salaire: 2000
};
// The salary change is valid
employe.salaire = 2100;
TypeScript ofrece dos soluciones para abordar este problema. La primera opción es declarar cada propiedad de un tipo como de solo lectura mediante la palabra clave readonly. Después de crear una instancia de un objeto, será imposible...
Iteración
Iterar un arreglo en un contexto donde los efectos secundarios están prohibidos puede resultar complicado. El problema se demuestra fácilmente con un ejemplo de una función que calcula una suma.
Ejemplo:
const sum = (numbers: ReadonlyArray<number>) => {
let result = 0;
numbers.forEach((number: number) => {
result += number;
});
return result;
};
En este ejemplo, es obligatorio declarar una variable mutable para poder recalcular la suma en cada iteración. Por tanto, la función es impura si se prohíben los efectos secundarios locales. Para calcular la suma en este caso específico, es preferible no utilizar el método forEach (lo mismo aplica a los bucles for y while). Prohibir el uso de bucles en programación lleva a la pregunta: ¿cómo iterar sobre una matriz sin utilizar un método u operador? ¡La respuesta es usar la recursividad!
Este concepto es relativamente sencillo de entender: se dice que una función es recursiva desde el momento en que se llama a sí misma. Sin embargo, es necesario incluir una terminación en la función para que no se ejecute infinitamente. Calcular el factorial de un número permite demostrar el beneficio de la recursividad.
Ejemplo:
const factorial = (number: number):...
Condiciones
Al igual que las iteraciones, el uso de condiciones puede resultar complicado cuando las mutaciones están prohibidas. La función divide, utilizada anteriormente en este capítulo, ilustra bien el hecho de que las condiciones pueden forzar el uso de mutaciones.
Una primera técnica para evitar mutaciones al utilizar una condición es el uso de un return al final de la condición. Esto le permite devolver un valor sin modificar una variable local. En programación funcional, para lograr una mayor compacidad del código, es común utilizar operadores ternarios. Al utilizar la función divide, un operador ternario permite evitar la creación de una mutación.
Ejemplo:
const divide = (number1: number, number2: number) => {
return number2 !== 0
? { success: true, result: number1 / number2 }
: { success: false, result: NaN };
};
const result1 = divide(200, 2);
// Log: { success: true, result: 100 }
console.log(result1);
const result2 = divide(200, 0);
// Log: { success: false, result: NaN }
console.log(result2);
En el caso de condiciones múltiples (o anidadas), los operadores ternarios pueden ser menos prácticos, más complejos...
Función parcial
Las funciones parciales permiten implementar el concepto de aplicación parcial. Es una técnica que reduce el número de parámetros de una función para producir otra con menos argumentos.
La ventaja de la aplicación parcial es que permite la definición de funciones incompletas que tienen un alto nivel de reutilización posterior.
Ejemplo:
interface Employee {
firstName: string;
lastName: string;
salary: number;
}
const increaseSalary = (percent: number) => {
return (employee: Readonly<Employee>) => {
const increase = employee.salary * (percent / 100);
const salary = employee.salary + increase;
return {
...employee,
salary
};
};
};
const increaseSalaryByTwoPercent = increaseSalary(2);
const evelyn = {
firstName: "Evelyn",
lastName: "Miller",
salary: 2000
};
...
Currying
En programación funcional, el Currying es una técnica para convertir una función con varios argumentos en un conjunto de funciones, cada una con un único argumento. El concepto de Currying suele confundirse con el de aplicación parcial. Este último permite generar una nueva función tomando menos argumentos que la original, mientras que el Currying produce tantas funciones como parámetros hay presentes en la función original.
Para ilustrar el uso del Currying, tomemos el ejemplo de una función que permite crear un objeto de tipo Person.
Ejemplo:
interface Person {
firstName: string;
lastName: string;
}
const create = (firstName: string, lastName: string) => {
const person: Person = {
firstName,
lastName
};
return person;
};
// Log: { firstName: 'Evelyn', lastName: 'Miller' }
const person = create("Evelyn", "Miller");
console.log(person);
Esta función se puede escribir de manera que permita usarla como una función de Currying.
Ejemplo:
const create = (firstName: string) => {
return (lastName:...
Pattern Matching
Muchos lenguajes orientados funcionalmente permiten el uso del concepto de Pattern Matching (filtrado por patrón en español), que permite verificar si un valor coincide con una regla (Pattern). Si esta se cumple, entonces es posible ejecutar un bloque de código que devuelva un valor. Una función de coincidencia (matching) recuperará el parámetro de entrada para analizarlo mediante un conjunto de patrones. Estos permitirán que la función de coincidencia defina qué bloque de código se ejecutará.
Desafortunadamente, el concepto de Pattern Matching no está presente en de forma nativa en TypeScript. El lenguaje aún no tiene las capacidades sintácticas para implementarlo. En la actualidad, el Pattern Matching está pendiente de estandarización en ECMAScript.
Es posible implementar el PatternMatching utilizando una función anónima autoejecutada con retorno. Esta función aceptará un valor como parámetro y usará condiciones (if, else...if, else o switch) para analizarlo y ejecutar un bloque de código en consecuencia.
Ejemplo:
const salaries: ReadonlyArray<number> = [1700, 2250, 2000, 1850];
const increasedSalary = (salary: number) => {
return ((s) => {
if (s <= 1800) {
// increase by 5%
return s + s * 0.05; ...
Composición de funciones
Una de las ventajas de la aplicación parcial es que permite combinar funciones entre sí, en particular mediante el uso de los conceptos de pipe (tubería) y composición.
TypeScript no tiene capacidades sintácticas que permitan el uso de composición o de pipe. Para este último, se está estandarizando un operador de pipeline según TC39.
Los pipes permiten encadenar llamadas a funciones una tras otra. Luego se llamará a la siguiente función con el valor de retorno de la anterior.
Ejemplo:
enum Gender {
Male,
Female
}
interface Employee {
firstName: string;
lastName: string;
salary: number;
gender: Gender;
}
const employees = [
{
firstName: "Bryan",
lastName: "Hill",
salary: 1700,
gender: Gender.Male
},
{
firstName: "Evelyn",
lastName: "Miller",
salary: 2250, ...
Para ir más lejos…
Este capítulo es solo una descripción general de cómo utilizar la programación funcional en TypeScript. Este paradigma es muy interesante y algunas de sus capacidades básicas se pueden utilizar fácilmente en combinación con la programación orientada a objetos. Sin embargo, para ir más allá es necesario interesarse por la teoría de categorías para comprender los tipos de datos algebraicos (Functor, Monad, Monoid...).
Algunas bibliotecas de código abierto también permiten ir más allá al implementar ciertos conceptos vistos a lo largo de este capítulo:
-
Immutable.js: biblioteca creada por Facebook que proporciona estructuras de datos inmutables implementando Structural Sharing (mediante el uso de Hash maps tries, una tabla de asociación que permite compartir estructuras de datos).
-
Ramda: biblioteca que proporciona funciones de utilidad para implementar programación funcional.
-
fp-ts: biblioteca inspirada en Haskell y Scala (dos lenguajes de programación funcionales). Contiene un conjunto de tipos, implementaciones y abstracciones entre las más útiles en programación funcional (ejemplo: Maybe, Option, Functor, Either…).