Track
/
Julia
Julia
/
Esercizi
/
Incontri
Incontri

Incontri

Esercizio di apprendimento

Introduzione

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

Che cos'è una funzione?

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.

Come gestiscono le funzioni numeri e tipi di argomenti diversi?

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.

  • Le funzioni sono «generiche»: possono avere più metodi associati.
  • Le funzioni sono solo nomi, finché non hanno dei metodi associati.
  • I metodi si possono aggiungere secondo necessità, anche in gran numero (l'operatore + è una funzione con 161 metodi, al momento).
  • Quando una funzione viene chiamata, Julia esamina la firma della chiamata: il numero di argomenti e il tipo attuale di ciascuno.
  • Viene selezionato il metodo che corrisponde più da vicino alla firma della chiamata.
    • In generale, il numero di argomenti deve corrispondere esattamente (anche se l'operatore splat/slurp ... può aggirare in parte questa regola).
    • Sono preferiti i metodi i cui tipi corrispondono esattamente alla firma della chiamata.
    • Se necessario, Julia esamina i supertipi di ogni argomento della firma della chiamata per trovare il metodo più specifico e più vicino.
  • Il metodo scelto viene dispatched con gli argomenti dati, e restituisce un risultato.
  • Se non viene trovato un metodo adatto, viene sollevato un MethodError.

Tieni presente che Julia ha una tipizzazione dinamica, quindi il dispatch dei metodi avviene a runtime.

Multiple dispatch: un esempio

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

Metodi parametrici

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)

Conclusione

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!

Istruzioni

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.

1. Definisci tipi personalizzati

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.

2. Ottieni il nome dell'animale domestico.

Implementa una funzione name() che restituisce il nome di un animale domestico.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. Definisci cosa succede quando cani e gatti si incontrano

Implementa i metodi meets() per i seguenti incontri:

  • Quando due cani si incontrano, si sniff a vicenda.
  • Quando un gatto incontra un cane, gli hisses.
  • Quando un cane incontra un gatto, lo chases.
  • Quando due gatti si incontrano, slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Definisci un incontro tra due entità.

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."

5. Definisci una reazione di riserva per gli incontri tra animali domestici

Cosa succede se incontrano un altro animale domestico durante la passeggiata, come un cavallo?

  • Se un animale domestico incontra un altro animale domestico che non riconosce, is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. Definisci una reazione di riserva se un animale domestico incontra qualcosa che non conosce

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?

  • Se un animale domestico incontra qualcosa che non ha mai visto prima, runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. Definisci una reazione di riserva generica

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.

  • Se due cose o esseri sconosciuti si incontrano, nothing happens.
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.
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
Julia Exercism

Vuoi iniziare Incontri?

Iscriviti a Exercism per imparare e padroneggiare Julia con 35 concetti128 esercizi e il mentoring di persone reali, tutto gratis.