Julia



Array         

NB: gli array e le matrici sono argomento primario in Julia e costituiscono un corposo paragrafo. Questo capitolo sarà aggiornato periodicamente.

Un linguaggio orientato alla programmazione scientifica non può non avere un supporto di alto livello per gli array, struttura dati tipica di ogni linguaggio (tranne gli esoterici e poco altro). Anche in Julia un array è una collezione di oggetti memorizzati in una griglia n-dimensionale, laddove n va da 1 ad un valore intero teoricamente grande a piacere. Gli oggetti memorizzati possono essere di qualsiasi natura, normalmente avremo interi, floating point o stringhe ma, come detto, non vi sono limiti alla tipologia di quello che potrete memorizzare in un array. I linguaggi di programmazione a tipizzazione statica prevedono che tutti gli elementi all'interno di un array siano dello stesso tipo. Julia, come consuetudine per i linguaggi dinamici, permette una maggior libertà (che, per inciso, può essere emulata in modo banale anche per alcuni i linguaggi statici,) in maniera nativa, come vedremo subito qui avanti. Gli elementi appartenenti ad un array sono individuabili tramite una indicizzazione, ognuno di essi avrà una sua n-upla di indici che lo caratterizza, con n pari alle dimensioni dell'array.
Formalmente, nella sua natura più semplice, un array è delimitato da una coppia di parentesi quadrate con i singoli elementi al suo interno separati tramite virgola; sono validi quindi i due esempi seguenti:

julia> a1 = [1,2,3]
3-element Array{Int64,1}:
1
2
3

julia> a2 = [1, 2.2, "ciao"]
3-element Array{Any,1}:
1
2.2
"ciao"

In questo caso abbiamo definito due array, a1 contenente solo interi e a2 che invece contiene un intero, un float ed una stringa e quindi viene specificato come tipo di definizione per l'array il generico Any, si tratta pur sempre di una forma di inferenza, se vogliamo. Nell'esempio abbiamo evidentemente a che fare con un array monodimensionale, il numero di dimensioni è indicato da quel "1" posto dopo il tipo. Più in generale invece la definizione formale generica, come data nella documentazione ufficiale, è la seguente:

[elem-1, elem-2, ..., elem-n]

dove ogni elemento all'interno può essere:

1) un valore scalare
2) un vettore booleano
3) un vettore di interi eventualmente anche vuoto
4) un range nel formato a:b o a:b:c

Il primo indice
In attesa di approfondire questo aspetto, vediamo alcune funzioni associate, alcune banali altre meno consuete a questa struttura dati per la sua manipolazione:

Funzione Uso
eltype(A) Espone il tipo degli elementi dell'array
length(A) restituisce la lunghezza ovvero il numero di elementi di un array
ndims(A) restituisce il numero di dimensioni di un array
eachindex(A) Un iteratore efficiente per visitare ogni elemento di A.
size(A) restituisce una n-upla che indica tutte le lunghezze di ogni dimensione dell'array
size(A, n) restituisce la lunghezza di una data dimensione n
stride(A, n) restituisce la distanza lineare tra elementi adiacenti per una data dimensione n.
strides(n) restituisce una n-upla delle stride per ogni dimensione

Oltre alla possibilità di definire direttamente gli array, come nell'esempio precedente, Julia ci offre una lunga serie di costruttori. Di seguito alcuni che ritengo possano essere i più utili:

1) Array(tipo, dimensioni)
Es: a1 = Array(Int,2)
Costruttore che produce un array non inizializzato, una dimensione, due elementi. Essendo non iniziallizzato potreste trovarvi dei valori strani se il costruttore va a pescare della memoria "sporca". Tuttavia , in aggijnta avete imposto un tipo base, ovvero gli elementi dovranno essere solo interi (naturalmente va bene qualsiasi altro tipo riconosciuto da Julia)

2) Se volete proporre una immediata inizializzione ad un array al quale imposto un tipo base potete scrive qualcosa come:
a4 = Array{Int64,1}([1,2,3,4])
Inserendo un elemento di tipo diverso dagli interi avrete un messaggio evidente da parte del compilatore.

3) Definire un array multidimensionale è in linea con la definizione al punto 1), nella sua espressione più semplice:
a8 = Array(Int64,2,3)
2x3 Array{Int64,2}:
8589934594 0 2147435856
2147483649 0 2147557456


Ovviamente,anche in questo esempio, i valori sono casuali ed esprimono il contenuto delle celle di memoria in cui viene creato l'array.
Tramite 1), 2) e 3)  abbiamo un sistema "standard", in linea con moltissimi altri linguaggi, per costruire il nostro array. Più avanti ne vedremo altri.

L'accesso al singolo elemento di un array avviene, come in moltissimi linguaggi, tramite l'operatore []che può essere utilizzato per selezionare l'elemento sia in lettura (ad esempio esporlo a video) sia in scrittura, quindi potete modificarne il valore. ovviamente cercare di accedere ad elementi fornendo un indice fuori dal range che caratterizza l'array dà origine ad un errore. Per accedere ad un array ad una dimensione basta indicare l'indice che vogliamo:
[n]
se lavoriamo su più dimensioni dobbiamo speficarle tutte per accedere al singolo elemento:
[n,m] accede all'elemento riga n colonna m di un array bidimensionale
E' interessante notare che gli array in Julia sono 1-based, ovvero il valore del primo indice è 1 invece di 0, come forse è più consueto.

julia> a1 = [1,2,3,4]
4-element Array{Int64,1}:
1
2
3
4

julia> a1[1]

1

