Maurizio Tomasi
Università degli Studi di Milano
C, C++, FreePascal, gfortran, Rust, GNAT Ada, Nim, …
CPython, R, Matlab, IDL, …
Java, LuaJIT, Julia, etc.
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.4.1 (2020-04-14)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia>
julia> mysum(a, b) = a + b
mysum (generic function with 1 method)
julia> mysum(1, 2)
3
julia> mysum(1.0, 2.0)
3.0
julia> mysum(1//3, 3//4)
13//12
julia> mysum(3+2im, 4-3im)
7 - 1im
Apparentemente, funziona come Python!
julia> @code_native mysum(1, 2)
.text
; ┌ @ REPL[1]:1 within `mysum'
; │┌ @ REPL[1]:1 within `+'
leaq (%rdi,%rsi), %rax
; │└
retq
nopw %cs:(%rax,%rax)
nop
; └
(L’uso di leaq
anziché add
è un’ottimizzazione di LLVM).
julia> @code_native mysum(1.0, 2.0)
.text
; ┌ @ REPL[1]:1 within `mysum'
; │┌ @ REPL[1]:1 within `+'
vaddsd %xmm1, %xmm0, %xmm0
; │└
retq
nopw %cs:(%rax,%rax)
nop
; └
Tempo di esecuzione: 0.07 s (meglio del C++!)
Julia non compila i programmi in un eseguibile, come il C++
La compilazione avviene quando il codice viene eseguito la prima volta
Invocare il comando import Plots; plot(…)
all’avvio richiede 14 secondi; chiamare altre volte plot
è invece immediato
Di conseguenza, Julia non va generalmente usato come C++ o come Python:
julia
, e poi eseguire i programmi col comando include("nomefile")
nomefile
viene compilato la prima volta, ma poi viene sempre rieseguita la versione già compilataimport Plots
una volta sola, così poi restano compilati per il resto della sessioneif
, for
, while
solo su valori di variabilivoid f(int a) {
// ????
}
int main() {
f(2 + 2); // Should print "A"
f(2 * 2); // Should print "B"
f(4); // Should print "B"
}
Come si può scrivere una funzione f
che stampa A
se l’argomento è calcolato mediante una somma, e B
altrimenti?
julia> expression = :(a + b / 2) # :() means "quote"
:(a + b / 2)
julia> dump(expression)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Symbol a
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol /
2: Symbol b
3: Int64 2
julia> expression
:(a + b / 2)
julia> a = 1; b = 2; eval(expression)
2.0
julia> expression.args[1] = :*
:*
julia> expression
:(a * (b / 2))
julia> eval(expression)
1.0
Calcolo esatto di derivate, integrali, etc.
Machine learning (uno degli obbiettivi del LISP!)
Definizione di costrutti nuovi per il linguaggio:
virtual
in C++)int main(int argc, const char *argv[]) {
FunzioneBase *f = nullptr;
if (argv[1] == "0") {
f = new Seno();
else {
f = new Coseno();
}
std::cout << f->Eval(0.3) << "\n";
}
In fase di compilazione, il compilatore non può sapere se verrà usato Seno
o Coseno
: questo viene deciso a runtime.
Supponiamo di avere a disposizione una classe molto complessa, che calcola la temperatura di brillanza di una stella in una certa banda:
Questa libreria è installata sul sistema che stiamo usando, in una directory che non possiamo modificare (es., /usr/lib
).
Supponiamo ora di aver definito una classe UnitValue
, che implementa un tipo di dato numerico a cui è associata una unità di misura:
Non possiamo però passare un dato UnitValue
a BrightnessTemperature::Eval
, perché accetta solo double
!
E non possiamo modificare il file che definisce la classe, perché è in una directory di sola lettura!
In ambito matematico capita molto spesso di dover definire funzioni che agiscono su più tipi (interi, floating-point, numeri complessi, matrici…):
Spesso non si può neppure prevedere su che tipi potrebbero dover agire queste funzioni. Questo è vero anche per codici scientifici:
UnitValue
di prima, in cui si controllano le unità di misuraValueWithError
, che memorizza un dato insieme alla barra di errore, e fa la propagazione degli erroriJulia non implementa costrutti OOP: non esistono classi in Julia!
Julia implementa il multiple dispatch:
Come l’overloading, si possono definire funzioni con lo stesso nome che accettano tipi diversi (impossibile in Python!)
Come le funzioni virtuali, la funzione da chiamare è decisa a runtime
Per decidere quale funzione virtuale chiamare nella sintassi
il C++ si basa sul solo tipo di f
(single dispatch)
Nel multiple dispatch, tutti i tipi degli argomenti di una funzione f(a, b, …)
sono usati da Julia per capire quale tipo chiamare
Vediamo un esempio
Codice preso da giordano.github.io/blog/2017-11-03-rock-paper-scissors
abstract type Shape end
struct Rock <: Shape end
struct Paper <: Shape end
struct Scissors <: Shape end
play(::Type{Paper}, ::Type{Rock}) = "Paper wins"
play(::Type{Paper}, ::Type{Scissors}) = "Scissors wins"
play(::Type{Rock}, ::Type{Scissors}) = "Rock wins"
play(::Type{T}, ::Type{T}) where {T<: Shape} = "Tie, try again"
play(a::Type{<:Shape}, b::Type{<:Shape}) = play(b, a) # Commutativity
julia> play(Paper, Scissors)
"Scissors wins"
julia> play(Rock, Rock)
"Tie, try again"
julia> play(Rock, Paper)
"Paper wins"
Tutta la logica del programma è implementata senza neppure un if
!
Cosmology
Installiamo e usiamo il pacchetto Cosmology
, che implementa alcuni calcoli basati sulle equazioni di Friedmann-Lemaître:
Measurements
Esiste un utile pacchetto, Measurements
, che associa a valori delle barre di errore. Implementa l’operatore ±
(come ⊕
nel nostro esempio sopra) per rendere la notazione più agile da scrivere e da leggere:
Measurements
Il pacchetto è in grado di tenere traccia di cancellazioni degli errori:
Cosmology
+ Measurements
Avevamo detto che nel nostro esempio in C++ non era possibile passare a f->Eval
un tipo che fosse diverso da double
.
In Julia, grazie al multiple dispatch, è possibile usare barre di errore col pacchetto Cosmology
senza doverne modificare il codice sorgente:
julia> z = 0.1 ± 0.003
0.1 ± 0.003
julia> Cosmology.age_gyr(c, z)
12.465336269441773 ± 0.03691682655261089
Abbiamo combinato tra loro due pacchetti diversissimi, e senza modificare il codice sorgente dell’uno o dell’altro!
Julia consente di definire strutture derivate (come le classi derivate in C++)
Ma un tipo primitivo, in Julia, deve essere astratto e non avere elementi:
Sembra una limitazione grave, ma in realtà è segno di un diverso approccio rispetto alla programmazione OOP del C++
Alcuni esempi di tipi astratti definiti in Julia:
I tipi di dati concreti (Int64
, Float32
) derivano da uno di questi tipi astratti
Si possono specializzare funzioni come si vuole:
Ho sviluppato un pacchetto, Harlequin.jl, per la simulazione dell’acquisizione dei dati di esperimenti spaziali di cosmologia
È necessario simulare in queste missioni il concetto di «flag», un codice associato a ciascuna delle misure fatte dall’esperimento (migliaia di miliardi) durante la sua vita che dica se il dato può essere usato oppure no.
Il vettore dei flag è formato da molti numeri ripetuti:
È dispendioso tenere in memoria un vettore come flag
Ho implementato un nuovo tipo di dato, RunLengthArray
, che mantiene in memoria una sequenza di coppie (valore,ripetizioni)
:
Il tipo RunLengthArray
si comporta però esattamente come un array, e può essere usato con qualsiasi funzione Julia:
Ho definito specializzazioni per quelle funzioni Julia che possono essere calcolate rapidamente: