View on GitHub

Declarativa

En este capítulo revisaremos a grandes rasgos ¿Qué es la programación funcional? Y cómo podemos utilizarla en python.

Introducción

La programación funcional es una forma de programar declarativamente. Que es un poco distinto a como normalmente se programa, pero que ofrece muchas ventajas.

Tiene su origen en el cálculo lambda del cual invitamos a investigar un poco. Este cálculo lambda es una abstracción matemática, la cual es la base para casi todos los lenguajes funcionales.

Hay que recordar que que python es un lenguaje orientado a objetos [![Ver aquí][../OO/OO.ipynb]] al cual se le han incorporado algunos de sus conceptos para poder emular de alguna manera lo que lenguajes funcionales hacen.

Declarativa

Puede haber confusión entre programación declarativa y programación funcional.

La programación funcional pertenece al paradigma de programación declarativa, la cual nos brinda otra forma de pensar en cómo resolver los problemas. En este conjunto encontramos también a la programación lógica.

Programación Funcional

En este tipo de programación, el programa le dice a la computadora qué son las cosas.

Por ejemplo, para explicar ‘Cómo luce un carro verde’, partiendo de que todos los carros que hemos visto son azules. Lo haríamos de la siguiente manera:

=Ese carro azul que viste, si tuvieras otro carro que es exactamente igual en todos los aspectos, excepto que es verde. Entonces eso es un carro verde.=

Es importante notar que lo que usamos para describir el enunciado anterior son verbos para definir cosas, como: ‘Tener’, ‘Ser’. Por lo que podemos definir a la programación funcional (Haciendo un mal uso del lenguaje) en ‘Esto es esto.’

Conceptos

Algunos conceptos que un lenguaje funcional debería tener son los siguientes:

Recursión

En general para recorrer estructuras (de datos o naturales como listas o números) podemos usar cíclos (for, while). En la programación funcional, estas maneras no existen, sino que se llama recursión. Una función recursiva es una función que se llama a si misma; Lo que permite que esta función se realice inumerables veces hasta que se cumpla lo que llamaremos cláusula de escape.

Las ventajas de esto, es que en ciertas ocasiones definir una función recursiva, en vez de utilizar ciclos puede resultar más intuitiva.

Las desventajas, es que para llevar a cabo la recursión, tenemos que actualizar el stack, que es una estructura que va guardando las llamadas hechas por la función (En python), lo que puede hacer que nos acabemos la memoria antes de poder tener el resultado.

El siguiente ejemplo ilustra cómo calcular el factorial de un número. Tanto de manera recursiva, como iterativa. ¿Puedes identificar cual es cuál?

Recordemos que el factorial de un número se define como el producto de todos los números (incluído el número) anteriores a él, hasta 1 (O cero, en cuyo caso el factorial de 0 es 1),

def fact(n):
    f = 1
    while n > 0:
        f = f * n
        n = n - 1
    return f

print(fact(5))
print('>>>>>>>>')

def factR(n):
    if n == 1:
        return 1
    else:
        return n * factR(n-1)

print(factR(5))

El ejercicio anterior se puede ver de la siguiente manera:

Iterativo 1.n= 5. Inicializamos f en 1.

  1. loop 1 Como 5 > 0, entonces entramos al loop 1 f = f * 5 -> f = 1 * 5 -> f = 5 2 n = n - 1 -> n = 5 - 1 -> n = 4 2 Como 4 > 0, entonces entramos al loop 3 f = f * 4 -> f = 5 * 4 -> f = 20 1 n = n - 1 -> n = 4 - 1 -> n = 3 4 Como 3 > 0, entonces entramos al loop 1 f = f * 3 -> f = 20 * 3 -> f = 60 2 n = n - 1 -> n = 3 - 1 -> n = 2 5 Como 2 > 0, entonces entramos al loop 1 f = f * 2 -> f = 60 * 2 -> f = 120 2 n = n - 1 -> n = 2 - 1 -> n = 1 6 Como 1 > 0, entonces entramos al loop 1 f = f * 1 -> f = 120 * 1 -> f = 120 2 n = n - 1 -> n = 1 - 1 -> n = 0 7 Como 0 > 0 no se cumple, salimos del loop
  2. Regresamos f que al final tiene el valor de 120

