J'essaie de comprendre la saisie dans Julia et rencontre le problème suivant avec Array
. J'ai écrit une fonction bloch_vector_2d(Array{Complex,2})
; la mise en œuvre détaillée n'est pas pertinente. Lors de l'appel, voici la plainte:
julia> Complex{Float64} <: Complex true julia> Array{Complex{Float64},2} <: Array{Complex,2} false
Le problème est qu'un tableau de type parent n'est pas automatiquement un parent d'un tableau de type enfant.
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
Je pense qu'il serait logique d'imposer en julia que Array{Complex{Float64},2} <: Array{Complex,2}
. Ou quelle est la bonne façon de mettre en œuvre cela dans Julia? Toute aide ou commentaire est apprécié!
3 Réponses :
Cette question est abordée en détail dans le Manuel de Julia ici .
En citant la partie pertinente:
En d'autres termes, dans le langage de la théorie des types, les paramètres de type de Julia sont invariants, plutôt que covariants (ou même contravariants). Ceci est pour des raisons pratiques: alors que toute instance de
Point{Float64}
peut conceptuellement être comme une instance dePoint{Real}
aussi, les deux types ont des représentations différentes en mémoire:
- Une instance de
Point{Float64}
peut être représentée de manière compacte et efficace comme une paire immédiate de valeurs 64 bits;- Une instance de
Point{Real}
doit pouvoir contenir n'importe quelle paire d'instances deReal
. Puisque les objets qui sont des instances de Real peuvent être de taille et de structure arbitraires, en pratique, une instance dePoint{Real}
doit être représentée comme une paire de pointeurs vers des objets Real alloués individuellement.
Revenons maintenant à votre question sur l'écriture d'une signature de méthode, alors vous avez:
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}
Notez la différence:
Array{<:Complex,2}
représente une union de tous les types qui sont des tableaux 2D dont eltype est un sous-type de Complex
(c'est-à-dire qu'aucun tableau n'aura ce type exact).Array{Complex,2}
est un type qu'un tableau peut avoir et ce type signifie que vous pouvez y stocker des valeurs Complex
qui peuvent avoir des paramètres mixtes.Voici un exemple:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2} true
Notez également que la notation Array{<:Complex,2}
est la même que pour écrire Array{T,2} where T<:Complex
(ou plus compact Matrix{T} where T<:Complex
).
Bien que la discussion sur "comment ça marche" ait été faite dans une autre réponse, la meilleure façon d'implémenter votre méthode est la suivante:
julia> bloch_vector_2d(ones(Complex{Float64},4,3)) 17.0 + 0.0im
Maintenant, cela fonctionnera comme ceci:
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
ou juste bloch_vector_2d(a::AbstractMatrix{<:Complex})
suivant la suggestion de ma réponse (je suppose que @chau veut restreindre la signature aux matrices étant donné le nom de la fonction).
C'est plus un commentaire, mais je n'hésite pas à le poster. Cette question revient si souvent. Je vais vous dire pourquoi ce phénomène doit se produire.
Un Bag{Apple}
est un Bag{Fruit}
, non? Parce que, quand j'ai un JuicePress{Fruit}
, je peux lui donner un Bag{Apple}
pour faire du jus, parce que les Apple
sont des Fruit
.
Mais maintenant, nous rencontrons un problème: mon usine de jus de fruits, dans laquelle je transforme différents fruits, est en panne. Je commande un nouveau JuicePress{Fruit}
. Maintenant, je reçois malheureusement un JuicePress{Lemon}
remplacement JuicePress{Lemon}
- mais les Lemon
sont des Fruit
, donc sûrement un JuicePress{Lemon}
est un JuicePress{Fruit}
, non?
Cependant, le lendemain, je donne des pommes à la nouvelle presse et la machine explose. J'espère que vous voyez pourquoi: JuicePress{Lemon}
n'est pas un JuicePress{Fruit}
. Au contraire: un JuicePress{Fruit}
est un JuicePress{Lemon}
- Je peux presser des citrons avec une presse JuicePress{Lemon}
fruits! Ils auraient pu m'envoyer un JuicePress{Plant}
, cependant, puisque Fruit
s are Plant
s.
Maintenant, nous pouvons obtenir plus abstrait. La vraie raison est la suivante: les arguments d'entrée de fonction sont contravariants , tandis que les arguments de sortie de fonction sont covariants (dans un cadre idéalisé) 2 . Autrement dit, quand nous avons
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
alors je peux passer dans des supertypes de A
, et finir avec des sous-types de B
Par conséquent, lorsque nous fixons le premier argument, la fonction induite
(Fruit -> Juice) <: (Apple -> Juice)
chaque fois que Apple <: Fruit
- c'est le cas covariant, il préserve la direction de <:
Mais quand nous réparons le second,
(Tree -> Apple) <: (Tree -> Fruit)
chaque fois que Fruit >: Apple
- ceci inverse la direction de <:
:, et est donc appelé variante contra .
Cela se répercute sur d'autres types de données paramétriques, car là aussi, vous avez généralement des paramètres "de type sortie" (comme dans le Bag
) et des paramètres "de type entrée" (comme avec JuicePress
). Il peut aussi y avoir des paramètres qui se comportent comme aucun des deux (par exemple, lorsqu'ils se produisent dans les deux modes) - ils sont alors appelés invariants .
Il y a maintenant deux façons dont les langages avec des types paramétriques résolvent ce problème. Le plus élégant, à mon avis, est de marquer chaque paramètre: aucune annotation signifie invariant, +
signifie covariant, -
signifie contravariant (cela a des raisons techniques - on dit que ces paramètres se produisent en position «positive» et «négative») . Nous avons donc eu le Bag[+T <: Fruit]
, ou le JuicePress[-T <: Fruit]
(devrait être la syntaxe Scala, mais je ne l'ai pas essayé). Cela rend le sous-typage plus compliqué, cependant.
L'autre voie à suivre est ce que fait Julia (et, BTW, Java): tous les types sont invariants 1 , mais vous pouvez spécifier les unions supérieures et inférieures sur le site d'appel. Alors tu dois dire
f : A -> B
Et c'est ainsi que nous arrivons aux autres réponses.
1 Sauf pour les tuples, mais c'est bizarre.
2 Cette terminologie provient de la théorie des catégories . Le Hom
-functor est contravariant dans le premier argument et covariant dans le second argument. Il existe une réalisation intuitive du sous-typage via le foncteur "oublieux" de la catégorie Typ
au poset de Typ
es sous la relation <:
:. Et la terminologie CT provient à son tour des tenseurs .
Merci! Je pense que je comprends. Le fait est que Julia est orientée fonctionnelle (visant à manipuler des flèches dans des catégories comme JuicePress et pas seulement Bag). Sa conception est-elle vraiment motivée par la théorie des catégories? Merci d'avoir édité ma question aussi!
Non, pas motivé. La terminologie vient de là, et elle correspond à une vue d'ensemble, mais le problème se pose tout seul dans chaque système avec des types paramétriques et des sous-typages.
Je pense qu'il serait peut-être moins ambigu d'écrire (Tree -> Apple) <: (Tree -> Fruit)
. Il m'a fallu une minute pour groover cette ligne.