Julia: Interfaces pour les types abstraits

Créé le 26 mai 2014  ·  171Commentaires  ·  Source: JuliaLang/julia

Je pense que cette demande de fonctionnalité n'a pas encore son propre problème bien qu'elle ait été discutée dans, par exemple, #5.

Je pense que ce serait formidable si nous pouvions définir explicitement des interfaces sur des types abstraits. Par interface, j'entends toutes les méthodes qui doivent être implémentées pour répondre aux exigences de type abstrait. Actuellement, l'interface n'est définie qu'implicitement et elle peut être dispersée sur plusieurs fichiers de sorte qu'il est très difficile de déterminer ce que l'on doit implémenter lorsqu'on dérive d'un type abstrait.

Les interfaces nous donneraient principalement deux choses :

  • auto-documentation des interfaces à un seul endroit
  • meilleurs messages d'erreur

Base.graphics possède une macro qui permet en fait de définir des interfaces en encodant un message d'erreur dans l'implémentation de secours. Je pense que c'est déjà très intelligent. Mais peut-être que lui donner la syntaxe suivante est encore plus soigné :

abstract MyType has print, size(::MyType,::Int), push!

Ici, ce serait bien si l'on pouvait spécifier différentes granularités. Les déclarations print et push! disent seulement qu'il doit y avoir des méthodes avec ce nom (et MyType comme premier paramètre) mais elles ne spécifient pas les types. En revanche, la déclaration size est complètement typée. Je pense que cela donne beaucoup de flexibilité et pour une déclaration d'interface non typée, on peut toujours donner des messages d'erreur assez spécifiques.

Comme je l'ai dit au #5, ces interfaces sont essentiellement ce qui est prévu en C++ comme Concept-light pour C++14 ou C++17. Et ayant fait pas mal de programmation de modèles C++, je suis certain qu'une certaine formalisation dans ce domaine serait également bonne pour Julia.

Commentaire le plus utile

Pour la discussion d'idées non spécifiques et de liens vers des travaux de fond pertinents, il serait préférable de démarrer un fil de discussion correspondant et d'y poster et d'en discuter.

Notez que presque tous les problèmes rencontrés et discutés dans les recherches sur la programmation générique dans les langages à typage statique ne concernent pas Julia. Les langages statiques sont presque exclusivement concernés par le problème de fournir une expressivité suffisante pour écrire le code qu'ils veulent tout en étant capable de vérifier de manière statique qu'il n'y a pas de violation du système de type. Nous n'avons aucun problème d'expressivité et n'exigeons pas de vérification de type statique, donc rien de tout cela n'a vraiment d'importance dans Julia.

Ce qui nous intéresse, c'est de permettre aux gens de documenter les attentes d'un protocole de manière structurée que le langage peut ensuite vérifier dynamiquement (à l'avance, si possible). Nous nous soucions également de permettre aux gens de s'occuper de choses comme des traits de caractère ; il reste ouvert si ceux-ci doivent être connectés.

Conclusion : si les travaux universitaires sur les protocoles dans les langages statiques peuvent être d'intérêt général, ils ne sont pas très utiles dans le contexte de Julia.

Tous les 171 commentaires

En général, je pense que c'est une bonne direction pour une meilleure programmation orientée interface.

Cependant, il manque quelque chose ici. Les signatures des méthodes (pas seulement leurs noms) sont également importantes pour une interface.

Ce n'est pas quelque chose de facile à mettre en œuvre et il y aura beaucoup de pièges. C'est probablement l'une des raisons pour lesquelles _Concepts_ n'a pas été accepté par C++ 11, et après trois ans, seule une version _lite_ très limitée entre dans C++ 14.

La méthode size dans mon exemple contenait la signature. D'autres @mustimplement de Base.graphics prennent également en compte la signature.

Je dois ajouter que nous avons déjà une partie de Concept-light qui est la possibilité de restreindre un type pour qu'il soit un sous-type d'un certain type abstrait. Les interfaces sont l'autre partie.

Cette macro est plutôt cool. J'ai défini manuellement des solutions de secours déclenchant des erreurs, et cela a plutôt bien fonctionné pour définir des interfaces. par exemple MathProgBase de JuliaOpt fait cela, et cela fonctionne bien. Je jouais avec un nouveau solveur (https://github.com/IainNZ/RationalSimplex.jl) et je devais continuer à implémenter des fonctions d'interface jusqu'à ce qu'il cesse de générer des erreurs pour le faire fonctionner.

Votre proposition ferait la même chose, n'est-ce pas ? Mais auriez-vous _à_ implémenter l'intégralité de l'interface ?

Comment cela gère-t-il les paramètres covariants/contravariants ?

Par example,

abstract A has foo(::A, ::Array)

type B <: A 
    ...
end

type C <: A
    ...
end

# is it ok to let the arguments to have more general types?
foo(x::Union(B, C), y::AbstractArray) = ....

@IAinNZ Oui, la proposition vise en fait à rendre @mustimplement un peu plus polyvalent de telle sorte que, par exemple, la signature puisse être fournie mais ne doit pas être fournie. Et mon sentiment est que c'est un tel "noyau" qu'il vaut la peine d'avoir sa propre syntaxe. Ce serait formidable d'imposer que toutes les méthodes soient réellement implémentées, mais la vérification de l'exécution actuelle, telle qu'elle est effectuée dans @mustimplement est déjà une bonne chose et pourrait être plus facile à implémenter.

@lindahua C'est un exemple intéressant. Faut y penser.

@lindahua One voudrait probablement que votre exemple fonctionne. @mustimplement ne fonctionnerait pas car il définit des signatures de méthode plus spécifiques.

Cela pourrait donc devoir être implémenté un peu plus profondément dans le compilateur. Sur la définition de type abstrait, il faut garder une trace des noms/signatures d'interface. Et à ce stade où actuellement une erreur "... non définie" est renvoyée, il faut générer le message d'erreur approprié.

Il est très facile de changer la façon dont MethodError print , lorsque nous avons une syntaxe et une API pour exprimer et accéder aux informations.

Une autre chose que cela pourrait nous apporter est une fonction dans base.Test pour vérifier qu'un type (tous les types ?) implémente pleinement les interfaces des types parents. Ce serait un test unitaire vraiment intéressant.

Merci @ivarne. Ainsi, la mise en œuvre pourrait ressembler à ceci :

  1. On a un dictionnaire global avec des types abstraits comme clés et des fonctions (+ signatures facultatives) comme valeurs.
  2. L'analyseur doit être adapté pour remplir le dict lorsqu'une déclaration has est analysée.
  3. MethodError doit rechercher si la fonction actuelle fait partie du dictionnaire global.

La majeure partie de la logique sera alors dans MethodError .

J'ai un peu expérimenté cela et en utilisant l'essentiel suivant https://gist.github.com/tknopp/ed53dc22b61062a2b283 je peux faire :

julia> abstract A
julia> addInterface(A,length)
julia> type B <: A end
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement length in order to be subtype of A ! in error at error.jl:22

lors de la définition de length aucune erreur n'est renvoyée :

julia> import Base.length
julia> length(::B) = 10
length (generic function with 34 methods)
julia> checkInterface(B)
true

Non pas que cela ne tienne pas compte actuellement de la signature.

J'ai un peu mis à jour le code dans l'essentiel pour que les signatures de fonctions puissent être prises en compte. C'est encore très hacky mais ce qui suit fonctionne maintenant :

julia> abstract A
julia> type B <: A end

julia> addInterface(A,:size,(A,Int64))
1-element Array{(DataType,DataType),1}:
 (A,Int64)
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement size in order to be subtype of A !
in error at error.jl:22

julia> import Base.size
julia> size(::B, ::Integer) = 333
size (generic function with 47 methods)
julia> checkInterface(B)
true

julia> addInterface(A,:size,(A,Float64))
2-element Array{(DataType,DataType),1}:
 (A,Int64)
 (A,Float64)
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement size in order to be subtype of A !
 in error at error.jl:22
 in string at string.jl:30

J'aurais dû ajouter que le cache d'interface dans l'essentiel fonctionne désormais sur des symboles au lieu de fonctions afin que l'on puisse ajouter une interface et déclarer la fonction par la suite. Je devrais peut-être faire la même chose avec la signature.

Je viens de voir que #2248 a déjà du matériel sur les interfaces.

J'allais attendre la publication de réflexions sur des fonctionnalités plus spéculatives comme les interfaces jusqu'à ce que nous ayons sorti la 0.3, mais puisque vous avez commencé la discussion, voici quelque chose que j'ai écrit il y a quelque temps.


Voici une maquette de la syntaxe pour la déclaration d'interface et l'implémentation de cette interface :

interface Iterable{T,S}
    start :: Iterable --> S
    done  :: (Iterable,S) --> Bool
    next  :: (Iterable,S) --> (T,S)
end

implement UnitRange{T} <: Iterable{T,T}
    start(r::UnitRange) = oftype(r.start + 1, r.start)
    next(r::UnitRange, state::T) = (oftype(T,state), state + 1)
    done(r::UnitRange, state::T) = i == oftype(i,r.stop) + 1
end

Décomposons cela en morceaux. Tout d'abord, il y a la syntaxe du type de fonction : A --> B est le type d'une fonction qui mappe des objets de type A au type B . Les tuples dans cette notation font la chose évidente. De manière isolée, je propose que f :: A --> B déclare que f est une fonction générique, en mappant le type A sur le type B . C'est une question légèrement ouverte ce que cela signifie. Cela signifie-t-il que lorsqu'il est appliqué à un argument de type A , f donnera un résultat de type B ? Cela signifie-t-il que f ne peut être appliqué qu'aux arguments de type A ? La conversion automatique doit-elle se produire n'importe où – en sortie, en entrée ? Pour l'instant, nous pouvons supposer que tout cela ne fait que créer une nouvelle fonction générique sans y ajouter de méthodes, et les types sont uniquement destinés à la documentation.

Deuxièmement, il y a la déclaration de l'interface Iterable{T,S} . Cela fait de Iterable un peu comme un module et un peu comme un type abstrait. C'est comme un module en ce sens qu'il a des liaisons avec des fonctions génériques appelées Iterable.start , Iterable.done et Iterable.next . C'est comme un type dans lequel Iterable et Iterable{T} et Iterable{T,S} peuvent être utilisés partout où les types abstraits le peuvent – ​​en particulier, dans la répartition des méthodes.

Troisièmement, il y a le bloc implement définissant comment UnitRange implémente l'interface Iterable . À l'intérieur du bloc implement , les fonctions Iterable.start , Iterable.done et Iterable.next sont disponibles, comme si l'utilisateur avait fait import Iterable: start, done, next , permettant l'ajout de méthodes à ces fonctions. Ce bloc ressemble à un modèle de la manière dont les déclarations de type paramétrique le sont – à l'intérieur du bloc, UnitRange signifie un UnitRange spécifique, pas le type parapluie.

Le principal avantage du bloc implement est qu'il évite d'avoir besoin des fonctions explicitement import que vous souhaitez étendre - elles sont implicitement importées pour vous, ce qui est bien car les gens sont généralement confus au sujet de import quand même. Cela semble être une façon beaucoup plus claire d'exprimer cela. Je soupçonne que la plupart des fonctions génériques de Base que les utilisateurs voudront étendre devraient appartenir à une interface, donc cela devrait éliminer la grande majorité des utilisations de import . Étant donné que vous pouvez toujours qualifier pleinement un nom, nous pourrions peut-être nous en débarrasser complètement.

Une autre idée que j'ai eue est la séparation des versions "interne" et "externe" des fonctions d'interface. Ce que je veux dire par là, c'est que la fonction "interne" est celle pour laquelle vous fournissez des méthodes pour implémenter une interface, tandis que la fonction "externe" est celle que vous appelez pour implémenter une fonctionnalité générique en termes d'interface. Considérez lorsque vous examinez les méthodes de la fonction sort! (à l'exclusion des méthodes déconseillées) :

julia> methods(sort!)
sort!(r::UnitRange{T<:Real}) at range.jl:498
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering) at sort.jl:242
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering) at sort.jl:259
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering) at sort.jl:289
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering,t) at sort.jl:289
sort!{T<:Union(Float64,Float32)}(v::AbstractArray{T<:Union(Float64,Float32),1},a::Algorithm,o::Union(ReverseOrdering{ForwardOrdering},ForwardOrdering)) at sort.jl:441
sort!{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),T<:Union(Float64,Float32)}(v::Array{Int64,1},a::Algorithm,o::Perm{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),Array{T<:Union(Float64,Float32),1}}) at sort.jl:442
sort!(v::AbstractArray{T,1},alg::Algorithm,order::Ordering) at sort.jl:329
sort!(v::AbstractArray{T,1}) at sort.jl:330
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int32}) at linalg/cholmod.jl:809
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int64}) at linalg/cholmod.jl:809

Certaines de ces méthodes sont destinées à la consommation publique, mais d'autres ne sont qu'une partie de la mise en œuvre interne des méthodes de tri public. Vraiment, la seule méthode publique que cela devrait avoir est celle-ci :

sort!(v::AbstractArray)

Le reste est du bruit et appartient à "l'intérieur". En particulier, le

sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering)

les types de méthodes sont ce qu'un algorithme de tri implémente pour se connecter à la machinerie de tri générique. Actuellement, Sort.Algorithm est un type abstrait, et InsertionSortAlg , QuickSortAlg et MergeSortAlg sont des sous-types concrets. Avec les interfaces, Sort.Algorithm pourrait être une interface à la place et les algorithmes spécifiques l'implémenteraient. Quelque chose comme ça:

# module Sort
interface Algorithm
    sort! :: (AbstractVector, Int, Int, Algorithm, Ordering) --> AbstractVector
end
implement InsertionSortAlg <: Algorithm
    function sort!(v::AbstractVector, lo::Int, hi::Int, ::InsertionSortAlg, o::Ordering)
        <strong i="17">@inbounds</strong> for i = lo+1:hi
            j = i
            x = v[i]
            while j > lo
                if lt(o, x, v[j-1])
                    v[j] = v[j-1]
                    j -= 1
                    continue
                end
                break
            end
            v[j] = x
        end
        return v
    end
end

La séparation que nous voulons pourrait alors être accomplie en définissant :

# module Sort
sort!(v::AbstractVector, alg::Algorithm, order::Ordering) =
    Algorithm.sort!(v,1,length(v),alg,order)

C'est _très_ proche de ce que nous faisons actuellement, sauf que nous appelons Algorithm.sort! au lieu de seulement sort! - et lors de l'implémentation de divers algorithmes de tri, la définition "interne" est une méthode de Algorithm.sort! pas la fonction sort! . Cela a pour effet de séparer l'implémentation de sort! de son interface externe.

@StefanKarpinski Merci beaucoup pour votre article ! Ce n'est sûrement pas 0.3 trucs. Je suis désolé d'avoir soulevé cela à ce moment-là. Je ne sais pas si 0.3 arrivera bientôt ou dans six mois ;-)

À première vue, j'aime vraiment (!) que la section d'implémentation soit définie avec son propre bloc de code. Cela permet de vérifier directement l'interface sur la définition de type.