Recursivo

  1. n=5.
    1. Como n no es 1. Tenemos n * factR(n - 1) -> 5 * factR(4)
  2. n=4. 1 Como n no es 1. Tenemos 5 * (n * factR(n - 1)) -> 5 * (4 * factR(3))
  3. n=3. 1 Como n no es 1. Tenemos 5 * (4 * (3 * (n * factR(n - 1)))) -> 5 * (4 * (3 * factR(2)))
  4. n=2. 1 Como n no es 1. Tenemos 5 * (4 * (3 * (2 * (n * factR(n - 1))))) -> 5 * (4 * (3 * (2 * factR(1))))
  5. n=1. 1 Como n es 1. Y la función nos dice que si eso pasa, regresemos 1. Tenemos 5 * (4 * (3 * (2 * (1)))) 1 5 * (4 * (3 * (2 * 1))) 2 5 * (4 * (3 * (2))) 3 5 * (4 * (6)) 4 5 * (24) 5 120

En este ejercicio podemos ver que la parte recursiva, no se puede evaluar, esto es, no regresa nada, hasta que hayamos llegado a la cláusula de escape (En este caso era que n == 1). Y esto se traduce en lo que mencionábamos anteriormente sobre el stack. Este stack se va a llenar de las llamadas que van desde el punto 1.1 hasta el punto 4.1. El punto 5. - 5.1. no agrega más cosas al stack sino que llega a la cláusula de escape, y apartir de ahí (5.1.0.) empezamos a liberar el stack

El siguiente ejercicio se vio en el curso básico, y trata de imprimir una lista usando un ciclo for. En este caso para efectos educativos vamos a regresar una cadena, la cuál va a ser la que se imprime.

abecedario = ["a", "b", "c", "d", "e"]

def printR(l):
    if not l:
        return ""
    else:
        return l[0] + "\n" + printR(l[1:])

print(printR(abecedario))

Veremos un par de ejercicios más tanto en su forma recursiva, como iterativa, también vamos a explicarlos y dar una idea de la ejecución. Por último, dejaremos unos ejercicios para qué tú los hagas.

El primer ejercicio es la secuencia de fibonacci, la cuál se define de la siguiente manera (Se puede encontrar en internet).

fib0 = 0

fib1 = 1

fibn = fib(n-1) + fib(n-2)

Donde n es el n-ésimo número de la secuencia de Fibonacci. Veamos el código.

def fib(n):
    i = 0
    j = 1
    c = 2
    while (c < n):
        i = i + j
        j = j + i
        c = c + 2
    return j

