¿Cuáles son los buenos modelos mentales para la programación funcional?

Vengo desde la perspectiva de Clojure (aunque he trabajado un poco con Scala, no puedo decir mucho al respecto en este momento)

Lo interesante es que me encuentro en el lado opuesto del espectro, así que lucho con los modismos de OO.

Por ejemplo:

  1. Me gusta la idea de la funcionalidad privada / pública en el código.
  2. Pero no me gusta el concepto de objetos que tienen su propio estado mutable oculto en su interior. Crea in-determinismo en tiempo de ejecución
  3. El concepto de interfaces (o contratos) es bueno. Puede escribir documentación “compatible con el compilador” para su módulo.
  4. La idea de que la taxonomía obligatoria se construya por adelantado es mala. (No puede anticipar su taxonomía antes de que se construya el sistema, y ​​la herencia basada en clases hace imposible programar de otra manera)

El FP es un mundo de transformadores. Transformas cualquier cosa en cualquier cosa.

  • Transforma la solicitud en una consulta de base de datos o un correo electrónico.
  • Transforma el resultado de la base de datos a un modelo
  • Transforma el modelo en un modelo de vista
  • Y transforma el modelo de vista en una cadena de HTML
  • Obtiene una lista de algo y la transforma en otra lista mediante la aplicación de una función de map .
  • Agregue resultados mediante la aplicación de una función de reduce . (Muy difícil para los novatos (como usar la recursión la primera vez), pero en un tiempo te acostumbras)

Por ejemplo. La solicitud promedio de la aplicación web se verá así:

Aquí tenemos solo un montón de transformadores desde la solicitud hasta la respuesta. (Y tenemos una comprobación nula usando some-> macro

;; algunos detendrán la ejecución, si los datos devueltos son nulos
(algunos->
;; solicitud de una consulta db
(solicitudes / solicitud-> solicitud de consulta de base de datos)
;; consulta de db a libros
(db-access / query-> book-data)
;; libros para ver libros
(ver modelos / datos de libros-> vista de libros)
;; libros a cadena html
(renderizador / plantilla de renderizado “/templates/book-template.html”))

Y calcular una suma de manzanas en una lista de manzanas se verá así

(reducir + (mapa: manzanas manzana))

El estado interno se puede capturar con cierres . Y, si ampliamos un concepto de cierres tendremos continuaciones.

La pereza nos permite escribir algo como esto:

Tomamos un rango infinito de números, lo filtramos para tomar la primera variante verdadera, y todo esto se hace de manera eficiente. (El idioma solo tendrá en cuenta los primeros 10 elementos

(primero (filtro # (= 10%) (rango)))

En su lenguaje codicioso promedio, este colgará para siempre.


Sí, FP es asombroso.

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)

  1. Comience con un trozo de masa
  2. Tire el trozo de masa y obtenga un trozo de masa de la misma manera; aplanarlo
  3. Tire la masa aplanada y obtenga una como esta, y agregue aceite
  4. Tire la masa engrasada y obtenga una igual, y agregue salsa de pizza
  5. Tire la masa salteada y obtenga una igual, y agregue queso
  6. Tire la pizza cruda y obtenga una igual, póngala en el horno
  7. Tire el horno con la pizza y consiga uno igual, pero con una pizza cocida.
  8. Tire el horno y la pizza cocida, pero obtenga una pizza cocida igual
  9. Tire la pizza cocida, pero coloque una igual dentro de una caja
  10. 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.

Pienso en la programación funcional (perezosa) (por ejemplo, Haskell) como una máquina de embutidos compleja.

La máquina de salchichas está compuesta de tuberías (las funciones) que solo aceptan ciertas formas de ingredientes (es decir, Tipos) como sus entradas y pasan su salida a la siguiente sección de tubería de la máquina.

Escribir un programa es como conectar la disposición correcta de las tuberías para convertir los ingredientes crudos (las entradas) en cualquier tipo de salchicha que desee (las salidas).

La evaluación diferida significa que girar el mango solo extrae la cantidad mínima de ingredientes a través de la máquina para producir cada salchicha a la vez.

Piensa en un supermercado. En un supermercado hay diferentes productos. Cada producto tiene un precio específico. Algunos productos tienen un precio diferente, algunos son igualmente caros (o baratos). Ahora pones todo tipo de productos en tu carrito. Después de recoger las cosas que necesita en su carrito, finalmente va a pagar. Independientemente de la cantidad de productos, pagará un precio por todos; para todos los productos que recibe a cambio, una factura. Ahora diferentes productos resultan en diferentes facturas. Pero también es posible que dos conjuntos de productos devuelvan la misma factura, incluso si los productos son diferentes. Aún así, en todos los casos obtienes una factura por cada carrito. Aquí los productos son los parámetros para las funciones que calculan la factura. Es posible obtener la misma factura con diferentes carros y diferentes productos, pero no es posible obtener dos facturas diferentes para un carro (o un producto).

Para mí, el concepto correspondiente sería “un programa es una función” y que todo el cálculo se realiza mediante evaluación / reducción. (Ref: Cálculo Lambda)