Pas de soucis - il n'y a pas vraiment de mal à spéculer sur les fonctionnalités futures pendant que nous essayons de stabiliser une version.

Votre approche est beaucoup plus fondamentale et essaie également de résoudre certains problèmes indépendants de l'interface. Cela apporte aussi en quelque sorte une nouvelle construction (c'est-à-dire l'interface) dans le langage qui rend le langage un peu plus complexe (ce qui n'est pas nécessairement une mauvaise chose).

Je vois "l'interface" davantage comme une annotation aux types abstraits. Si on y met le has , on peut spécifier une interface mais ce n'est pas obligatoire.

Comme je l'ai dit j'aimerais beaucoup que l'interface puisse être directement validée sur sa déclaration. L'approche la moins invasive ici pourrait être de permettre la définition de méthodes à l'intérieur d'une déclaration de type. Donc en prenant ton exemple quelque chose comme

type UnitRange{T} <: Iterable{T,T}
    start(r::UnitRange) = oftype(r.start + 1, r.start)
    next(r::UnitRange, state::T) = (oftype(T,state), state + 1)
    done(r::UnitRange, state::T) = i == oftype(i,r.stop) + 1
end

On serait toujours autorisé à définir la fonction en dehors de la déclaration de type. La seule différence serait que les déclarations de fonctions internes sont validées par rapport aux interfaces.

Mais encore une fois, peut-être que mon "approche la moins invasive" est trop myope. Je ne sais pas vraiment.

Un problème avec l'insertion de ces définitions à l'intérieur du bloc de type est que pour ce faire, nous aurons vraiment besoin d'un héritage multiple d'interfaces au moins, et il est possible qu'il puisse y avoir des collisions de noms entre différentes interfaces. Vous pouvez également ajouter le fait qu'un type prend en charge une interface à un moment donné _après_ avoir défini le type, bien que je n'en sois pas certain.

@StefanKarpinski C'est formidable de voir que vous pensez à cela.

Le package Graphs est celui qui a le plus besoin du système d'interface. Il serait intéressant de voir comment ce système peut exprimer les interfaces décrites ici : http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : Je ne vois pas vraiment le problème avec l'héritage multiple et les déclarations de fonction en bloc. Dans le bloc de type, toutes les interfaces héritées devraient être vérifiées.

Mais je comprends en quelque sorte que l'on puisse vouloir laisser l'implémentation de l'interface "ouverte". Et la déclaration de fonction dans le type pourrait trop compliquer le langage. Peut-être que l'approche que j'ai implémentée dans #7025 est suffisante. Mettez un verify_interface après les déclarations de fonction (ou dans un test unitaire) ou reportez-le au MethodError .

Ce problème est que différentes interfaces peuvent avoir une fonction générique portant le même nom, ce qui provoquerait une collision de noms et nécessiterait une importation explicite ou l'ajout de méthodes par un nom pleinement qualifié. Cela rend également moins clair quelles définitions de méthode appartiennent à quelles interfaces - c'est pourquoi la collision de noms peut se produire en premier lieu.

Au fait, je suis d'accord pour dire que l'ajout d'interfaces en tant qu'autre "chose" dans le langage semble un peu trop non orthogonal. Après tout, comme je l'ai mentionné dans la proposition, ils sont un peu comme des modules et un peu comme des types. Il semble qu'une unification des concepts soit possible, mais je ne sais pas comment.

Je préfère le modèle interface-as-library au modèle interface-as-language-feature pour plusieurs raisons : il maintient le langage plus simple (certes préférence et non une objection concrète) et cela signifie que la fonctionnalité reste facultative et peut être facilement amélioré ou entièrement remplacé sans salir avec la langue réelle.

Plus précisément, je pense que la proposition (ou au moins la forme de la proposition) de @tknopp est meilleure que celle de @StefanKarpinski - elle fournit une vérification du temps de définition sans rien exiger de nouveau dans le langage. Le principal inconvénient que je vois est le manque de capacité à gérer les variables de type ; Je pense que cela peut être géré en faisant en sorte que la définition de l'interface fournisse le type _predicates_ pour les types de fonctions requises.

L'une des principales motivations de ma proposition est la grande confusion causée par le fait de devoir _importer_ des fonctions génériques - mais pas de les exporter - afin d'y ajouter des méthodes. La plupart du temps, cela se produit lorsque quelqu'un essaie d'implémenter une interface non officielle, ce qui donne l'impression que c'est ce qui se passe.

Cela semble être un problème orthogonal à résoudre, à moins que vous ne vouliez restreindre entièrement les méthodes à l'appartenance à des interfaces.

Non, cela ne semble certainement pas être une bonne restriction.

@StefanKarpinski vous mentionnez que vous pourriez envoyer sur une interface. Toujours dans la syntaxe implement l'idée est qu'un type particulier implémente l'interface.

Cela semble un peu en contradiction avec la répartition multiple, car en général, les méthodes n'appartiennent pas à un type particulier, elles appartiennent à un tuple de types. Donc, si les méthodes n'appartiennent pas à des types, comment les interfaces (qui sont essentiellement des ensembles de méthodes) peuvent-elles appartenir à un type ?

Disons que j'utilise la bibliothèque M :

module M

abstract A
abstract B

type A2 <: A end
type A3 <: A end
type B2 <: B end

function f(a::A2, b::B2)
    # do stuff
end

function f(a::A3, b::B2)
    # do stuff
end

export f, A, B, A2, A3, B2
end # module M

maintenant je veux écrire une fonction générique qui prend un A et un B

using M

function userfunc(a::A, b::B, i::Int)
    res = f(a, b)
    res + i
end

Dans cet exemple, la fonction f forme une interface ad hoc qui prend un A et un B , et je veux pouvoir supposer que je peux appeler le f fonction sur eux. Dans ce cas, il n'est pas clair lequel d'entre eux doit être considéré pour implémenter l'interface.

Les autres modules qui souhaitent fournir des sous-types concrets de A et B devraient fournir des implémentations de f . Pour éviter l'explosion combinatoire des méthodes requises, je m'attendrais à ce que la bibliothèque définisse f rapport aux types abstraits :

module N

using M

type SpecialA <: A end
type SpecialB <: B end

function M.f(a::SpecialA, b::SpecialB)
    # do stuff
end

function M.f(a::A, b::SpecialB)
    # do stuff
end

function M.f(a::SpecialA, b::B)
    # do stuff
end

export SpecialA, SpecialB

end # module N

Certes, cet exemple semble assez artificiel, mais j'espère qu'il illustre que (dans mon esprit du moins) il semble qu'il y ait une inadéquation fondamentale entre l'envoi multiple et le concept d'un type particulier implémentant une interface.

Je vois cependant votre point de vue sur la confusion import . Il m'a fallu quelques essais sur cet exemple pour me rappeler que lorsque j'ai mis using M , puis j'ai essayé d'ajouter des méthodes à f cela n'a pas fait ce à quoi je m'attendais, et j'ai dû ajouter les méthodes à M.f (ou j'aurais pu utiliser import ). Je ne pense pas que les interfaces soient la solution à ce problème cependant. Existe-t-il un problème distinct pour réfléchir à des moyens de rendre l'ajout de méthodes plus intuitif ?

@abe-egnor Je pense aussi qu'une approche plus ouverte semble plus réalisable. Mon prototype #7025 manque essentiellement de deux choses :
a) une meilleure syntaxe pour définir les interfaces
b) définitions de types paramétriques

Comme je ne suis pas tellement un gourou de type paramétrique, je suis en quelque sorte sûr que b) peut être résolu par quelqu'un avec une expérience plus profonde.
Concernant a) on pourrait aller avec une macro. Personnellement, je pense que nous pourrions passer un peu de support linguistique pour définir directement l'interface dans le cadre de la définition de type abstrait. L'approche has peut-être trop myope. Un bloc de code pourrait rendre cela plus agréable. En fait, cela est fortement lié à #4935 où une interface "interne" est définie alors qu'il s'agit de l'interface publique. Ceux-ci n'ont pas besoin d'être regroupés car je pense que ce problème est beaucoup plus important que le #4935. Mais toujours en termes de syntaxe, on peut vouloir prendre en compte les deux cas d'utilisation.

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 a mon premier coup de couteau au type de mise en œuvre auquel je pensais. En bref, une interface est une fonction de types à un dict qui définit le nom et les types de paramètres des fonctions requises pour cette interface. La macro @implement appelle simplement la fonction pour les types donnés, puis assemble les types dans les définitions de fonction données, en vérifiant que toutes les fonctions ont été définies.

Bons points:

  • Syntaxe simple pour la définition et la mise en œuvre des interfaces.
  • Orthogonal à, mais s'intègre bien avec, d'autres fonctionnalités linguistiques.
  • Le calcul du type d'interface peut être arbitrairement sophistiqué (ce ne sont que des fonctions sur les paramètres de type d'interface)

Mauvais points:

  • Ne fonctionne pas bien avec les types paramétrés si vous souhaitez utiliser le paramètre comme type d'interface. C'est un inconvénient assez important, mais je ne vois pas de moyen immédiat d'y remédier.

Je pense avoir une solution au problème de paramétrage - en bref, la définition de l'interface doit être une macro sur les expressions de type, pas une fonction sur les valeurs de type. La macro @implement peut alors étendre les paramètres de type aux définitions de fonction, permettant quelque chose comme :

<strong i="7">@interface</strong> stack(Container, Data) begin
  stack_push!(Container, Data)
end

<strong i="8">@implement</strong> stack{T}(Vector{T}, T) begin
  stack_push!(vec, x) = push!(vec, x)
end

Dans ce cas, les paramètres de type sont étendus aux méthodes définies dans l'interface, il s'étend donc à stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , ce qui, à mon avis, est exactement la bonne chose.

Je retravaillerai mon implémentation initiale pour le faire dès que j'aurai le temps ; probablement de l'ordre d'une semaine.

J'ai parcouru un peu Internet pour voir ce que font les autres langages de programmation à propos des interfaces, de l'héritage et autres et j'ai eu quelques idées. (Au cas où quelqu'un serait intéressé ici, les notes très approximatives que j'ai prises https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

En bref, les interfaces pourraient peut-être être implémentées par :

  • autoriser l'héritage multiple pour les types abstraits, et
  • permettant des fonctions génériques en tant que champs de types abstraits.

Cela transformerait les types abstraits en interfaces et les sous-types concrets seraient alors nécessaires pour implémenter cette interface.

La longue histoire :

Ce que j'ai trouvé, c'est que quelques-uns des langages "modernes" suppriment le polymorphisme des sous-types, c'est-à-dire qu'il n'y a pas de regroupement direct de types, et à la place, ils regroupent leurs types en fonction de leur appartenance aux interfaces / traits / classes de types. Dans certains langages, les interfaces/traits/classes de types peuvent avoir un ordre entre elles et hériter les unes des autres. Ils semblent également (pour la plupart) heureux de ce choix. Les exemples sont : Allez ,
Rouille , Haskell .
Go est le moins strict des trois et laisse ses interfaces spécifiées implicitement, c'est-à-dire que si un type implémente l'ensemble spécifique de fonctions d'une interface alors il appartient à cette interface. Pour Rust, l'interface (traits) doit être implémentée explicitement dans un bloc impl . Ni Go ni Rust n'ont de multiméthodes. Haskell a des multiméthodes et elles sont en fait directement liées à l'interface (type class).