L'iterazione sugli elementi può avvenire tramite il consueto uso del ciclo for, eseguito all'interno del range corretto, oppure, meglio, tramite la funzione eachindex, vista in precedenza:

julia> a1 = [1,2,3,4,5]
julia> for x in eachindex(a1)
println(x)
end
1
2
3
4
5

julia> for x in 1:length(a1)
println(a1[x])
end
1
2
3
4
5

si noti l'uso di length, banale.
tramite eltype potete, ad esempio, testare se gli elementi di un array appartengono ad un certo tipo:

julia> println(eltype(a1) <: Int64)
true


Vediamo ora altri comportamenti interessanti tramite le funzioni che li determinano.

A livello di costruttori, abbiamo già il metodo "classico" troviamo anche le seguenti interessanti funzioni:

zeros(tipo, n) - crea un array di n elementi del tipo specificato inizializzati a zero (se il tipo non viene specificato il default è float64)
ones(tipo, n)  - crea un array di n elementi del tipo specificato inizializzati a uno (se il tipo non viene specificato il default è float64)
cell(n)        - crea un array vuoto delle dimensioni indicate
trues(n)       - crea un array booleano con tutti gli elementi true
falses(n)      - crea un array booleano con tutti gli elementi false
rand(n)        - crea un array riempito con valori random di tipo float64 (ovviamente rand(n,m, ... ,r) crea una matrice riempita con valori random float64).

Ad esempio, vediamo all'opera cell:

julia> ar1 = cell(3)
3-element Array{Any,1}:
#undef
#undef
#undef

julia> ar1[1] = 1
1

julia> ar1[2] = "ciao"
"ciao"

julia> ar1
3-element Array{Any,1}:
1
"ciao"
#undef

collect permette di creare un array basandoci su di un range:

a = collect(1:3)

Un sistema molto potente è quello dell'array comprehension che ha il formato generale:

A = [ F(x,y,...) for x=rx, y=ry, ... ]

ovvero, l'array viene creato valutando la funzione F sulle variabile x, y.... prendendo ciascun valore in quelli della sua eventuale lista. Un semplice esempio è quello che segue:

julia> ac = [n ^ 2 for n in 1:5]
5-element Array{Int64,1}:
1
4
9
16
25


Infine chiudiamo con
fill(x, n) che crea un array avente n elementi uguali a x. Esiste poi fill!(nomearray, x) (si noti il punto esclamativo) che sostuisce tutti gli elementi dell'array "nomearray" con x.

julia> ab = fill(3,2)
2-element Array{Int64,1}:
3
3

julia> fill!(ab, 9)
2-element Array{Int64,1}:
9
9

Gli array possono essere concatenati facilmente, in Julia, tramite l'operatore ; (punto e virgola):

julia> a = [1,2]
2-element Array{Int64,1}:
1
2

julia> b = [3,4]
2-element Array{Int64,1}:
3
4

julia> c = [a;b]
4-element Array{Int64,1}:
1
2
3
4


oppure attraverso append!:

julia> a1 = [1,2,3]
3-element Array{Int64,1}:
1
2
3

julia> b1 = [4,5,6]
3-element Array{Int64,1}:
4
5
6

julia> a1b1 = append!(a1,b1)
6-element Array{Int64,1}:
1
2
3
4
5
6

Si noti tuttavia che append accetta solo due parametri (è logico, si tratta di una operazione che appende un array ad un altro), mentre col ; si possono concatenare anche più di 2 array.
Un problema comune è quello di effettuare una copia degli array. Julia mette a disposizione due funzioni:

copy     -  che crea un copia dell'array originale
deepcopy -  che crea una deepcopy, nel senso che di ogni elemento viene creata una deepcopy, insomma non vengono creati riferimenti. E' un discorso che approfondiremo.

per il momento queste due istruzioni sono, in pratica, equivalenti.

julia> arr1 = [1,2,3,4]
4-element Array{Int64,1}:
1
2
3
4

julia> arr2 = copy(arr1)
4-element Array{Int64,1}:
1
2
3
4


A titolo informativo, sono argomenti che approfondiremo, segnalo la funzione eye nel doppio formato:

eye(n) -   che crea una matrice identità (n x n)
eye(n,m) - che crea una matrice identità (n x m)

julia> aa = eye(3)
3x3 Array{Float64,2}:
1.0 0.0 0.0
0.0 1.0 0.0
0.0 0.0 1.0

Per eliminare elementi da un array vi sono varie funzioni disponibili, molto semplici nell'uso, che elenco qui di seguito:

pop!(array) -                  elimina l'ultimo elemento
shift!(array) -                elimina il primo elemento
deleteat!(array, posizione) -  elimina l'elemento alla posizione specificata
deleteat!(array, range) -      elimina tutti gli elementi i cui indici stanno nel range, estremi inclusi

julia> a = [1,2,3,4,5,6]
6-element Array{Int64,1}:
1
2
3
4
5
6

julia> deleteat!(a,2:4)
3-element Array{Int64,1}:
1
5
6

Infine c'è empty!(array) che elimina tutti gli elementi dell'array.

Per estendere un array abbiamo push!

julia> a = [1,2,3]
3-element Array{Int64,1}:
1
2
3

julia> push!(a,4)
4-element Array{Int64,1}:
1
2
3
4


trovare un elemento in un array è semplice con la funzione in che possiamo usare in due formati, come si evince dal seguente esempio:

julia> a = [1,2,3,4]
4-element Array{Int64,1}:
1
2
3
4

julia> 3 in a
true

julia> in(3,a)
true

julia> in(5,a)
false


mentre findfirst(array, elemento) trova la prima ricorrenza dell'elemento.