Introducción a los principios básicos de la programación funcional

Después de mucho tiempo aprendiendo y trabajando con programación orientada a objetos, di un paso atrás para pensar en la complejidad del sistema.

" Complexity is anything that makes software hard to understand or to modify." - John Outerhout

Investigando un poco, encontré conceptos de programación funcional como inmutabilidad y función pura. Esos conceptos son grandes ventajas para crear funciones sin efectos secundarios, por lo que es más fácil mantener los sistemas, con algunos otros beneficios.

En esta publicación, te contaré más sobre la programación funcional y algunos conceptos importantes, con muchos ejemplos de código.

Este artículo utiliza Clojure como ejemplo de lenguaje de programación para explicar la programación funcional. Si no se siente cómodo con un tipo de lenguaje LISP, también publiqué la misma publicación en JavaScript. Eche un vistazo: Principios de programación funcional en Javascript

¿Qué es la programación funcional?

La programación funcional es un paradigma de programación, un estilo de construcción de la estructura y los elementos de los programas de computadora, que trata la computación como la evaluación de funciones matemáticas y evita el estado cambiante y los datos mutables - Wikipedia

Funciones puras

El primer concepto fundamental que aprendemos cuando queremos entender la programación funcional son las funciones puras . Pero, ¿qué significa esto realmente? ¿Qué hace que una función sea pura?

Entonces, ¿cómo sabemos si una función es pureo no? Aquí hay una definición muy estricta de pureza:

  • Devuelve el mismo resultado si se le dan los mismos argumentos (también se le conoce como deterministic)
  • No causa ningún efecto secundario observable.

Devuelve el mismo resultado si se le dan los mismos argumentos

Imagina que queremos implementar una función que calcula el área de un círculo. Una función impura recibiría radiuscomo parámetro y luego calcularía radius * radius * PI. En Clojure, el operador es lo primero, por lo que se radius * radius * PIconvierte en (* radius radius PI):

¿Por qué es esta una función impura? Simplemente porque usa un objeto global que no se pasó como parámetro a la función.

Ahora imagine que algunos matemáticos argumentan que el PIvalor es en realidad 42y cambie el valor del objeto global.

Nuestra función impura ahora resultará en 10 * 10 * 42= 4200. Para el mismo parámetro ( radius = 10), tenemos un resultado diferente. ¡Arreglemoslo!

TA-DA?! Ahora siempre pasaremos el I valor P como parámetro a la función. Entonces ahora solo estamos accediendo a los parámetros pasados ​​a la función. No external object.

  • Para los parámetros radius = 10& PI = 3.14, siempre tendremos el mismo resultado:314.0
  • Para los parámetros radius = 10& PI = 42, siempre tendremos el mismo resultado:4200

Lectura de archivos

Si nuestra función lee archivos externos, no es una función pura, el contenido del archivo puede cambiar.

Generación de números aleatorios

Cualquier función que se base en un generador de números aleatorios no puede ser pura.

No causa ningún efecto secundario observable.

Los ejemplos de efectos secundarios observables incluyen la modificación de un objeto global o un parámetro pasado por referencia.

Ahora queremos implementar una función para recibir un valor entero y devolver el valor aumentado en 1.

Tenemos el countervalor. Nuestra función impura recibe ese valor y reasigna el contador con el valor aumentado en 1.

Observación : se desaconseja la mutabilidad en la programación funcional.

Estamos modificando el objeto global. ¿Pero cómo lo haríamos pure? Simplemente devuelva el valor aumentado en 1. Tan simple como eso.

Vea que nuestra función pura increase-counterdevuelve 2, pero el countervalor sigue siendo el mismo. La función devuelve el valor incrementado sin alterar el valor de la variable.

Si seguimos estas dos simples reglas, será más fácil comprender nuestros programas. Ahora todas las funciones están aisladas y no pueden afectar a otras partes de nuestro sistema.

Las funciones puras son estables, consistentes y predecibles. Dados los mismos parámetros, las funciones puras siempre devolverán el mismo resultado. No necesitamos pensar en situaciones en las que el mismo parámetro tiene resultados diferentes, porque nunca sucederá.

Beneficios de las funciones puras

Definitivamente, el código es más fácil de probar. No necesitamos burlarnos de nada. Entonces podemos probar funciones puras unitarias con diferentes contextos:

  • Dado un parámetro A→ esperar que la función devuelva un valorB
  • Dado un parámetro C→ esperar que la función devuelva un valorD

