A questo punto del percorso, hai già visto molte definizioni di funzioni. Di solito venivano presentate senza troppi commenti sull'output nella REPL:
julia> f(x) = x^2
f (generic function with 1 method)
Perché «generic function with 1 method»?
Supponiamo di voler essere più specifici sui tipi degli argomenti.
julia> f(x::Integer) = x^2
f (generic function with 2 methods)
julia> f(x::Real) = x^2
f (generic function with 3 methods)
Oppure possiamo cambiare il numero di argomenti:
julia> f(x, y) = x * y
f (generic function with 4 methods)
Sembra che le nuove definizioni della stessa funzione si chiamino «metodi». I metodi vengono aggiunti a un elenco, invece di sovrascrivere la vecchia definizione.
Possiamo elencare facilmente i metodi:
julia> methods(f) # output edited for brevity
# 4 methods for generic function "f" from Main:
[1] f(x, y)
[2] f(x::Integer)
[3] f(x::Real)
[4] f(x)
Se chiamiamo la funzione f, Julia in qualche modo sa quale metodo usare:
julia> f(2.5) # Float64
6.25
julia> f(2) # Int64
4
julia> f(2, 3) # Int64, Int64
6
Secondo il manuale, una funzione è un oggetto che mappa una tupla di argomenti su un valore restituito.
Di solito pensiamo agli argomenti della funzione in f(2, 3) come a due elementi separati, ma il compilatore li vede come un'unica tupla.
Questo vale anche per un solo argomento (anche se dobbiamo aggiungere una virgola per renderlo più chiaro):
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
Vedendo la funzione come una scatola nera, possiamo dire che entra una tupla ed esce un valore restituito. Più formalmente, la tupla viene mappata su un valore restituito.
Di solito questo viene chiamato «polimorfismo».
Questo è un tema importante dell'informatica da diversi decenni, e sono stati adottati approcci diversi.
L'approccio di Julia è insolito, anche se non unico.
+ è una funzione con 161 metodi, al momento).... può aggirare in parte questa regola).dispatched con gli argomenti dati, e restituisce un risultato.MethodError.Tieni presente che Julia ha una tipizzazione dinamica, quindi il dispatch dei metodi avviene a runtime.
La sezione precedente conteneva molte parole: ora traduciamo quelle idee in codice. Per semplicità, le funzioni qui sotto hanno un solo argomento, ma non è raro trovare funzioni con decine di argomenti (per esempio nei pacchetti per il plotting o nel machine learning).
Cominciamo definendo alcuni tipi personalizzati per rappresentare forme geometriche.
julia> abstract type Shape end
julia> struct Circle <: Shape
radius::Float64
end
julia> struct Square <: Shape
side::Float64
end
julia> struct Rectangle <: Shape
length::Float64
width::Float64
end
Poi possiamo definire dei metodi per calcolare l'area di ogni forma. Ognuno prende un solo argomento, ogni volta di un tipo concreto diverso definito dall'utente.
julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)
julia> area(s::Square) = s.side^2
area (generic function with 2 methods)
julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)
Ora possiamo creare alcune forme e calcolare le aree, affidandoci al meccanismo di dispatch di Julia per usare la formula corretta per ciascuna.
julia> circle = Circle(2.3)
Circle(2.3)
julia> area(circle) # call signature Tuple{Circle}
16.619025137490002
julia> square = Square(1.6)
Square(1.6)
julia> area(square) # call signature Tuple{Square}
2.5600000000000005
julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)
julia> area(rectangle) # call signature Tuple{Rectangle}
2.94
Le firme dei metodi possono accettare valori di tipo fittizi, più o meno allo stesso modo dei tipi parametrici.
La sintassi è particolare: richiede una clausola where:
julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type([1, 2], 3)
true
julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)
L'esempio qui sopra richiede che entrambi gli argomenti usino lo stesso tipo T, qualunque esso sia.
Possiamo essere più specifici imponendo un vincolo su T:
julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)
Con questo metodo, solo tipi numerici identici restituiscono true.
Si possono avere più variabili fittizie, separate da virgole nella clausola where.
julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)
Il multiple dispatch è molto flessibile e molto performante in Julia, perciò è usato ampiamente.
Come suggerisce l'esempio dei 161 metodi per +, chi programma in Julia cerca di definire metodi con tipi specificati in modo ristretto.
Sommare due interi è molto diverso dal sommare due numeri in virgola mobile a livello di bytecode, e sommare un intero a un numero in virgola mobile aggiunge l'ulteriore complicazione della promozione di tipo.
Più il tipo è specifico, più il compilatore può ottimizzare: un fattore di importanza marginale in un piccolo esercizio su Exercism, ma vitale per le prestazioni nelle grandi simulazioni numeriche per cui Julia è pensata.
Se sai quali input riceverà un metodo, dillo al compilatore!
In questo esercizio implementerai una simulazione di incontri. Questo ti farà familiarizzare con le basi del multiple dispatch, il paradigma principale di Julia.
For this exercise, all function definitions should be a single-line declaration with no if/else logic.
In generale, gli incontri prevedono che un'entità a incontri un'altra entità b e reagisca a essa.
encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."
All'inizio, simuleremo cosa succede quando cani e gatti si incontrano.
Per prima cosa, aggiungi un tipo astratto Pet.
Poi definisci i tipi Cat e Dog come sottotipi di Pet.
Ognuno ha un singolo campo name.
Implementa una funzione name() che restituisce il nome di un animale domestico.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Implementa i metodi meets() per i seguenti incontri:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
Implementa la funzione encounter() con un metodo, per riportare cosa succede quando due entità con nome si incontrano.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
Cosa succede se incontrano un altro animale domestico durante la passeggiata, come un cavallo?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
Ci sono molte altre cose che gli animali domestici possono incontrare e che non sono animali domestici: auto, umani, piante, disastri naturali, asteroidi… Cosa succede allora?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
Ci sono molti altri incontri che potrebbero verificarsi nella nostra simulazione. Un'auto incontra un cane, due elettroni si incontrano e interagiscono…
È impossibile coprire tutti gli incontri in anticipo, quindi implementeremo una reazione di riserva generica.
julia> γ1 = Photon("γ1")
Photon("γ1")
julia> γ2 = Photon("γ2")
Photon("γ2")
julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens."
# This is true, photons just pass through each other.
Iscriviti a Exercism per imparare e padroneggiare Julia con 35 concetti128 esercizi e il mentoring di persone reali, tutto gratis.