Tracks
/
Julia
Julia
/
Temario
/
Composición de funciones
Co

Composición de funciones en Julia

1 ejercicio

Acerca de Composición de funciones

Julia anima a los desarrolladores a incluir tanto código como sea posible dentro de funciones que se puedan compilar JIT, y crear muchas funciones pequeñas es, por diseño, eficiente.

Eso suele dar lugar a muchas funciones pequeñas y sencillas, que hay que combinar para llevar a cabo tareas no triviales.

Una opción obvia es anidar llamadas a funciones. El siguiente ejemplo es muy rebuscado, pero sirve para ilustrar la idea.

julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

La desventaja de este enfoque es que la legibilidad cae en picado a medida que el anidamiento se hace más profundo.

Necesitamos un enfoque más sencillo y flexible.

Composición

Esta es la técnica predilecta de los matemáticos, y Julia copia la sintaxis matemática.

Un número arbitrario de funciones se puede composed entre sí con operadores ∘ (se escriben como \circ y luego tabulador). El resultado se puede usar como una única función.

julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse

julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])

Un par de puntos que conviene tener en cuenta:

  • Las funciones de partida aparecen en el mismo orden que al anidarlas, y se ejecutan de derecha a izquierda.
  • El broadcasting no es sencillo de usar al componer, pero se puede aplicar al llamar a la función compuesta.

Tuberías

Una alternativa que se podría considerar el enfoque de los desarrolladores, en lugar del de los matemáticos.

Las Pipelines se llevan usando mucho tiempo en los scripts de shell de Unix y, más recientemente, se han popularizado en los lenguajes de programación convencionales (a veces se atribuye a F# haber sido pionero en su adopción).

El concepto básico es empezar con unos datos y luego pasarlos por una secuencia de funciones para obtener el resultado.

El operador de tubería es |> (como en F# y en las versiones recientes de R), aunque Julia también tiene una versión de broadcasting, .|>.

julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

La ejecución ahora es estrictamente de izquierda a derecha: la salida de cada función fluye en la dirección de la flecha para convertirse en la entrada de la siguiente función.

Limitaciones, soluciones alternativas y otras opciones

No es casualidad que las funciones usadas para ilustrar la composición y las tuberías reciban todas un único argumento.

Algunos lenguajes puramente funcionales canalizan el primer argumento hacia una función, pero permiten incluir otros.

En cambio, Julia solo espera nombres de funciones (o algo equivalente) en una tubería, sin argumentos adicionales.

Hay razones técnicas importantes para ello (relacionadas con el hecho de que currying no forma parte estándar del diseño del lenguaje). Las muchas personas que no entiendan el currying deberían limitarse a aceptar que esta limitación no es un descuido y que no es probable que cambie en futuras versiones de Julia.

Soluciones alternativas

Necesitamos funciones de un solo argumento que hagan lo que haga falta. Por suerte, definir nuevas funciones en Julia es fácil.

Lo más sencillo sería usar una anonymous function. Por ejemplo, si tenemos un único string de entrada y queremos dividirlo por guiones bajos:

julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Ese vector se podría canalizar después a otras funciones, como antes.

Encerrar la función anónima entre paréntesis es opcional en este caso, pero, en general, es una forma útil de reducir la ambigüedad.

Del mismo modo, podríamos crear una función con nombre antes en el programa y reutilizarla cuando sea necesario.

Los Closures quedan fuera del alcance de este concepto, pero cualquiera que los conozca de otros lenguajes reconocerá que ofrecen una forma más flexible de crear funciones de un solo argumento.

julia> function makesplit(sep)
           fs(str) = split(str, sep)
           fs
       end
makesplit (generic function with 1 method)

julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)

julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

Una vez definida makesplit(), se puede usar para trabajar con cualquier separador. Ten en cuenta que makesplit('_') es una llamada a función que evalúa a otra función, la cual, a su vez, recibe la entrada de la tubería.

Si esto te parece confuso, es normal al principio (pero se vuelve más claro con la práctica).

Otras opciones

Ha habido una larga discusión sobre cómo hacer que las tuberías sean más versátiles en Julia base, pero las distintas propuestas son mutuamente incompatibles y no se ha llegado a un acuerdo.

Mientras tanto, los usuarios han adoptado el enfoque habitual de crear diversos paquetes instalables que cubren necesidades concretas. Ninguno funcionará dentro de Exercism, pero échales un vistazo si te interesan:

Editar en GitHub El enlace se abre en una nueva ventana o pestaña

Aprende Composición de funciones