Trilhas
/
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

Julia incentiva quem programa a colocar o máximo de código possível dentro de funções que possam ser compiladas por JIT, e criar muitas funções pequenas é eficiente por escolha de projeto.

Isso tende a deixar muitas funções pequenas e simples, que precisam ser combinadas para realizar tarefas não triviais.

Uma abordagem óbvia é aninhar chamadas de função. O exemplo a seguir é bastante artificial, mas ilustra o ponto.

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 dessa abordagem é que a legibilidade cai rapidamente conforme o aninhamento fica mais profundo.

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

Composição

É a técnica preferida dos matemáticos, e Julia copia a sintaxe matemática.

Um número arbitrário de funções pode ser composed com operadores ∘ (digitados 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 observar:

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

Pipelining

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

Pipelines são usados há muito tempo em scripts de shell do Unix, e mais recentemente se tornaram populares em linguagens de programação mainstream (às vezes se atribui ao F# o pioneirismo na adoção deles).

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

O operador pipe é |> (como no F# e em versões recentes do R), embora Julia também tenha uma versão com 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 a saída de cada função fluindo na direção da seta para se tornar a entrada da próxima função.

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

Não é coincidência que as funções usadas para ilustrar composição e pipelining recebam todas um único argumento.

Algumas linguagens puramente funcionais passam o primeiro argumento para uma função, mas permitem que outros sejam incluídos.

Em contrapartida, Julia espera apenas nomes de funções (ou algo equivalente) em um pipeline, sem nenhum argumento adicional.

Há razões técnicas importantes para isso (relacionadas ao fato de que currying não faz parte do projeto padrão da linguagem). As muitas pessoas que não entendem currying devem apenas aceitar que essa limitação não é um descuido, e provavelmente não vai mudar em versões futuras de Julia.

Alternativas

Precisamos de funções de um único argumento que façam o que for necessário. Felizmente, definir novas funções em Julia é fácil.

Da forma mais simples, poderíamos usar uma anonymous function. Por exemplo, se temos uma única string de entrada e queremos separar por underscores:

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

Esse vetor poderia então ser encadeado para outras funções, como antes.

Colocar a função anônima entre parênteses é opcional neste caso, mas, de modo geral, é uma forma útil de reduzir ambiguidades.

Da mesma forma, poderíamos criar uma função nomeada, mais cedo no programa, e reutilizá-la quando necessário.

Closures estão além do escopo deste Conceito, mas quem já as conhece de outras linguagens vai reconhecer que elas oferecem uma forma mais flexível de criar funções de um único 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"

Depois que makesplit() estiver definida, ela pode ser usada para trabalhar com qualquer separador. Repare que makesplit('_') é uma chamada de função que resulta em outra função, que por sua vez recebe a entrada do pipe.

Se isso parecer confuso, é normal no começo (mas fica mais claro com a prática).

Outras opções

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

Enquanto isso, os usuários adotaram a abordagem habitual de criar vários pacotes instaláveis que atendem a necessidades específicas. Nenhum deles funciona dentro do Exercism, mas dê uma olhada neles se tiver interesse:

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Composição de funções