Técnicamente, una Higher Order Function es simplemente una función que hace al menos una de estas cosas:
En el mundo de la programación funcional, el uso suele ser más restringido.
El término suele referirse a funciones como filter, map y reduce, que aplican una función recibida a los elementos de una colección.
A estas alturas del temario, ya hemos visto varias formas de aplicar una operación a todos los elementos de una colección iterable, como un Vector:
Este concepto se centrará en las funciones de orden superior (conocidas en cualquier lenguaje funcional, como Haskell o F#).
Otros enfoques posibles son:
La función filter() toma una función recibida con un valor de retorno booleano y la aplica a una colección.
Solo los elementos que devuelven true se incluyen en el valor de retorno, que es del mismo tipo básico que la entrada (ver más abajo).
julia> filter(iseven, 1:6)
3-element Vector{Int64}:
2
4
6
# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"
# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)
Con arrays multidimensionales, filter aplana las dimensiones de la entrada y devuelve un Vector: la principal excepción a cualquier regla sobre que el tipo de salida coincida con el tipo de entrada.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> filter(isodd, m)
3-element Vector{Int64}:
1
5
3
Los ejemplos anteriores usan funciones integradas, pero el uso de funciones anónimas es muy común en este contexto.
julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
3
6
9
12
15
18
También hay una versión in-place, filter!(), como la hay para muchas de las funciones de este concepto.
La función map() transforma una colección aplicando una función a cada elemento.
En casos simples esto puede ser parecido al broadcasting, con la forma de la salida igual a la de la entrada.
julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
2
5
10
17
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> map(√, m)
2×3 Matrix{Float64}:
1.0 1.41421 1.73205
2.0 2.23607 2.44949
map() también opera elemento por elemento sobre varias colecciones.
julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
3
8
Conceptualmente, podemos pensar en esto como equivalente a ejecutar zip() sobre las distintas colecciones de entrada y después map() sobre cada elemento del resultado intermedio.
¡Esto es solo una analogía aproximada, que no implica nada sobre la implementación!
Al igual que con zip(), las colecciones con formas que no coinciden se truncan a la dimensión o dimensiones de la más pequeña.
A veces solo se necesitan los efectos secundarios de la función recibida, como una escritura en una base de datos o un push! a un array.
En ese caso está disponible la función de orden superior foreach(), que siempre devuelve nothing.
La función reduce() toma una función de dos argumentos y la aplica a una colección, lo que produce una reducción de dimensiones.
Eso puede sonar confuso en abstracto, pero piensa en funciones como sum() o prod(), que reciben una colección y devuelven un solo valor.
julia> sum(1:4) # add
10
julia> prod(1:4) # multiply
24
Estas funciones especiales están muy optimizadas y siempre deberías usarlas cuando estén disponibles.
Otros ejemplos son maximum() y minimum(), las funciones lógicas all() y any(), y muchas funciones estadísticas.
Solo a modo de ilustración, considera la misma funcionalidad implementada con la más genérica reduce() (recuerda que los operadores infijos + y * en realidad son funciones por dentro).
julia> reduce(+, 1:4) # add
10
julia> reduce(*, 1:4) # multiply
24
Como sum() y otras funciones de agregación, reduce() puede recibir un argumento de palabra clave opcional dims, para especificar la dimensión o dimensiones que se van a reducir.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
5 7 9
Estos son ejemplos fáciles, porque la suma y la multiplicación son conmutativas (1+2 == 2+1) y asociativas ( (1+2)+3 == 1+(2+3) ).
¡Esto no es ni de lejos universal! Incluso operaciones tan comunes como la resta y la división no son asociativas.
Está el problema adicional de que los errores de punto flotante pueden acumularse en colecciones grandes, así que una reducción de izquierda a derecha puede dar una respuesta ligeramente distinta que una de derecha a izquierda.
La dirección de la función reduce de Julia depende de la implementación y no está garantizada.
Para controlar la dirección de forma explícita, están las funciones foldl() y foldr(), que en teoría empiezan por la «izquierda» y la «derecha» respectivamente (en realidad por arriba y por abajo, en el caso de un Vector).
julia> foldl(-, 1:3) # (1 - 2) - 3
-4
julia> foldr(-, 1:3) # 1 - (2 - 3)
2
Ten en cuenta que están pensadas para colecciones que se pueden tratar como unidimensionales y devuelven un resultado escalar.
El uso de un argumento dims no está admitido en foldl y foldr, solo en reduce.
Combinar una operación map con una reduce es muy común en distintos ámbitos de la programación.
Podríamos ejecutar map de forma secuencial y después ejecutar reduce sobre una colección intermedia.
Sin embargo, esto es ineficiente en el mejor de los casos y escala muy mal a medida que la colección crece.
Se recomienda encarecidamente usar la función combinada mapreduce() en su lugar.
Puede implementar un algoritmo mucho más eficiente que intercala las operaciones de map y reduce.
El primer argumento es la función con la que se hace el map y el segundo es el operador de reduce.
julia> mapreduce(x -> x^2 + 1, +, 1:3)
17
# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17
Como era de esperar, Julia también tiene las funciones mapfoldl() y mapfoldr() para las situaciones en que la dirección es importante.
Estamos iniciando un club de quesos, que usará aprendizaje automático para seleccionar nuevos quesos y ofrecérselos a nuestros clientes amantes del queso, basándose en sus historiales y gustos.
Quienes se unen por primera vez deben llenar una encuesta inicial para que podamos reunir algunos datos básicos con los que empezar. Gracias a esto, se ha descubierto que existe un subconjunto de clientes enfáticos que carecen de matices en sus críticas. Como esto puede terminar sesgando de forma irrevocable un algoritmo más matizado, hay un algoritmo aparte configurado para atender sus necesidades. Se te pide que proporciones algunas funciones auxiliares para manejar sus datos.
Si bien puede haber distintas maneras de resolver las siguientes tareas, cada una se puede resolver con una sola función de orden superior, distinta en cada caso.
El sistema de calificaciones se basa en cinco estrellas, que simplemente consiste en los enteros 1:5.
Los clientes enfáticos solo dan calificaciones de 1 o 5, y queremos saber si un cliente muestra este comportamiento.
Implementa all_15(), que recibe un vector de calificaciones y devuelve true si todas las calificaciones son 1 o 5, y false en caso contrario.
julia> ratings = [2, 3, 4, 4, 1];
julia> all_15(ratings)
false
julia> ratings = [1, 5, 5, 1, 5];
julia> all_15(ratings)
true
Necesitamos separar a los clientes más enfáticos de los demás.
Implementa emphatics(), que recibe un diccionario de clientes y calificaciones.
Devuelve un diccionario similar con quienes solo usan calificaciones de 1 o 5 estrellas.
julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);
julia> names = ("c1", "c2", "c3", "c4");
julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
"c2" => [1, 1, 5, 5, 1]
"c1" => [2, 3, 5, 1, 1]
"c3" => [4, 5, 5, 3, 2]
"c4" => [5, 5, 1, 1, 5]
julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
"c2" => [1, 1, 5, 5, 1]
"c4" => [5, 5, 1, 1, 5]
Dado que los clientes enfáticos solo usan calificaciones de 1 y 5, será más conveniente desde el punto de vista computacional cambiarlas a 0 y 1.
Implementa tobinary(), que recibe un vector de calificaciones enfáticas.
Devuelve calificaciones binarias, donde 1 se ha cambiado a 0 y 5 se ha cambiado a 1.
julia> ratings = [1, 1, 5, 5, 1];
julia> tobinary(ratings)
5-element Vector{Int64}:
0
0
1
1
0
Nuestros algoritmos usan entradas de tipo Matrix, así que tendremos que transformar los datos en una.
Implementa tobinarymatrix(), que recibe un vector de vectores de calificaciones enfáticas.
Devuelve una Matrix con los datos transformados, donde cada vector de calificaciones es una fila de la matriz.
julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];
julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
0 0 1 1 0
1 1 0 0 1
Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.