Un ejemplo simple sería una función para recibir una colección de números y esperar que incremente cada elemento de esta colección.

Recibimos la numberscolección, la usamos mapcon la incfunción para incrementar cada número y devolvemos una nueva lista de números incrementados.

Para el input[1 2 3 4 5], lo esperado outputsería [2 3 4 5 6].

Inmutabilidad

No cambia con el tiempo o no se puede cambiar.

Cuando los datos son inmutables, su estado no puede cambiardespués de su creación. Si desea cambiar un objeto inmutable, no puede. En su lugar, crea un nuevo objeto con el nuevo valor.

En Javascript usamos comúnmente el forbucle. Esta siguiente fordeclaración tiene algunas variables mutables.

Para cada iteración, cambiamos el iy el sumOfValueestado . Pero, ¿cómo manejamos la mutabilidad en la iteración? ¡Recursión! ¡De vuelta a Clojure!

Entonces aquí tenemos la sumfunción que recibe un vector de valores numéricos. Los recursaltos de nuevo al loophasta que obtenemos el vector vacío (nuestra recursión base case). Para cada "iteración" agregaremos el valor al totalacumulador.

Con la recursividad, mantenemos nuestras variablesinmutable.

Observación : ¡Sí! Podemos utilizar reducepara implementar esta función. Veremos esto en el Higher Order Functionstema.

También es muy común construir el estado final de un objeto. Imagina que tenemos una cadena y queremos transformar esta cadena en un url slug.

En programación orientada a objetos en Ruby, crearíamos una clase, digamos, UrlSlugify. Y esta clase tendrá un slugify!método para transformar la entrada de cadena en un url slug.

¡Hermoso! ¡Está implementado! Aquí tenemos una programación imperativa que dice exactamente lo que queremos hacer en cada slugifyproceso: primero en minúsculas, luego elimine los espacios en blanco inútiles y, finalmente, reemplace los espacios en blanco restantes con guiones.

Pero estamos mutando el estado de entrada en este proceso.

Podemos manejar esta mutación haciendo composición de funciones o encadenamiento de funciones. En otras palabras, el resultado de una función se utilizará como entrada para la siguiente función, sin modificar la cadena de entrada original.

Aquí tenemos:

  • trim: elimina los espacios en blanco de ambos extremos de una cadena
  • lower-case: convierte la cadena a minúsculas
  • replace: reemplaza todas las instancias de coincidencia con reemplazo en una cadena dada

Combinamos las tres funciones y podemos hacer "slugify"nuestra cadena.

Hablando de funciones de combinación , podemos usar la compfunción para componer las tres funciones. Vamos a ver:

Transparencia referencial

Implementemos un square function:

Esta función (pura) siempre tendrá la misma salida, dada la misma entrada.

Pasar “2” como parámetro de la square functionvoluntad siempre devuelve 4. Así que ahora podemos reemplazar el (square 2)por 4. ¡Eso es todo! Nuestra función es referentially transparent.

Básicamente, si una función produce constantemente el mismo resultado para la misma entrada, es referencialmente transparente.

funciones puras + datos inmutables = transparencia referencial

Con este concepto, algo interesante que podemos hacer es memorizar la función. Imagina que tenemos esta función:

Los (+ 5 8)iguales 13. Esta función siempre dará como resultado 13. Entonces podemos hacer esto:

Y esta expresión siempre resultará en 16. Podemos reemplazar toda la expresión con una constante numérica y memorizarla.

Funciona como entidades de primera clase

La idea de las funciones como entidades de primera clase es que las funciones también se tratan como valores y se utilizan como datos.

En Clojure es común usarlo defnpara definir funciones, pero esto es solo azúcar sintáctico para (def foo (fn ...)). fndevuelve la función en sí. defndevuelve un varque apunta a un objeto de función.

Las funciones como entidades de primera clase pueden:

  • referirse a él desde constantes y variables
  • pasarlo como parámetro a otras funciones
  • devolverlo como resultado de otras funciones

La idea es tratar las funciones como valores y pasar funciones como datos. De esta forma podemos combinar diferentes funciones para crear nuevas funciones con nuevo comportamiento.

Imagina que tenemos una función que suma dos valores y luego duplica el valor. Algo como esto:

Ahora una función que resta valores y devuelve el doble:

