Percursos
/
Julia
Julia
/
Programa
/
Composição de funções
Co

Composição de funções em Julia

1 exercício

Sobre Composição de funções

A Julia incentiva os programadores a colocar o máximo de código possível dentro de funções que podem ser compiladas JIT, e criar muitas funções pequenas é, por design, eficiente.

Isso tende a deixar muitas funções pequenas e simples, que é preciso combinar para realizar tarefas não triviais.

Uma abordagem óbvia é aninhar chamadas de funções. O exemplo seguinte é muito artificial, mas ilustra a ideia.

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)

A desvantagem desta abordagem é que a legibilidade cai rapidamente à medida que o aninhamento se torna mais profundo.

Precisamos de uma abordagem mais simples e mais flexível.

Composição

Esta é a técnica muito apreciada pelos matemáticos, e a Julia copia a sintaxe matemática.

Um número arbitrário de funções pode ser composed em conjunto com operadores ∘ (introduzido como \circ e depois Tab). O resultado pode ser usado como uma única função.

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"])

Alguns pontos a ter em conta:

  • As funções de partida aparecem na mesma ordem que no aninhamento, e são executadas da direita para a esquerda.
  • O broadcasting não é simples de usar ao compor, mas pode ser aplicado ao chamar a função composta.

Pipelines

Uma alternativa pode ser encarada como a abordagem dos programadores, em vez da dos matemáticos.

Os Pipelines são usados há muito tempo em scripts de shell do Unix e, mais recentemente, tornaram-se populares em linguagens de programação generalizadas (por vezes atribui-se ao F# o mérito de ter sido pioneiro na sua adoção).

O conceito básico é começar com alguns dados e depois encadeá-los por uma sequência de funções para obter o resultado.

O operador de encadeamento é |> (como no F# e em versões recentes do R), embora a Julia também tenha uma versão 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)

A execução é agora estritamente da esquerda para a direita, com o resultado de cada função a fluir no sentido da seta para se tornar o valor de entrada da função seguinte.

Limitações, soluções alternativas e outras opções

Não é por acaso que as funções usadas para ilustrar a composição e os pipelines recebem todas um único argumento.

Algumas linguagens puramente funcionais encadeiam o primeiro argumento numa função, mas permitem que se incluam outros.

Em contraste, a Julia só espera nomes de funções (ou algo equivalente) num pipeline, sem argumentos adicionais.

Há razões técnicas importantes para isto (relacionadas com o facto de o currying não fazer parte padrão do desenho da linguagem). As muitas pessoas que não compreendem o currying devem apenas aceitar que esta limitação não é um descuido e que não é provável que mude em versões futuras da Julia.

Soluções alternativas

Precisamos de funções de um só argumento que façam o que for preciso. Felizmente, definir novas funções na Julia é fácil.

Da forma mais simples, podíamos usar uma anonymous function. Por exemplo, se tivermos uma única string de entrada e a quisermos dividir nos underscores:

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

Esse vetor poderia depois ser encadeado com outras funções, como antes.

Envolver a função anónima entre parênteses é opcional neste caso, mas, de um modo mais geral, é uma forma útil de reduzir a ambiguidade.

Do mesmo modo, podíamos criar uma função com nome, mais cedo no programa, e reutilizá-la conforme necessário.

Os Closures estão para lá do âmbito deste conceito, mas quem os conheça de outras linguagens vai reconhecer que oferecem uma forma mais flexível de criar funções de um só 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"

Assim que makesplit() estiver definida, pode ser usada para trabalhar com qualquer separador. Repara que makesplit('_') é uma chamada de função que resulta noutra função, que por sua vez recebe o valor de entrada do pipeline.

Se isto parecer confuso, é normal ao início (mas torna-se mais claro com a prática).

Outras opções

Houve uma longa discussão sobre tornar os pipelines mais versáteis na Julia base, mas as várias sugestões são mutuamente incompatíveis e não se chegou a acordo.

Entretanto, os utilizadores seguiram a abordagem habitual de criar vários pacotes instaláveis que respondem a necessidades específicas. Nenhum deles funciona no Exercism, mas dá uma olhadela a estes se tiveres interesse:

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Composição de funções