Líneas de montaje.
const compose = (… fns) => x =>
fns.reduceRight ((x, f) => f (x), x);
const curry = (f, … initialArgs) => (… adicionalesArgs) => {
const args = [… initialArgs, … adicionalArgs];
return (args.length> = f.length)? f (… args): curry (f, … args);
};
Usando estos dos pequeños ayudantes prácticos y elegantes, en este caso, ES6, aunque en la mayoría de los otros idiomas están incorporados o pueden implementarse de manera similar siempre que la verificación de tipo no encaje dentro de reduceRight
, me permite construir todo tipo de líneas de montaje.
const map = curry ((transform, array) => array.map (transform));
const filter = curry ((predicado, matriz) => array.filter (predicado));
const reduce = curry ((reductor, semilla, matriz) =>
array.reduce (reductor, semilla));
Eso es genial, por supuesto, pero no ayuda a ver qué está pasando realmente …
función castToEmployee (personConfig) {
volver nuevo empleado (personConfig);
}
función isWoman (persona) {
return person.gender === “mujer”;
}
recuento de funciones (x) {
retorno x + 1;
}
const countFemaleEmployees = componer (
reducir (contar, 0),
filtro (isWoman),
mapa (castToEmployee)
);
countFemaleEmployees ([bob, susan, doug, jim, laura, sarah]); // 3
Esto funciona bastante bien, como un ensamblaje que toma una matriz de datos … tal vez esos datos provienen del servidor, o de la base de datos, o de algún otro proceso en el sistema, pero los datos no son la parte importante, aquí.
He establecido una línea de ensamblaje de comportamientos que quiero aplicar a lo que sea que pase. Primero lanzo a un empleado, y luego filtro a cualquiera que no sea “femenino”, luego cuento cada instancia restante.
Pero mira cuán estúpidamente simple es cada una de esas funciones.
El código de producción del mundo real tampoco tiene que ser mucho más complicado que eso.
const processFlights = compose (
filter (filterByUserPreferences (filterConfig)),
filtro (removeMalformedFlight),
map (flightData => new Flight (flightData))
);
const paginateItems = curry ((config, items) => {/ *… * /});
const displayFlights = vuelos => {
/ * ponerlos en la página, de alguna manera * /
};
getFlights ()
.then (processFlights)
.then (paginateItems (pageConfig))
.then (displayFlights);
Todas mis funciones son puras.
- no operan en estado oculto
- no cambian el mundo fuera de sus muros
- no modifican los objetos que reciben, sino que hacen nuevas copias
- su salida no cambia según la información externa que no se pasó
Como todos son puros, puedo conectarlos de esta manera, y luego puedo alimentar datos a través de la línea de ensamblaje. Las líneas de montaje están hechas para soportar la fabricación de más de un automóvil, ¿verdad? O más de un pastel. O más de una computadora.
Cada estación hace su trabajo tomando algo y pasando algo. A medida que una instancia de datos se mueve a lo largo de la línea de ensamblaje, la salida de una estación se convierte en la entrada de la siguiente estación, y esa estación emite algo que luego se ingresa para otra cosa.
Una vez que se cae al final de la línea, está listo para ser enviado a otro lugar. A veces eso es para el consumidor, pero a veces eso sale de una línea de ensamblaje y luego se alimenta a otra línea de ensamblaje.
Piense en fábricas como FoxConn que fabrican piezas que se utilizan en iPhones. La parte está “terminada” cuando sale de la línea de montaje en FoxConn. Pero esa parte terminada es solo una parte de muchas que se utilizan para hacer el teléfono. Al igual que los transistores se “hicieron” en un proceso separado de nivel inferior, antes de que FoxConn los usara para hacer sus partes.
Por lo tanto, puede ver la composición (que podría estar compuesta de composiciones) involucrada en el cambio de silicio y cobre a transistores, a componentes más grandes, a un teléfono.
La mayor diferencia entre el mundo real y el mundo de las computadoras es que necesitamos copiar el objeto que se nos da como entrada, en lugar de modificarlo directamente.
Pasos para hacer una pizza funcional (FP)
- Comience con un trozo de masa
- Tire el trozo de masa y obtenga un trozo de masa de la misma manera; aplanarlo
- Tire la masa aplanada y obtenga una como esta, y agregue aceite
- Tire la masa engrasada y obtenga una igual, y agregue salsa de pizza
- Tire la masa salteada y obtenga una igual, y agregue queso
- Tire la pizza cruda y obtenga una igual, póngala en el horno
- Tire el horno con la pizza y consiga uno igual, pero con una pizza cocida.
- Tire el horno y la pizza cocida, pero obtenga una pizza cocida igual
- Tire la pizza cocida, pero coloque una igual dentro de una caja
- …
- …
- Tire al repartidor, y su auto, y la caja, y el cortador de pizza, y sus platos, y su estómago, y reemplácelo con un estómago nuevo, pero con pizza adentro …
¿Pero por qué haríamos esto? ¿Por qué seríamos tan derrochadores y seguir tirando cosas para reconstruirlas desde cero?
La respuesta es simple:
En el mundo real, si pasa la puerta de un automóvil y necesita agregarle una ventana, nadie más tiene acceso a la puerta al mismo tiempo.
const coffee = nuevo café ();
función addMilk (bebida) {drink.add (nueva leche ()); }
función addCream (bebida) {drink.add (nueva Crema ()); }
función addSugar (bebida) {drink.add (nuevo Sugar ()); }
función quaff (drink) {consume (drink.emptyContents ()); }
// Ahora, asumimos que simplemente podemos decorar esa bebida …
addCream (café);
addCream (café);
addSugar (café);
addSugar (café);
quaff (bebida);
//… pero qué pasa si algún otro hilo llama a `addCream`
// en ese mismo café, mientras estás en medio de beberlo?
// … ¿y si algún otro proceso vacía el contenido de tu
// café, cuando tenías la intención de usarlo?
// … ¿y si lo molestas y no es tuyo para beber?
copia de función (obj) {
devolver Object.assign ({}, obj);
}
// actualiza las funciones para devolver copias modificadas
const addCream = bebida => {
const safeDrink = copiar (beber);
safeDrink.add (nueva Crema ());
volver safeDrink;
};
// podríamos hacer que `drink.add` devuelva` this` y acortar todo
const addSugar = drink => copy (drink) .add (nuevo Sugar ());
// ahora sabes que nadie te va a tomar el café …
// y no viertes accidentalmente crema en un vaso que
// alguien más ya terminó y está a punto de pagar
beber a grandes tragos(
añade azucar(
añade azucar(
addCream (
addCream (nuevo café ())))));
Podemos ver que ahora estamos sirviendo café caliente a través de este sistema para tener algo útil al final, y hemos garantizado que nadie más se va a meter con el café que sacamos del otro lado (ni estamos jodiendo) la línea de montaje de cualquier otra persona, cambiando sus valores en ellos). Pero ahora tenemos muchas funciones puras ejecutándose en valores para pasar resultados a otras funciones puras, y eso significa muchas llamadas de funciones anidadas.
¿Podemos hacer que sea más plano y más fácil trabajar con él?
Por supuesto que podemos; Vimos eso al principio.
const makeDoubleDouble = componer (
añade azucar,
añade azucar,
addCream,
addCream
);
const doubleDouble = makeDoubleDouble (nuevo Coffee ());
const secondDoubleDouble = makeDoubleDouble (nuevo Coffee ());
quaff (doubleDouble);
quaff (secondDoubleDouble);
Mirando eso, ¿qué tan fácil podría ser agregar sabores, o agregar crema batida, o una inyección de espresso, si es el caso?
¿Qué tan fácil sería descubrir que el café sabe mejor si agrega el azúcar antes de la crema? ¿O agregar un azúcar después de cada crema?
const makeRidiculousDoubleDouble = componer (
addChocolateShavings,
addChocolateSyrup,
addWhippedCream,
addFlavourShot,
addEspresso,
makeDoubleDouble
);
quaff (makeRidiculousDoubleDouble (new Coffee ()));
takeInsulinShot ();
Tenga en cuenta que la primera función allí es `makeDoubleDouble` de antes. Estamos usando una función compuesta dentro de nuestra función compuesta, al igual que las líneas de ensamblaje del mundo real que obtienen piezas de líneas de ensamblaje más pequeñas. Son partes prefabricadas (o lo son, una vez que salen de la composición más pequeña).
De hecho, el patrón de decorador que alguna vez fue útil (pero resulta ser principalmente práctico) puede renacer fácilmente en estas tuberías.
const makeRedEye = componer (
addEspressoShot,
addEspressoShot
);
const makeFancy = componer (
addChocolateShavings,
addChocolateSyrup,
addWhippedCream
);
const addFlavourShot = curry ((FlavourShot, drink) =>
drink.add (nuevo FlavourShot ()));
const makeRidiculousDoubleDouble = componer (
makeFancy,
addFlavourShot (avellana),
makeRedEye,
makeDoubleDouble
);
Eche un vistazo: la función sigue haciendo exactamente lo mismo que antes, pero ahora tiene líneas de ensamblaje en miniatura dentro de ella. Esas líneas de ensamblaje se pueden reutilizar en otras partes del sistema. Debería poder agregar espresso a casi cualquier cosa; Lo mismo vale para hacer que la bebida sea ridículamente elegante.
La única pregunta que queda debe ser:
“¿Por qué estamos leyendo estas funciones de componer de abajo hacia arriba, en lugar de arriba hacia abajo?”
Y la respuesta es “¡Porque las matemáticas!”
[matemáticas] f (x) = fx [/ matemáticas]
Al igual que el cálculo antiguo, si nuestras funciones son puras, entonces [math] f (x) [/ math] es siempre [math] fx [/ math].
[matemáticas] doble (2) [/ matemáticas] es siempre [matemáticas] 4 [/ matemáticas]
[matemáticas] g (f (x)) = gfx [/ matemáticas]
Sabemos que si [math] double (2) [/ math] es [math] 4 [/ math], entonces [math] add1 (double (2)) [/ math] siempre debería ser [math] 5 [/ math ]
De hecho, sabemos esto tan bien que podemos decir que no hay diferencia entre pasar [math] f (x) [/ math] o [math] fx [/ math].
[math] add1 (double (2)) [/ math] y [math] add1 (4) [/ math] son exactamente lo mismo. Asumiendo que [math] f [/ math] es puro, no hay diferencia entre [math] g (f (x)) [/ math] y [math] g (fx) [/ math]. Esta información súper asombrosa se llama “Transparencia referencial” y viene gratis con funciones puras.
De cualquier manera…
[matemáticas] f (x) = fx [/ matemáticas] “F de X es igual a FX”
[matemáticas] g (f (x)) = gfx [/ matemáticas] O [matemáticas] g (fx) = gfx [/ matemáticas]
[matemática] h (g (f (x))) = hgfx [/ matemática] OR [matemática] h (g (fx)) = hgfx [/ matemática] OR [matemática] h (gfx) = hgfx [/ matemática]
Eso es genial … … pero ¿qué pasa si no tenemos [matemáticas] x [/ matemáticas]? ¿Qué hacemos si tenemos 80 datos y sabemos que queremos ejecutar exactamente la misma tubería en cada uno?
La respuesta es [matemáticas] COMPONER [/ matemáticas]. Lo que queremos es una forma de decir [matemáticas] hgf = h. g. f [/ math] para que podamos poner [math] x [/ math] y obtener [math] hgfx [/ math] y podamos poner [math] y [/ math] y obtener [math] hgfy [/ matemáticas], y así sucesivamente.
Entonces [math] COMPOSE [/ math] como lo escribimos en ES6 arriba, parece
const hgf = compose(h, g, f);
Si tiene dificultades para pensar de esa manera, un montón de bibliotecas usan pipe(f, g, h);
para significar exactamente lo mismo, pero definido de arriba a abajo, para parecerse más al código y menos a las matemáticas.
Espero que eso ayude. Sé que realmente no traté con la programación basada en colecciones (map / filter / reduce / flatMap / etc) o Functors / Monads o streams o algo por el estilo …
Eso podría ser solo un toque por la borda para una publicación.