Estas funciones tienen una lógica similar, pero la diferencia son las funciones de los operadores. Si podemos tratar las funciones como valores y pasarlos como argumentos, podemos construir una función que reciba la función del operador y la use dentro de nuestra función. ¡Vamos a construirlo!

¡Hecho! Ahora tenemos un fargumento y lo usamos para procesar ay b. Pasamos las funciones +y -para componer con la double-operatorfunción y crear un nuevo comportamiento.

Funciones de orden superior

Cuando hablamos de funciones de orden superior, nos referimos a una función que:

  • toma una o más funciones como argumentos, o
  • devuelve una función como resultado

La double-operatorfunción que implementamos anteriormente es una función de orden superior porque toma una función de operador como argumento y la usa.

Usted probablemente ha oído hablar filter, mapy reduce. Echemos un vistazo a estos.

Filtrar

Dada una colección, queremos filtrar por un atributo. La función de filtro espera un valor trueo falsepara determinar si el elemento debe o no debe incluirse en la colección de resultados. Básicamente, si la expresión de devolución de llamada es true, la función de filtro incluirá el elemento en la colección de resultados. De lo contrario, no lo hará.

Un ejemplo simple es cuando tenemos una colección de números enteros y solo queremos los números pares.

Enfoque imperativo

Una forma imperativa de hacerlo con Javascript es:

  • crear un vector vacío evenNumbers
  • iterar sobre el numbersvector
  • empuja los números pares al evenNumbersvector

Podemos usar la filterfunción de orden superior para recibir la even?función y devolver una lista de números pares:

Un problema interesante que resolví en Hacker Rank FP Path fue el problema de la matriz de filtros . La idea del problema es filtrar una matriz dada de enteros y generar solo aquellos valores que sean menores que un valor especificado X.

Una solución de Javascript imperativa para este problema es algo como:

Decimos exactamente lo que nuestra función necesita hacer: iterar sobre la colección, comparar el elemento actual de la colección con xy enviar este elemento a resultArraysi pasa la condición.

Enfoque declarativo

Pero queremos una forma más declarativa de resolver este problema y usar también la filterfunción de orden superior.

Una solución declarativa de Clojure sería algo como esto:

Esta sintaxis parece un poco extraña en primer lugar, pero es fácil de entender.

#(> x%) es solo una función anónima que recibe es x y la compara con cada elemento de la colección n. % representa el parámetro de la función anónima, en este caso el elemento actual dentro de t he filter.

También podemos hacer esto con mapas. Imagina que tenemos un mapa de personas con su namey age. Y queremos filtrar solo a las personas con un valor de edad específico, en este ejemplo, las personas que tienen más de 21 años.

Resumen de código:

  • tenemos una lista de personas (con namey age).
  • tenemos la función anónima #(< 21 (:age %)). ¿Recuerda que hel% representa el elemento actual de la colección? Bueno, el elemento de la colección es un mapa de personas. Si tenemos do (:age {:name "TK" :age 26}), devuelve el valor de edad e,26 en este caso.
  • filtramos a todas las personas en función de esta función anónima.

Mapa

La idea de mapa es transformar una colección.

El mapmétodo transforma una colección aplicando una función a todos sus elementos y construyendo una nueva colección a partir de los valores devueltos.

Consigamos la misma peoplecolección de arriba. No queremos filtrar por "edad avanzada" ahora. Solo queremos una lista de cadenas, algo así como TK is 26 years old. Entonces, la cadena final podría ser :name is :age years oldwhere :namey :ageare atributos de cada elemento de la peoplecolección.

De forma imperativa en Javascript, sería:

De forma declarativa de Clojure, sería:

La idea es transformar una colección determinada en una nueva colección.

Otro problema interesante de Hacker Rank fue el problema de la lista de actualizaciones . Solo queremos actualizar los valores de una colección determinada con sus valores absolutos.

Por ejemplo, la entrada [1 2 3 -4 5]necesita que la salida sea [1 2 3 4 5]. El valor absoluto de -4es 4.

Una solución simple sería una actualización in situ para cada valor de colección.

Usamos la Math.absfunción para transformar el valor en su valor absoluto y realizamos la actualización in situ.

Esta no es una forma funcional de implementar esta solución.

Primero, aprendimos sobre la inmutabilidad. Sabemos lo importante que es la inmutabilidad para que nuestras funciones sean más consistentes y predecibles. La idea es construir una nueva colección con todos los valores absolutos.

