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.
É 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:
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.
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.
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).
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: