Julia



Funzioni          

Come in tutti i linguaggi anche in Julia le funzioni sono sequenze di operazioni che mappano determinati valori (da 0 ad n a piacere) restituendo un output, che può essere ancher nullo. In Julia, come specificato nella documentazione ufficiale, le funzioni sono meno rigide di quelle rispondenti alla definizione matematica, in quanto possono essere influenzate dallo stato del programma e possono a loro volta influenzarlo. L'aspetto generale di una funzione è il seguente:

function nome funzione(eventuali parametri o argomenti)
istruzioni
end


Partiamo subito da alcune considerazioni:

1) la keyword introduttiva è, evidentemente, function.
2) ad essa segue il nome della funzione e gli eventuali parametri (detti anche argomenti). A tutti (tranne che in presenza di presenza di optional arguments, come vedremo) tali parametri vengono attribuiti dei valori dal chiamante in maniera posizionale (eccezione è quella degli argomenti con nome)..
3) la parola end è obbligatoria per segnalare il termine della funzione
4) la funzione è in grado di restituire un valore anche senza l'uso del classico return (che però esiste anche in Julia, come vedremo); in particolare ad essere passato al chiamante è il valore dell'ultima espressione

L'espressione precedente può essere vista in forma più funzionale come segue:

f(x,y) = x + y

Con questa notazione il corpo della funzione deve essere espresso con un'unica istruzione o con una compound (vedremo di che si tratta).
Come in altri linguaggi anche in Julia una funzione viene richiamata semplicemente col suo nome seguita dai giusti parametri.

  Esempio 5.1
1
2
3
4
5
function somma(x, y)
x + y
end

print(somma(2,3))

L'esempio non dovrebbe creare difficoltà di comprensione. L'output che sarà esposto è ovviamente costituito dal numero 5.
Nell'esempio seguente invece vediamo una comoda modalità di copia delle funzioni ed un modo particolare,utile in qualche occasione, che fa uso della particolare istruzione apply:

  Esempio 5.2
1
2
3
4
5
6
7
8
9
function somma(x, y)
x + y
end

g = somma;

println(somma(2,3))
println(g(3,4))
print(apply(somma,5,6))

La riga 5 e la 9 espongono quanto anticipato.
Ovviamente una funzione può non avere parametri e può non restituire nulla. Non c'è problema:

  Esempio 5.3
1
2
3
4
5
function scrivi()
print("ciao")
end

scrivi()

Abbiamo parlato di return. La sua funzione è quello di provocare un'uscita forzata dal blocco del metodo nel quale viene invocato. In questo modo però bisogna fare attenzione ad eventuali istruzioni successive del blocco in cui abbiamo il nostro return in quanto esse risulteranno inaccessibili e non saranno mai eseguite. Se siamo al livello più alto del metod usciamo dallo stesso. L'esecuzione del prossimo programma di test lo prova:

  Esempio 5.4
1
2
3
4
5
6
7
function f(x,y)
return x - y
x + y
print("ciao")
end

print(f(2,3))