def fibR(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fibR(n-1) + fibR(n-2)

print(fib(5))
print('>>>>>>>>')
print(fibR(5))

Para ver que el resultado es correcto, podemos corroborar en internet, o calcularlo burdamente:

n f(n)
0 0
1 1
2 1
3 2
4 3
5 5

Donde f(n) es el n-ésimo número de Fibonacci en la posición n.

La idea para la función recursiva, es sencilla, es una simple traducción de la fórmula. Para calcular el fibonacci de 5, necesitamos el de 4 y el de 3, y a su vez, para calcular estos, necesitamos el de 2 y 1, y el de 2 y 3. Para calcular estos necesitamos el de 1 y 0 (En el caso del 2). El Fibonacci de 1 ya está dado por la fórmula, por lo que no lo necesitamos calcular. Y para calcular el de 3 (nuevamente, esto podría representar un problema pues estamos calculando varios resultados varias veces, es lo que mencionamos del stack), basta con calcular el de 1 y 2, y continuamos hasta llegar a los casos de 1 y 0.

Podemos ver que las ejecuciones tienden a calcular dos números por cada número que queramos calcular, esto hasta llegar al caso base.

Para la parte iterativa, no es tan intuitiva. i, j, van a representar a los iteradores que van a representar el n-ésimo y n-1-ésimo número de fibonacci. La c es el contador (Lo iniciamos en 2, pues ya tenemos calculados los dos primeros, que son i, j). Y mientras la c no sea igual a el número que queremos (n), vamos recorriendo nuestros iteradores según una pequeña variación de la definición. Como vamos aumentando ambos iteradores, a nuestra c le sumamos de dos en dos.

Como último ejemplo, vamos a hacer una función que sume dos números naturales, pero solo usando la función sucesor que es solo sumar de uno en uno. Y predecesor que es, restar de uno en uno.

No tiene muchas aplicaciones pero es un buen ejemplo para ilustrar la recursión en números.

def sumaR(n, m):
    if n == 0:
        return m
    if m == 0:
        return n
    return sumaR(n+1, m-1)

def suma(n, m):
    while (m > 0):
        n = n + 1
        m = m - 1
    return n

print(sumaR(5, 5))
print('>>>>>>>>>')
print(suma(5, 5))

La idea en ambas funciones es decrementar en uno un número, y sumarle uno al otro número, esto porque podemos la suma de n y m como 1+1+1+… n veces + 1+1+1+… m veces. Por lo que el análisis para ambas funciones es básicamente el mismo.

Como ejercicios:

  1. Crea una función mult y multR que emulen la multiplicación usando solo predecesor y sucesor, esto es, sumando y restando una unidad. Te puedes ayudar de las funciones de arriba.
  2. Los números de Tribonacci usan la misma idea que los números de Fibonacci, la diferencia es que se usan los 3 números anteriores al n-ésimo número para calcularlo.

    T0 = 0

    T1 = 1

    T2 = 1

    Tn = T(n-1) + T(n-2) + T(n-3)

    Crea una función recursiva y una iterativa que calcule el n-ésimo número de Tribonacci.

Funciones Lambda

El cálculo lambda es el más pequeño lenguaje universal de programación, consiste en una regla de transformación simple (Sustituir variables) y un esquema simple para definir funciones.

En el cálculo lambda, las funciones no necesitan ser explícitamente nombradas, esto es por ejemplo en python una función que sume dos números, la llamaríamos suma, en el cálculo lambda, solo la escribiríamos como λx.λy → x+y esto se lee como:

Una función que recibe dos argumentos y regresa la suma de ambos.

Otra cosa importante, es que el nombre de los argumentos no importa, esto es que λx.λy → x+y es lo mismo que λa.λb → a+b.

Y entrando a temas un poco más oscuros, podemos ver que toda función que requiera dos argumentos, puede ser reescrita como una función que acepta un único argumento, pero que devuelve otra función, la cual a su vez acepta un único argumento.

Tomemos el ejemplo de la suma λx.λy → x+y, se puede ver como λx → (λy → x+y).

Esto se conoce como currificación y se puede generalizar a funciones que aceptan cualquier número de argumentos

¿Ejemplo?

También las funciones labmda pueden aceptar una función como argumento.

Veamos el ejemplo de la suma en python.

def suma(x, y):
    return x + y

a = suma(2,3)
print(a)

Generalmente usamos estas funciones lambda o también llamada función anónima cuando creamos una función que no vamos a usar varias veces. Recordemos que la idea de poder crear funciones, es mandarlas a llamar cuando las necesitemos, pero ¿Y si solo la necesitamos para algo? Es aquí donde entran las funciones lambda.

print((lambda x, y : x + y)(2,3))

Como podemos ver, en el código anterior, solo creamos una función lambda, que regresa la suma de sus dos argumentos y la mandamos a llamar con los valores deseados. Esto nos lleva al punto de la sintaxis.

Recordemos que en el cálculo lambda, la función suma se escribe λx.λy → x+y, pero también podríamos escribirla λx.y → x+y, solo para ahorrar caracteres. Teniendo esto en cuenta, podemos ver por el código de arriba que es muy parecido, solamente en vez de escribir λ, la escribimos con palabras: lambda, luego, en vez de separar a los argumentos con un ., los separamos con ,, en vez de poner , ponemos : y la misma expresión que tenemos el cálculo lambda, la ponemos en el código de python.

Ya estando en python, podemos ver que una función la podemos guardar en una varible de la siguiente manera:

suma = lambda x, y: x+y

Y que al mandar a llamar a suma con sus respectivos parámentros, podemos guardar el resultado en otra variable, e imprimir esta.

resultado = suma(10,1)
print(resultado)

Como ya vimos más arriba, también podríamos no guardar la función ni el resultado y solamente imprimir la función pasandole los argumentos en el print(). ¿Por qué sería útil guardar la función en una varible? Pues si por ejemplo estás escribiendo un método donde usas una función varias veces, pero esta función solo se usa en este método, entonces convendría guardar esa función dentro del método y mandarla a llamar.

¿Qué puedo poner de expresión en el cuerpo de la función lambda? Podemos crear cualquier expresión que esté bien escrita sitácticamente, un if por ejemplo, operadores booleanos, operadores en números, incluso podríamos llamar otras funciones ya definidas.

Veamos unos ejemplos de esto.

op_log = lambda x : True and x # operadores lógicos
op_num = lambda x : x % 2      # operadores en números
func = lambda x : print(x)     # llamar a otra función
# lambda con un if
op_if = lambda x, y : "mayor que" if (x > y) else "menor o igual"

print(op_log(False))
print(op_num(5))
func("hola")
print(op_if(5,4))

Operaciones con lambdas

En lenguajes funcionales existe el concepto de map, filter. Python no es un lenguaje funcional, pero soporta algunas de sus características.

filter Dada una colección, esta puede ser una lista, un conjunto, tuplas o cualquier contenedor de cualquier iterador, y una función, la cuál va a ‘filtrar’ los elementos de nuestra colección.

def f(x):
    return x < 6

secuencia = [1,2,3,4,5,6,7,8,9,10]
filtrado = filter(f, secuencia)
for f in filtrado:
    print(f)

En el ejemplo anterior, podemos apreciar que nuestra función f nos dice si un número es o no menor a 5. Usando filter, y esa función tenemos el resultado deseado, pero también podemos usar lambdas!.

De esta manera nos evitamos crear una función que probablemente usaríamos una única vez. También nuestro código se ve más elegante.

La función en filter puede filtrar respecto a lo que queramos que cumpla la secuencia, esto quiere decir que la función debe mapear los elementos en la colección a True o False.

map Esta función lo que hace es tomar un iterable, por ejemplo, una lista, un diccionario, etc y una función la cuál se va a ‘mapear’ a los elementos de nuestro iterable.

def f(x):
    return x%2

secuencia = [1,2,3,4,5,6,7,8,9,10]
mapeado = map(f, secuencia)
for m in mapeado:
    print(m)

En el ejemplo anterior, vemos que nuestra función f toma un número y nos regresa el resultado de aplicarle módulo dos a ese número, esto es que nuestros posibles resultados son (1,0). Usando lambdas, podríamos no hacer uso de declarar una función, sino declararla en el cuerpo del método map()

mapeado_lambda = map(lambda x: x%2, secuencia)
for m in mapeado_lambda:
    print(m)

En ambas funciones, podemos usar funciones ya hechas, o hacer uso de las lambdas, que son una herramienta muy poderosa a la hora de programar.

También, algo que no es tan importante, pero sin dudas vale la pena mencionarlo es que en nuestros ejemplos guardamos el resultado de filter y map en una variable (Porque a python no le importan mucho los tipos), pero cuando queremos imprimir esa variable, nos va a arrojar un error. Es por esto que iteramos ese objeto con un un for y vamos imprimiendo, esto se podría ahorrar con lo siguiente:

map_list_2 = list(map(lambda x : x%2, secuencia))
filter_list_2 = list(filter(lambda x : x>2, secuencia))

print(map_list_2)
print(filter_list_2)

Lo cual transforma a nuestro objeto en un objeto de tipo lista y eso lo entiende perfecto un print()

Ventajas y desventajas

La programación funcional en un lenguaje puramente funcional tiene muchas ventajas y muchas otras desventajas, pero cuando hablamos de la programación funcional en python, este al tener objetos y poder escribir el código de manera imperativa, nos da lo mejor de los dos mundos, podríamos siempre escribir código funcionalmente y cuando encontremos algo que se vea complicado de manera funcional, podemos recurrir a la forma imperativa (Orientación a objetos, estructurada). Y al revés, podríamos programar siempre de forma imperativa y cuando haya algo que se vea más elegante o se pueda de forma funcional, usar la programación funcional.

Fuentes*

  1. https://www.ecured.cu/C%C3%A1lculo_lambda

  2. https://stackabuse.com/lambda-functions-in-python/