Tracce
/
Julia
Julia
/
Programma
/
Composizione di funzioni
Co

Composizione di funzioni in Julia

1 esercizio

Informazioni su Composizione di funzioni

Julia incoraggia chi programma a mettere quanto più codice possibile dentro funzioni che possono essere compilate JIT, e creare molte funzioni piccole è performante per precisa scelta di progettazione.

La conseguenza è che spesso restano molte funzioni piccole e semplici, che devono essere combinate tra loro per svolgere compiti non banali.

Un approccio ovvio è annidare le chiamate di funzione. L'esempio seguente è molto artificioso, ma rende l'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)

Il difetto di questo approccio è che la leggibilità cala rapidamente man mano che l'annidamento diventa più profondo.

Ci serve un approccio più semplice e più flessibile.

Composizione

È la tecnica prediletta dai matematici, e Julia ne copia la sintassi.

Un numero arbitrario di funzioni può essere composed insieme con gli operatori ∘ (che si digitano con \circ e poi il tasto tab). Il risultato può essere usato come un'unica funzione.

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 paio di cose da notare:

  • Le funzioni di partenza compaiono nello stesso ordine di quando si annida, e vengono eseguite da destra a sinistra.
  • Il broadcasting non è semplice da usare quando si compone, ma si può applicare quando si chiama la funzione composta.

Pipeline

Un'alternativa si potrebbe considerare l'approccio dei programmatori, più che quello dei matematici.

Le Pipelines sono usate da tempo negli script della shell Unix, e più di recente sono diventate popolari nei linguaggi di programmazione più diffusi (a F# viene talvolta attribuito il merito di averne diffuso l'adozione per primi).

Il concetto di base è partire da alcuni dati, per poi farli passare attraverso una sequenza di funzioni e ottenere il risultato.

L'operatore pipe è |> (come in F# e nelle versioni recenti di R), anche se Julia ha anche una versione broadcast, .|>.

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)

Ora l'esecuzione è strettamente da sinistra a destra: l'output di ogni funzione scorre nella direzione della freccia e diventa l'input della funzione successiva.

Limiti, soluzioni alternative e altre opzioni

Non è un caso che le funzioni usate per illustrare la composizione e le pipeline prendano tutte un singolo argomento.

Alcuni linguaggi puramente funzionali fanno passare il primo argomento dentro una funzione, ma permettono di aggiungerne altri.

Al contrario, Julia si aspetta solo i nomi delle funzioni (o qualcosa di equivalente) in una pipeline, senza argomenti aggiuntivi.

Ci sono importanti ragioni tecniche per questo (legate al fatto che il currying non fa parte in modo standard del progetto del linguaggio). Le molte persone che non hanno idea di cosa sia il currying devono solo accettare che questo limite non è una svista, e che è improbabile che cambi nelle future versioni di Julia.

Soluzioni alternative

Ci servono funzioni a singolo argomento che facciano tutto il necessario. Per fortuna, definire nuove funzioni in Julia è facile.

Il modo più semplice è usare una anonymous function. Per esempio, se abbiamo un'unica stringa di input e vogliamo dividerla sugli underscore:

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

Quel vettore potrebbe poi essere passato ad altre funzioni tramite pipe, come prima.

Racchiudere la funzione anonima tra parentesi è facoltativo in questo caso, ma più in generale è un modo utile per ridurre le ambiguità.

Allo stesso modo, potremmo creare una funzione con un nome, prima nel programma, e riutilizzarla quando serve.

Le Closures vanno oltre lo scopo di questo concetto, ma chi le conosce da altri linguaggi riconoscerà che offrono un modo più flessibile per creare funzioni a singolo argomento.

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 volta definita makesplit(), la si può usare per lavorare con qualsiasi separatore. Nota che makesplit('_') è una chiamata di funzione che dà come risultato un'altra funzione, la quale a sua volta riceve l'input dalla pipe.

Se tutto questo ti sembra confuso, è normale all'inizio (ma con la pratica diventa più chiaro).

Altre opzioni

C'è stata una lunga discussione su come rendere le pipe più versatili nella base di Julia, ma i vari suggerimenti sono incompatibili tra loro e non si è raggiunto alcun accordo.

Nel frattempo, gli utenti hanno adottato il solito approccio: creare vari package installabili che rispondono a esigenze specifiche. Nessuno di questi funzionerà all'interno di Exercism, ma se ti interessa dai un'occhiata a questi:

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Composizione di funzioni