Array di tipo astratto in julia in functions

Nov 11 2020

Cerco di capire la digitazione in Julia e incontro il seguente problema con Array. Ho scritto una funzione bloch_vector_2d(Array{Complex,2}); l'implementazione dettagliata è irrilevante. Quando si chiama, ecco il reclamo:

julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
  bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
  bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
 [1] top-level scope at REPL[64]:1

Il problema è che un array di tipo genitore non è automaticamente genitore di un array di tipo figlio.

julia> Complex{Float64} <: Complex
true

julia> Array{Complex{Float64},2} <: Array{Complex,2}
false

Penso che avrebbe senso imporlo a Julia Array{Complex{Float64},2} <: Array{Complex,2}. O qual è il modo giusto per implementarlo in Julia? Eventuali aiuti o commenti sono apprezzati!

Risposte

6 BogumiłKamiński Nov 11 2020 at 19:13

Questo problema è discusso in dettaglio nel manuale Julia qui .

Citando la parte rilevante di esso:

In altre parole, nel gergo della teoria dei tipi, i parametri di tipo di Julia sono invarianti, piuttosto che essere covarianti (o addirittura controvarianti). Questo per ragioni pratiche: mentre qualsiasi istanza di Point{Float64}può concettualmente essere anche un'istanza di Point{Real}, i due tipi hanno rappresentazioni diverse in memoria:

  • Un'istanza di Point{Float64}può essere rappresentata in modo compatto ed efficiente come una coppia immediata di valori a 64 bit;
  • Un'istanza di Point{Real}deve essere in grado di contenere qualsiasi coppia di istanze di Real. Poiché gli oggetti che sono istanze di Real possono essere di dimensioni e struttura arbitrarie, in pratica un'istanza di Point{Real}deve essere rappresentata come una coppia di puntatori a oggetti Real allocati individualmente.

Ora, tornando alla tua domanda su come scrivere una firma del metodo, hai:

julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true

Nota la differenza:

  • Array{<:Complex,2}rappresenta un'unione di tutti i tipi che sono array 2D il cui eltype è un sottotipo di Complex(cioè nessun array avrà questo tipo esatto).
  • Array{Complex,2}è un tipo che può avere un array e questo tipo significa che è possibile memorizzare Complexin esso valori che possono avere parametri misti.

Ecco un esempio:

julia> x = Complex[im 1im;
                   1.0im Float16(1)im]
2×2 Array{Complex,2}:
   im         0+1im
 0.0+1.0im  0.0+1.0im

julia> typeof.(x)
2×2 Array{DataType,2}:
 Complex{Bool}     Complex{Int64}
 Complex{Float64}  Complex{Float16}

Notare inoltre che la notazione Array{<:Complex,2}è la stessa della scrittura Array{T,2} where T<:Complex(o più compatta Matrix{T} where T<:Complex).

4 PrzemyslawSzufel Nov 11 2020 at 22:40

Sebbene la discussione sul "come funziona" sia stata fatta in un'altra risposta, il modo migliore per implementare il tuo metodo è il seguente:

function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
    sum(a) + 5*one(T)  # returning something to see how this is working
end

Ora funzionerà in questo modo:

julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 + 0.0im
4 phipsgabler Nov 12 2020 at 02:17

Questo è più un commento, ma non posso esitare a pubblicarlo. Questa domanda apprende così spesso. Ti dirò perché deve sorgere quel fenomeno.

A Bag{Apple}è un Bag{Fruit}, giusto? Perché, quando ho a JuicePress{Fruit}, posso dargli una Bag{Apple}per fare un po 'di succo, perché Apples sono Fruits.

Ma ora ci imbattiamo in un problema: la mia fabbrica di succhi di frutta, in cui lavoro diversi frutti, ha un fallimento. Ordino un nuovo JuicePress{Fruit}. Ora, purtroppo mi viene consegnato un sostituto JuicePress{Lemon}, ma Lemons sono Fruit, quindi sicuramente un JuicePress{Lemon}è un JuicePress{Fruit}, giusto?

Tuttavia, il giorno successivo, do le mele alla nuova pressa e la macchina esplode. Spero che tu capisca perché: nonJuicePress{Lemon} è un file . Al contrario: a è a - posso spremere i limoni con una pressa agnostica della frutta! Avrebbero potuto inviarmi un , però, poiché s sono s.JuicePress{Fruit}JuicePress{Fruit}JuicePress{Lemon}JuicePress{Plant}FruitPlant

Ora possiamo essere più astratti. La vera ragione è: gli argomenti di input della funzione sono controvarianti , mentre gli argomenti di output della funzione sono covarianti (in un'impostazione idealizzata) 2 . Cioè, quando abbiamo

f : A -> B

quindi posso passare in supertipi diA e finire con i sottotipi diB . Quindi, quando fissiamo il primo argomento, la funzione indotta

(Tree -> Apple) <: (Tree -> Fruit)

ogniqualvolta Apple <: Fruit- questo è il caso covariante, conserva la direzione di <:. Ma quando aggiustiamo il secondo,

(Fruit -> Juice) <: (Apple -> Juice)

ogni volta Fruit >: Apple- questo inverte la direzione di <:, e quindi è chiamato variante contra .

Questo si trasferisce ad altri tipi di dati parametrici, poiché anche lì di solito si hanno parametri "simili a output" (come in Bag) e parametri "simili a input" (come con JuicePress). Possono esserci anche parametri che non si comportano come nessuno dei due (ad esempio, quando si verificano in entrambe le mode) - questi sono quindi chiamati invarianti .

Ci sono ora due modi in cui le lingue con tipi parametrici risolvono questo problema. Quello, secondo me, più elegante è marcare ogni parametro: nessuna annotazione significa invariante, +significa covariante, -significa controvariante (questo ha ragioni tecniche - si dice che quei parametri si trovano in "posizione positiva" e "posizione negativa"). Quindi abbiamo avuto il Bag[+T <: Fruit], o il JuicePress[-T <: Fruit](dovrebbe essere la sintassi Scala, ma non l'ho provato). Tuttavia, questo rende la sottotipizzazione più complicata.

L'altra strada da percorrere è ciò che fa Julia (e, a proposito, Java): tutti i tipi sono invarianti 1 , ma è possibile specificare unioni superiori e inferiori nel sito della chiamata. Quindi devi dire

makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}

Ed è così che arriviamo alle altre risposte.


1 Ad eccezione delle tuple, ma è strano.

2 Questa terminologia deriva dalla teoria delle categorie . Il Hom-functor è controvariante nel primo e covariante nel secondo argomento. C'è una realizzazione intuitiva della sottotipizzazione attraverso il funtore "smemorato" dalla categoria Typal poset di Types sotto la <:relazione. E la terminologia TC a sua volta proviene dai tensori .