Quanto alle righe 3 e 4 non viene mai eseguito. Ovviamente return può essere utile in altre situazioni e col tempo vedremo alcuni esempi. L'importante è avere chiara la sua forza reale dal momento che l'interprete non aiuta nel senso che non rivela il codice non raggiungibile come invece fanno alcuni compilatori (quello del linguaggio C#, se ben ricordo, è un esempio in questo senso).
Una cosa interessante evidenziata sul manuale di Julia è che anche molti operatori sono in realtà funzioni essi stessi con l'unica particolarità di accettare una sintassi non standard rispetto a quella canonica delle funzioni. Si veda ad esempio quanto segue:

julia> 1 + 2 + 3
6
julia> +(1,2,3)
6
julia> f = *
julia> f(3,2)
6

Come vedete il + si comporta proprio come una funzione ed il * stesso può essere assegnato una nuova funzione.

Abbiamo toccato solo in superficie l'argomento relativo alle funzioni. Passiamo ora a qualche cosa di più corposo e parliamo delle funzioni anonime. La base è che le funzioni in Julia sono oggetti di prima classe e pertanto possono essere assegnate a variabili, o essere passte come valore di ritorno da un'altra funzione. E' naturalmente possono essere create anonime, senza alcun nome. Un esempio applicativo tipico è in collaborazione con altre funzioni:
 
julia> map(x -> x/3, [1,2,3])
3-element Array{Float64,1}:
0.333333
0.666667
1.0


x -> x/3 è la nostra funzione che divide quindi per 3 ogni input che gli venga passato. La notazione è quella che potete intuire:

argomento -> corpo della funzione

In questo caso la lettura è semplice ma qualorà passaste una funzione più complessa della semplice divisione proposta nell'esempio, Julia vi offre una sintassi alternativa che meglio divide le cose e che si basa sulla keyword do tramite la quale l'esempio precedente risulterebbe così:

julia> map([1,2,3]) do x x/3 end

che permette una miglior separazione tra le varie entità in gioco.

Una funzione anonima può avere più argomenti o anche non averne:

(x,y,z) -> x + y +z
() -> 3


Negli esempi precedenti abbiamo potuto vedere che le funzioni possono inviare una valore di ritorno. Questo però è limitativo in quanto in realtà possiamo restituire verso il chiamante  quanti valori vogliamo ed anche abbinarli ad altrettante variabili in maniera molto semplice.

  Esempio 5.5
1
2
3
4
5
6
7
function minusplus(a, b)
a + b, a - b
end

x, y = minusplus(7,3)
println(x)
println(y)

Va anche detto che la riga 2 deve essere scritta in quel modo.  Se la spezzassimo scrivendo:

a + b
a - b


l'interprete restituirebbe un valore solo, l'ultimo, e non ci sarebbe corrispondenza con i valori richiesti alla riga 5. Nulla vi vieta, magari per questioni di miglior leggibilità del programma, di preporre la keyword return alla riga 2.

Il documento principale del linguaggio prosegue poi presentando le funzioni aventi un numero variabile di parametri in input. Questo è tipico anche del linguaggio stesso, in fondo una funzione come print può avere uno o due o quanti parametri vogliamo. Per indicare questa possibilità all'interprete è sufficiente attaccare 3 punti all'ultima variabile che risulta in realtà opzionale. Detto così suona un po' ostile vediamo un esempio e le cose si chiariranno:

  Esempio 5.6
1
2
3
4
5
6
7
function foo(a, b, x...)
println("ciao")
end

foo(2,2)
foo(2,3,4)
foo(1,2,3,4,5,6)

La riga 1 presenta quanto esposto. La funzione è correttamente richiamata con 2 parametri (a e b sono obbligatori), 3, o quanti ne vogliamo. Naturalmente per avere 0 pragarafi obbligatori, come per la già citato comando print,  e quindi la massima libertà si doveva scrivere:

foo(x...)

Ai parametri è possibile attribuire un valore di default, così che non è necessario passarli direttamente. Vengono chiamati, nel gergo di Julia, optional arguments (in altri linguaggi si chiamano anche defaulted parameters) I valori di default, se non sovrascritti hanno pieno valore all'interno della funzione. Al solito un esempio vale più delle parole:

  Esempio 5.7
1
2
3
4
5
6
function foo (x, y = 10)
x + y
end

println(foo(5))
println(foo(3,4))

L'output è:

15
7

Perchè è facile capirlo: alla riga 5 la funzione foo cìviene richiamata attribuendogli un solo parametro in quanto all'altro è assegnato un valore di deafult (se non l'avesse, naturalmente, l'intrprete segnalerebbe errore), alla riga 6 vengono passati due valori e quindi il valore attribuito di deafult a y viene sovrascritto. Questa tecnica ha una limitazione, ovvero il parametro con il valore di default deve essere l'ultimo della serie o comunque ci deve essere una serie finale di parametri con valore. Firmare una funzione come segue:

function foo (x, y = 10, z)

dà origine al seguente errore.

syntax: optional positional arguments must occur at end

chiaro no?

Invece:

function foo (x, y = 10, z = 20)

va bene perchè i parametri finali sono tutti inizializzati.
Per quanto detto non è possibile mischiare firme aventi un numero variabile di parametri, appena visti in precedenza, con quest'ultima possibilità.

La cosa non finisce qui; Julia permette anche l'uso dei named parameters, o named arguments, i parametri con nome. Si tratta della possibilità di appiccicare un identificativo ad un parametro il che permette allo stesso di essere inizializzato con sicurezza. Si adatta quindi a scenari in un cui il metodo abbia molti parametri eventualmente anche opzionali e noi vogliamo essere sicuri di attribuire quel valore inizale a quel particolare parametro. Al momento, i parametri con nome devono essere inizializzati con un valore quindi sono anche optional arguments. In questo caso i parametri possono essere inizializzati in maniera non posizionale ma venendo richiamati tramite il loro nome. Ecco un rapido esempio:

  Esempio 5.8
1
2
3
4
5
function foo (x, y; z=10)
x + y + z
end

println(foo(z = 3,4, 19))

Come si vede alla riga 5 i valori sono passati non più in ordine, z è richiamato col suo nome (se volete la prova fate stampare il valore di z a video)

Le funzioni costituiscono un argomento centrale in un linguaggio, appunto, "funzionale" come Julia e certamente questa rapida carrellata non è sufficiente ad illustrarne tutte le potenzialità e la grande utilità. Torneremo quindi sull'argomento, per il momento si può provare ad ampliare gli esempi proposti.