Rutas
/
Julia
Julia
/
Ejercicios
/
Club del queso
Club del queso

Club del queso

Ejercicio de aprendizaje

Introducción

Técnicamente, una Higher Order Function es simplemente una función que cumple al menos una de las siguientes condiciones:

  • Acepta una función como uno de sus argumentos.
  • Devuelve una función como resultado.

En el mundo de la programación funcional, el uso tiende a ser más restringido. El término suele referirse a funciones como filter, map y reduce, que aplican una función pasada como argumento a los elementos de una colección.

Operar sobre colecciones

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:

  • Usar un bucle (como la mayoría de los lenguajes de programación desde los albores de la informática).
  • Usar una comprensión (estilo Python).
  • Usar broadcasting (sintaxis característica de Julia, aunque con una gran deuda con R, Matlab y NumPy).

Este concepto se centrará en las funciones de orden superior (familiares en cualquier lenguaje funcional, como Haskell o F#).

Otros enfoques posibles incluyen:

  • Recursión (como en los lenguajes de la familia ML).
    • Julia lo permite, pero sin optimización de llamadas finales corre el riesgo de desbordar la pila.
  • Metaprogramación con macros (tradicionalmente una característica de Lisp).
    • Esto se usa ampliamente en programación avanzada de Julia, pero procede con precaución en la mayoría de los casos.
    • Es probable que otras opciones sean más fáciles de escribir y mucho más fáciles de depurar.

Filtrado

La función filter() toma una función pasada como argumento con un valor de retorno booleano y la aplica a una colección. Solo los elementos que devuelven true se incluyen en el valor devuelto, 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 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.

Mapeo

La función map() transforma una colección aplicando una función a cada elemento. Esto puede ser similar al broadcasting en casos simples, con la forma de la salida coincidiendo con 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 operará elemento a elemento en múltiples colecciones.

julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
 3
 8

Conceptualmente, podemos pensar en esto como equivalente a ejecutar zip() en las múltiples colecciones de entrada, y luego map() en cada elemento del resultado intermedio. Esto es solo una analogía aproximada, ¡no implica nada sobre la implementación!

Al igual que con zip(), las colecciones de forma dispar se truncan a la(s) dimensión(es) de la más pequeña.

A veces solo se necesitan los efectos secundarios de la función pasada como argumento, como una escritura en una base de datos o un push! a un array. Entonces está disponible la función de orden superior foreach(), que siempre devuelve nothing.

Reducción

La función reduce() toma una función de 2 argumentos y la aplica a una colección, lo que conduce a una reducción de dimensión.

Eso puede sonar confuso en abstracto, pero considera funciones como sum() o prod() que toman 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 altamente optimizadas y siempre deben usarse cuando estén disponibles. Otros ejemplos incluyen 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 internamente).

julia> reduce(+, 1:4) # add
10

julia> reduce(*, 1:4) # multiply
24

Al igual que con sum() y otras funciones de agregación, reduce() puede tomar un argumento de palabra clave opcional dims, para especificar la(s) dimensión(es) 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 tanto conmutativas (1+2 == 2+1) como asociativas ( (1+2)+3 == 1+(2+3) ).

¡Esto está lejos de ser universal! Incluso operaciones tan comunes como la resta y la división no son asociativas.

Existe el problema adicional de que los errores de coma flotante pueden acumularse en colecciones grandes, por lo que una reducción de izquierda a derecha puede producir una respuesta ligeramente diferente que 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 explícitamente, existen las funciones foldl() y foldr(), que en teoría comienzan por la «izquierda» y la «derecha» respectivamente (en realidad arriba y abajo, para un Vector).

julia> foldl(-, 1:3) # (1 - 2) - 3
-4

julia> foldr(-, 1:3) # 1 - (2 - 3)
2

Ten en cuenta que estas están pensadas para colecciones que pueden tratarse como unidimensionales, devolviendo un resultado escalar. El uso de un argumento dims no está soportado para foldl y foldr, solo para reduce.

MapReduce

Combinar una operación map con una reduce es muy común en varios dominios de programación.

Podríamos ejecutar secuencialmente map, y luego ejecutar reduce en 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 se hace más grande.

Se recomienda encarecidamente usar la función combinada mapreduce() en su lugar. Puede implementar un algoritmo mucho más eficiente que entrelaza las operaciones de map/reduce.

El primer argumento es la función con la que mapear, el segundo argumento es el operador de reducción.

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 funciones mapfoldl() y mapfoldr() para situaciones en las que la dirección es importante.

Instrucciones

Estamos poniendo en marcha un club de queso que usará aprendizaje automático (ML) para seleccionar nuevos quesos que ofrecer a nuestros clientes amantes del queso, basándose en sus historiales y sus gustos.

Se pide a los nuevos miembros que rellenen una encuesta inicial para que podamos recopilar 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 acabar 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.

Note

Aunque puede haber distintas formas de resolver las tareas siguientes, cada una puede resolverse con una única función de orden superior diferente.

1. Clasificar a los clientes

El sistema de valoración se basa en cinco estrellas, que simplemente consta de los números enteros 1:5. Los clientes enfáticos solo darán valoraciones de 1 o 5, y queremos saber si un cliente muestra este comportamiento.

Implementa all_15(), que recibe un vector de valoraciones y devuelve true si todas las valoraciones 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

2. Separar a los clientes enfáticos

Necesitamos separar a los clientes más enfáticos de los demás.

Implementa emphatics(), que recibe un diccionario de clientes y valoraciones. Devuelve un diccionario similar con aquellos que solo usan valoraciones 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]

3. Convertir las valoraciones a binario

Dado que los clientes enfáticos solo usan valoraciones 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 valoraciones enfáticas. Devuelve valoraciones 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

4. Convertir las valoraciones en una matriz

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 valoraciones enfáticas. Devuelve una Matrix con los datos transformados, donde cada vector de valoraciones es una fila en 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
Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Julia Exercism

¿Listo para empezar Club del queso?

Regístrate en Exercism para aprender y dominar Julia con 35 conceptos128 ejercicios y mentoría humana real, todo gratis.