Dans un certain sens, c'est similaire à ce que fait Julia aussi, les types abstraits sont comme une interface (implicite), c'est-à-dire qu'ils concernent le comportement et non les champs. C'est ce que @StefanKarpinski a également observé dans l'un de ses messages ci-dessus et a déclaré qu'avoir en plus des interfaces "semble un peu trop non orthogonal". Ainsi, Julia a une hiérarchie de types (c'est-à-dire un polymorphisme de sous-type) alors que Go/Rust/Haskell n'en a pas.

Que diriez-vous de transformer les types abstraits de Julia en une classe d'interface / trait / type, tout en gardant tous les types dans la hiérarchie None<: ... <:Any ? Cela impliquerait :
1) autoriser l'héritage multiple pour les types (abstraits) (problème #5)
2) permettre d'associer des fonctions à des types abstraits (c'est-à-dire définir une interface)
3) Permet de spécifier cette interface, à la fois pour les types abstraits (c'est-à-dire une implémentation par défaut) et les types concrets.

Je pense que cela pourrait conduire à un graphe de type à grain plus fin que celui que nous avons actuellement et pourrait être mis en œuvre étape par étape. Par exemple, un type de tableau serait reconstitué :

abstract Container  <: Iterable, Indexable, ...
end

abstract AbstractArray <: Container, Arithmetic, ...
    ...
end

abstract  Associative{K,V} <: Iterable, Indexable, Eq
    haskey :: (Associative, _) --> Bool
end

abstract Iterable{T,S}
    start :: Iterable --> S
    done  :: (Iterable,S) --> Bool
    next  :: (Iterable,S) --> (T,S)
end

abstract Indexable{A,I}
    getindex  :: (A,I) --> eltype(A)
    setindex! :: (A,I) --> A
    get! :: (A, I, eltype(A)) --> eltype(A)
    get :: (A, I, eltype(A)) --> eltype(A)
end

abstract Eq{A,B}
    == :: (A,B) --> Boolean
end
...

Ainsi, les types fondamentalement abstraits peuvent alors avoir des fonctions génériques en tant que champs (c'est-à-dire devenir une interface) alors que les types concrets n'ont que des champs normaux. Cela peut par exemple résoudre le problème de trop de choses dérivées de AbstractArray, car les gens pourraient simplement choisir les éléments utiles pour leur conteneur plutôt que de dériver de AbstractArray.

Si c'est une bonne idée, il y a beaucoup à faire (en particulier comment spécifier les types et les paramètres de type), mais cela vaut peut-être la peine d'y réfléchir ?

@ssfrr a commenté ci-dessus que les interfaces et la répartition multiple sont incompatibles. Cela ne devrait pas être le cas car, par exemple, dans Haskell, les multiméthodes ne sont possibles qu'en utilisant des classes de types.

J'ai également découvert en lisant l'article de @StefanKarpinski qu'utiliser directement abstract au lieu de interface pouvait avoir un sens. Cependant dans ce cas, il est important que abstract hérite d'une propriété cruciale de interface : la possibilité pour un type de implement et de interface _après_ d'être défini. Ensuite, je peux utiliser un type typA de lib A avec un algorithme algoB de lib B en déclarant dans mon code que typA implémente l'interface requise par algoB (je suppose que cela implique que les types concrets ont une sorte d'héritage multiple ouvert).

@mauro3 , j'aime beaucoup votre suggestion. Pour moi, c'est très "julien" et naturel. Je pense également que c'est une intégration unique et puissante d'interfaces, d'héritage multiple et de "champs" de type abstrait (bien que pas vraiment, car les champs ne seraient que des méthodes/fonctions, pas des valeurs). Je pense également que cela se marie bien avec l'idée de @StefanKarpinski de distinguer les méthodes d'interface "internes" des méthodes d'interface "externes", puisque vous pourriez implémenter sa proposition pour l'exemple sort! en déclarant abstract Algorithm et Algorithm.sort! .

désolé tout le monde

------------------ 原始邮件 ------------------
 : "Jacob Quinn" [email protected] ;
: 2014年9月12日(星期五) 上午6:23
: "JuliaLang/julia" [email protected];
抄送 : « Mettre en œuvre » [email protected] ;
主题: Re: [julia] Interfaces pour les types abstraits (#6975)

@mauro3 , j'aime beaucoup votre suggestion. Pour moi, c'est très "julien" et naturel. Je pense également que c'est une intégration unique et puissante d'interfaces, d'héritage multiple et de "champs" de type abstrait (bien que pas vraiment, car les champs ne seraient que des méthodes/fonctions, pas des valeurs). Je pense également que cela se marie bien avec l'idée de @StefanKarpinski de distinguer les méthodes d'interface "internes" et "externes", puisque vous pourriez implémenter sa proposition pour le tri ! exemple en déclarant abstract Algorithm et Algorithm.sort!.

-
Répondez directement à cet e-mail ou consultez-le sur GitHub.

@implement Très désolé; Je ne sais pas comment nous vous avons contacté. Si vous ne le saviez pas déjà, vous pouvez vous retirer de ces notifications en utilisant le bouton "Se désinscrire" sur le côté droit de l'écran.

Non, je veux juste dire que je ne peux pas trop t'aider pour dire sarry

------------------ 原始邮件 ------------------
: "pao" [email protected];
: 2014年9月13日(星期六) 晚上9:50
: "JuliaLang/julia" [email protected];
抄送 : « Mettre en œuvre » [email protected] ;
主题: Re: [julia] Interfaces pour les types abstraits (#6975)

@implement Très désolé; Je ne sais pas comment nous vous avons contacté. Si vous ne le saviez pas déjà, vous pouvez vous retirer de ces notifications en utilisant le bouton "Se désinscrire" sur le côté droit de l'écran.

-
Répondez directement à cet e-mail ou consultez-le sur GitHub.

On ne s'attend pas à ce que vous le fassiez ! C'était un accident, puisqu'il s'agit d'une macro Julia portant le même nom que votre nom d'utilisateur. Merci!

Je viens de voir qu'il y a des fonctionnalités potentiellement intéressantes (peut-être pertinentes pour ce problème) travaillées dans Rust : http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , en particulier : https ://github.com/rust-lang/rfcs/pull/195

Après avoir vu THTT ("Tim Holy Trait Trick"), j'ai réfléchi un peu plus aux interfaces/traits au cours des dernières semaines. J'ai proposé quelques idées et une implémentation : Traits.jl . Premièrement, (je pense) les traits doivent être considérés comme un contrat impliquant un ou plusieurs types . Cela signifie que le simple fait d'attacher les fonctions d'une interface à un type abstrait, comme moi et d'autres l'ont suggéré ci-dessus, ne fonctionne pas (du moins pas dans le cas général d'un trait impliquant plusieurs types). Et deuxièmement, les méthodes devraient pouvoir utiliser des traits pour la répartition , comme @StefanKarpinski l'a suggéré ci-dessus.

Nuff a dit, voici un exemple utilisant mon package Traits.jl :

<strong i="12">@traitdef</strong> Eq{X,Y} begin
    # note that anything is part of Eq as ==(::Any,::Any) is defined
    ==(X,Y) -> Bool
end

<strong i="13">@traitdef</strong> Cmp{X,Y} <: Eq{X,Y} begin
    isless(X,Y) -> Bool
end

Cela déclare que Eq et Cmp sont des contrats entre les types X et Y . Cmp a Eq comme supertrait, c'est-à-dire que le Eq et le Cmp doivent être remplis. Dans le corps @traitdef , les signatures de fonction spécifient quelles méthodes doivent être définies. Les types de retour ne font rien pour le moment. Les types n'ont pas besoin d'implémenter explicitement un trait, il suffit d'implémenter les fonctions. Je peux vérifier si, disons, Cmp{Int,Float64} est bien un trait :

julia> istrait(Cmp{Int,Float64})
true

julia> istrait(Cmp{Int,String})
false

La mise en œuvre explicite des traits n'est pas encore dans le package mais devrait être assez simple à ajouter.

Une fonction utilisant _trait-dispatch_ peut être définie ainsi

<strong i="31">@traitfn</strong> ft1{X,Y; Cmp{X,Y}}(x::X,y::Y) = x>y ? 5 : 6

Cela déclare une fonction ft1 qui prend deux arguments avec la contrainte dont leurs types ont besoin pour remplir Cmp{X,Y} . Je peux ajouter une autre méthode de répartition sur un autre trait :

<strong i="37">@traitdef</strong> MyT{X,Y} begin
    foobar(X,Y) -> Bool
end
# and implement it for a type:
type A
    a
end
foobar(a::A, b::A) = a.a==b.a

<strong i="38">@traitfn</strong> ft1{X,Y; MyT{X,Y}}(x::X,y::Y) = foobar(x,y) ? -99 : -999

Ces traits-fonctions peuvent maintenant être appelés comme des fonctions normales :

julia> ft1(4,5)
6

julia> ft1(A(5), A(6))
-999

L'ajout ultérieur d'un autre type à un trait est facile (ce qui ne serait pas le cas avec Unions for ft1) :

julia> ft1("asdf", 5)
ERROR: TraitException("No matching trait found for function ft1")
 in _trait_type_ft1 at

julia> foobar(a::String, b::Int) = length(a)==b  # adds {String, Int} to MyTr
foobar (generic function with 2 methods)

julia> ft1("asdf", 5)
-999

L'_Implémentation_ des fonctions de traits et leur répartition est basée sur l'astuce de Tim et sur des fonctions mises en scène, voir ci-dessous. La définition des traits est relativement triviale, voir ici pour une implémentation manuelle de tout cela.

En bref, la répartition des traits se transforme

<strong i="51">@traitfn</strong> f{X,Y; Trait1{X,Y}}(x::X,y::Y) = x+y

dans quelque chose comme ça (un peu simplifié)

f(x,y) = _f(x,y, checkfn(x,y))
_f{X,Y}(x::X,y::Y,::Type{Trait1{X,Y}}) = x+y
# default
checkfn{T,S}(x::T,y::S) = error("Function f not implemented for type ($T,$S)")
# add types-tuples to Trait1 by modifying the checkfn function:
checkfn(::Int, ::Int) = Trait1{Int,Int}
f(1,2) # 3

Dans le package, la génération de checkfn est automatisée par stagedfuncitons. Mais voir le README de Traits.jl pour plus de détails.

_Performance_ Pour les fonctions de trait simples, le code machine produit est identique à leurs homologues de type canard, c'est-à-dire aussi bon que possible. Pour les fonctions plus longues, il existe des différences, jusqu'à environ 20 % de longueur. Je ne sais pas pourquoi, car je pensais que tout cela devrait être intégré.

(édité le 27 octobre pour refléter des changements mineurs dans Traits.jl )

Le package Traits.jl est-il prêt à être exploré ? Le fichier readme dit "implémenter les interfaces avec @traitimpl (pas encore fait...)" -- est-ce une lacune importante ?

Il est prêt à être exploré (y compris les bugs :-). Le @traitimpl manquant signifie simplement qu'au lieu de

<strong i="7">@traitimpl</strong> Cmp{T1, T2} begin
   isless(t1::T1, t2::T2) = t1.t < t2.f
end

vous définissez simplement la ou les fonctions manuellement

Base.isless(t1::T1, t2::T2) = t1.t < t2.f

pour deux de vos types T1 et T2 .

J'ai ajouté la macro @traitimpl , donc l'exemple ci-dessus fonctionne maintenant. J'ai également mis à jour le README avec des détails sur l'utilisation. Et j'ai ajouté un exemple d'implémentation d'une partie de l'interface @lindahua Graphs.jl :
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

C'est vraiment cool. J'aime particulièrement qu'il reconnaisse que les interfaces en général sont une propriété de tuples de types, et non de types individuels.

Je trouve ça aussi très sympa. Il y a beaucoup à aimer dans cette approche. Bon travail.

:+1:

Merci pour le bon retour ! J'ai mis à jour / refactorisé un peu le code et il devrait raisonnablement être sans bug et bon pour jouer.
À ce stade, ce serait probablement bien si les gens pouvaient essayer cela pour voir si cela correspond à leurs cas d'utilisation.

C'est l'un de ces packages qui permet de regarder son propre code sous un nouveau jour. Très cool.

Désolé, je n'ai pas encore eu le temps de regarder cela sérieusement, mais je sais qu'une fois que je le ferai, je voudrai remanier certaines choses...

Je vais refactoriser mes packages aussi :)

Je me demandais, il me semble que si des traits sont disponibles (et permettent une répartition multiple, comme la suggestion ci-dessus), alors il n'y a pas besoin de mécanisme de hiérarchie de types abstraits, ni de types abstraits du tout. Pourrait-il être?

Une fois les traits implémentés, chaque fonction dans la base et plus tard dans l'ensemble de l'écosystème finirait par exposer une API publique basée uniquement sur les traits, et les types abstraits disparaîtraient. Bien sûr, le processus pourrait être catalysé par la dépréciation des types abstraits

En y réfléchissant un peu plus, remplacer les types abstraits par des traits nécessiterait de paramétrer des types comme celui-ci :

Array{X; Cmp{X}} # an array of comparables
myvar::Type{X; Cmp{X}} # just a variable which is comparable

Je suis d'accord avec le point mauro3 ci-dessus, qui a des traits (par sa définition, qui je pense est très bonne) équivaut à des types abstraits qui

  • autoriser l'héritage multiple, et
  • autoriser les fonctions génériques comme champs

J'ajouterais également que pour permettre l'attribution de traits aux types après leur définition, il faudrait également autoriser "l'héritage paresseux", c'est-à-dire dire au compilateur qu'un type hérite d'un type abstrait après avoir été défini.

Donc, dans l'ensemble, il me semble que développer un concept de trait/d'interface en dehors des types abstraits induirait une duplication, introduisant différentes manières d'atteindre la même chose. Je pense maintenant que la meilleure façon d'introduire ces concepts est d'ajouter lentement des fonctionnalités aux types abstraits

EDIT : bien sûr, à un moment donné, hériter de types concrets à partir de types abstraits devrait être déprécié et finalement interdit. Les traits de type seraient déterminés implicitement ou explicitement mais jamais par héritage

Les types abstraits ne sont-ils pas juste un exemple "ennuyeux" de traits ?

Si oui, serait-il possible de conserver la syntaxe actuelle et de simplement changer sa signification en trait (donner la liberté orthogonale etc. si l'utilisateur le veut) ?

_Je me demande si cela pourrait également répondre à l'exemple de Point{Float64} <: Pointy{Real} (je ne sais pas s'il y a un numéro de problème) ?_

Oui, je pense que tu as raison. La fonctionnalité des traits peut être obtenue en améliorant les types abstraits julia actuels. Ils ont besoin
1) héritage multiple
2) signatures de fonction
3) "l'héritage paresseux", pour donner explicitement un nouveau trait à un type déjà défini

Cela semble être beaucoup de travail, mais peut-être que cela peut être développé lentement sans trop de casse pour la communauté. Alors au moins on a ça ;)

Je pense que quoi que nous choisissions, ce sera un grand changement, un changement sur lequel nous ne sommes pas prêts à commencer à travailler dans la 0.4. Si je devais deviner, je parierais que nous sommes plus susceptibles d'aller dans le sens des traits que dans le sens de l'ajout d'un héritage multiple traditionnel. Mais ma boule de cristal est sur le fritz, il est donc difficile de savoir ce qui va se passer sans essayer des trucs.

FWIW, j'ai trouvé la discussion de Simon Peyton-Jones sur les classes de types dans l'exposé ci-dessous très instructif sur la façon d'utiliser quelque chose comme des traits au lieu du sous-typage : http://research.microsoft.com/en-us/um/people/simonpj/ papers/haskell-retrospective/ECOOP-July09.pdf

Oui, une boîte entière de vers !

@johnmyleswhite , merci pour le lien, très intéressant. Voici un lien vers la vidéo de celui-ci, qui vaut la peine d'être regardé pour combler les lacunes. Cette présentation semble toucher à beaucoup de questions que nous avons ici. Et curieusement, l'implémentation des classes de types est assez similaire à ce qui se trouve dans Traits.jl (l'astuce de Tim, les traits étant des types de données). Le https://www.haskell.org/haskellwiki/Multi-parameter_type_class de Haskell ressemble beaucoup à Traits.jl. L'une de ses questions dans l'exposé est la suivante : « une fois que nous avons adopté sans réserve les génériques, avons-nous encore vraiment besoin d'un sous-typage ? » (les génériques sont des fonctions paramétriques-polymorphes, je pense, voir ) C'est un peu ce à quoi @skariel et @hayd ont

En référence à @skariel et @hayd , je pense que les traits à paramètre unique (comme dans Traits.jl) sont en effet très proches des types abstraits, sauf qu'ils peuvent avoir une autre hiérarchie, c'est-à-dire un héritage multiple.

Mais les traits multi-paramètres semblent être un peu différents, du moins ils l'étaient dans mon esprit. Comme je les ai vus, les paramètres de type des types abstraits semblent concerner principalement les autres types contenus dans un type, par exemple, Associative{Int,String} dit que le dict contient des clés Int et String valeurs. Alors que Tr{Associative,Int,String}... dit qu'il y a un "contrat" ​​entre Associative , Int s et Strings . Mais alors, peut-être que Associative{Int,String} devrait être lu de cette façon aussi, c'est-à-dire qu'il existe des méthodes comme getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@mauro3 L'important serait de passer des objets de type Associative en argument à une fonction, afin qu'elle puisse ensuite créer elle-même Associative{Int,String} :

function f(A::Associative)
  a = A{Int,String}()  # create new associative
  a[1] = "one"
  return a
end

Vous l'appelleriez par exemple f(Dict) .

@eschnett , désolé, je ne comprends pas ce que vous voulez dire.

@mauro3 Je pense que je pensais d'une manière trop compliquée; ignore moi.

J'ai mis à jour Traits.jl avec :

  • résolution des ambiguïtés des traits
  • types associés
  • utiliser @doc pour obtenir de l'aide
  • meilleur test des méthodes de spécification des caractères

Voir https://github.com/mauro3/Traits.jl/blob/master/NEWS.md pour plus de détails. Commentaires bienvenus!

@Rory-Finnegan a mis en place un package d'interface https://github.com/Rory-Finnegan/Interfaces.jl

J'en ai récemment discuté avec @mdcfrancis et nous pensons que quelque chose de similaire aux protocoles de Clojure serait simple et pratique. Les fonctionnalités de base sont (1) les protocoles sont un nouveau type de type, (2) vous les définissez en répertoriant certaines signatures de méthode, (3) d'autres types les implémentent implicitement simplement en ayant des définitions de méthode correspondantes. Vous écririez par exemple

protocol Iterable
    start(::_)
    done(::_, state)
    next(::_, state)
end

et nous avons isa(Iterable, Protocol) et Protocol <: Type . Naturellement, vous pouvez expédier sur ceux-ci. Vous pouvez vérifier si un type implémente un protocole en utilisant T <: Iterable .

Voici les règles de sous-typage :

Soit P, Q des types de protocole
soit T un type non protocolaire

| entrée | résultat |
| --- | --- |
| P < : Tout | vrai |
| Bas <: P | vrai |
| (union,unionall,var) <: P | utiliser la règle normale ; traiter P comme un type de base |
| P <: (union,unionall,var) | utiliser la règle normale |
| P <: P | vrai |
| P < : Q | vérifier les méthodes(Q) <: méthodes(P) |
| P < : T | faux |
| T <: P | Les méthodes de P existent avec T substitué à _ |

Le dernier est le plus gros : pour tester T <:P, vous substituez T à _ dans la définition de P et vérifiez method_exists pour chaque signature. Bien sûr, en soi, cela signifie que les définitions de secours qui renvoient des erreurs « vous devez implémenter cela » deviennent une très mauvaise chose. Espérons que ce soit plus un problème cosmétique.

Un autre problème est que cette définition est circulaire si par exemple start(::Iterable) est défini. Une telle définition n'a pas vraiment de sens. Nous pourrions en quelque sorte empêcher cela ou détecter ce cycle lors de la vérification des sous-types. Je ne suis pas sûr à 100% que la simple détection de cycle le résout, mais cela semble plausible.

Pour l'intersection de types, nous avons :

| entrée | résultat |
| --- | --- |
| P (union,unionall,tvar) | utiliser la règle normale |
| P Q | P |
| P T | T |

Il existe plusieurs options pour P Q :

  1. Sur-approximé en retournant P ou Q (par exemple, celui qui est lexicographiquement en premier). C'est valable en ce qui concerne l'inférence de type, mais cela peut être gênant ailleurs.
  2. Renvoie un nouveau protocole ad-hoc qui contient l'union des signatures dans P et Q.
  3. Types d'intersections. Peut-être limité aux protocoles uniquement.

P T est délicat. T est une bonne approximation conservatrice, car les types non protocolaires sont "plus petits" que les types protocolaires dans le sens où ils vous restreignent à une région de la hiérarchie des types, tandis que les types protocolaires ne le font pas (puisque n'importe quel type peut implémenter n'importe quel protocole ). Faire mieux que cela semble nécessiter des types d'intersection généraux, que je préférerais éviter dans l'implémentation initiale car cela nécessite une refonte de l'algorithme de sous-typage et ouvre worm-can après worm-can.

Spécificité : P n'est plus spécifique que Q lorsque P<:Q. mais comme P Q est toujours non vide, les définitions avec des protocoles différents dans le même emplacement sont souvent ambiguës, ce qui semble être ce que vous voudriez (par exemple, vous diriez "si x est Itérable, faites ceci, mais si x est imprimable, faites-le cette").
Cependant, il n'y a pas de moyen pratique d'exprimer la définition de désambiguïsation requise, donc cela devrait peut-être être une erreur.

Après #13412, un protocole peut être "encodé" en tant que UnionAll _ sur une union de types de tuples (où le premier élément de chaque tuple interne est le type de la fonction en question). C'est un avantage de cette conception qui ne m'était pas venu à l'esprit auparavant. Par exemple, le sous-typage structurel des protocoles semble tomber automatiquement.

Bien entendu, ces protocoles sont du style "à paramètre unique". J'aime la simplicité de cela, et je ne sais pas comment gérer des groupes de types aussi élégamment que T <: Iterable .

Il y a eu quelques commentaires autour de cette idée dans le passé, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516.

Soutenirions-nous, par exemple

protocol Iterable{T}
    start(::_)::T
    done(::_, state::T)
    next(::_, state::T)
end

Wow, j'aime beaucoup ça (surtout avec l'extension @Keno ) !

+1 C'est exactement ce que je veux !

@Keno C'est certainement un bon chemin de mise à niveau pour cette fonctionnalité, mais il y a des raisons de le reporter. Tout ce qui concerne les types de retour est bien sûr très problématique. Le paramètre lui-même est conceptuellement correct et serait génial, mais il est un peu difficile à mettre en œuvre. Cela nécessite de maintenir un environnement de type autour du processus qui vérifie l'existence de toutes les méthodes.

On dirait que vous pouvez insérer des traits (comme l'indexation linéaire O (1) pour les types de type tableau) dans ce schéma. Vous définiriez une méthode factice telle que hassomeproperty(::T) = true (mais _pas_ hassomeproperty(::Any) = false ) puis vous auriez

protocol MyProperty
hassomeproperty(::_)
end

_ pourrait-il apparaître plusieurs fois dans la même méthode dans la définition du protocole, comme

protocol Comparable
  >(::_, ::_)
  =(::_, ::_0
end

Pourrait _ apparaître plusieurs fois dans la même méthode dans la définition du protocole

Oui. Il vous suffit de déposer le type de candidat pour chaque instance de _ .

@JeffBezanson a vraiment hâte. La « distance » du protocole est particulièrement intéressante pour moi. En ce sens, je peux implémenter un protocole spécifique/personnalisé pour un type sans que l'auteur du type ait connaissance de l'existence du protocole.

Qu'en est-il du fait que les méthodes peuvent être définies dynamiquement (par exemple avec @eval ) à tout moment ? Ensuite, il n'est généralement pas possible de savoir si un type est un sous-type d'un protocole donné, ce qui semblerait contrecarrer les optimisations qui évitent la répartition dynamique dans de nombreux cas.

Oui, cela aggrave #265 :) C'est le même problème où la répartition et le code généré doivent changer lorsque des méthodes sont ajoutées, juste avec plus de bords de dépendance.

C'est bon de voir ça avancer ! Bien sûr, je serais celui qui soutiendrait que les traits multi-paramètres sont la voie à suivre. Mais 95% des traits seraient probablement un seul paramètre de toute façon. C'est juste qu'ils s'adapteraient si bien avec une expédition multiple ! Cela pourrait probablement être réexaminé plus tard si besoin est. Assez dit.

Quelques commentaires :

La suggestion de @Keno (et vraiment state dans l'original de Jeff) est connue sous le nom de types associés. Notez qu'ils sont également utiles sans types de retour. Rust a une entrée manuelle décente. Je pense que c'est une bonne idée, mais pas aussi nécessaire que dans Rust. Cependant, je ne pense pas que cela devrait être un paramètre du trait : lors de la définition d'une fonction répartie sur Iterable je ne saurais pas ce qu'est T .

D'après mon expérience, method_exists est inutilisable dans sa forme actuelle pour cela (#8959). Mais vraisemblablement, cela sera corrigé dans #8974 (ou avec cela). J'ai trouvé que la correspondance entre les signatures de méthode et les signatures de trait était la partie la plus difficile lors de l'exécution de Traits.jl, en particulier pour tenir compte des fonctions paramétrées et vararg ( voir ).

Vraisemblablement, l'héritage serait également possible?

J'aimerais vraiment voir un mécanisme qui permette de définir des implémentations par défaut. Le classique est que pour la comparaison, vous n'avez besoin de définir que deux des = , < , > , <= , >= . C'est peut-être là que le cycle mentionné par Jeff est réellement utile. En poursuivant l'exemple ci-dessus, définir start(::Indexable) = 1 et done(i::Indexable,state)=length(i)==state ferait les valeurs par défaut. Ainsi, de nombreux types n'auraient besoin de définir que next .

Bons points. Je pense que les types associés sont quelque peu différents du paramètre dans Iterable{T} . Dans mon codage, le paramètre quantifierait simplement existentiellement sur tout ce qui se trouve à l'intérieur --- "existe-t-il un T tel que le type Foo implémente ce protocole ?".

Oui, il semble que nous pourrions facilement autoriser protocol Foo <: Bar, Baz , et simplement copier les signatures de Bar et Baz dans Foo.

Les traits multi-paramètres sont définitivement puissants. Je pense qu'il est très intéressant de réfléchir à la façon de les intégrer avec le sous-typage. Vous pourriez avoir quelque chose comme TypePair{A,B} <: Trait , mais cela ne semble pas tout à fait correct.

Je pense que votre proposition (en termes de fonctionnalités) ressemble en fait plus à Swift qu'à Clojure.

Il semble étrange (et je pense qu'une source de confusion future) de mélanger le sous-typage nominal (types) et structurel (protocole) (mais je suppose que c'est inévitable).

Je suis aussi un peu sceptique quant au pouvoir expressif des protocoles pour les opérations Maths/Matrix. Je pense que réfléchir à des exemples plus compliqués (opérations matricielles) serait plus instructif que Iteration qui a une interface clairement spécifiée. Voir par exemple la bibliothèque core.matrix .

Je suis d'accord; à ce stade, nous devrions collecter des exemples de protocoles et voir s'ils font ce que nous voulons.

De la façon dont vous imaginez cela, les protocoles seraient-ils des espaces de noms auxquels leurs méthodes appartiennent ? C'est-à-dire quand tu écris

protocol Iterable
    start(::_)
    done(::_, state)
    next(::_, state)
end

il semblerait naturel que cela définisse les fonctions génériques start , done et next et que leurs noms pleinement qualifiés soient Iterable.start , Iterable.done et Iterable.next . Un type implémenterait Iterable mais implémenterait toutes les fonctions génériques du protocole Iterable . J'ai proposé quelque chose de très similaire à celui-ci il y a quelque temps (je ne le trouve pas maintenant), mais l'autre côté étant que lorsque vous souhaitez implémenter un protocole, vous faites ceci :

implement T <: Iterable
    # in here `start`, `done` and `next` are automatically imported
    start(x::T) = something
    done(x::T, state) = whatever
    next(x::T, state) = etcetera, nextstate
end

Cela contrecarrerait la " distance" mentionnée par implement éliminerait presque tous les besoins d'utiliser import au lieu de using , ce qui serait un énorme avantage.

J'ai proposé quelque chose de très similaire à celui-ci il y a quelque temps (je ne le trouve pas maintenant)

Peut-être https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 et versions antérieures https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Modifier : également https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

Ouais, c'est ça.

@StefanKarpinski commentaires rapides,

  • toutes les classes qui implémentent actuellement iterable devront être modifiées pour implémenter explicitement le protocole si nous faisons comme vous le proposez, la proposition actuelle en ajoutant simplement la définition à la base "lèvera" toutes les classes existantes au protocole.
  • si je définis MyModule.MySuperIterable qui ajoute une fonction supplémentaire à la définition itérable, je devrais écrire tout un tas de code de passe-partout pour chaque classe plutôt que d'ajouter la seule méthode supplémentaire.
  • Je ne pense pas que ce que vous proposez contrecarre l'éloignement, cela signifie simplement que je devrais écrire beaucoup de code supplémentaire pour atteindre le même objectif.

Si une sorte d'héritage sur les protocoles était autorisé, MySuperIterabe,
pourrait étendre Base.Iterable, afin de réutiliser les méthodes existantes.

Le problème serait si vous vouliez juste une sélection des méthodes dans un
protocole, mais cela semblerait indiquer que le protocole original devrait
être un protocole composite dès le départ.

@mdcfrancis - le premier point est bon, bien que ce que je propose ne briserait aucun code existant, cela signifierait simplement que le code des gens devrait "s'inscrire" aux protocoles pour leurs types avant de pouvoir compter sur l'envoi travail.

Pouvez-vous développer le point MyModule.MySuperIterable ? Je ne vois pas d'où vient la verbosité supplémentaire. Vous pourriez avoir quelque chose comme ceci, par exemple :

protocol Enumerable <: Iterable
    # inherits start, next and done; adds the following:
    length(::_) # => Integer
end

C'est essentiellement ce que @ivarne a dit.

Dans ma conception spécifique ci-dessus, les protocoles ne sont pas des espaces de noms, juste des déclarations sur d'autres types et fonctions. Cependant, c'est probablement parce que je me concentre sur le système de type de base. Je pourrais imaginer du sucre de syntaxe qui s'étend à une combinaison de modules et de protocoles, par exemple

module Iterable

function start end
function done end
function next end

jeff_protocol the_protocol
    start(::_)
    done(::_, state)
    next(::_, state)
end

end

Ensuite, dans les contextes où Iterable est traité comme un type, nous utilisons Iterable.the_protocol .

J'aime cette perspective parce que les protocoles jeff/mdcfrancis semblent très orthogonaux à tout le reste ici. La sensation légère de ne pas avoir besoin de dire « X implémente le protocole Y » à moins que vous ne le vouliez me semble « julian ».

Je ne sais pas pourquoi j'ai souscrit à ce numéro et quand je l'ai fait. Mais il se trouve que cette proposition de protocole peut résoudre la question que j'ai soulevée ici .

Je n'ai rien à ajouter sur une base technique, mais comme exemple de "protocoles" utilisés à l'état sauvage dans Julia (en quelque sorte), JuMP déterminerait la fonctionnalité d'un solveur, par exemple :

https://github.com/JuliaOpt/JuMP.jl/blob/master/src/solvers.jl#L223 -L246

        # If we already have an MPB model for the solver...
        if m.internalModelLoaded
            # ... and if the solver supports updating bounds/objective
            if applicable(MathProgBase.setvarLB!, m.internalModel, m.colLower) &&
               applicable(MathProgBase.setvarUB!, m.internalModel, m.colUpper) &&
               applicable(MathProgBase.setconstrLB!, m.internalModel, rowlb) &&
               applicable(MathProgBase.setconstrUB!, m.internalModel, rowub) &&
               applicable(MathProgBase.setobj!, m.internalModel, f) &&
               applicable(MathProgBase.setsense!, m.internalModel, m.objSense)
                MathProgBase.setvarLB!(m.internalModel, copy(m.colLower))
                MathProgBase.setvarUB!(m.internalModel, copy(m.colUpper))
                MathProgBase.setconstrLB!(m.internalModel, rowlb)
                MathProgBase.setconstrUB!(m.internalModel, rowub)
                MathProgBase.setobj!(m.internalModel, f)
                MathProgBase.setsense!(m.internalModel, m.objSense)
            else
                # The solver doesn't support changing bounds/objective
                # We need to build the model from scratch
                if !suppress_warnings
                    Base.warn_once("Solver does not appear to support hot-starts. Model will be built from scratch.")
                end
                m.internalModelLoaded = false
            end
        end

Cool, c'est utile. Est-il suffisant que m.internalModel soit la chose qui implémente le protocole, ou les deux arguments sont-ils importants ?

Oui, il suffit que m.internalModel implémente le protocole. Les autres arguments ne sont pour la plupart que des vecteurs.

Oui, suffisant pour que m.internalModel implémente le protocole

Un bon moyen de trouver des exemples de protocoles dans la nature est probablement de rechercher des appels applicable et method_exists .

Elixir semble également implémenter des protocoles, mais le nombre de protocoles dans la bibliothèque standard (en dehors de la définition) semble assez limité.

Quelle serait la relation entre les protocoles et les types abstraits ? La description originale du problème proposait quelque chose comme attacher un protocole à un type abstrait. En effet, il me semble que la plupart des protocoles (maintenant informels) sont actuellement implémentés sous forme de types abstraits. À quoi serviraient les types abstraits lorsque la prise en charge des protocoles serait ajoutée ? Une hiérarchie de types sans aucun moyen de déclarer son API ne semble pas très utile.

Très bonne question. Il y a beaucoup d'options là-bas. Tout d'abord, il est important de souligner que les types abstraits et les protocoles sont assez orthogonaux, même s'il s'agit de deux manières de regrouper des objets. Les types abstraits sont purement nominaux ; ils marquent les objets comme appartenant à l'ensemble. Les protocoles sont purement structurels ; un objet appartient à l'ensemble s'il possède certaines propriétés. Certaines options sont donc

  1. Ayez juste les deux.
  2. Être capable d'associer des protocoles à un type abstrait, par exemple de sorte que lorsqu'un type se déclare un sous-type, sa conformité avec le(s) protocole(s) soit vérifiée.
  3. Supprimez complètement les types abstraits.

Si nous avons quelque chose comme (2), je pense qu'il est important de reconnaître qu'il ne s'agit pas vraiment d'une caractéristique unique, mais d'une combinaison de typage nominal et structurel.

Une chose pour laquelle les types abstraits semblent utiles est leurs paramètres, par exemple l'écriture de convert(AbstractArray{Int}, x) . Si AbstractArray était un protocole, le type d'élément Int n'aurait pas nécessairement besoin d'être mentionné dans la définition du protocole. Il s'agit d'informations supplémentaires sur le type, _à côté_ des méthodes requises. Ainsi, AbstractArray{T} et AbstractArray{S} seraient toujours des types différents, malgré la spécification des mêmes méthodes, nous avons donc réintroduit le typage nominal. Ainsi, cette utilisation des paramètres de type semble nécessiter un type nominal quelconque.

Alors, est-ce que 2. nous donnerait un héritage abstrait multiple ?

Alors, est-ce que 2. nous donnerait un héritage abstrait multiple ?

Non. Ce serait un moyen d'intégrer ou de combiner les fonctionnalités, mais chaque fonctionnalité aurait toujours les propriétés qu'elle possède actuellement.

Je dois ajouter que permettre l'héritage abstrait multiple est encore une autre décision de conception presque orthogonale. Dans tous les cas, le problème avec l'utilisation excessive de types nominaux abstraits est (1) vous risquez de perdre l'implémentation des protocoles après coup (la personne A définit le type, la personne B définit le protocole et son implémentation pour A), (2) vous risquez de perdre le sous-typage structurel des protocoles.

Les paramètres de type du système actuel ne font-ils pas en quelque sorte partie de l'interface implicite ? Par exemple, cette définition repose sur cela : ndims{T,n}(::AbstractArray{T,n}) = n et de nombreuses fonctions définies par l'utilisateur le font aussi.

Ainsi, dans un nouveau protocole + système d'héritage abstrait, nous aurions un AbstractArray{T,N} et un ProtoAbstractArray . Maintenant, un type qui n'était pas nominalement un AbstractArray devrait être capable de spécifier ce que sont les paramètres T et N , vraisemblablement grâce au codage en dur de eltype et ndims . Ensuite, toutes les fonctions paramétrées sur les AbstractArray s devraient être réécrites pour utiliser eltype et ndims place des paramètres. Donc, il serait peut-être plus logique que le protocole porte également les paramètres, donc les types associés pourraient être très utiles après tout. (Notez que les types concrets auraient toujours besoin de paramètres.)

De plus, un regroupement de types dans un protocole en utilisant l' astuce de https://github.com/JuliaLang/julia/issues/6975#issuecomment -161056795 s'apparente à un typage nominal : le regroupement est uniquement dû à la sélection de types et les types ne partagent aucune interface (utilisable). Alors peut-être que les types abstraits et les protocoles se chevauchent un peu ?

Oui, les paramètres d'un type abstrait sont définitivement une sorte d'interface, et dans une certaine mesure redondants avec eltype et ndims . La principale différence semble être que vous pouvez les envoyer directement, sans appel de méthode supplémentaire. Je suis d'accord qu'avec les types associés, nous serions beaucoup plus proches du remplacement des types abstraits par des protocoles/traits. A quoi pourrait ressembler la syntaxe ? Idéalement, ce serait plus faible que l'appel de méthode, car je préférerais ne pas avoir de dépendance circulaire entre le sous-typage et l'appel de méthode.

La question restante est de savoir s'il est utile d'implémenter un protocole _sans_ faire partie du type abstrait associé. Un exemple pourrait être les chaînes, qui sont itérables et indexables, mais sont souvent traitées comme des quantités "scalaires" au lieu de conteneurs. Je ne sais pas à quelle fréquence cela se produit.

Je ne pense pas bien comprendre votre déclaration de "méthode d'appel". Donc, cette suggestion de syntaxe n'est peut-être pas ce que vous avez demandé :

protocol PAbstractArray{T,N}
    size(_)
    getindex(_, i::Int)
    ...
end

type MyType1
    a::Array{Int,1}
    ...
end

impl MyType for PAbstractArray{Int,1}
    size(_) = size(_.a)
    getindex(_, i::Int) = getindex(_.a,i)
    ...
end

# an implicit definition could look like:
associatedT(::Type{PAbstractArray}, :T, ::Type{MyType}) = Int
associatedT(::Type{PAbstractArray}, :N, ::Type{MyType}) = 1
size(mt::MyType) = size(mt.a)
getindex(mt::MyType, i::Int) = getindex(mt.a,i)


# parameterized type
type MyType2{TT, N, T}
    a::Array{T, N}
    ...
end

impl MyType2{TT,N,T} for PAbstractArray{T,N}
    size(_) = size(_.a)
    getindex(_, i::Int) = getindex(_.a,i)
    ...
end

Cela pourrait fonctionner, selon la façon dont le sous-typage des types de protocole est défini. Par exemple, étant donné

protocol PAbstractArray{eltype,ndims}
    size(_)
    getindex(_, i::Int)
    ...
end

protocol Indexable{eltype}
    getindex(_, i::Int)
end

avons-nous PAbstractArray{Int,1} <: Indexable{Int} ? Je pense que cela pourrait très bien fonctionner si les paramètres correspondent par nom. Nous pourrions peut-être aussi automatiser la définition qui fait que eltype(x) renvoie le paramètre eltype de type x .

Je n'aime pas particulièrement mettre les définitions de méthode dans un bloc impl , principalement parce qu'une seule définition de méthode peut appartenir à plusieurs protocoles.

Il semble donc qu'avec un tel mécanisme, nous n'aurions plus besoin de types abstraits. AbstractArray{T,N} pourrait devenir un protocole. Ensuite, nous obtenons automatiquement un héritage multiple (de protocoles). De plus, l'impossibilité d'hériter de types concrets (ce qui est une plainte que nous entendons parfois de la part des nouveaux arrivants) est évidente, car seul l'héritage de protocole serait pris en charge.

A part : ce serait vraiment sympa de pouvoir exprimer le trait Callable . Cela devrait ressembler à quelque chose comme ça :

protocol Callable
    ::TupleCons{_, Bottom}
end

TupleCons correspond séparément au premier élément d'un tuple et au reste des éléments. L'idée est que cela correspond tant que la table de méthode pour _ n'est pas vide (Bottom étant un sous-type de chaque type de tuple d'argument). En fait, nous pourrions vouloir faire la syntaxe Tuple{a,b} pour TupleCons{a, TupleCons{b, EmptyTuple}} (voir aussi #11242).

Je ne pense pas que ce soit vrai, tous les paramètres de type sont quantifiés existentiellement _avec des contraintes_ donc les types abstraits et les protocoles ne sont pas directement substituables.

@jakebolewski pouvez-vous penser à un exemple ? Évidemment, ils ne seront jamais exactement la même chose ; Je dirais que la question est plutôt de savoir si nous pouvons en masser un de telle sorte que nous puissions nous en sortir sans avoir les deux.

Peut-être que je ne comprends pas, mais comment les protocoles peuvent-ils encoder des types abstraits modérément complexes avec des contraintes, tels que :

typealias BigMatrix ∃T, T <: Union{BigInt,BigFloat} AbstractArray{T,2}

sans avoir à énumérer nominalement toutes les possibilités ?

La proposition Protocol proposée est strictement moins expressive par rapport au sous-typage abstrait qui est tout ce que j'essayais de mettre en évidence.

Je pourrais imaginer ce qui suit (naturellement, en étirant le design jusqu'à ses limites pratiques) :

BigMatrix = ∃T, T<:Union{BigInt, BigFloat} protocol { eltype = T, ndims = 2 }

allant de pair avec l'observation que nous avons besoin de quelque chose comme des types associés ou des propriétés de type nommé pour correspondre à l'expressivité des types abstraits existants. Avec cela, nous pourrions potentiellement avoir une quasi-compatibilité :

AbstractArray = ∃T ∃N protocol { eltype=T, ndims=N }

Le sous-typage structurel pour les champs de données des objets ne m'a jamais semblé très utile, mais appliqué aux propriétés de _types_ à la place, il semble avoir beaucoup de sens.

J'ai également réalisé que cela peut fournir une échappatoire aux problèmes d'ambiguïté : l'intersection de deux types est vide s'ils ont des valeurs en conflit pour un paramètre. Donc, si nous voulons un type Number sans ambiguïté, nous pourrions avoir

protocol Number
    super = Number
    +(_, _)
    ...
end

Ceci considère super comme juste une autre propriété de type.

J'aime la syntaxe du protocole proposé, mais j'ai quelques notes.

Mais alors je peux tout mal comprendre. Je n'ai commencé que récemment à vraiment considérer Julia comme quelque chose sur laquelle je veux travailler, et je n'ai pas encore une compréhension parfaite du système de caractères.

(a) Je pense que ce serait plus intéressant avec les caractéristiques de trait sur lesquelles @mauro3 a travaillé ci-dessus. Surtout qu'à quoi bon l'envoi multiple si on ne peut pas avoir plusieurs protocoles d'envoi ! J'écrirai plus tard mon point de vue sur ce qu'est un exemple du monde réel. Mais l'essentiel se résume à "Y a-t-il un comportement permettant à ces deux objets d'interagir". Je me trompe peut-être, et tout cela peut être enchâssé dans des protocoles, par exemple :

protocol Foo{bar}
    ...
end

protocol Bar{foo<:Foo}
   ...
end

Et cela expose également le problème clé de ne pas permettre au protocole Foo de référencer le protocole Bar dans la même définition.

(b)

avons-nous PAbstractArray{Int,1} <: Indexable{Int} ? Je pense que cela pourrait très bien fonctionner si les paramètres correspondent par nom.

Je ne sais pas pourquoi nous devons faire correspondre les paramètres par _name_ (je considère que ce sont les noms eltype , si j'ai mal compris, veuillez ignorer cette section). Pourquoi ne pas simplement faire correspondre les signatures de fonction potentielles. Mon principal problème avec le nommage est qu'il empêche ce qui suit :

module SomeBigLibrary
  # Assuming required definitions

  protocol Baz{el1type}
    Base.foo(_, i::el1type) # say `convert`
    baz(_)
  end
end

module SomeOtherLibrary
  # Assuming required definitions

  protocol Bar{el2type}
    Base.foo(_, i::el2type)
    bar(_)
  end
end

module My
  # Assuming required definitions

  protocol Protocol{el_type} # What do I put here to get both subtypes correctly!
    Base.foo(_, i::el_type)
    SomeBigLibrary.baz(_)
    SomeOtherLibrary.bar(_)
  end
end

D'un autre côté, cela garantit que votre protocole n'expose que la hiérarchie de types spécifique que nous voulons également. Si le nom ne correspond pas à Iterable nous n'obtenons pas les avantages de l'implémentation d'iterable (et ne dessinons pas non plus d'avantage dans la dépendance). Mais je ne sais pas ce qu'un utilisateur en retire, en plus de la possibilité de faire ce qui suit...

(c) Donc, il me manque peut-être quelque chose, mais l'objectif principal où les types nommés sont utiles n'est-il pas de décrire le comportement des différentes parties d'un sur-ensemble ? Considérez la hiérarchie Number et les types abstraits Signed et Unsigned , les deux implémenteraient le protocole Integer , mais se comporteraient parfois assez différemment. Pour les distinguer, sommes-nous maintenant obligés de définir un negate spécial sur les types Signed uniquement (particulièrement difficile sans types de retour où nous pouvons vouloir nier un type Unsigned ) ?

Je pense que c'est le problème que vous décrivez dans l'exemple super = Number . Lorsque nous déclarons bitstype Int16 <: Signed (mon autre question est même de savoir comment Number ou Signed tant que protocoles avec leurs propriétés de type sont appliqués au type concret ?), cela attache-t-il les propriétés de type de le protocole Signed ( super = Signed ) le marquant comme étant différent des types marqués par le protocole Unsigned ? Parce que c'est une solution étrange de mon point de vue, et pas seulement parce que je trouve les paramètres de type nommés étranges. Si deux protocoles correspondent exactement à l'exception du type qu'ils ont placé en super, en quoi sont-ils différents de toute façon ? Et si la différence réside dans les comportements parmi les sous-ensembles d'un type plus large (le protocole), ne réinventons-nous pas vraiment le but des types abstraits ?

(d) Le problème est que nous voulons que les types abstraits fassent la différence entre les comportements et que nous voulons que les protocoles garantissent certaines capacités (souvent indépendamment des autres comportements), à travers lesquelles les comportements sont exprimés. Mais nous essayons de compléter les capacités que les protocoles nous permettent d'assurer et la partition des types abstraits de comportements.

La solution à laquelle nous sautons souvent est du type " ont des types qui déclarent leur intention d'implémenter une classe abstraite et de vérifier la conformité ", ce qui est problématique dans l'implémentation (références circulaires, conduisant à ajouter des définitions de fonction à l'intérieur du bloc de type ou impl blocks), et supprime la belle qualité des protocoles étant basée sur l'ensemble actuel de méthodes et les types sur lesquels elles opèrent. Ces problèmes empêchent de toute façon de placer les protocoles dans la hiérarchie abstraite.

Mais plus important encore, les protocoles ne décrivent pas le comportement, ils décrivent des capacités complexes à travers plusieurs fonctions (comme l'itération), le comportement de cette itération est décrit par les types abstraits (qu'il soit trié ou même ordonné, par exemple). D'un autre côté, la combinaison protocole + type abstrait est utile une fois que nous pouvons mettre la main sur un type réel car elle nous permet de distribuer des capacités (méthodes utilitaires de capacité), des comportements (méthodes de haut niveau) ou les deux (détails d'implémentation méthodes).

(e) Si nous permettons aux protocoles d'hériter de plusieurs protocoles (ils sont fondamentalement structurels de toute façon) et d'autant de types abstraits que de types concrets (par exemple sans héritage abstrait multiple, un), nous pouvons autoriser la création de types de protocoles purs, de types abstraits purs, et protocole + types abstraits.

Je pense que cela résout le problème Signed vs Unsigned ci-dessus :

  • Définissez deux protocoles, tous deux héritant du IntegerProtocol (héritant de n'importe quelle structure de protocole, NumberAddingProtocol , IntegerSteppingProtocol , etc.) un du AbstractSignedInteger et l'autre du AbstractUnsignedInteger ).
  • Ensuite, un utilisateur de type Signed est assuré à la fois de la fonctionnalité (depuis le protocole) et du comportement (de la hiérarchie abstraite).
  • Un type concret AbstractSignedInteger sans les protocoles n'est pas utilisable _de toute façon_.
  • Mais de manière intéressante (et en tant que future fonctionnalité déjà mentionnée ci-dessus), nous pourrions éventuellement créer la capacité de résoudre les fonctionnalités manquantes, si seulement le IntegerSteppingProtocol (qui est trivial et fondamentalement juste un alias pour une seule fonction) existait pour un étant donné les AbstractUnsignedInteger concrets, nous pourrions essayer de résoudre Signed en implémentant les autres protocoles en fonction de cela. Peut-être même avec quelque chose comme convert .

Tout en conservant tous les types existants en transformant la plupart d'entre eux en types protocole + abstrait, et en laissant certains types abstraits purs.

Edit : (f) Exemple de syntaxe (y compris la partie (a) ).

Edit 2 : Correction de quelques erreurs ( :< au lieu de <: ), correction d'un mauvais choix ( Foo au lieu de ::Foo )

protocol {T<: Number}(Foo <: AbstractFoo; Bar <: AbstractBar) # Abstract inheritance
    IterableProtocol(::Foo) # Explicit protocol inheritance.

    # Implicit protocol inheritance.
    start(::Bar)
    next(::Bar, state) # These states should really share an anonymous internal type
    done(::Bar, state)

    # Custom method for protocol involving both participants, defines Foo / Bar relationship.
    set(::Foo, ::Bar, v::T)

    # Custom method only on Bar
    bar(::Bar)
end

# Protocols both Foo{T} and Bar{T}.

Je vois des problèmes avec cette syntaxe comme :

  • Types internes anonymes au protocole (par exemple les variables d'état).
  • Types de retour.
  • Difficile d'implémenter efficacement la sémantique.

_Abstract type_ définit ce qu'une entité est. _Protocol_ définit ce que l'entité fait. Au sein d'un même package, ces deux concepts sont interchangeables : une entité _est_ ce qu'elle _fait_. Et "type abstrait" est plus direct. Cependant, entre deux packages, il y a une différence : vous n'exigez pas ce que votre client "est", mais vous exigez ce que votre client "fait". Ici, "type abstrait" ne donne aucune information à ce sujet.

À mon avis, un protocole est un type abstrait envoyé unique. Cela peut aider à l'extension et à la coopération du package. Ainsi, au sein d'un même package, où les entités sont étroitement liées, utilisez le type abstrait pour faciliter le développement (en profitant d'envois multiples) ; entre les packages, où les entités sont plus indépendantes, utilisez un protocole pour réduire l'exposition à la mise en œuvre.

@mason-bially

Je ne sais pas pourquoi nous devons faire correspondre les paramètres par leur nom

Je veux dire correspondance par nom _par opposition à_ correspondance par position. Ces noms agiraient comme des enregistrements structurellement sous-typés. Si nous avons

protocol Collection{T}
    eltype = T
end

alors tout ce qui a une propriété appelée eltype est un sous-type de Collection . L'ordre et la position de ces "paramètres" n'ont pas d'importance.

Si deux protocoles correspondent exactement à l'exception du type qu'ils ont placé en super, en quoi sont-ils différents de toute façon ? Et si la différence réside dans les comportements parmi les sous-ensembles d'un type plus large (le protocole), ne réinventons-nous pas vraiment le but des types abstraits ?

C'est un bon point. Les paramètres nommés ramènent en fait de nombreuses propriétés des types abstraits. Je partais de l'idée que nous pourrions avoir besoin d'avoir à la fois des protocoles et des types abstraits, puis j'essayais d'unifier et de généraliser les fonctionnalités. Après tout, lorsque vous déclarez type Foo <: Bar actuellement, à un certain niveau, ce que vous avez vraiment fait est défini sur Foo.super === Bar . Nous devrions donc peut-être prendre en charge cela directement, ainsi que toute autre paire clé/valeur que vous pourriez vouloir associer.

"les types déclarent leur intention d'implémenter une classe abstraite et vérifient la conformité"

Oui, je suis contre faire de cette approche la fonctionnalité principale.

Si on permet aux protocoles d'hériter de plusieurs protocoles... et autant de types abstraits

Cela signifie-t-il dire par exemple "T est un sous-type du protocole P s'il a les méthodes x, y, z, et se déclare être un sous-type de AbstractArray" ? Je pense que ce genre de "protocole + type abstrait" est très similaire à ce que vous obtiendriez avec ma proposition de propriété super = T . Certes, dans ma version, je n'ai pas encore compris comment les enchaîner dans une hiérarchie comme celle que nous avons maintenant (par exemple Integer <: Real <: Number ).

Avoir un protocole héritant d'un type abstrait (nominal) semble être une contrainte très forte sur celui-ci. Y aurait-il des sous-types du type abstrait qui _pas_ implémenteraient le protocole ? Mon intuition est qu'il vaut mieux garder les protocoles et les types abstraits comme des choses orthogonales.

protocol {T :< Number}(Foo :< AbstractFoo; Bar :< AbstractBar) # Abstract inheritance
    IterableProtocol(Foo) # Explicit protocol inheritance.

    # Implicit protocol inheritance.
    start(Bar)
...

Je ne comprends pas cette syntaxe.

  • Ce protocole a-t-il un nom ?
  • Que signifient exactement les éléments à l'intérieur de { } et ( ) ?
  • Comment utilisez-vous ce protocole ? Pouvez-vous expédier dessus? Si oui, que signifie définir f(x::ThisProtocol)=... , étant donné que le protocole concerne plusieurs types ?

alors tout ce qui a une propriété appelée eltype est un sous-type de Collection. L'ordre et la position de ces "paramètres" n'ont pas d'importance.

Aha, il y avait mon malentendu, c'est plus logique. A savoir la possibilité d'attribuer :

el1type = el_type
el2type = el_type

pour résoudre mon exemple de problème.

Nous devrions donc peut-être prendre en charge cela directement, ainsi que toute autre paire clé/valeur que vous pourriez vouloir associer.

Et cette fonctionnalité clé/valeur serait sur tous les types, car nous remplacerions abstract par celle-ci. C'est une bonne solution générale. Votre solution a beaucoup plus de sens pour moi maintenant.

Certes, dans ma version, je n'ai pas encore compris comment les enchaîner dans une hiérarchie comme celle que nous avons maintenant (par exemple Integer <: Real <: Number).

Je pense que vous pourriez utiliser super (par exemple avec Integer 's super as Real ) et ensuite soit faire super spécial et agir comme un type nommé ou ajouter un moyen d'ajouter un code de résolution de type personnalisé (ala python) et de créer une règle par défaut pour le paramètre super .

Avoir un protocole héritant d'un type abstrait (nominal) semble être une contrainte très forte sur celui-ci. Y aurait-il des sous-types du type abstrait qui n'implémenteraient pas le protocole ? Mon intuition est qu'il vaut mieux garder les protocoles et les types abstraits comme des choses orthogonales.

Ah oui, la contrainte abstraite était entièrement facultative ! Tout mon argument était que les protocoles et les types abstraits sont orthogonaux. Vous utiliseriez abstract + protocol pour vous assurer d'obtenir une combinaison de certains comportements _et_ capacités associées. Si vous voulez uniquement les capacités (pour les fonctions utilitaires) ou uniquement le comportement, vous les utilisez orthogonalement.

Ce protocole a-t-il un nom ?

Deux protocoles avec deux noms ( Foo et Bar ) qui proviennent d'un seul bloc, mais je suis habitué à utiliser des macros pour développer plusieurs définitions comme celle-ci. Cette partie de ma syntaxe était une tentative de résoudre la partie (a) . Si vous ignorez cela, la première ligne pourrait simplement être protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (avec une autre définition séparée pour le protocole Bar ). De plus, tous les Number , AbstractBar et AbstractFoo seraient facultatifs, comme dans les définitions de type normales,

Que signifie exactement les éléments à l'intérieur de { } et ( ) ?

Le {} est la section de définition de type paramétrique standard. Permettre l'utilisation de Foo{Float64} pour décrire un type implémentant le protocole Foo utilisant par exemple Float64 . Le () est essentiellement une liste de liaisons variables pour le corps du protocole (donc plusieurs protocoles peuvent être décrits à la fois). Votre confusion est probablement de ma faute car j'ai mal tapé :< au lieu de <: dans mon original. Cela peut également valoir la peine de les échanger pour conserver la structure <<name>> <<parametric>> <<bindings>> , où <<name>> peut parfois être une liste de liaisons.

Comment utilisez-vous ce protocole ? Pouvez-vous expédier dessus? Si oui, que signifie définir f(x::ThisProtocol)=... , étant donné que le protocole concerne plusieurs types ?

Votre exemple d'envoi semble correct pour sa syntaxe à mon avis, considérez en effet les définitions suivantes :

protocol FooProtocol # Single protocol definition shortcut
    foo(::FooProtocol) # I changed my syntax here, protocol names inside the protocol block should referenced as types
end

abstract FooAbstract

# This next line could use better syntax, like a type alias with an Intersection or something.
protocol Foo <: FooAbstract
    FooProtocol(::Foo)
end

type Bar <: FooAbstract
  a
end

type Baz
  b
end

type Bax <: FooAbstract
  c
end

f(f::Any) = ... # def (0)

foo(x::Bar) = ... # def (1a)
foo(x::Baz) = ... # def (1b)

f(x::FooProtocol) = ... # def (2); Least specific type (structural)

f(Bar(...)) # Would call def (2)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (0)

f(x::FooAbstract) = ... # def (3); Named type, more specific than structural

f(Bar(...)) # Would call def (3)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)

f(x::Foo) = ... # def (4); Named structural type, more specific than equivalent named type

f(Bar(...)) # Would call def (4)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)

En fait, les protocoles utilisent le type Top nommé (Any) à moins qu'un type abstrait plus spécifique ne soit donné pour vérifier la structure. En effet, cela peut valoir la peine d'autoriser quelque chose comme typealias Foo Intersect{FooProtocol, Foo} (_Edit : Intersect était le mauvais nom, peut-être Join à la place Intersect avait raison la première fois_) plutôt que d'utiliser la syntaxe du protocole pour le faire.

Ah super, ça a beaucoup plus de sens pour moi maintenant ! Définir plusieurs protocoles ensemble dans le même bloc est intéressant ; Il va falloir que je réfléchisse un peu plus à ça.

J'ai nettoyé tous mes exemples il y a quelques minutes. Plus tôt dans le fil, quelqu'un a mentionné la collecte d'un corpus de protocoles pour tester des idées, je pense que c'est une excellente idée.

Les multiples protocoles dans le même bloc sont en quelque sorte une bête noire lorsque j'essaie de décrire des relations complexes entre des objets avec des annotations de type correctes des deux côtés dans définir/compiler lorsque vous chargez des langages (par exemple, comme python ; Java par exemple ne ' j'ai pas le problème). D'un autre côté, la plupart d'entre eux sont probablement facilement corrigés, du point de vue de la convivialité, avec des multi-méthodes de toute façon; mais des considérations de performances peuvent résulter de la saisie correcte des fonctionnalités dans les protocoles (optimisation des protocoles en les spécialisant en vtables, par exemple).

Vous avez mentionné plus tôt que les protocoles pourraient être (faussement) implémentés par des méthodes utilisant ::Any Je pense que ce serait un cas assez simple à ignorer s'il s'agissait de cela. Le type concret ne serait pas classé comme protocole si la méthode d'implémentation était envoyée sur ::Any . Par contre je ne suis pas convaincu que cela pose forcément un problème.

Pour commencer, si la méthode ::Any est ajoutée après coup (par exemple parce que quelqu'un a proposé un système plus générique pour la gérer), c'est toujours une implémentation valide, et si nous utilisons également des protocoles comme fonctionnalité d'optimisation, alors les versions spécialisées des méthodes distribuées ::Any fonctionnent toujours pour des gains de performances. Donc, à la fin, je serais contre les ignorer.

Mais cela peut valoir la peine d'avoir une syntaxe qui permet au définisseur de protocole de choisir entre les deux options (celle que nous choisissons par défaut, autorise l'autre). Pour le premier, une syntaxe de transfert pour la méthode distribuée ::Any , disons le mot-clé global (voir également la section suivante). Pour le second moyen d'exiger une méthode plus spécifique, je ne peux pas penser à un mot-clé utile existant.

Edit : Suppression d'un tas de trucs inutiles.

Votre Join est exactement l'intersection des types de protocole. C'est en fait une "rencontre". Et heureusement, le type Join n'est pas nécessaire, car les types de protocoles sont déjà fermés sous intersection : pour calculer l'intersection, il suffit de retourner un nouveau type de protocole avec les deux listes de méthodes concaténées.

Je ne suis pas trop inquiet à l'idée que les protocoles soient banalisés par des définitions de ::Any . Pour moi, la règle "recherchez les définitions correspondantes sauf que Any ne compte pas" va à l'encontre du rasoir d'Occam. Sans parler du fait que l'enfilage du drapeau "ignorer tout" via l'algorithme de sous-typage serait assez ennuyeux. Je ne suis même pas sûr que l'algorithme résultant soit cohérent.

J'aime beaucoup l'idée des protocoles (cela me rappelle un peu les CLUsters), je suis juste curieux, comment cela s'intégrerait-il avec le nouveau sous-typage qui a été discuté par Jeff à JuliaCon, et avec les traits ? (deux choses que j'aimerais quand même beaucoup voir chez Julia).

Cela ajouterait un nouveau type de type avec ses propres règles de sous-typage (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). À première vue, ils semblent être compatibles avec le reste du système et peuvent simplement être branchés.

Ces protocoles sont à peu près la version "à paramètre unique " des traits de

Votre Join est exactement l'intersection des types de protocole.

Je me suis en quelque sorte convaincu que j'avais tort plus tôt quand j'ai dit que c'était l'intersection. Bien que nous aurions toujours besoin d'un moyen d'intersecter les types sur une ligne (comme Union ).

Éditer:

J'aime toujours aussi généraliser les protocoles et les types abstraits dans un seul système et autoriser des règles personnalisées pour leur résolution (par exemple pour super pour décrire le système de type abstrait actuel). Je pense que si cela est fait correctement, cela permettrait aux gens d'ajouter des systèmes de types personnalisés et éventuellement des optimisations personnalisées pour ces systèmes de types. Bien que je ne sois pas sûr que le protocole soit le bon mot-clé, mais au moins nous pourrions transformer abstract en macro, ce serait cool.

des champs de blé : mieux vaut lever le commun à travers le protocole et l'abstrait que de chercher leur généralisation comme destination.

quelle?

Le processus de généralisation de l'intention, de la capacité et du potentiel des protocoles et de ceux des types abstraits est un moyen moins efficace de résoudre leur synthèse qualitativement la plus satisfaisante. Cela fonctionne mieux d'abord pour glaner leurs points communs intrinsèques d'objectif, de modèle, de processus. Et développer cette compréhension, permettant d'affiner sa perspective pour former la synthèse.

Quelle que soit la réalisation féconde pour Julia, elle se construit sur l'échafaudage qu'offre la synthèse. Une synthèse plus claire est la force constructive et la puissance inductive.

Quoi?

Je pense qu'il dit que nous devrions d'abord déterminer ce que nous voulons des protocoles et pourquoi ils sont utiles. Ensuite, une fois que nous aurons cela et des types abstraits, il sera plus facile d'en faire une synthèse générale.

De simples protocoles

(1) défendre

Un protocole peut être étendu pour devenir un protocole (plus élaboré).
Un protocole peut être réduit pour devenir un protocole (moins élaboré).
Un protocole peut être réalisé comme une interface conforme [dans le logiciel].
Un protocole peut être interrogé pour déterminer la conformité d'une interface.

(2) suggérant

Les protocoles doivent prendre en charge les numéros de version spécifiques au protocole, par défaut.

Il serait bon de soutenir une certaine manière de faire ceci:
Lorsqu'une interface est conforme à un protocole, répondez true ; quand une interface
est fidèle à un sous-ensemble du protocole et serait conforme s'il était augmenté,
répondez incomplet et répondez faux dans le cas contraire. Une fonction doit lister tous
augmentation nécessaire pour une interface incomplète par rapport à un protocole.

(3) rêver

Un protocole peut être un type particulier de module. Ses exportations serviraient
comme comparand initial pour déterminer si une interface est conforme.
Tous les types et fonctions [exportés] spécifiés par protocole peuvent être déclarés à l'aide de
@abstract , @type , @immutable et @function pour prendre en charge l'abstraction innée.

[pao : passez aux citations de code, mais notez que le cheval a déjà quitté l'écurie lorsque vous faites cela après coup...]

(vous devez citer le @mentions !)

merci -- c'est corrigé

Le mercredi 16 décembre 2015 à 03h01, Mauro [email protected] a écrit :

(vous devez citer les @mentions !)

-
Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

désolé j'aurais dû être plus clair : code-quote utilisant ` et non "

Correction du correctif de citation.

merci -- excusez mon ignorance antérieure

J'ai essayé de comprendre cette récente discussion sur l'ajout d'un type de protocole. Peut-être que j'ai mal compris quelque chose, mais pourquoi est-il nécessaire d'avoir des protocoles nommés à la place en utilisant simplement le nom du type abstrait associé que le protocole est sur le point de décrire ?

De mon point de vue, il est tout à fait naturel d'étendre le système de type abstrait actuel avec un moyen de décrire le comportement attendu du type. Tout comme initialement proposé dans ce fil, mais peut-être avec la syntaxe de Jeffs

abstract Iterable
    start(::_)
    done(::_, state)
    next(::_, state)
end

En empruntant cette voie, il ne serait pas nécessaire d'indiquer spécialement qu'un sous-type implémente l'interface. Cela se ferait implicitement par sous-typage.

L'objectif principal d'un mécanisme d'interface explicite est à mon humble avis d'obtenir de meilleurs messages d'erreur et d'effectuer un meilleur test de vérification.

Donc une déclaration de type comme :

type Foo <: Iterable
  ...
end

Définissons-nous les fonctions dans la section ... ? Si non, quand est-ce que nous nous trompons sur les fonctions manquantes (et les complexités liées à cela) ? De plus, que se passe-t-il pour les types qui implémentent plusieurs protocoles, permettons-nous l'héritage abstrait multiple ? Comment gérons-nous la résolution des super-méthodes ? Qu'est-ce que cela fait avec l'envoi multiple (il semble simplement le supprimer et y coller un système d'objets java-esque) ? Comment définissons-nous de nouvelles spécialisations de type pour les méthodes une fois que le premier type a été défini ? Comment définissons-nous les protocoles après avoir défini le type ?

Ces questions sont toutes plus faciles à résoudre en créant un nouveau type (ou en créant une nouvelle formulation de type).

Il n'y a pas nécessairement de type abstrait connexe pour chaque protocole (il ne devrait probablement pas y en avoir vraiment). Des multiples des interfaces actuelles peuvent être implémentées par le même type. Ce qui n'est pas descriptible avec le système de type abstrait actuel. D'où le problème.

  • L'héritage multiple abstrait (implémentant plusieurs protocoles) est orthogonal à cette fonctionnalité (comme indiqué par Jeff ci-dessus). Ce n'est donc pas que nous obtenons cette fonctionnalité simplement parce que des protocoles sont ajoutés au langage.
  • Votre prochain commentaire concerne la question de savoir quand vérifier l'interface. Je pense que cela ne doit pas être lié aux définitions de fonctions dans le bloc, qui ne me semblent pas Julian. Au lieu de cela, il existe trois solutions simples :

    1. comme implémenté dans #7025, utilisez une méthode verify_interface qui peut être appelée après toutes les définitions de fonction ou dans un test unitaire

    2. On ne peut pas du tout vérifier l'interface et la reporter à un message d'erreur amélioré dans "MethodError". En fait, c'est une belle solution de repli pour 1.

    3. Vérifiez toutes les interfaces soit à la fin d'une unité de temps de compilation, soit à la fin d'une phase de chargement de module. Actuellement, il est également possible d'avoir :

function a()
  b()
end

function b()
end

Ainsi, je ne pense pas que des définitions de fonction dans le bloc seraient nécessaires ici.

  • Votre dernier point est qu'il peut y avoir des protocoles qui ne sont pas liés à des types abstraits. C'est actuellement certainement vrai (par exemple le protocole informel "Iterable"). Cependant, de mon point de vue, c'est simplement à cause du manque d'héritage abstrait multiple. Si c'est le problème, s'il vous plaît, ajoutons simplement un héritage multiple abstrait au lieu d'ajouter une nouvelle fonctionnalité de langage qui vise à résoudre ce problème. Je pense également que la mise en œuvre de plusieurs interfaces est absolument cruciale et c'est absolument commun en Java/C#.

Je pense que la différence entre une chose de type "protocole" et l'héritage multiple est qu'un type peut être ajouté à un protocole après avoir été défini. C'est utile si vous voulez faire fonctionner votre paquet (de définition de protocoles) avec des types existants. On pourrait permettre de modifier les supertypes d'un type après sa création, mais à ce stade, il est probablement préférable de l'appeler "protocole" ou quelque chose du genre.

Hm permet donc de définir des interfaces alternatives/améliorées aux types existants. Je ne sais toujours pas où cela serait vraiment nécessaire. Lorsque l'on souhaite ajouter quelque chose à une interface existante (lorsque nous suivons l'approche proposée dans l'OP), il suffit de sous-typer et d'ajouter des méthodes d'interface supplémentaires au sous-type. C'est la bonne chose à propos de cette approche. Il s'échelonne assez bien.

Exemple : disons que j'avais un package qui sérialise les types. Une méthode tobits doit être implémentée pour un type, alors toutes les fonctions de ce package fonctionneront avec le type. Appelons cela le protocole Serializer (c'est- tobits dire que Array (ou tout autre type) en implémentant tobits . Avec l'héritage multiple, je ne pouvais pas faire fonctionner Array avec Serialzer car je ne peux pas ajouter de supertype à Array après sa définition. Je pense que c'est un cas d'utilisation important.

D'accord, comprends ça. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 est un problème similaire, où la solution consiste essentiellement à utiliser duck-typing. Si nous pouvions avoir quelque chose qui résout ce problème avec élégance, ce serait vraiment bien. Mais c'est quelque chose qui doit être pris en charge au niveau de la répartition. Si je comprends correctement l'idée de protocole ci-dessus, on pourrait mettre un type abstrait ou un protocole comme annotation de type dans la fonction. Ici, il serait bien de fusionner ces deux concepts avec un seul outil suffisamment puissant.

Je suis d'accord : ce sera très déroutant d'avoir à la fois des types abstraits et des protocoles. Si je me souviens bien, il a été soutenu ci-dessus que les types abstraits ont une sémantique qui ne peut pas être modélisée avec des protocoles, c'est-à-dire que les types abstraits ont des fonctionnalités que les protocoles n'ont pas. Même si c'est forcément le cas (je n'en suis pas convaincu), ce sera toujours déroutant car il y a un si grand chevauchement entre les deux concepts. Ainsi, les types abstraits devraient être supprimés au profit des protocoles.

Dans la mesure où il existe un consensus ci-dessus sur les protocoles, ils mettent l'accent sur la spécification des interfaces. Des types abstraits peuvent avoir été utilisés pour faire certains de ces protocoles absents. Cela ne veut pas dire que c'est leur utilisation la plus importante. Dites-moi quels protocoles sont et ne sont pas, alors je pourrais vous dire en quoi les types abstraits diffèrent et ce qu'ils apportent. Je n'ai jamais considéré que les types abstraits concernaient autant l'interface que la typologie. Jeter une approche naturelle de la flexibilité typologique coûte cher.

@JeffreySarnoff +1

Pensez à la hiérarchie des types de nombre. Les différents types abstraits, par exemple Signed, Unsigned, ne sont pas définis par leur interface. Il n'y a pas d'ensemble de méthodes qui définit "Unsigned". C'est simplement une déclaration très utile.

Je ne vois pas le problème, vraiment. Si les types Signed et Unsigned prennent en charge le même ensemble de méthodes, nous pouvons créer deux protocoles avec des interfaces identiques. Néanmoins, déclarer un type comme Signed plutôt que Unsigned peut être utilisé pour la répartition (c'est-à-dire que les méthodes de la même fonction agissent différemment). La clé ici est d'exiger une déclaration explicite avant de considérer qu'un type implémente un protocole, plutôt que de le détecter implicitement sur la base des méthodes qu'il implémente.

Mais avoir des protocoles implicitement associés est également important, comme dans https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Les protocoles peuvent non seulement définir des fonctions qui peuvent être appelées, mais peuvent également documenter (soit implicitement, soit de manière testable par machine) les propriétés qui doivent être conservées. Tel que:

abs(x::Unsigned) == x
signbit(x::Unsigned) == false
-abs(x::Signed) <= 0

Cette différence de comportement visible de l'extérieur entre Signed et Unsigned est ce qui rend cette distinction utile.

S'il existe une distinction entre les types qui est si "abstraite" qu'elle ne peut pas être vérifiée immédiatement, du moins théoriquement, de l'extérieur, alors il est probable que l'on ait besoin de connaître l'implémentation d'un type pour faire le bon choix. C'est là que le abstract actuel peut être utile. Cela va probablement dans le sens des types de données algébriques.

Il n'y a aucune raison pour que les protocoles ne soient pas utilisés pour regrouper simplement des types, c'est-à-dire sans nécessiter de méthodes définies (et c'est possible avec la conception "actuelle" en utilisant l'astuce : https://github.com/JuliaLang/julia/issues/ 6975#issuecomment-161056795). (Notez également que cela n'interfère pas avec les protocoles définis implicitement.)

Considérant l'exemple (Un)signed : que ferais-je si j'avais un type qui est Signed mais qui, pour une raison quelconque, doit également être un sous-type d'un autre type abstrait ? Ce ne serait pas possible.

@eschnett : les types abstraits, pour le moment, n'ont rien à voir avec l'implémentation de leurs sous-types. Bien que cela ait été discuté : #4935.

Les types de données algébriques sont un bon exemple où le raffinement successif est intrinsèquement significatif.
Toute taxonomie est bien plus naturellement donnée, et plus directement utile comme hiérarchie de type abstrait que comme mélange de spécifications de protocole.

La remarque concernant le fait d'avoir un type qui est un sous-type de plusieurs hiérarchies de types abstraits est également importante. Il y a beaucoup de puissance utilitaire qui vient avec un héritage multiple d'abstractions.

@mauro3 Oui, je sais. Je pensais à quelque chose d'équivalent aux unions discriminées, mais implémenté aussi efficacement que les tuples au lieu de via le système de types (comme les unions sont actuellement implémentées). Cela engloberait les énumérations, les types nullables et pourrait être capable de gérer quelques autres cas plus efficacement que les types abstraits actuellement.

Par exemple, comme des tuples avec des éléments anonymes :

DiscriminatedUnion{Int16, UInt32, Float64}

ou avec des éléments nommés :

discriminated_union MyType
    i::Int16
    u::UInt32
    f::Float64
end

Le point que j'essayais de faire valoir est que les types abstraits sont un bon moyen de mapper une telle construction à Julia.

Il n'y a aucune raison pour que les protocoles ne soient pas utilisés pour regrouper simplement des types, c'est-à-dire sans nécessiter de méthodes définies (et c'est possible avec la conception "courante" en utilisant l'astuce : #6975 (commentaire)). (Notez également que cela n'interfère pas avec les protocoles définis implicitement.)

J'ai l'impression qu'il faudrait faire attention à cela pour obtenir des performances, une considération que peu de gens semblent considérer assez souvent. Dans l'exemple, il semblerait que l'on veuille simplement définir la version non quelconque afin que le compilateur puisse toujours choisir la fonction au moment de la compilation (plutôt que d'avoir à appeler une fonction pour choisir la bonne au moment de l'exécution, ou que le compilateur inspecte les fonctions pour déterminer leurs résultats). Personnellement, je pense que l'utilisation de plusieurs "héritages" abstraits comme balises serait une meilleure solution.

Je pense que nous devrions réduire au minimum les astuces et la connaissance du système de types (bien qu'il puisse être enveloppé dans une macro, cela ressemblerait à un étrange hack d'une macro ; si nous utilisons des macros pour manipuler le système de types, je pense que @ La solution unifiée de JeffBezanson résoudrait mieux ce problème).

Considérant l'exemple (non) signé : que ferais-je si j'avais un type qui est signé mais qui, pour une raison quelconque, doit également être un sous-type d'un autre type abstrait ? Ce ne serait pas possible.

Héritage abstrait multiple.


Je crois que tout ce terrain a déjà été parcouru, cette conversation semble tourner en rond (bien que des cercles plus serrés à chaque fois). Je crois qu'il a été mentionné qu'il fallait acquérir un corpus ou des problèmes d'utilisation de protocoles. Cela nous permettrait de juger plus facilement des solutions.

Pendant que nous réitérons les choses :) Je veux rappeler à tout le monde que les types abstraits sont nominaux alors que les protocoles sont structurels, donc je privilégie les conceptions qui les traitent comme orthogonales, _à moins_ que nous puissions réellement proposer un "encodage" acceptable des types abstraits dans les protocoles (peut-être avec une utilisation intelligente des types associés). Des points bonus, bien sûr, si cela donne également un héritage abstrait multiple. J'ai l'impression que c'est possible mais nous n'en sommes pas encore là.

@JeffBezanson Les « types associés » sont-ils distincts des « types concrets associés à [un] protocole » ?

Oui je crois bien; Je veux dire "types associés" au sens technique d'un protocole spécifiant une paire clé-valeur où la "valeur" est un type, de la même manière que les protocoles spécifient des méthodes. par exemple "type Foo suit le protocole Container s'il a un eltype " ou "type Foo suit le protocole Matrix si son paramètre ndims est 2".

les types abstraits sont nominaux tandis que les protocoles sont structurels et
les types abstraits sont qualitatifs tandis que les protocoles sont opérationnels et
les types abstraits (avec héritage multiple) orchestrent pendant que les protocoles conduisent

Même s'il y avait un encodage de l'un dans l'autre, le "hey, salut.. comment vas-tu ? allons-y !" de Julia doit présenter les deux clairement - la notion généralement intentionnelle de protocole et les types abstraits multihéritables (une notion d'objectif généralisé). S'il y a un dépliement astucieux qui donne à Julia les deux, enveloppés séparément, c'est plus probablement le cas que l'un à travers l'un et l'autre.

@mason-bially : nous devrions donc également ajouter l'héritage multiple ? Cela laisserait toujours le problème que les supertypes ne peuvent pas être ajoutés après la création d'un type (à moins que cela ne soit également autorisé).

@JeffBezanson : rien ne nous empêcherait d'autoriser des protocoles purement nominaux.

@mauro3 Pourquoi la décision d'autoriser ou non l'insertion de supertypes post facto devrait-elle être liée à l'héritage multiple ? Et il existe différentes sortes de création de supertypes, certaines sont manifestement inoffensives présupposant la possibilité d'interposer un nouveau quoi qu'il en soit : flotteurs doubles et flotteurs système ensemble sans interférer avec les flotteurs système vivant comme des sous-types de AbstractFloat. Peut-être moins facile à autoriser, serait la possibilité de sous-secter les sous-types actuels d'Integer, et ainsi d'éviter de nombreux messages "ambigus avec .. définir f(Bool) avant.."; ou pour introduire un supertype de Signé qui est un sous-type d'Integer et ouvrir la hiérarchie numérique à une gestion transparente, par exemple des nombres ordinaux.

Désolé si j'ai initié un autre tour du cercle. Le sujet est assez complexe et nous devons vraiment nous assurer que la solution est super simple à utiliser. Nous devons donc couvrir :

  • solution générale
  • pas de dégradation des performances
  • facilité d'utilisation (et aussi facile à comprendre!)

Étant donné que ce qui a été initialement proposé dans #6975 est assez différent de l'idée de protocole discutée plus tard, il peut être bon d'avoir une sorte de JEP qui décrit à quoi pourraient ressembler les protocoles.

Un exemple de la façon dont vous pouvez définir une interface formelle et la valider à l'aide de la version 0.4 actuelle (sans macros), la répartition repose actuellement sur la répartition du style des traits, à moins que des modifications ne soient apportées à gf.c. Cela utilise des fonctions générées pour la validation, tous les calculs de type sont effectués dans l'espace de type.

Je commence à l'utiliser comme contrôle d'exécution dans un DSL que nous définissons où je dois m'assurer que le type fourni est un itérateur de dates.

Il prend actuellement en charge l'héritage multiple de super types, le nom de champ _super n'est pas utilisé par le runtime et peut être n'importe quel symbole valide. Vous pouvez fournir n autres types au _super Tuple.

https://github.com/mdcfrancis/tc.jl/blob/master/test/runtests.jl

Soulignant juste ici que j'ai fait un suivi sur une discussion de JuliaCon sur la syntaxe possible sur les traits à https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040

Guy Steele a de bonnes idées sur les traits dans un langage d'envoi multiple (Forteresse), voir son keynote JuliaCon 2016 : https://youtu.be/EZD3Scuv02g .

Quelques faits saillants : système de grands traits pour les propriétés algébriques, test unitaire des propriétés de trait pour les types qui implémentent un trait, et que le système qu'ils ont mis en œuvre était peut-être trop compliqué et qu'il ferait quelque chose de plus simple maintenant.

Nouveau cas d'utilisation AD du compilateur Swift pour tensorflow pour les protocoles :
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy et @Keno pourraient être intéressés par cela. A un tout nouveau contenu

Je pense que cette présentation mérite l'attention lors de l'exploration de l'espace de conception pour ce problème.

Pour la discussion d'idées non spécifiques et de liens vers des travaux de fond pertinents, il serait préférable de démarrer un fil de discussion correspondant et d'y poster et d'en discuter.

Notez que presque tous les problèmes rencontrés et discutés dans les recherches sur la programmation générique dans les langages à typage statique ne concernent pas Julia. Les langages statiques sont presque exclusivement concernés par le problème de fournir une expressivité suffisante pour écrire le code qu'ils veulent tout en étant capable de vérifier de manière statique qu'il n'y a pas de violation du système de type. Nous n'avons aucun problème d'expressivité et n'exigeons pas de vérification de type statique, donc rien de tout cela n'a vraiment d'importance dans Julia.

Ce qui nous intéresse, c'est de permettre aux gens de documenter les attentes d'un protocole de manière structurée que le langage peut ensuite vérifier dynamiquement (à l'avance, si possible). Nous nous soucions également de permettre aux gens de s'occuper de choses comme des traits de caractère ; il reste ouvert si ceux-ci doivent être connectés.

Conclusion : si les travaux universitaires sur les protocoles dans les langages statiques peuvent être d'intérêt général, ils ne sont pas très utiles dans le contexte de Julia.

Ce qui nous intéresse, c'est de permettre aux gens de documenter les attentes d'un protocole de manière structurée que le langage peut ensuite vérifier dynamiquement (à l'avance, si possible). Nous nous soucions également de permettre aux gens de s'occuper de choses comme des traits de caractère ; il reste ouvert si ceux-ci doivent être connectés.

_c'est le_ :ticket:

En plus d'éviter des changements de rupture, l'élimination des types abstraits et l'introduction d'interfaces implicites de style golang seraient-elles réalisables dans Julia ?

Non, ce ne serait pas le cas.

eh bien, n'est-ce pas à cela que servent les protocoles/caractéristiques ? Il y a eu une discussion pour savoir si les protocoles doivent être implicites ou explicites.

Je pense que depuis 0.3 (2014), l'expérience a montré que les interfaces implicites (c'est-à-dire non imposées par le langage/compilateur) fonctionnent très bien. De plus, ayant été témoin de l'évolution de certains packages, je pense que les meilleures interfaces ont été développées de manière organique et n'ont été formalisées (= documentées) que plus tard.

Je ne suis pas sûr qu'une description formelle des interfaces, imposée par le langage d'une manière ou d'une autre, soit nécessaire. Mais pendant que cela est décidé, il serait bon d'encourager les éléments suivants (dans la documentation, les didacticiels et les guides de style) :

  1. Les "interfaces" sont bon marché et légères, juste un tas de fonctions avec un comportement prescrit pour un ensemble de types (oui, les types sont le bon niveau de granularité — pour x::T , T devrait être suffisant pour décider si x implémente l'interface) . Donc, si l'on définit un package avec un comportement extensible, il est vraiment logique de documenter l'interface.

  2. Les interfaces n'ont pas besoin d'être décrites par des relations de sous-type . Les types sans supertype commun (non trivial) peuvent implémenter la même interface. Un type peut implémenter plusieurs interfaces.

  3. Le transfert/la composition nécessite implicitement des interfaces. "Comment faire en sorte qu'un wrapper hérite de toutes les méthodes du parent" est une question qui revient souvent, mais ce n'est pas la bonne question. La solution pratique consiste à disposer d'une interface principale et à l'implémenter uniquement pour le wrapper.

  4. Les traits sont bon marché et doivent être utilisés généreusement. Base.IndexStyle est un excellent exemple canonique.

Les points suivants gagneraient à être clarifiés, car je ne suis pas sûr de la meilleure pratique :

  1. L'interface devrait-elle avoir une fonction de requête, comme par exemple Tables.istable pour décider si un objet implémente l'interface ? Je pense que c'est une bonne pratique, si un appelant peut travailler avec diverses interfaces alternatives et doit parcourir la liste des solutions de secours.

  2. Quel est le meilleur endroit pour une documentation d'interface dans une docstring ? Je dirais la fonction de requête ci-dessus.

  1. oui, les types sont le bon niveau de granularité

Pourquoi est-ce si? Certains aspects des types peuvent être pris en compte dans les interfaces (à des fins de répartition), comme l'itération. Sinon, vous devrez réécrire le code ou imposer une structure inutile.

  1. Les interfaces n'ont pas besoin d'être décrites par des relations de sous-type .

Ce n'est peut-être pas nécessaire, mais serait-ce mieux ? Je peux avoir une fonction dispatch sur un type itérable. Un type itérable en mosaïque ne devrait-il pas remplir cela implicitement? Pourquoi l'utilisateur devrait-il les dessiner autour de types nominaux alors qu'il ne se soucie que de l'interface ?

Quel est l'intérêt du sous-typage nominal si vous ne les utilisez essentiellement que comme interfaces abstraites ? Les traits semblent être plus granulaires et puissants, ce serait donc une meilleure généralisation. Il semble donc que les types soient presque des traits, mais nous devons avoir des traits pour contourner leurs limites (et vice versa).

Quel est l'intérêt du sous-typage nominal si vous ne les utilisez essentiellement que comme interfaces abstraites ?

Envoi : vous pouvez envoyer sur le type nominal de quelque chose. Si vous n'avez pas besoin de déterminer si un type implémente une interface ou non, vous pouvez simplement le saisir. C'est pour cela que les gens utilisent généralement les traits Holy : le trait vous permet d'appeler une implémentation qui suppose qu'une interface est implémentée (par exemple "ayant une longueur connue"). Quelque chose que les gens semblent vouloir, c'est éviter cette couche d'indirection, mais c'est comme si c'était simplement une commodité, pas une nécessité.

Pourquoi est-ce si? Certains aspects des types peuvent être pris en compte dans les interfaces (à des fins de répartition), comme l'itération. Sinon, vous devrez réécrire le code ou imposer une structure inutile.

Je crois que @tpapp disait que vous n'avez besoin que du type pour déterminer si quelque chose implémente ou non une interface, pas que toutes les interfaces peuvent être représentées avec des hiérarchies de types.

Juste une idée, en utilisant les MacroTools de forward :

C'est parfois ennuyeux de transmettre beaucoup de méthodes

<strong i="9">@forward</strong> Foo.x a b c d ...

et si nous pouvions utiliser le type de Foo.x et une liste de méthodes, puis en déduire laquelle transmettre ? Ce sera une sorte de inheritance et peut être implémenté avec des fonctionnalités existantes (macros + fonction générée), cela ressemble également à une sorte d'interface, mais nous n'avons besoin de rien d'autre dans le langage.

Je sais que nous n'avons jamais pu dresser une liste de ce qui va hériter (c'est aussi pourquoi le modèle statique class est moins flexible), parfois vous n'en avez besoin que de quelques-uns, mais c'est juste pratique pour les fonctions de base ( par exemple, quelqu'un veut définir un wrapper (sous-type de AbstractArray ) autour de Array , la plupart des fonctions sont simplement transférées)

@datnamer : comme d'autres l'ont précisé, les interfaces ne doivent pas être plus granulaires que les types (c'est-à-dire que l'implémentation de l'interface ne doit jamais dépendre de la valeur , étant donné le type). Cela correspond bien au modèle d'optimisation du compilateur et n'est pas une contrainte en pratique.

Peut-être que je n'étais pas clair, mais le but de ma réponse était de souligner que nous avons déjà des interfaces dans la mesure où cela est utile dans Julia , et elles sont légères, rapides et deviennent omniprésentes à mesure que l'écosystème mûrit.

Une spécification formelle pour décrire une interface ajoute peu de valeur IMO : cela reviendrait à simplement documenter et vérifier que certaines méthodes sont disponibles. Ce dernier fait partie d'une interface, mais l'autre partie est la sémantique implémentée par ces méthodes (par exemple si A est un tableau, axes(A) me donne une plage de coordonnées qui sont valables pour getindex ). Les spécifications formelles des interfaces ne peuvent pas les traiter en général, donc je suis d'avis qu'elles ajouteraient simplement un passe-partout avec peu de valeur. Je crains également que cela ne fasse qu'élever une (petite) barrière à l'entrée pour peu d'avantages.

Cependant, ce que j'aimerais voir, c'est

  1. documentation pour de plus en plus d'interfaces (dans une docstring),

  2. des suites de tests pour détecter les erreurs évidentes des interfaces matures pour les types nouvellement définis (par exemple, beaucoup de T <: AbstractArray implémentent eltype(::T) et non eltype(::Type{T}) .

@tpapp Cela a du sens pour moi maintenant, merci.

@StefanKarpinski Je ne comprends pas très bien. Les traits ne sont pas des types nominaux (n'est-ce pas ?), néanmoins, ils peuvent être utilisés pour l'expédition.

Mon propos est essentiellement celui de @tknopp et @mauro3 ici : https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Qu'en ayant des traits et un typage abstrait, il y a une complexité et une confusion supplémentaires en ayant deux concepts très similaires.

Quelque chose que les gens semblent vouloir, c'est éviter cette couche d'indirection, mais c'est comme si c'était simplement une commodité, pas une nécessité.

Les sections de la hiérarchie des traits peuvent-elles être réparties et regroupées par éléments tels que les unions et les intersections, avec des paramètres de type, de manière robuste ? Je ne l'ai pas essayé, mais j'ai l'impression que cela nécessite une prise en charge linguistique. Problème d'expression IE dans le domaine de type.

Edit : Je pense que le problème était ma fusion d'interfaces et de traits, tels qu'ils sont utilisés ici.

Je viens de poster ceci ici parce que c'est amusant : il semble que Concepts a définitivement été accepté et fera partie de C++20. Des trucs intéressants !

https://herbsutter.com/2019/02/23/trip-report-winter-iso-c-standards-meeting-kona/
https://en.cppreference.com/w/cpp/language/constraints

Je pense que les traits sont un très bon moyen de résoudre ce problème et les traits sacrés ont certainement parcouru un long chemin. Cependant, je pense que ce dont Julia a vraiment besoin, c'est d'un moyen de regrouper les fonctions qui appartiennent à un trait. Cela serait utile pour des raisons de documentation mais aussi pour la lisibilité du code. D'après ce que j'ai vu jusqu'à présent, je pense qu'une syntaxe de trait comme dans Rust serait la voie à suivre.

Je pense que c'est super important, et le cas d'utilisation le plus important serait l'indexation des itérateurs. Voici une proposition pour le type de syntaxe qui, vous l'espérez, fonctionnera. Désolé si cela a déjà été proposé (fil long...).

import Base: Generator
<strong i="6">@require</strong> getindex(AbstractArray, Vararg{Int})
function getindex(container::Generator, index...)
    iterator = container.iter
    if <strong i="7">@works</strong> getindex(iterator, index...)
        container.f(getindex(iterator, index...))
    else
        <strong i="8">@interfaceerror</strong> getindex(iterator, index...)
    end
end
Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

omus picture omus  ·  3Commentaires

felixrehren picture felixrehren  ·  3Commentaires

TotalVerb picture TotalVerb  ·  3Commentaires

i-apellaniz picture i-apellaniz  ·  3Commentaires

manor picture manor  ·  3Commentaires