En segundo lugar, ¿por qué no utilizar mapaquí para "transformar" todos los datos?

Mi primera idea fue construir una to-absolutefunción para manejar solo un valor.

Si es negativo, queremos transformarlo en un valor positivo (el valor absoluto). De lo contrario, no necesitamos transformarlo.

Ahora que sabemos cómo hacer absolutepara un valor, podemos usar esta función para pasar como argumento a la mapfunción. ¿Recuerda que a higher order functionpuede recibir una función como argumento y usarla? ¡Sí, el mapa puede hacerlo!

Guau. ¡Tan hermosa! ?

Reducir

La idea de reducir es recibir una función y una colección, y devolver un valor creado al combinar los elementos.

Un ejemplo común del que habla la gente es obtener el monto total de un pedido. Imagina que estás en un sitio web de compras. Que ha añadido Product 1, Product 2, Product 3, y Product 4a su carro de la compra (orden). Ahora queremos calcular el monto total del carrito de compras.

De manera imperativa, iteraríamos la lista de pedidos y sumaríamos la cantidad de cada producto al monto total.

Usando reduce, podemos construir una función para manejar amount sumy pasarla como argumento a la reducefunción.

Aquí tenemos shopping-cartla función sum-amountque recibe la corriente total-amounty el current-productobjeto para sumellos.

La get-total-amountfunción se utiliza para reducela shopping-cartmediante el uso de la sum-amountya partir de 0.

Otra forma de obtener la cantidad total es componer mapy reduce. ¿Qué quiero decir con eso? Podemos usar mappara transformar el shopping-carten una colección de amountvalores, y luego usar la reducefunción con +función.

El get-amountrecibe el objeto de productos y devuelve sólo el amountvalor. Entonces lo que tenemos aquí es [10 30 20 60]. Y luego reducecombina todos los elementos sumando. ¡Hermoso!

Echamos un vistazo a cómo funciona cada función de orden superior. Quiero mostrarles un ejemplo de cómo podemos componer las tres funciones en un ejemplo simple.

Hablando de eso shopping cart, imagina que tenemos esta lista de productos en nuestro pedido:

Queremos la cantidad total de todos los libros en nuestro carrito de compras. Simple como eso. ¿El algoritmo?

  • filtrar por tipo de libro
  • transformar el carrito de compras en una colección de cantidades usando el mapa
  • combinar todos los elementos sumándolos con reducir

¡Hecho! ?

Recursos

He organizado algunos recursos que leí y estudié. Estoy compartiendo los que encontré realmente interesantes. Para obtener más recursos, visite mi repositorio de Github de programación funcional .

  • Recursos específicos de Ruby
  • Recursos específicos de JavaScript
  • Recursos específicos de Clojure

Intros

  • Aprendiendo FP en JS
  • Introducción a FP con Python
  • Descripción general de FP
  • Una introducción rápida a JS funcional
  • ¿Qué es FP?
  • Jerga de programación funcional

Funciones puras

  • ¿Qué es una función pura?
  • Programación funcional pura 1
  • Programación funcional pura 2

Datos inmutables

  • DS inmutable para programación funcional
  • Por qué el estado mutable compartido es la raíz de todo mal
  • Compartición estructural en Clojure: Parte 1
  • Compartición estructural en Clojure: Parte 2
  • Compartición estructural en Clojure: Parte 3
  • Compartición estructural en Clojure: parte final

Funciones de orden superior

  • Eloquent JS: funciones de orden superior
  • Función divertida y divertida Filtro
  • Fun Fun Fun Mapa divertido
  • Fun fun funcion divertida Básica Reducir
  • Función divertida y divertida Avanzada Reducir
  • Funciones de orden superior de Clojure
  • Filtro de función pura
  • Mapa puramente funcional
  • Reducir puramente funcional

Programación declarativa

  • Programación declarativa vs imperativa

¡Eso es!

Hola gente, espero que se hayan divertido leyendo esta publicación, ¡y espero que hayan aprendido mucho aquí! Este fue mi intento de compartir lo que estoy aprendiendo.

Aquí está el repositorio con todos los códigos de este artículo.

Ven a aprender conmigo. Estoy compartiendo recursos y mi código en este repositorio de programación funcional de aprendizaje .

Espero que hayas visto algo útil aquí. ¡Y hasta la próxima! :)

Mi Twitter y Github. ☺

TK.