4
votes

Tableaux de type abstrait dans julia dans les fonctions

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é!


0 commentaires

3 Réponses :


6
votes

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 de Point{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 de Real . Puisque les objets qui sont des instances de Real peuvent être de taille et de structure arbitraires, en pratique, une instance de Point{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 ).


0 commentaires

4
votes

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


1 commentaires

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



4
votes

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 .


3 commentaires

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.