3 Programar con Funciones
> + #<procedure:+>
3.1 Funciones parametrizadas por funciones
Dado que las funciones son valores, una función puede recibir a otra función como parámetro. Esto permite abstraer ciertos procesos. Por ejemplo, si uno quiere agregar 1 a todos los elementos de una lista, puede recorrer la lista y construir una nueva lista con los elementos incrementados. De igual manera, si uno quiere convertir todos los números de una lista en strings, tiene que recorrer la lista y aplicar la conversión a cada elemento. Este recorrido puede ser parametrizado por la función a aplicar a cada elemento. La función que hace esto se llama map:
(define my-list '(1 2 3 4 5))
> (map add1 my-list) '(2 3 4 5 6)
> (map number->string my-list) '("1" "2" "3" "4" "5")
Otro procesamiento típico de una lista de elementos es calcular un valor consolidado a partir de todos los elementos de la lista. Por ejemplo, sacar la suma de los elementos de una lista. O determinar cuál es el máximo de la lista.
> (foldl + 0 my-list) 15
> (foldl + 100 my-list) 115
> (foldl max 0 my-list) 5
fold(l/r) también es conocido como reduce en otros lenguajes (más precisamente, en general reduce es como foldl donde el valor inicial es el primer elemento de la lista). La composición de map y reduce es la base del framework MapReduce de Google, que es clave para el procesamiento escalable de consultas sobre grandes cantidades de datos.
(foldl + 0 '(1 2 3)) ↦ (+ 3 (+ 2 (+ 1 0))) ↦ (+ 3 (+ 2 1)) ↦ (+ 3 3) ↦ 6 (foldr + 0 '(1 2 3)) ↦ (+ 1 (+ 2 (+ 3 0))) ↦ (+ 1 (+ 2 3)) ↦ (+ 1 5) ↦ 6
> (foldl cons '() my-list) '(5 4 3 2 1)
> (foldr cons '() my-list) '(1 2 3 4 5)
Hay muchas más funciones que reciben otras funciones. Las posibilidades son infinitas. Un ejemplo muy práctico también es seleccionar en una lista todos los elementos que cumplen cierta condición. Esto se hace con filter, pasandole un predicado como parámetro. También existe sort, parametrizado por una función de comparación.
> (filter even? my-list) '(2 4)
> (sort my-list >) '(5 4 3 2 1)
Programar de esa manera es mucho más declarativo y conciso que la programación imperativa tradicional. La clave está en el poder de abstracción que permite tener funciones (programas) como parámetros de otras funciones.
Ejercicio: estudie la documentación de las funciones para iterar, filtrar y buscar, y úselas sobre listas de su elección.
Ejercicio: defina, usando foldl, la función (reject lst pred) que retorna la lista de los elementos de la lista lst que no cumplen con el predicado pred.
3.2 Funciones anónimas
> add1 #<procedure:add1>
> (add1 2) 3
> add2 add2: undefined;
cannot reference undefined identifier
(define (add2 x) (+ x 2))
> (map add2 '(1 2 3)) '(3 4 5)
Similarmente, si necesitamos incrementar de 124 a todos los elementos de una lista, podemos definir add124. Sin embargo, tener que darle un nombre a tal función es un poco artificial, dado que es muy probable que no la necesitemos más adelante.
> (lambda (x) (+ x 124)) #<procedure>
> (map (λ (x) (+ x 124)) '(1 2 3)) '(125 126 127)
> (foldl (λ (x y) (and x y)) #t '(a b c d #f e)) #f
(define (foo x) x)
(define foo (λ (x) x))
También se puede introducir funciones con nombre local:
> (let ([f (λ (x) x)]) (f 10)) 10
> f f: undefined;
cannot reference undefined identifier
> (letrec ([fact (λ (n) (if (< n 2) 1 (* n (fact (sub1 n)))))]) (fact 10)) 3628800
Ejercicio: use la función findf para buscar el primer elemento múltiplo de 13 en una lista dada (retorna #f si no hay); defina el predicado como una función anónima. Puede usar la función modulo.
3.3 Funciones que producen funciones
Tener la posibilidad de crear funciones anónimas en cualquier parte permite hacer algo sumamente poderoso: definir funciones que crean otras funciones. O sea, generadores de funciones. Por ejemplo, volviendo a nuestras funciones add1, add2, add124, vemos que sus definiciones son muy similares. Lo único que cambia es el número usado en la suma con el parámetro.
Podemos ahora definir una única función addn que es capaz de fabricar una de esas funciones:
(define (addn n) (λ (x) (+ x n)))
(define add10 (addn 10)) (define add124 (addn 124))
> (add10 12) 22
> (map add124 my-list) '(125 126 127 128 129)
> (map (addn 20) my-list) '(21 22 23 24 25)
(define (more-than n) (λ (m) (> m n)))
> (filter (more-than 10) '(13 2 31 45 9 10)) '(13 31 45)
> (filter (more-than 20) '(13 2 31 45 9 10)) '(31 45)
Esa capacidad de crear dinámicamente funciones similares pero con atributos propios (por ejemplo, el valor de n en las distintas funciones creadas por addn) es muy similar a la creación de objetos a partir de clases en Java. Volveremos a explorar esta conexión en el OOPLAI.
Ejercicio: defina la función (negate pred) que retorna un predicado que es la negación de pred.
Ejercicio: use negate y filter para definir la función (reject lst pred) (que vimos anteriormente) de manera más concisa aún.
Esta técnica se llama currificación (currying)
> (define cons1 ((curry cons) 1))
> (map cons1 '(a b c d)) '((1 . a) (1 . b) (1 . c) (1 . d))
Esta técnica se llama memoization.