Julia: Schnittstellen für abstrakte Typen

Erstellt am 26. Mai 2014  ·  171Kommentare  ·  Quelle: JuliaLang/julia

Ich denke, dieser Feature-Request hat noch kein eigenes Problem, obwohl es zB in #5 diskutiert wurde.

Ich fände es toll, wenn wir Schnittstellen für abstrakte Typen explizit definieren könnten. Mit Schnittstelle meine ich alle Methoden, die implementiert werden müssen, um die Anforderungen des abstrakten Typs zu erfüllen. Derzeit ist die Schnittstelle nur implizit definiert und kann über mehrere Dateien verstreut sein, so dass bei der Ableitung von einem abstrakten Typ nur sehr schwer zu bestimmen ist, was zu implementieren ist.

Schnittstellen würden uns in erster Linie zwei Dinge geben:

  • Selbstdokumentation der Schnittstellen an einem Ort
  • bessere Fehlermeldungen

Base.graphics verfügt über ein Makro, das es tatsächlich ermöglicht, Schnittstellen zu definieren, indem eine Fehlermeldung in der Fallback-Implementierung kodiert wird. Das finde ich schon sehr clever. Aber vielleicht ist es noch sauberer, ihm die folgende Syntax zu geben:

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

Hier wäre es schön, wenn man verschiedene Granularitäten angeben könnte. Die Deklarationen print und push! sagen nur, dass es irgendwelche Methoden mit diesem Namen geben muss (und MyType als erster Parameter), aber sie geben keine Typen an. Im Gegensatz dazu ist die size Deklaration vollständig typisiert. Ich denke, das gibt viel Flexibilität und für eine untypisierte Schnittstellendeklaration könnte man immer noch ganz spezifische Fehlermeldungen ausgeben.

Wie ich in #5 gesagt habe, sind solche Schnittstellen im Grunde das, was in C++ als Concept-light für C++14 oder C++17 geplant ist. Und nachdem ich einiges an C++-Template-Programmierung gemacht habe, bin ich mir sicher, dass eine gewisse Formalisierung in diesem Bereich auch für Julia gut wäre.

Hilfreichster Kommentar

Zur Diskussion unspezifischer Ideen und Links zu einschlägiger Hintergrundarbeit wäre es besser, einen entsprechenden Diskursthread zu starten und dort zu posten und zu diskutieren.

Beachten Sie, dass fast alle Probleme, die bei der Forschung zur generischen Programmierung in statisch typisierten Sprachen aufgetreten und diskutiert wurden, für Julia irrelevant sind. Statische Sprachen beschäftigen sich fast ausschließlich mit dem Problem, genügend Ausdruckskraft bereitzustellen, um den gewünschten Code zu schreiben, während sie dennoch statisch typüberprüfen können, ob es keine Typsystemverletzungen gibt. Wir haben keine Probleme mit der Ausdruckskraft und benötigen keine statische Typprüfung, daher spielt das alles in Julia keine Rolle.

Was uns wichtig ist, ist es den Leuten zu ermöglichen, die Erwartungen an ein Protokoll strukturiert zu dokumentieren, die die Sprache dann dynamisch überprüfen kann (vorab, wenn möglich). Wir kümmern uns auch darum, dass die Leute Dinge wie Eigenschaften versenden können; es bleibt offen, ob die angeschlossen werden sollen.

Fazit: Während die akademische Arbeit an Protokollen in statischen Sprachen von allgemeinem Interesse sein mag, ist sie im Kontext von Julia nicht sehr hilfreich.

Alle 171 Kommentare

Im Allgemeinen denke ich, dass dies eine gute Richtung für eine bessere schnittstellenorientierte Programmierung ist.

Allerdings fehlt hier etwas. Auch die Signaturen der Methoden (nicht nur deren Namen) sind für eine Schnittstelle von Bedeutung.

Dies ist nicht einfach zu implementieren und es wird viele Fallstricke geben. Das ist wahrscheinlich einer der Gründe, warum _Concepts_ von C++ 11 nicht akzeptiert wurde und nach drei Jahren nur noch eine sehr eingeschränkte _lite_-Version in C++ 14 einsteigt.

Die Methode size in meinem Beispiel enthielt die Signatur. Weitere @mustimplement von Base.graphics berücksichtigen auch die Signatur.

Ich sollte hinzufügen, dass wir bereits einen Teil von Concept-light , nämlich die Möglichkeit, einen Typ als Untertyp eines bestimmten abstrakten Typs einzuschränken. Die Schnittstellen sind der andere Teil.

Das Makro ist ziemlich cool. Ich habe fehlerauslösende Fallbacks manuell definiert und es hat ziemlich gut funktioniert, um Schnittstellen zu definieren. zB MathProgBase von JuliaOpt tut dies, und es funktioniert gut. Ich spielte mit einem neuen Solver (https://github.com/IainNZ/RationalSimplex.jl) herum und musste einfach so lange Schnittstellenfunktionen implementieren, bis er keine Fehler mehr auslöste, damit es funktionierte.

Ihr Vorschlag würde ähnliches bewirken, oder? Aber würden Sie _müssen_, die gesamte Schnittstelle zu implementieren?

Wie verhält sich das mit kovarianten/kontravarianten Parametern?

Beispielsweise,

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 Ja, bei dem Vorschlag geht es eigentlich darum, @mustimplement etwas vielseitiger zu machen, so dass zB die Signatur bereitgestellt werden kann, aber nicht muss. Und mein Gefühl ist, dass dies ein solcher "Kern" ist, dass es sich lohnt, eine eigene Syntax zu erhalten. Es wäre toll, durchzusetzen, dass alle Methoden wirklich implementiert sind, aber die aktuelle Laufzeitprüfung, wie sie in @mustimplement ist bereits eine großartige Sache und möglicherweise einfacher zu implementieren.

@lindahua Das ist ein interessantes Beispiel. Darüber muss man nachdenken.

@lindahua Man möchte wahrscheinlich, dass Ihr Beispiel einfach funktioniert. @mustimplement würde nicht funktionieren, da es spezifischere Methodensignaturen definiert.

Dies muss also möglicherweise etwas tiefer im Compiler implementiert werden. Bei der abstrakten Typdefinition muss man die Schnittstellennamen/Signaturen verfolgen. Und an der Stelle, an der aktuell ein "... nicht definiert"-Fehler ausgegeben wird, muss die entsprechende Fehlermeldung generiert werden.

Es ist sehr einfach zu ändern, wie MethodError print , wenn wir eine Syntax und eine API haben, um die Informationen auszudrücken und darauf zuzugreifen.

Eine andere Sache, die uns dies bringen könnte, ist eine Funktion in base.Test , um zu überprüfen, ob ein Typ (alle Typen?) die Schnittstellen der Elterntypen vollständig implementiert. Das wäre ein wirklich schöner Unit-Test.

Danke @ivarne. Die Implementierung könnte also wie folgt aussehen:

  1. Man hat ein globales Wörterbuch mit abstrakten Typen als Schlüssel und Funktionen (+ optionale Signaturen) als Werte.
  2. Der Parser muss angepasst werden, um das Diktat zu füllen, wenn eine has Deklaration geparst wird.
  3. MethodError muss nachschlagen, ob die aktuelle Funktion Teil des globalen Wörterbuchs ist.

Die meiste Logik befindet sich dann in MethodError .

Ich habe damit ein wenig experimentiert und mit dem folgenden Kern https://gist.github.com/tknopp/ed53dc22b61062a2b283 kann ich Folgendes tun:

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

beim Definieren von length kein Fehler ausgegeben:

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

Nicht, dass dabei die Signatur derzeit nicht berücksichtigt wird.

Ich habe den Code im Wesentlichen etwas aktualisiert, damit Funktionssignaturen berücksichtigt werden können. Es ist immer noch sehr hackig, aber Folgendes funktioniert jetzt:

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

Ich hätte hinzufügen sollen, dass der Interface-Cache im Gist jetzt mit Symbolen statt mit Funktionen arbeitet, so dass man ein Interface hinzufügen und die Funktion danach deklarieren kann. Das muss ich wohl auch mit der Signatur machen.

Habe gerade gesehen, dass #2248 bereits Material zu Schnittstellen enthält.

Ich wollte mit der Veröffentlichung von Gedanken zu spekulativeren Funktionen wie Schnittstellen warten, bis wir 0.3 auf den Markt gebracht haben, aber da Sie die Diskussion begonnen haben, habe ich vor einiger Zeit Folgendes geschrieben.


Hier ist ein Modell der Syntax für die Schnittstellendeklaration und die Implementierung dieser Schnittstelle:

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

Lassen Sie uns das in Stücke zerlegen. Zunächst gibt es die Syntax des Funktionstyps: A --> B ist der Funktionstyp, der Objekte vom Typ A dem Typ B . Tupel in dieser Notation tun das Offensichtliche. Isoliert schlage ich vor, dass f :: A --> B deklarieren würde, dass f eine generische Funktion ist, die den Typ A dem Typ B . Es ist eine etwas offene Frage, was das bedeutet. Bedeutet das, dass f bei Anwendung auf ein Argument vom Typ A ein Ergebnis vom Typ B liefert? Bedeutet das, dass f nur auf Argumente vom Typ A angewendet werden kann? Soll irgendwo eine automatische Konvertierung erfolgen – bei der Ausgabe, bei der Eingabe? Im Moment können wir davon ausgehen, dass dies lediglich eine neue generische Funktion erstellt, ohne ihr Methoden hinzuzufügen, und die Typen nur zur Dokumentation dienen.

Zweitens gibt es die Deklaration der Schnittstelle Iterable{T,S} . Das macht Iterable ein bisschen wie ein Modul und ein bisschen wie ein abstrakter Typ. Es ist wie ein Modul, da es Bindungen zu generischen Funktionen namens Iterable.start , Iterable.done und Iterable.next . Es ist wie ein Typ, in dem Iterable und Iterable{T} und Iterable{T,S} überall dort verwendet werden können, wo abstrakte Typen verwendet werden können – insbesondere im Methodenversand.

Drittens gibt es den implement Block, der definiert, wie UnitRange die Iterable Schnittstelle implementiert. Innerhalb des implement Blocks stehen die Funktionen Iterable.start , Iterable.done und Iterable.next Verfügung, als ob der Benutzer import Iterable: start, done, next das Hinzufügen von Methoden zu diesen Funktionen. Dieser Block ist template-ähnlich wie parametrische Typdeklarationen – innerhalb des Blocks bedeutet UnitRange einen bestimmten UnitRange , nicht den Umbrella-Typ.

Der Hauptvorteil des implement Blocks besteht darin, dass er die expliziten import Funktionen vermeidet, die Sie erweitern möchten – sie werden implizit für Sie importiert, was schön ist, da die Leute im Allgemeinen über import verwirrt sind. Base , die Benutzer erweitern möchten, zu einer Schnittstelle gehören sollten, daher sollte dies die überwiegende Mehrheit der Verwendungen für import eliminieren. Da Sie einen Namen immer vollständig qualifizieren können, könnten wir ihn vielleicht ganz abschaffen.

Eine andere Idee, die mir aufgefallen ist, ist die Trennung der "inneren" und "äußeren" Versionen der Schnittstellenfunktionen. Was ich damit meine ist, dass die "innere" Funktion diejenige ist, die Sie Methoden zum Implementieren einer Schnittstelle bereitstellen, während die "äußere" Funktion diejenige ist, die Sie aufrufen, um generische Funktionalität in Bezug auf eine Schnittstelle zu implementieren. Betrachten Sie die Methoden der Funktion sort! (mit Ausnahme der veralteten Methoden):

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

Einige dieser Methoden sind für den öffentlichen Gebrauch bestimmt, andere sind jedoch nur ein Teil der internen Implementierung der öffentlichen Sortiermethoden. Wirklich, die einzige öffentliche Methode, die dies haben sollte, ist diese:

sort!(v::AbstractArray)

Der Rest ist Lärm und gehört ins "innen". Insbesondere die

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)

Arten von Methoden sind das, was ein Sortieralgorithmus implementiert, um sich in die generische Sortiermaschinerie einzuklinken. Derzeit ist Sort.Algorithm ein abstrakter Typ, und InsertionSortAlg , QuickSortAlg und MergeSortAlg sind konkrete Untertypen davon. Bei Schnittstellen könnte Sort.Algorithm stattdessen eine Schnittstelle sein und die spezifischen Algorithmen würden sie implementieren. Etwas wie das:

# 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

Die gewünschte Trennung könnte dann durch die Definition erreicht werden:

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

Dies ist _sehr_ nah an dem, was wir derzeit tun, außer dass wir Algorithm.sort! statt nur sort! aufrufen – und bei der Implementierung verschiedener Sortieralgorithmen ist die "innere" Definition eine Methode von Algorithm.sort! nicht die sort! Funktion. Dadurch wird die Implementierung von sort! von der externen Schnittstelle getrennt.

@StefanKarpinski Vielen Dank für Ihren Beitrag! Das ist sicher kein 0.3-Zeug. Es tut mir leid, dass ich das jetzt zur Sprache gebracht habe. Ich bin mir nur nicht sicher, ob 0.3 bald oder in einem halben Jahr passieren wird ;-)

Auf den ersten Blick gefällt mir wirklich (!), dass der implementierende Abschnitt einen eigenen Codeblock definiert. Dies ermöglicht die direkte Überprüfung der Schnittstelle in der Typdefinition.

Keine Sorge – es schadet nicht wirklich, über zukünftige Features zu spekulieren, während wir versuchen, eine Veröffentlichung zu stabilisieren.

Ihr Ansatz ist viel grundlegender und versucht, auch einige schnittstellenunabhängige Probleme zu lösen. Es bringt auch irgendwie ein neues Konstrukt (zB die Schnittstelle) in die Sprache, das die Sprache ein wenig komplexer macht (was nicht unbedingt schlecht ist).

Ich sehe "die Schnittstelle" eher als Anmerkung zu abstrakten Typen. Wenn man das has hinzufügt, kann man eine Schnittstelle angeben, muss es aber nicht.

Wie gesagt, ich würde mich sehr freuen, wenn die Schnittstelle direkt bei ihrer Deklaration validiert werden könnte. Der am wenigsten invasive Ansatz könnte darin bestehen, Methoden innerhalb einer Typdeklaration zu definieren. Also nimm dein Beispiel so etwas wie

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

Es wäre immer noch erlaubt, die Funktion außerhalb der Typdeklaration zu definieren. Der einzige Unterschied besteht darin, dass innere Funktionsdeklarationen gegen Schnittstellen validiert werden.

Aber vielleicht ist mein "am wenigsten invasiver Ansatz" zu kurzsichtig. Weiß nicht wirklich.

Ein Problem beim Einfügen dieser Definition in den Typblock besteht darin, dass wir dazu wirklich mindestens mehrere Schnittstellen benötigen, und es ist denkbar, dass Namenskollisionen zwischen verschiedenen Schnittstellen auftreten können. Vielleicht möchten Sie auch die Tatsache hinzufügen, dass ein Typ irgendwann _nach_ der Definition des Typs eine Schnittstelle unterstützt, obwohl ich mir da nicht sicher bin.

@StefanKarpinski Es ist

Das Graphs-Paket ist eines, das das Schnittstellensystem am meisten benötigt. Es wäre interessant zu sehen, wie dieses System die hier skizzierten Schnittstellen ausdrücken kann: http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : Ich sehe das Problem mit mehrfacher Vererbung und In-Block-Funktionsdeklarationen nicht vollständig. Innerhalb des Typblocks müssten alle geerbten Schnittstellen überprüft werden.

Aber ich verstehe irgendwie, dass man die Schnittstellenimplementierung "offen" lassen möchte. Und die Deklaration von typinternen Funktionen könnte die Sprache zu sehr komplizieren. Vielleicht ist der Ansatz, den ich in #7025 implementiert habe, ausreichend. Fügen Sie entweder ein verify_interface nach den Funktionsdeklarationen ein (oder in einem Komponententest) oder verschieben Sie es auf das MethodError .

Dieses Problem besteht darin, dass verschiedene Schnittstellen generische Funktionen mit demselben Namen haben können, was zu einer Namenskollision führen und einen expliziten Import oder das Hinzufügen von Methoden mit einem vollständig qualifizierten Namen erfordern würde. Es macht auch unübersichtlich, welche Methodendefinitionen zu welchen Interfaces gehören – weshalb es überhaupt zur Namenskollision kommen kann.

Übrigens, ich stimme zu, dass sich das Hinzufügen von Schnittstellen als ein weiteres "Ding" in der Sprache etwas zu nicht orthogonal anfühlt. Schließlich sind sie, wie ich im Vorschlag erwähnt habe, ein bisschen wie Module und ein bisschen wie Typen. Es fühlt sich an, als ob eine Vereinheitlichung der Konzepte möglich wäre, aber mir ist nicht klar, wie.

Ich bevorzuge das Interface-as-Library-Modell dem Interface-as-Language-Feature-Modell aus mehreren Gründen: Es hält die Sprache einfacher (zugegeben, Präferenz und kein konkreter Einwand) und bedeutet, dass das Feature optional bleibt und leicht hinzugefügt werden kann verbessert oder ganz ersetzt, ohne mit der eigentlichen Sprache herumzuspielen.

Insbesondere denke ich, dass der Vorschlag (oder zumindest die Form des Vorschlags) von @tknopp besser ist als der von @StefanKarpinski - er bietet eine Definitionszeitprüfung, ohne dass etwas Neues in der Sprache erforderlich ist. Der größte Nachteil, den ich sehe, ist die fehlende Fähigkeit, mit Typvariablen umzugehen; Ich denke, dies kann gehandhabt werden, indem die Schnittstellendefinition den Typ _predicates_ für die Typen der erforderlichen Funktionen bereitstellt.

Einer der Hauptgründe für meinen Vorschlag ist die große Verwirrung, die dadurch entsteht, dass generische Funktionen _importiert_, aber nicht exportiert werden müssen, um ihnen Methoden hinzuzufügen. Meistens passiert dies, wenn jemand versucht, eine inoffizielle Schnittstelle zu implementieren, daher sieht es so aus, als ob das passiert.

Das scheint ein zu lösendes orthogonales Problem zu sein, es sei denn, Sie möchten Methoden vollständig auf die Zugehörigkeit zu Schnittstellen beschränken.

Nein, das scheint sicherlich keine gute Einschränkung zu sein.

@StefanKarpinski Sie erwähnen, dass Sie über eine Schnittstelle versenden könnten. Auch in der implement Syntax besteht die Idee darin, dass ein bestimmter Typ die Schnittstelle implementiert.

Dies scheint ein wenig im Widerspruch zu multiplem Dispatch zu stehen, da Methoden im Allgemeinen nicht zu einem bestimmten Typ gehören, sondern zu einem Tupel von Typen. Wenn also Methoden nicht zu Typen gehören, wie können dann Schnittstellen (die im Grunde genommen Gruppen von Methoden sind) zu einem Typ gehören?

Angenommen, ich verwende Bibliothek 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

jetzt möchte ich eine generische Funktion schreiben, die ein A und ein B . nimmt

using M

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

In diesem Beispiel bildet die Funktion f eine Ad-hoc-Schnittstelle, die ein A und ein B , und ich möchte annehmen können, dass ich das f aufrufen kann

Von anderen Modulen, die konkrete Untertypen von A und B bereitstellen möchten, sollte erwartet werden, dass sie Implementierungen von f bereitstellen. Um die kombinatorische Explosion der erforderlichen Methoden zu vermeiden, würde ich erwarten, dass die Bibliothek f für die abstrakten Typen definiert:

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

Zugegeben, dieses Beispiel fühlt sich ziemlich konstruiert an, aber hoffentlich veranschaulicht es (zumindest in meinen Augen) es fühlt sich an, als ob es eine grundlegende Diskrepanz zwischen mehreren Dispatchs und dem Konzept eines bestimmten Typs gibt, der eine Schnittstelle implementiert.

Ich verstehe Ihren Standpunkt bezüglich der import Verwirrung jedoch. Ich habe ein paar Versuche an diesem Beispiel gebraucht, um mich daran zu erinnern, dass, als ich using M und dann versucht habe, Methoden zu f hinzuzufügen, es nicht das tat, was ich erwartet hatte, und ich die Methoden hinzufügen musste zu M.f (oder ich hätte import ). Ich glaube jedoch nicht, dass Schnittstellen die Lösung für dieses Problem sind. Gibt es ein separates Problem beim Brainstorming von Möglichkeiten, das Hinzufügen von Methoden intuitiver zu gestalten?

@abe-egnor Ich denke auch, dass ein offenerer Ansatz machbarer erscheint. Meinem Prototyp #7025 fehlen im Wesentlichen zwei Dinge:
a) eine bessere Syntax zum Definieren von Schnittstellen
b) parametrische Typdefinitionen

Da ich nicht so sehr ein Guru vom parametrischen Typ bin, bin ich mir ziemlich sicher, dass b) von jemandem mit tieferer Erfahrung lösbar ist.
Zu a) könnte man mit einem Makro gehen. Persönlich denke ich, dass wir etwas Sprachunterstützung für die direkte Definition der Schnittstelle als Teil der abstrakten Typdefinition ausgeben könnten. Der Ansatz von has könnte zu kurzsichtig sein. Ein Codeblock könnte dies schöner machen. Tatsächlich hängt dies stark mit #4935 zusammen, wo eine "interne" Schnittstelle definiert ist, während es hier um die öffentliche Schnittstelle geht. Diese müssen nicht gebündelt werden, da ich denke, dass dieses Problem viel wichtiger ist als #4935. Trotzdem sollte man in Bezug auf die Syntax beide Anwendungsfälle berücksichtigen.

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 hat meinen ersten Versuch mit der Art von Implementierung, an die ich gedacht habe. Kurz gesagt, eine Schnittstelle ist eine Funktion von Typen zu einem Diktat, die den Namen und die Parametertypen der erforderlichen Funktionen für diese Schnittstelle definiert. Das @implement Makro ruft einfach die Funktion für die angegebenen Typen auf und fügt dann die Typen in die angegebenen Funktionsdefinitionen ein und überprüft, ob alle Funktionen definiert wurden.

Gute Argumente:

  • Einfache Syntax zur Definition und Implementierung von Schnittstellen.
  • Orthogonal zu anderen Sprachfeatures, spielt aber gut damit.
  • Die Berechnung des Schnittstellentyps kann beliebig ausgefallen sein (es sind nur Funktionen über die Parameter des Schnittstellentyps).

Schlechte Punkte:

  • Spielt nicht gut mit parametrisierten Typen, wenn Sie den Parameter als Schnittstellentyp verwenden möchten. Dies ist ein ziemlich erheblicher Nachteil, aber ich sehe keinen sofortigen Weg, um ihn anzugehen.

Ich glaube, ich habe eine Lösung für das Parametrisierungsproblem - kurz gesagt, die Schnittstellendefinition sollte ein Makro über Typausdrücken sein, keine Funktion über Typwerten. Das @implement Makro kann dann Typparameter auf Funktionsdefinitionen erweitern, was Folgendes ermöglicht:

<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

In diesem Fall werden die Typparameter auf die in der Schnittstelle definierten Methoden erweitert, also auf stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , was meiner Meinung nach genau das Richtige ist.

Ich werde meine anfängliche Implementierung überarbeiten, um dies zu tun, sobald ich die Zeit habe; wahrscheinlich in der Größenordnung einer Woche.

Ich habe ein bisschen im Internet gestöbert, um zu sehen, was andere Programmiersprachen in Bezug auf Schnittstellen, Vererbung und dergleichen tun, und bin auf ein paar Ideen gekommen. (Falls es jemanden interessiert, hier die sehr groben Notizen, die ich gemacht habe https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

Die Kurzfassung ist, dass vielleicht Schnittstellen implementiert werden könnten durch:

  • Mehrfachvererbung für abstrakte Typen zulassen und
  • erlaubt generische Funktionen als Felder abstrakter Typen.

Dies würde abstrakte Typen in Schnittstellen verwandeln und die konkreten Untertypen wären dann erforderlich, um diese Schnittstelle zu implementieren.

Die lange Geschichte:

Was ich festgestellt habe, ist, dass einige der "modernen" Sprachen den Subtyp-Polymorphismus abschaffen, dh es gibt keine direkte Gruppierung von Typen, sondern sie gruppieren ihre Typen nach ihrer Zugehörigkeit zu Interfaces / Traits / Typklassen. In einigen Sprachen können die Schnittstellen / Eigenschaften / Typklassen eine Reihenfolge aufweisen und voneinander erben. Sie scheinen auch (meistens) glücklich über diese Wahl zu sein. Beispiele sind: Geh ,
Rost , Haskell .
Go ist die am wenigsten strenge der drei und lässt seine Schnittstellen implizit angeben, dh wenn ein Typ den spezifischen Satz von Funktionen einer Schnittstelle implementiert, gehört er zu dieser Schnittstelle. Für Rust muss das Interface (Traits) explizit in einem impl Block implementiert werden. Weder Go noch Rust haben Multimethoden. Haskell hat Multimethoden und sie sind tatsächlich direkt mit der Schnittstelle (Typklasse) verbunden.

In gewisser Weise ist dies ähnlich wie bei Julia, die abstrakten Typen sind wie eine (implizite) Schnittstelle, dh es geht um Verhalten und nicht um Felder. Dies hat @StefanKarpinski auch in einem seiner obigen Beiträge beobachtet und festgestellt, dass sich zusätzlich Schnittstellen "ein wenig zu nicht orthogonal"

Wie wäre es, Julias abstrakte Typen in eine Interface- / Merkmals- / Typklasse zu verwandeln, während alle Typen in der Hierarchie None<: ... <:Any beibehalten werden? Dies würde bedeuten:
1) Mehrfachvererbung für (abstrakte) Typen zulassen (Problem #5)
2) Zuordnen von Funktionen zu abstrakten Typen ermöglichen (dh eine Schnittstelle definieren)
3) Erlauben Sie, diese Schnittstelle sowohl für abstrakte (dh eine Standardimplementierung) als auch für konkrete Typen anzugeben.

Ich denke, dies könnte zu einem feinkörnigeren Typdiagramm führen, als wir es jetzt haben, und könnte Schritt für Schritt implementiert werden. Zum Beispiel würde ein Array-Typ zusammengesetzt:

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

Im Grunde können also abstrakte Typen generische Funktionen als Felder haben (dh zu einer Schnittstelle werden), während konkrete Typen nur normale Felder haben. Dies kann zum Beispiel das Problem lösen, dass zu viele Dinge von AbstractArray abgeleitet werden, da die Leute einfach die nützlichen Teile für ihren Container auswählen könnten, anstatt sie von AbstractArray abzuleiten.

Wenn dies überhaupt eine gute Idee ist, gibt es noch einiges zu erarbeiten (insbesondere wie man Typen und Typparameter spezifiziert), aber vielleicht eine Überlegung wert?

@ssfrr hat oben kommentiert, dass Schnittstellen und Mehrfachversand nicht kompatibel sind. Das sollte nicht der Fall sein, da zB in Haskell Multimethoden nur durch die Verwendung von Typklassen möglich sind.

Ich habe auch beim Lesen des @StefanKarpinski - abstract anstelle von interface sinnvoll sein könnte. In diesem Fall ist es jedoch wichtig, dass abstract eine entscheidende Eigenschaft von interface abstract erbt: die Möglichkeit, dass ein Typ implement interface _nach_ definiert wird. Dann kann ich einen Typ typA aus lib A mit einem Algorithmus algoB aus lib B verwenden, indem ich in meinem Code deklariere, dass typA die von algoB benötigte Schnittstelle implementiert (ich vermute, dass dies impliziert, dass konkrete Typen eine Art offene Mehrfachvererbung haben).

@mauro3 , dein Vorschlag @ StefanKarpinskis Idee sort! Beispiel implementieren könnten, indem Sie abstract Algorithm und Algorithm.sort! deklarieren

Entschuldigung an alle

------------------ 原始邮件 ------------------
: "Jacob Quinn" [email protected];
Spielzeit: 2014年9月12日(星期五) 6:23
: "JuliaLang/julia" [email protected];
: "Implementieren" [email protected];
主题: Re: [julia] Schnittstellen für abstrakte Typen (#6975)

@mauro3 , dein Vorschlag @ StefanKarpinskis Idee


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an.

@implement Sehr Entschuldigung; Ich bin mir nicht sicher, wie wir Sie angepingt haben. Wenn Sie es noch nicht wussten, können Sie sich über die Schaltfläche "Abmelden" auf der rechten Seite des Bildschirms aus diesen Benachrichtigungen entfernen.

Nein, ich möchte nur sagen, dass ich dir nicht zu sehr helfen kann, srry zu sagen

------------------ 原始邮件 ------------------
发件人: "pao" [email protected];
Spielzeit: 2014年9月13日(星期六) 9:50
: "JuliaLang/julia" [email protected];
: "Implementieren" [email protected];
主题: Re: [julia] Schnittstellen für abstrakte Typen (#6975)

@implement Sehr Entschuldigung; Ich bin mir nicht sicher, wie wir Sie angepingt haben. Wenn Sie es noch nicht wussten, können Sie sich über die Schaltfläche "Abmelden" auf der rechten Seite des Bildschirms aus diesen Benachrichtigungen entfernen.


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an.

Das erwarten wir von Ihnen nicht! Es war ein Unfall, da wir über ein Julia-Makro mit demselben Namen wie Ihr Benutzername sprechen. Danke!

Ich habe gerade gesehen, dass in Rust einige potenziell interessante Funktionen (vielleicht relevant für dieses Problem) bearbeitet wurden: http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , insbesondere: https ://github.com/rust-lang/rfcs/pull/195

Nachdem ich THTT ("Tim Holy Trait Trick") gesehen habe, habe ich in den letzten Wochen über Interfaces/Traits nachgedacht. Ich habe mir einige Ideen und eine Umsetzung einfallen lassen : Vertrag betrachtet werden, der einen oder mehrere Typen umfasst . Das bedeutet, dass es nicht funktioniert, die Funktionen einer Schnittstelle einfach an einen abstrakten Typ zu binden, wie ich und andere oben vorgeschlagen haben (zumindest nicht im allgemeinen Fall eines Merkmals mit mehreren Typen). Und zweitens sollten Methoden in der Lage sein, Merkmale für den Versand zu verwenden , wie @StefanKarpinski oben vorgeschlagen hat.

Nuff sagte, hier ein Beispiel mit meinem Paket 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

Dies erklärt, dass Eq und Cmp Verträge zwischen den Typen X und Y . Cmp hat Eq als Supermerkmal, dh sowohl Eq als auch Cmp müssen erfüllt sein. Im Hauptteil @traitdef die Funktionssignaturen an, welche Methoden definiert werden müssen. Die Rückgabetypen tun im Moment nichts. Typen müssen kein Merkmal explizit implementieren, es reicht aus, nur die Funktionen zu implementieren. Ich kann überprüfen, ob beispielsweise Cmp{Int,Float64} tatsächlich ein Merkmal ist:

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

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

Eine explizite Trait-Implementierung ist noch nicht im Paket enthalten, sollte aber relativ einfach hinzuzufügen sein.

Eine Funktion, die _trait-dispatch_ verwendet, kann wie folgt definiert werden

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

Dies deklariert eine Funktion ft1 die zwei Argumente mit der Einschränkung akzeptiert, dass ihre Typen Cmp{X,Y} erfüllen müssen. Ich kann eine weitere Methode zum Dispatching für ein anderes Merkmal hinzufügen:

<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

Diese Merkmalsfunktionen können nun wie normale Funktionen aufgerufen werden:

julia> ft1(4,5)
6

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

Das Hinzufügen eines anderen Typs zu einem Merkmal später ist einfach (was bei Unions for ft1 nicht der Fall wäre):

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

Die _Implementierung_ von Trait-Funktionen und deren Verteilung basiert auf Tims Trick und auf inszenierten Funktionen, siehe unten. Die Definition von Eigenschaften ist relativ trivial, siehe hier für eine manuelle Implementierung von allem.

Kurz gesagt, die Zugversendung dreht sich

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

in so etwas (ein bisschen vereinfacht)

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

Im Paket wird die Generierung von checkfn durch Stagedfuncitons automatisiert. Weitere Informationen finden Sie jedoch in der README-Datei von Traits.jl.

_Performance_ Für einfache Trait-Funktionen ist der erzeugte Maschinencode identisch mit ihren Enten-Typ-Gegenstücken, also so gut wie es nur geht. Bei längeren Funktionen gibt es Unterschiede bis zu ~20% in der Länge. Ich bin mir nicht sicher, warum, da ich dachte, dass dies alles weggelassen werden sollte.

(bearbeitet am 27. Oktober, um kleinere Änderungen in Traits.jl widerzuspiegeln)

Ist das Traits.jl-Paket bereit zum Erkunden? In der Readme- Datei steht "Implementiere Schnittstellen mit

Es ist bereit zum Erkunden (einschließlich Fehlern :-). Das fehlende @traitimpl bedeutet nur, dass statt

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

Sie definieren die Funktion(en) einfach manuell

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

für zwei Ihrer Typen T1 und T2 .

Ich habe das Makro @traitimpl hinzugefügt, sodass das obige Beispiel jetzt funktioniert. Ich habe auch die README mit Details zur Verwendung aktualisiert. Und ich habe ein Beispiel hinzugefügt, das einen Teil der @lindahua Graphs.jl-Schnittstelle implementiert:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

Das ist wirklich cool. Mir gefällt besonders, dass es erkennt, dass Schnittstellen im Allgemeinen eine Eigenschaft von Tupeln von Typen sind und nicht von einzelnen Typen.

Das finde ich auch sehr cool. An diesem Ansatz gibt es viel zu mögen. Gute Arbeit.

:+1:

Danke für die guten Rückmeldungen! Ich habe den Code ein wenig aktualisiert / umgestaltet und er sollte einigermaßen fehlerfrei und gut zum Herumspielen sein.
An dieser Stelle wäre es wahrscheinlich gut, wenn die Leute dies ausprobieren könnten, um zu sehen, ob es zu ihren Anwendungsfällen passt.

Dies ist eines dieser Pakete, das den eigenen Code in einem neuen Licht betrachten lässt. Sehr cool.

Tut mir leid, ich hatte noch keine Zeit, mir das ernsthaft anzusehen, aber ich weiß, dass ich, sobald ich es tue, einige Dinge überarbeiten möchte ...

Ich werde meine Pakete auch umgestalten :)

Ich habe mich gewundert, es scheint mir, dass, wenn Merkmale verfügbar sind (und wie im obigen Vorschlag mehrere Zuteilungen möglich sind), kein abstrakter Typhierarchiemechanismus oder überhaupt abstrakte Typen erforderlich sind. Kann das sein?

Nachdem die Merkmale implementiert wurden, würde jede Funktion in der Basis und später im gesamten Ökosystem schließlich eine öffentliche API bereitstellen, die ausschließlich auf Merkmalen basiert, und abstrakte Typen würden verschwinden. Natürlich könnte der Prozess durch veraltete abstrakte Typen katalysiert werden

Wenn man dies etwas genauer betrachtet, würde das Ersetzen abstrakter Typen durch Merkmale eine Parametrisierung von Typen wie folgt erfordern:

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

Ich stimme dem obigen Mauro3-Punkt zu, der mit Eigenschaften (nach seiner Definition, die ich für sehr gut halte) abstrakten Typen entspricht, die

  • Mehrfachvererbung zulassen und
  • generische Funktionen als Felder zulassen

Ich würde auch hinzufügen, dass man, um zuzulassen, dass Typen nach ihrer Definition Merkmale zugewiesen werden, auch "faule Vererbung" zulassen müssen, dh dem Compiler mitteilen, dass ein Typ von einem abstrakten Typ erbt, nachdem er definiert wurde.

Alles in allem scheint es mir also, dass die Entwicklung eines Merkmals-/Schnittstellenkonzepts außerhalb abstrakter Typen zu einer Verdoppelung führen würde, wodurch verschiedene Wege eingeführt würden, um dasselbe zu erreichen. Ich denke, der beste Weg, diese Konzepte einzuführen, besteht darin, abstrakten Typen langsam Funktionen hinzuzufügen

BEARBEITEN : Natürlich müsste das Vererben konkreter Typen von abstrakten zu einem bestimmten Zeitpunkt veraltet und endgültig verboten werden. Typmerkmale würden implizit oder explizit bestimmt, aber niemals durch Vererbung

Sind abstrakte Typen nicht nur ein "langweiliges" Beispiel für Merkmale?

Wenn ja, könnte es möglich sein, die aktuelle Syntax beizubehalten und einfach ihre Bedeutung in ein Merkmal zu ändern (wobei die orthogonale Freiheit usw. gegeben wird, wenn der Benutzer dies wünscht)?

_Ich frage mich, ob dies möglicherweise auch das Point{Float64} <: Pointy{Real} Beispiel anspricht (nicht sicher, ob es eine Problemnummer gibt)?_

Ja, ich denke du hast recht. Trait-Funktionalität kann durch die Verbesserung aktueller abstrakter Julia-Typen erreicht werden. Sie brauchen
1) Mehrfachvererbung
2) Funktionssignaturen
3) "faule Vererbung", um einem bereits definierten Typ explizit ein neues Merkmal zu geben

Scheint viel Arbeit zu sein, aber vielleicht kann dies langsam erwachsen werden, ohne dass die Community viel Schaden nimmt. Also zumindest haben wir das verstanden ;)

Ich denke, was auch immer wir wählen, wird eine große Veränderung sein, an der wir in 0.4 noch nicht arbeiten können. Wenn ich raten müsste, würde ich wetten, dass wir uns eher in Richtung Merkmale bewegen als in Richtung der traditionellen Mehrfachvererbung. Aber meine Kristallkugel ist auf der Kippe, daher ist es schwer zu sagen, was passieren wird, ohne nur Sachen auszuprobieren.

FWIW, ich fand Simon Peyton-Jones' Diskussion über Typklassen im folgenden Vortrag sehr informativ darüber, wie man so etwas wie Traits anstelle von Subtyping verwendet: http://research.microsoft.com/en-us/um/people/simonpj/ papers/haskell-retrospective/ECOOP-Juli09.pdf

Ja, eine ganze Dose Würmer!

@johnmyleswhite , danke für den Link, sehr interessant. Hier ein Link zum Video davon, das es sich lohnt anzusehen, um die Lücken zu füllen. Diese Präsentation scheint viele Fragen zu berühren, die wir hier haben. Und interessanterweise ist die Implementierung von Typklassen ziemlich ähnlich zu der in Traits.jl (Tims Trick, dass Traits Datentypen sind). Haskells https://www.haskell.org/haskellwiki/Multi-parameter_type_class ist Traits.jl sehr ähnlich. Eine seiner Fragen im Vortrag lautet: "Wenn wir die Generika erst einmal von ganzem Herzen übernommen haben, brauchen wir wirklich noch Subtyping?" (Generika sind parametrisch-polymorphe Funktionen, denke ich, siehe ) Worüber @skariel und @hayd oben

In Bezug auf @skariel und @hayd denke ich, dass Einzelparameter-Traits (wie in Traits.jl) in der Tat abstrakten Typen sehr ähnlich sind, außer dass sie eine andere Hierarchie haben können, dh mehrfache Vererbung.

Aber Multiparameter-Merkmale scheinen etwas anders zu sein, zumindest waren sie in meinem Kopf. Wie ich sie gesehen habe, scheinen Typparameter abstrakter Typen hauptsächlich davon zu sein, welche anderen Typen in einem Typ enthalten sind, z. B. sagt Associative{Int,String} , dass das Diktat Int Schlüssel und String Werte. Während Tr{Associative,Int,String}... sagt, dass es einen "Vertrag" zwischen Associative , Int s und Strings . Aber dann sollte Associative{Int,String} vielleicht auch so gelesen werden, dh es gibt Methoden wie getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@mauro3 Wichtig wäre, Objekte vom Typ Associative als Argument an eine Funktion zu übergeben, damit diese dann Associative{Int,String} selbst erzeugen kann:

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

Sie würden dies zB als f(Dict) .

@eschnett , sorry, ich verstehe nicht was du meinst.

@mauro3 Ich glaube, ich habe zu kompliziert gedacht; ignoriere mich.

Ich habe Traits.jl aktualisiert mit:

  • Auflösung von Merkmalsmehrdeutigkeiten
  • zugehörige Typen
  • Verwenden von @doc für Hilfe
  • besseres Testen von merkmalsspezifischen Methoden

Weitere Informationen finden Sie unter https://github.com/mauro3/Traits.jl/blob/master/NEWS.md . Rückmeldung willkommen!

@Rory-Finnegan hat ein Schnittstellenpaket zusammengestellt https://github.com/Rory-Finnegan/Interfaces.jl

Ich habe dies kürzlich mit @mdcfrancis besprochen und wir denken, dass etwas, das den Protokollen von Clojure ähnelt, einfach und praktisch wäre. Die grundlegenden Funktionen sind (1) Protokolle sind eine neue Art von Typ, (2) Sie definieren sie, indem Sie einige Methodensignaturen auflisten, (3) andere Typen implementieren sie implizit, indem sie nur übereinstimmende Methodendefinitionen haben. Du würdest zB schreiben

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

und wir haben isa(Iterable, Protocol) und Protocol <: Type . Diese können Sie selbstverständlich versenden. Ob ein Typ ein Protokoll implementiert, können Sie mit T <: Iterable überprüfen.

Hier sind die Subtyping-Regeln:

seien P, Q Protokolltypen
sei T ein Nicht-Protokolltyp

| Eingabe | Ergebnis |
| --- | --- |
| P <: Beliebig | wahr |
| Unten <: P | wahr |
| (union,unionall,var) <: P | Verwenden Sie die normale Regel; P als Basistyp behandeln |
| P <: (union,unionall,var) | benutze normale Regel |
| P <: P | wahr |
| P <: Q | Prüfmethoden(Q) <: Methoden(P) |
| P <: T | falsch |
| T <: P | Die Methoden von P existieren mit T ersetzt für _ |

Die letzte ist die große: Um T <: P zu testen, ersetzen Sie T durch _ in der Definition von P und prüfen method_exists für jede Signatur. Natürlich bedeutet dies an sich, dass Fallback-Definitionen, die "Sie müssen dies implementieren"-Fehler auslösen, eine sehr schlechte Sache werden. Hoffentlich ist das eher ein kosmetisches Problem.

Ein weiteres Problem ist, dass diese Definition zirkulär ist, wenn zB start(::Iterable) definiert ist. Eine solche Definition ist nicht wirklich sinnvoll. Wir könnten dies irgendwie verhindern oder diesen Zyklus während der Untertypprüfung erkennen. Ich bin mir nicht 100% sicher, ob die einfache Zykluserkennung das Problem behebt, aber es scheint plausibel.

Für die Typüberschneidung gilt:

| Eingabe | Ergebnis |
| --- | --- |
| P ∩ (union,unionall,tvar) | benutze normale Regel |
| P ∩ Q | P |
| P ∩ T | T |

Es gibt mehrere Optionen für P ∩ Q:

  1. Übernähern durch Rückgabe von P oder Q (zB je nachdem, was lexikographisch zuerst eintritt). Dies ist in Bezug auf die Typinferenz in Ordnung, kann aber an anderer Stelle ärgerlich sein.
  2. Geben Sie ein neues Ad-hoc-Protokoll zurück, das die Vereinigung der Signaturen in P und Q enthält.
  3. Kreuzungstypen. Vielleicht nur auf Protokolle beschränkt.

P ∩ T ist knifflig. T ist eine gute konservative Annäherung, da Nicht-Protokolltypen "kleiner" sind als Protokolltypen in dem Sinne, dass sie Sie auf einen Bereich der Typhierarchie beschränken, während Protokolltypen dies nicht tun (da überhaupt jeder Typ jedes Protokoll implementieren kann). ). Um es besser zu machen, scheinen allgemeine Schnitttypen erforderlich zu sein, die ich bei der anfänglichen Implementierung eher vermeiden würde, da dies eine Überarbeitung des Subtyping-Algorithmus erfordert und eine Wurmdose nach der anderen öffnet.

Spezifität: P ist nur dann spezifischer als Q, wenn P<:Q. aber da P ∩ Q immer nicht leer ist, sind Definitionen mit unterschiedlichen Protokollen im selben Slot oft mehrdeutig, was so aussieht, wie Sie es wollen (zB würden Sie sagen "wenn x iterierbar ist, tun Sie dies, aber wenn x druckbar ist, tun Sie es" das").
Es gibt jedoch keine praktische Möglichkeit, die erforderliche eindeutige Definition auszudrücken, daher sollte dies möglicherweise ein Fehler sein.

Nach #13412 kann ein Protokoll als UnionAll _ über eine Union von Tupeltypen "kodiert" werden (wobei das erste Element jedes inneren Tupels der Typ der fraglichen Funktion ist). Das ist ein Vorteil dieses Designs, der mir vorher nicht in den Sinn gekommen ist. Zum Beispiel scheint die strukturelle Untertypisierung von Protokollen einfach automatisch herauszufallen.

Natürlich sind diese Protokolle der "Single-Parameter"-Stil. Ich mag die Einfachheit und bin mir nicht sicher, wie man Gruppen von Typen so elegant wie T <: Iterable .

Zu dieser Idee gab es in der Vergangenheit einige Kommentare, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516.

Würden wir unterstützen, zB

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

Wow, das gefällt mir wirklich (besonders mit der Erweiterung von @Keno )!

+1 Genau das will ich!

@Keno Das ist definitiv ein schöner Upgrade-Pfad für diese Funktion, aber es gibt Gründe, sie zu verschieben. Alles, was Rückgabearten betrifft, ist natürlich sehr problematisch. Der Parameter selbst ist konzeptionell in Ordnung und wäre großartig, ist aber etwas schwierig zu implementieren. Es erfordert die Aufrechterhaltung einer Typumgebung um den Prozess herum, die prüft, ob alle Methoden vorhanden sind.

Es scheint, als könnten Sie Merkmale (wie die lineare Indizierung von O(1) für Array-ähnliche Typen) in dieses Schema integrieren. Sie definieren eine Dummy-Methode wie hassomeproperty(::T) = true (aber _nicht_ hassomeproperty(::Any) = false ) und haben dann

protocol MyProperty
hassomeproperty(::_)
end

Könnte _ mehrmals in derselben Methode in der Protokolldefinition vorkommen, wie

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

Könnte _ mehrmals in derselben Methode in der Protokolldefinition vorkommen

Ja. Geben Sie einfach den Kandidatentyp für jede Instanz von _ .

@JeffBezanson freut sich sehr darauf. Besonders hervorzuheben ist für mich die „Abgelegenheit“ des Protokolls. Dadurch kann ich ein spezifisches/benutzerdefiniertes Protokoll für einen Typ implementieren, ohne dass der Autor des Typs Kenntnis von der Existenz des Protokolls hat.

Wie sieht es damit aus, dass Methoden jederzeit dynamisch definiert werden können (zB mit @eval )? Ob ein Typ ein Untertyp eines bestimmten Protokolls ist, ist im Allgemeinen statisch nicht erkennbar, was in vielen Fällen Optimierungen zunichte machen würde, die den dynamischen Versand vermeiden.

Ja, das macht #265 noch schlimmer :) Es ist das gleiche Problem, bei dem sich Dispatch und generierter Code ändern müssen, wenn Methoden hinzugefügt werden, nur mit mehr Abhängigkeitskanten.

Es ist gut zu sehen, wie sich das entwickelt! Natürlich wäre ich derjenige, der argumentiert, dass Multiparameter-Merkmale der richtige Weg sind. Aber 95 % der Merkmale wären wahrscheinlich sowieso ein einzelner Parameter. Nur würden sie bei Mehrfachversand so gut passen! Dies könnte wahrscheinlich später noch einmal überprüft werden, wenn es sein muss. Genug gesagt.

Ein paar Kommentare:

Der Vorschlag von @Keno (und eigentlich state in Jeffs Original) wird als assoziierte Typen bezeichnet. Beachten Sie, dass sie auch ohne Rückgabetypen nützlich sind. Rust hat eine anständige manuelle Eingabe . Ich denke, sie sind eine gute Idee, wenn auch nicht so notwendig wie in Rust. Ich glaube jedoch nicht, dass es ein Parameter des Merkmals sein sollte: Wenn ich eine Funktion definiere, die auf Iterable würde ich nicht wissen, was T ist.

Meiner Erfahrung nach ist method_exists in seiner jetzigen Form dafür unbrauchbar (#8959). Aber vermutlich wird dies in #8974 (oder damit) behoben. Ich fand das Abgleichen von Methodensignaturen mit Trait-Sigantures der schwierigste Teil bei der Erstellung von Traits.jl, insbesondere um parametrisierte und vararg-Funktionen zu berücksichtigen ( siehe ).

Vermutlich wäre auch eine Vererbung möglich?

Ich würde wirklich gerne einen Mechanismus sehen, der die Definition von Standardimplementierungen ermöglicht. Der Klassiker ist, dass Sie zum Vergleich nur zwei von = , < , > , <= , >= . Vielleicht ist hier der von Jeff erwähnte Zyklus tatsächlich nützlich. Um das obige Beispiel fortzusetzen, würde die Definition von start(::Indexable) = 1 und done(i::Indexable,state)=length(i)==state diese zu den Standardeinstellungen machen. Daher müssten viele Typen nur next .

Gute Argumente. Ich denke, die zugeordneten Typen unterscheiden sich etwas vom Parameter in Iterable{T} . In meiner Codierung würde der Parameter alles darin existentiell quantifizieren --- "Gibt es ein T, so dass der Typ Foo dieses Protokoll implementiert?".

Ja, anscheinend könnten wir protocol Foo <: Bar, Baz problemlos zulassen und einfach die Unterschriften von Bar und Baz in Foo kopieren.

Multiparameter-Merkmale sind definitiv mächtig. Ich denke, es ist sehr interessant, darüber nachzudenken, wie man sie mit Subtyping integriert. Sie könnten etwas wie TypePair{A,B} <: Trait , aber das scheint nicht ganz richtig zu sein.

Ich denke, Ihr Vorschlag (in Bezug auf die Funktionen) ähnelt eher Swift als Clojure.

Es erscheint seltsam (und ich denke, eine Quelle zukünftiger Verwirrung), nominale (Typen) und strukturelle (Protokoll) Subtypisierungen zu mischen (aber ich denke, das ist unvermeidlich).

Ich bin auch etwas skeptisch gegenüber der Ausdruckskraft von Protokollen für Mathe-/Matrix-Operationen. Ich denke, das Durchdenken komplizierterer Beispiele (Matrixoperationen) wäre aufschlussreicher als Iteration, die eine klar spezifizierte Schnittstelle hat. Siehe zum Beispiel die Bibliothek core.matrix .

Ich stimme zu; An dieser Stelle sollten wir Beispiele für Protokolle sammeln und sehen, ob sie das tun, was wir wollen.

Wären Protokolle nach Ihrer Vorstellung Namensräume, zu denen ihre Methoden gehören? Dh wenn du schreibst

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

es wäre naheliegend, die generischen Funktionen start , done und next und ihre vollqualifizierten Namen Iterable.start , Iterable.done und Iterable.next . Ein Typ würde Iterable implementieren, aber alle generischen Funktionen im Iterable Protokoll implementieren. Ich habe vor einiger Zeit etwas sehr Ähnliches vorgeschlagen (kann es jetzt nicht finden), aber die andere Seite ist, dass Sie, wenn Sie ein Protokoll implementieren möchten, Folgendes tun:

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

Dies würde der von @mdcfrancis erwähnten " entgegenwirken , wenn ich es verstehe, aber implement Blöcke fast alle Notwendigkeiten beseitigen würden, import anstelle von using , was ein großer Vorteil wäre.

Ich habe vor einiger Zeit etwas sehr ähnliches vorgeschlagen (kann es jetzt nicht finden)

Vielleicht https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 und früher https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Bearbeiten: Auch https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

Ja, das ist es.

@StefanKarpinski kurze Kommentare,

  • alle Klassen, die derzeit iterable implementieren, müssen modifiziert werden, um das Protokoll explizit zu implementieren. Wenn wir so vorgehen, wie Sie es vorschlagen, wird der aktuelle Vorschlag durch einfaches Hinzufügen der Definition zu base alle vorhandenen Klassen in das Protokoll 'heben'.
  • Wenn ich MyModule.MySuperIterable definiere, das der iterierbaren Definition eine zusätzliche Funktion hinzufügt, müsste ich eine Menge Boilerplate-Code für jede Klasse schreiben, anstatt die eine zusätzliche Methode hinzuzufügen.
  • Ich glaube nicht, dass das, was Sie vorschlagen, der Abgeschiedenheit entgegenwirkt, es bedeutet nur, dass ich eine Menge zusätzlichen Code schreiben müsste, um das gleiche Ziel zu erreichen.

Wenn eine Art Vererbung von Protokollen erlaubt wäre, MySuperIterabe,
Base.Iterable erweitern könnte, um die bestehenden Methoden wiederzuverwenden.

Das Problem wäre, wenn Sie nur eine Auswahl der Methoden in a
Protokoll, aber das scheint darauf hinzudeuten, dass das ursprüngliche Protokoll
von Anfang an ein zusammengesetztes Protokoll sein.

@mdcfrancis – der erste Punkt ist gut, obwohl das, was ich vorschlage, keinen bestehenden Code brechen würde, es würde nur bedeuten, dass der Code der Leute Protokolle für ihre Typen "zustimmen" müsste, bevor sie sich auf den Versand verlassen können Arbeiten.

Können Sie den Punkt MyModule.MySuperIterable erweitern? Ich verstehe nicht, woher die zusätzliche Ausführlichkeit kommt. Sie könnten zum Beispiel so etwas haben:

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

Was @ivarne im Wesentlichen gesagt hat.

In meinem obigen spezifischen Design sind Protokolle keine Namensräume, sondern nur Aussagen über andere Typen und Funktionen. Dies liegt jedoch wahrscheinlich daran, dass ich mich auf das Kerntypsystem konzentriere. Ich könnte mir Syntaxzucker vorstellen, der sich auf eine Kombination von Modulen und Protokollen erweitert, zB

module Iterable

function start end
function done end
function next end

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

end

In Kontexten, in denen Iterable als Typ behandelt wird, verwenden wir dann Iterable.the_protocol .

Ich mag diese Perspektive, weil Jeff/mdcfrancis-Protokolle sich hier sehr orthogonal zu allem anderen anfühlen. Das leichte Gefühl, nicht sagen zu müssen "X implementiert Protokoll Y", es sei denn, Sie möchten sich für mich "julianisch" anfühlen.

Ich weiß nicht, warum und wann ich diese Ausgabe abonniert habe. Aber es kommt vor, dass dieser Protokollvorschlag die Frage, die ich hier aufgeworfen

Ich habe technisch nichts hinzuzufügen, aber als Beispiel für die Verwendung von "Protokollen" in der Wildnis in Julia (sozusagen) wäre JuMP, das die Funktionalität eines Solvers bestimmt, z.

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, das ist nützlich. Reicht es aus, dass m.internalModel das Protokoll implementiert, oder sind beide Argumente wichtig?

Ja, m.internalModel , um das Protokoll zu implementieren. Die anderen Argumente sind meist nur Vektoren.

Ja, ausreichend für m.internalModel , um das Protokoll zu implementieren

Ein guter Weg, um Beispiele für Protokolle in freier Wildbahn zu finden, ist wahrscheinlich die Suche nach applicable und method_exists Aufrufen.

Elixir scheint auch Protokolle zu implementieren, aber die Anzahl der Protokolle in der Standardbibliothek (von der Definition abgesehen) scheint ziemlich begrenzt zu sein.

Wie wäre die Beziehung zwischen Protokollen und abstrakten Typen? Die ursprüngliche Problembeschreibung schlug etwa vor, einem abstrakten Typ ein Protokoll anzuhängen. Tatsächlich scheint es mir, dass die meisten (jetzt informellen) Protokolle derzeit als abstrakte Typen implementiert sind. Wofür würden abstrakte Typen verwendet, wenn die Unterstützung für Protokolle hinzugefügt wird? Eine Typhierarchie ohne eine Möglichkeit, ihre API zu deklarieren, klingt nicht allzu nützlich.

Sehr gute Frage. Da gibt es viele Möglichkeiten. Zunächst ist es wichtig, darauf hinzuweisen, dass abstrakte Typen und Protokolle recht orthogonal sind, obwohl sie beide Möglichkeiten zum Gruppieren von Objekten darstellen. Abstrakte Typen sind rein nominell; sie kennzeichnen Objekte als zur Menge gehörend. Protokolle sind rein strukturell; ein Objekt gehört zur Menge, wenn es zufällig bestimmte Eigenschaften hat. Einige Optionen sind also

  1. Habe einfach beides.
  2. In der Lage sein, Protokolle einem abstrakten Typ zuzuordnen, z. B. damit ein Typ, der sich selbst als Untertyp deklariert, auf Übereinstimmung mit dem/den Protokoll(en) überprüft wird.
  3. Entfernen Sie abstrakte Typen vollständig.

Wenn wir so etwas wie (2) haben, ist es meiner Meinung nach wichtig zu erkennen, dass es sich nicht wirklich um ein einzelnes Merkmal handelt, sondern um eine Kombination aus nominaler und struktureller Typisierung.

Eine Sache, für die abstrakte Typen nützlich erscheinen, sind ihre Parameter, zum Beispiel das Schreiben von convert(AbstractArray{Int}, x) . Wäre AbstractArray ein Protokoll, müsste der Elementtyp Int in der Protokolldefinition nicht unbedingt erwähnt werden. Es sind zusätzliche Informationen über den Typ, _abgesehen_ von denen Methoden erforderlich sind. AbstractArray{T} und AbstractArray{S} wären also trotz der Angabe derselben Methoden immer noch unterschiedliche Typen, daher haben wir die nominale Typisierung wieder eingeführt. Diese Verwendung von Typparametern scheint also eine nominale Typisierung zu erfordern.

Würde 2. uns also eine mehrfache abstrakte Vererbung geben?

Würde 2. uns also eine mehrfache abstrakte Vererbung geben?

Nein. Es wäre eine Möglichkeit, die Funktionen zu integrieren oder zu kombinieren, aber jede Funktion hätte immer noch die Eigenschaften, die sie jetzt hat.

Ich sollte hinzufügen, dass die Zulassung mehrfacher abstrakter Vererbung eine weitere fast orthogonale Designentscheidung ist. In jedem Fall besteht das Problem bei der zu starken Verwendung abstrakter nominaler Typen darin, dass (1) Sie möglicherweise die nachträgliche Implementierung von Protokollen verlieren (Person A definiert den Typ, Person B definiert das Protokoll und seine Implementierung für A), (2) Sie könnten die strukturelle Subtypisierung von Protokollen verlieren.

Sind die Typparameter im aktuellen System nicht irgendwie Teil der impliziten Schnittstelle? Diese Definition beruht zum Beispiel darauf: ndims{T,n}(::AbstractArray{T,n}) = n und viele benutzerdefinierte Funktionen tun es auch.

In einem neuen Protokoll + abstrakten Vererbungssystem hätten wir also AbstractArray{T,N} und ProtoAbstractArray . Nun müsste ein Typ, der nominell kein AbstractArray , in der Lage sein, anzugeben, was die Parameter T und N sind, vermutlich durch Hartcodierung von eltype und ndims . Dann müssten alle parametrisierten Funktionen auf AbstractArray s neu geschrieben werden, um eltype und ndims anstelle von Parametern zu verwenden. Vielleicht wäre es also sinnvoller, dass das Protokoll auch die Parameter trägt, sodass zugehörige Typen doch sehr nützlich sein könnten. (Beachten Sie, dass konkrete Typen immer noch Parameter benötigen würden.)

Auch eine Gruppierung von Typen in ein Protokoll @malmaud ‚s Trick: https://github.com/JuliaLang/julia/issues/6975#issuecomment -161.056.795 ist vergleichbar mit der nominalen Typisierung: die Gruppierung ist ausschließlich auf Arten Kommissionierung und die Typen haben keine (benutzbare) Schnittstelle. Vielleicht überschneiden sich abstrakte Typen und Protokolle also ziemlich?

Ja, die Parameter eines abstrakten Typs sind definitiv eine Art Schnittstelle und mit eltype und ndims teilweise überflüssig. Der Hauptunterschied scheint darin zu bestehen, dass Sie direkt auf sie senden können, ohne einen zusätzlichen Methodenaufruf. Ich stimme zu, dass wir mit assoziierten Typen viel näher daran wären, abstrakte Typen durch Protokolle/Eigenschaften zu ersetzen. Wie könnte die Syntax aussehen? Im Idealfall wäre es schwächer als der Methodenaufruf, da ich lieber keine zirkuläre Abhängigkeit zwischen Untertypisierung und Methodenaufruf haben möchte.

Die verbleibende Frage ist, ob es sinnvoll ist, ein Protokoll zu implementieren, _ohne_ Teil des zugehörigen abstrakten Typs zu werden. Ein Beispiel könnten Zeichenfolgen sein, die iterierbar und indiziert werden können, aber oft als "skalare" Mengen anstelle von Containern behandelt werden. Ich weiß nicht, wie oft das vorkommt.

Ich glaube, ich verstehe deine "Methodenaufrufe"-Aussage nicht ganz. Dieser Vorschlag für die Syntax entspricht möglicherweise nicht dem, wonach Sie gefragt haben:

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

Das könnte funktionieren, je nachdem, wie die Untertypisierung von Protokolltypen definiert ist. Zum Beispiel gegeben

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

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

Haben wir PAbstractArray{Int,1} <: Indexable{Int} ? Ich denke, dies könnte sehr gut funktionieren, wenn die Parameter namentlich übereinstimmen. Wir könnten vielleicht auch die Definition automatisieren, die dazu führt, dass eltype(x) den Parameter eltype vom Typ von x zurückgibt.

Ich mag es nicht besonders, Methodendefinitionen in einen impl Block einzufügen, hauptsächlich weil eine einzelne Methodendefinition zu mehreren Protokollen gehören kann.

Es sieht also so aus, als würden wir mit einem solchen Mechanismus keine abstrakten Typen mehr benötigen. AbstractArray{T,N} könnte ein Protokoll werden. Dann erhalten wir automatisch Mehrfachvererbung (von Protokollen). Auch die Unmöglichkeit, von konkreten Typen zu erben (was wir manchmal von Neulingen hören), ist offensichtlich, da nur die Protokollvererbung unterstützt würde.

Abgesehen davon: Es wäre wirklich schön, das Merkmal Callable ausdrücken zu können. Es müsste ungefähr so ​​aussehen:

protocol Callable
    ::TupleCons{_, Bottom}
end

wobei TupleCons separat auf das erste Element eines Tupels und den Rest der Elemente passt. Die Idee ist, dass dies übereinstimmt, solange die Methodentabelle für _ nicht leer ist (Bottom ist ein Untertyp jedes Argumenttupeltyps). Tatsächlich möchten wir vielleicht eine Tuple{a,b} Syntax für TupleCons{a, TupleCons{b, EmptyTuple}} erstellen (siehe auch #11242).

Ich glaube nicht, dass das stimmt, alle Typparameter sind existentiell _mit Einschränkungen_ quantifiziert, sodass abstrakte Typen und Protokolle nicht direkt austauschbar sind.

@jakebolewski fällt dir ein Beispiel ein? Offensichtlich werden sie nie genau dasselbe sein; Ich würde sagen, die Frage ist eher, ob wir einen so massieren können, dass wir ohne beide auskommen.

Vielleicht übersehe ich den Punkt, aber wie können Protokolle mäßig komplexe abstrakte Typen mit Einschränkungen codieren, wie zum Beispiel:

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

ohne alle Möglichkeiten nominell aufzählen zu müssen?

Der vorgeschlagene Protocol Vorschlag ist im Vergleich zur abstrakten Subtypisierung streng weniger ausdrucksstark, was alles ist, was ich hervorheben wollte.

Folgendes könnte ich mir vorstellen (das Design natürlich bis an seine praktischen Grenzen strecken):

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

einhergehend mit der Beobachtung, dass wir so etwas wie assoziierte Typen oder benannte Typeigenschaften brauchen, um der Ausdruckskraft existierender abstrakter Typen zu entsprechen. Damit könnten wir möglicherweise eine nahezu Kompatibilität haben:

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

Strukturelles Subtyping für die Datenfelder von Objekten schien mir nie sehr nützlich, aber auf die Eigenschaften von _types_ angewendet scheint es sehr sinnvoll zu sein.

Mir wurde auch klar, dass dies eine Fluchtmöglichkeit vor Mehrdeutigkeitsproblemen bieten kann: Die Schnittmenge zweier Typen ist leer, wenn sie für einen Parameter widersprüchliche Werte haben. Wenn wir also einen eindeutigen Number Typ wollen, könnten wir das haben

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

Dabei wird super nur als eine weitere Typeigenschaft angesehen.

Ich mag die vorgeschlagene Protokollsyntax, aber ich habe einige Anmerkungen.

Aber dann kann ich alles falsch verstehen. Ich habe erst vor kurzem angefangen, Julia wirklich als etwas zu betrachten, an dem ich arbeiten möchte, und ich habe noch kein perfektes Verständnis des Typsystems.

(a) Ich denke, es wäre interessanter mit den Merkmalsfunktionen, an denen @mauro3 oben gearbeitet hat. Vor allem, denn was nützt der Mehrfachversand, wenn Sie nicht mehrere Versandprotokolle haben können! Ich schreibe später meine Ansicht darüber auf, was ein Beispiel aus der realen Welt ist. Aber die allgemeine Kernaussage lautet: "Gibt es ein Verhalten, das es diesen beiden Objekten ermöglicht, zu interagieren?" Ich kann mich irren, und all das kann in Protokollen eingeschlossen werden, beispielsweise durch:

protocol Foo{bar}
    ...
end

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

Und das enthüllt auch das Hauptproblem, dass das Foo-Protokoll nicht in derselben Definition auf das Bar-Protokoll verweisen kann.

(B)

haben wir PAAbstractArray{Int,1} <: Indexable{Int} ? Ich denke, dies könnte sehr gut funktionieren, wenn die Parameter namentlich übereinstimmen.

Ich bin mir nicht sicher, warum wir die Parameter nach _name_ abgleichen müssen (ich nehme an, dass dies die eltype Namen sind, wenn ich mich falsch verstanden habe, ignoriere bitte diesen Abschnitt). Warum nicht einfach die potenziellen Funktionssignaturen abgleichen. Mein Hauptproblem bei der Benennung besteht darin, dass Folgendes verhindert wird:

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

Auf der anderen Seite stellt es sicher, dass Ihr Protokoll nur die spezifische Typhierarchie offenlegt, die wir auch wollen. Wenn wir keine Namensübereinstimmung mit Iterable angeben, profitieren wir nicht von der Implementierung von iterable (und ziehen auch keinen Vorteil in der Abhängigkeit). Aber ich bin mir nicht sicher, was ein Benutzer davon _gewinnen_ kann, abgesehen von der Möglichkeit, Folgendes zu tun ...

(c) Vielleicht übersehe ich etwas, aber ist der Hauptzweck, bei dem benannte Typen nützlich sind, nicht die Beschreibung des Verhaltens verschiedener Teile einer Obermenge? Betrachten Sie die Number Hierarchie und die abstrakten Typen Signed und Unsigned , beide würden das Integer Protokoll implementieren, sich aber manchmal ganz anders verhalten. Um zwischen ihnen zu unterscheiden, müssen wir jetzt ein spezielles negate nur für Signed Typen definieren (besonders schwierig ohne Rückgabetypen, wo wir eigentlich einen Unsigned Typ negieren wollen)?

Ich denke, dies ist das Problem, das Sie im Beispiel super = Number . Wenn wir bitstype Int16 <: Signed deklarieren (meine andere Frage ist sogar, wie Number oder Signed als Protokolle mit ihren Typeigenschaften auf den konkreten Typ angewendet werden?), hängt das die Typeigenschaften von . an? das Signed ( super = Signed ) Protokoll, das es als unterschiedlich von den Typen markiert, die durch das Unsigned Protokoll gekennzeichnet sind? Weil das aus meiner Sicht eine seltsame Lösung ist, und nicht nur, weil ich die benannten Typparameter seltsam finde. Wenn zwei Protokolle mit Ausnahme des Typs, den sie in super platziert haben, genau übereinstimmen, wie unterscheiden sie sich dann überhaupt? Und wenn der Unterschied im Verhalten zwischen Teilmengen eines größeren Typs (des Protokolls) liegt, erfinden wir dann nicht wirklich nur den Zweck abstrakter Typen neu?

(d) Das Problem ist, dass wir wollen, dass abstrakte Typen zwischen Verhalten unterscheiden, und wir wollen, dass Protokolle bestimmte Fähigkeiten (oft unabhängig von anderem Verhalten) sicherstellen, durch die Verhaltensweisen ausgedrückt werden. Aber wir versuchen, die Fähigkeiten zu vervollständigen, die uns Protokolle ermöglichen, und die Verhaltensweisen abstrakter Typen aufzuteilen.

Die Lösung, zu der wir oft springen, ist in der Art von "Typen erklären ihre Absicht, eine abstrakte Klasse zu implementieren und auf Konformität zu prüfen", was bei der Implementierung problematisch ist (zirkuläre Referenzen, die zum Hinzufügen von Funktionsdefinitionen innerhalb des Typblocks oder impl Blöcke) und entfernt die gute Qualität von Protokollen, die auf dem aktuellen Satz von Methoden und den Typen, auf denen sie arbeiten, basieren. Diese Probleme schließen es sowieso aus, Protokolle in die abstrakte Hierarchie einzuordnen.

Aber noch wichtiger ist, dass Protokolle kein Verhalten beschreiben, sondern komplexe Fähigkeiten über mehrere Funktionen hinweg (wie Iteration). Das Verhalten dieser Iteration wird durch die abstrakten Typen beschrieben (ob sortiert oder sogar geordnet). Auf der anderen Seite ist die Kombination von Protokoll + abstraktem Typ nützlich, sobald wir einen tatsächlichen Typ in die Finger bekommen, da sie es uns ermöglicht, Fähigkeiten (Methoden des Fähigkeitsdienstprogramms), Verhaltensweisen (High-Level-Methoden) oder beides (Implementierungsdetails) zu verteilen Methoden).

(e) Wenn wir Protokollen erlauben, mehrere Protokolle zu erben (sie sind im Grunde ohnehin strukturell) und so viele abstrakte Typen wie konkrete Typen (zB ohne mehrfache abstrakte Vererbung, eins), können wir die Erzeugung reiner Protokolltypen, reiner abstrakter Typen, und Protokoll + abstrakte Typen.

Ich glaube, dies behebt das obige Problem Signed vs. Unsigned :

  • Definieren Sie zwei Protokolle, die beide vom allgemeinen IntegerProtocol erben (jegliche Protokollstruktur erben, NumberAddingProtocol , IntegerSteppingProtocol usw.), eines von AbstractSignedInteger und das andere von AbstractUnsignedInteger ).
  • Dann wird einem Benutzer des Typs Signed sowohl Funktionalität (aus dem Protokoll) als auch Verhalten (aus der abstrakten Hierarchie) garantiert.
  • Ein konkreter Typ AbstractSignedInteger ohne die Protokolle ist _jedenfalls_ nicht verwendbar.
  • Aber interessanterweise (und als zukünftiges Feature bereits oben erwähnt) könnten wir irgendwann die Fähigkeit schaffen, nach fehlenden Features zu suchen, wenn nur IntegerSteppingProtocol (was trivial und im Grunde nur ein Alias ​​für eine einzelne Funktion ist) für a existierte gegebenen konkreten AbstractUnsignedInteger könnten wir versuchen, für Signed indem wir die anderen Protokolle entsprechend implementieren. Vielleicht sogar mit so etwas wie convert .

Während alle vorhandenen Typen beibehalten werden, indem die meisten von ihnen in Protokoll- und abstrakte Typen umgewandelt werden, und einige als reine abstrakte Typen belassen werden.

Bearbeiten: (f) Syntaxbeispiel (einschließlich Teil (a) ).

Edit 2 : Einige Fehler korrigiert ( :< statt <: ), eine schlechte Wahl behoben ( Foo statt ::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}.

Ich sehe Probleme mit dieser Syntax als:

  • Anonyme interne Typen des Protokolls (zB die Zustandsvariablen).
  • Rückgabetypen.
  • Schwierig, die Semantik effizient zu implementieren.

_Abstrakter Typ_ definiert, was eine Entität ist . _Protokoll_ definiert, was eine Entität tut . Innerhalb eines einzelnen Pakets sind diese beiden Konzepte austauschbar: Eine Entität _ist_ was sie _macht_. Und "abstrakter Typ" ist direkter. Zwischen zwei Paketen gibt es jedoch einen Unterschied: Sie benötigen nicht das, was Ihr Client "ist", sondern Sie benötigen das, was Ihr Client "tut". Hier gibt "abstrakter Typ" keine Auskunft darüber.

Meiner Meinung nach ist ein Protokoll ein einzelner gesendeter abstrakter Typ. Es kann die Paketerweiterung und Zusammenarbeit unterstützen. Verwenden Sie daher innerhalb eines einzelnen Pakets, bei dem die Entitäten eng miteinander verbunden sind, abstrakte Typen, um die Entwicklung zu erleichtern (indem Sie von mehreren Dispatchings profitieren); zwischen Paketen, bei denen die Entitäten unabhängiger sind, verwenden Sie das Protokoll, um die Gefährdung der Implementierung zu reduzieren.

@mason-bially

Ich bin mir nicht sicher, warum wir die Parameter nach Namen abgleichen müssen

Ich meine die Übereinstimmung mit dem Namen _im Gegensatz zu der Übereinstimmung mit der Position. Diese Namen würden sich wie strukturell subtypisierte Datensätze verhalten. Wenn wir haben

protocol Collection{T}
    eltype = T
end

dann ist alles mit einer Eigenschaft namens eltype ein Untertyp von Collection . Die Reihenfolge und Position dieser "Parameter" spielt keine Rolle.

Wenn zwei Protokolle mit Ausnahme des Typs, den sie in super platziert haben, genau übereinstimmen, wie unterscheiden sie sich dann überhaupt? Und wenn der Unterschied im Verhalten zwischen Teilmengen eines größeren Typs (des Protokolls) liegt, erfinden wir dann nicht wirklich nur den Zweck abstrakter Typen neu?

Das ist ein gerechter Punkt. Die genannten Parameter bringen tatsächlich viele der Eigenschaften abstrakter Typen zurück. Ich begann mit der Idee, dass wir möglicherweise sowohl Protokolle als auch abstrakte Typen benötigen, und versuchte dann, die Funktionen zu vereinheitlichen und zu verallgemeinern. Immerhin, wenn Sie derzeit type Foo <: Bar deklarieren, haben Sie in gewisser Weise tatsächlich Foo.super === Bar . Vielleicht sollten wir dies also direkt unterstützen, zusammen mit anderen Schlüssel/Wert-Paaren, die Sie möglicherweise zuordnen möchten.

"haben Typen, die ihre Absicht erklären, eine abstrakte Klasse zu implementieren und auf Konformität zu prüfen"

Ja, ich bin dagegen, diesen Ansatz zum Kernfeature zu machen.

Wenn wir zulassen, dass Protokolle mehrere Protokolle erben ... und so viele abstrakte Typen

Bedeutet dies, dass zB "T ein Untertyp von Protokoll P ist, wenn es die Methoden x, y, z hat und sich selbst als Untertyp von AbstractArray deklariert"? Ich denke, diese Art von "Protokoll + abstrakter Typ" ist dem, was Sie mit meinem super = T Eigenschaftsvorschlag erhalten würden, sehr ähnlich. Zugegeben, in meiner Version habe ich noch nicht herausgefunden, wie man sie in eine Hierarchie wie wir jetzt verkettet (zB Integer <: Real <: Number ).

Ein Protokoll von einem (nominalen) abstrakten Typ erben zu lassen, scheint eine sehr starke Einschränkung zu sein. Würde es Untertypen des abstrakten Typs geben, die das Protokoll _nicht_ implementiert haben? Mein Bauchgefühl ist, dass es besser ist, Protokolle und abstrakte Typen als orthogonale Dinge zu belassen.

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

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

Ich verstehe diese Syntax nicht.

  • Hat dieses Protokoll einen Namen?
  • Was bedeutet das Zeug in { } und ( ) genau?
  • Wie verwenden Sie dieses Protokoll? Können Sie darauf versenden? Wenn ja, was bedeutet die Definition von f(x::ThisProtocol)=... , da das Protokoll mehrere Typen betrifft?

dann ist alles mit einer Eigenschaft namens eltype ein Untertyp von Collection. Die Reihenfolge und Position dieser "Parameter" spielt keine Rolle.

Aha, da war mein Missverständnis, das macht mehr Sinn. Nämlich die Fähigkeit zuzuweisen:

el1type = el_type
el2type = el_type

um mein Beispielproblem zu lösen.

Vielleicht sollten wir dies also direkt unterstützen, zusammen mit anderen Schlüssel/Wert-Paaren, die Sie möglicherweise zuordnen möchten.

Und diese Schlüssel/Wert-Funktion wäre für alle Typen verfügbar, da wir abstrakt durch sie ersetzen würden. Das ist eine schöne allgemeine Lösung. Deine Lösung macht für mich jetzt viel mehr Sinn.

Zugegeben, in meiner Version habe ich noch nicht herausgefunden, wie man sie in eine Hierarchie wie wir jetzt verkettet (zB Integer <: Real <: Number).

Ich denke, Sie könnten super (zB mit Integer super als Real ) und dann entweder super Besonderem machen und sich wie ein benannter Typ verhalten oder hinzufügen eine Möglichkeit, benutzerdefinierten Typauflösungscode (ala Python) hinzuzufügen und eine Standardregel für den Parameter super erstellen.

Ein Protokoll von einem (nominalen) abstrakten Typ erben zu lassen, scheint eine sehr starke Einschränkung zu sein. Würde es Untertypen des abstrakten Typs geben, die das Protokoll nicht implementiert haben? Mein Bauchgefühl ist, dass es besser ist, Protokolle und abstrakte Typen als orthogonale Dinge zu belassen.

Oh ja, die abstrakte Einschränkung war völlig optional! Mein ganzer Punkt war, dass Protokolle und abstrakte Typen orthogonal sind. Sie würden abstrakt + Protokoll verwenden, um sicherzustellen, dass Sie eine Kombination aus bestimmten Verhalten _und_ zugehörigen Fähigkeiten erhalten. Wenn Sie nur die Fähigkeiten (für Hilfsfunktionen) oder nur das Verhalten wünschen, verwenden Sie sie orthogonal.

Hat dieses Protokoll einen Namen?

Zwei Protokolle mit zwei Namen ( Foo und Bar ), die aus einem Block stammen, aber dann bin ich es gewohnt, Makros zu verwenden, um mehrere Definitionen wie diese zu erweitern. Dieser Teil meiner Syntax war ein Versuch, Teil (a) zu lösen. Wenn Sie dies ignorieren, könnte die erste Zeile einfach protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (mit einer anderen, separaten Definition für das Protokoll Bar ). Außerdem wären alle Number , AbstractBar und AbstractFoo optional, wie in normalen Typdefinitionen,

Was bedeutet das Zeug in { } und ( ) genau?

{} ist der Standardabschnitt zur parametrischen Typdefinition. Ermöglicht die Verwendung von Foo{Float64} , um einen Typ zu beschreiben, der das Foo Protokoll implementiert, indem beispielsweise Float64 . Das () ist im Grunde eine variable Bindungsliste für den Protokollrumpf (damit mehrere Protokolle gleichzeitig beschrieben werden können). Ihre Verwirrung ist wahrscheinlich meine Schuld, weil ich in meinem Original :< anstelle von <: falsch geschrieben habe. Es kann sich auch lohnen, sie auszutauschen, um die <<name>> <<parametric>> <<bindings>> Struktur beizubehalten, wobei <<name>> manchmal eine Liste von Bindungen sein kann.

Wie verwenden Sie dieses Protokoll? Können Sie darauf versenden? Wenn ja, was bedeutet die Definition von f(x::ThisProtocol)=... , wenn das Protokoll mehrere Typen betrifft?

Ihr Versandbeispiel scheint meiner Meinung nach Syntax dafür richtig zu sein, bedenken Sie tatsächlich die folgenden Definitionen:

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)

Effektiv verwenden Protokolle den benannten Top-Typ (Any), es sei denn, es wird ein spezifischerer abstrakter Typ zum Überprüfen der Struktur angegeben. Tatsächlich kann es sich lohnen, etwas wie typealias Foo Intersect{FooProtocol, Foo} (_Edit: Intersect war der falsche Name, vielleicht Join statt Intersect war beim ersten Mal richtig_) zuzulassen, anstatt die Protokollsyntax dafür zu verwenden.

Ah super, das macht für mich jetzt viel mehr Sinn! Es ist interessant, mehrere Protokolle zusammen im selben Block zu definieren; Darüber muss ich noch etwas nachdenken.

Ich habe alle meine Beispiele vor ein paar Minuten aufgeräumt. Zuvor in dem Thread erwähnte jemand, dass man einen Korpus von Protokollen zusammenstellt, um Ideen zu testen. Ich denke, das ist eine großartige Idee.

Die mehreren Protokolle im selben Block sind eine Art Ärgernis, wenn ich versuche, komplexe Beziehungen zwischen Objekten mit auf beiden Seiten korrekten Typanmerkungen in Define/Compile zu beschreiben, wenn Sie Sprachen laden (z. B. Python; Java z. habe das Problem nicht). Auf der anderen Seite sind die meisten von ihnen wahrscheinlich leicht zu beheben, was die Benutzerfreundlichkeit angeht, mit Multimethoden sowieso; aber Leistungsüberlegungen können sich aus der korrekten Typisierung der Funktionen in Protokollen ergeben (Optimierung von Protokollen durch Spezialisierung auf Vtables, sagen wir).

Sie haben vorhin erwähnt, dass Protokolle (fälschlicherweise) durch Methoden implementiert werden könnten, die ::Any Ich denke, das wäre ein ziemlich einfacher Fall, den man einfach ignorieren könnte, wenn es dazu kommen würde. Der konkrete Typ würde nicht als Protokoll klassifiziert, wenn die implementierende Methode auf ::Any . Auf der anderen Seite bin ich nicht davon überzeugt, dass dies unbedingt ein Problem ist.

Für den Anfang, wenn die Methode ::Any nachträglich hinzugefügt wird (zB weil jemand ein allgemeineres System entwickelt hat, um es zu handhaben), ist es immer noch eine gültige Implementierung, und wenn wir Protokolle auch als Optimierungsfunktion verwenden, dann spezialisierte Versionen von ::Any Methoden funktionieren immer noch für Leistungssteigerungen. Also am Ende wäre ich eigentlich dagegen, sie zu ignorieren.

Aber es könnte sich lohnen, eine Syntax zu haben, die es dem Protokolldefinierer ermöglicht, zwischen den beiden Optionen zu wählen (je nachdem, was wir als Standard festlegen, erlauben Sie die andere). Für die erste, eine Weiterleitungssyntax für die Methode ::Any , beispielsweise das Schlüsselwort global (siehe auch den nächsten Abschnitt). Für die zweite Möglichkeit, eine spezifischere Methode zu erfordern, fällt mir kein nützliches Schlüsselwort ein.

Edit: Ein paar sinnlose Sachen entfernt.

Ihr Join ist genau die Schnittmenge der Protokolltypen. Es ist eigentlich ein "Treffen". Und glücklicherweise ist der Typ Join nicht erforderlich, da Protokolltypen bereits unter Schnittmenge geschlossen sind: Um die Schnittmenge zu berechnen, geben Sie einfach einen neuen Protokolltyp mit den beiden verketteten Methodenlisten zurück.

Ich mache mir keine Sorgen, dass Protokolle durch ::Any Definitionen trivialisiert werden. Für mich läuft die Regel "Suche nach übereinstimmenden Definitionen, außer dass Any nicht zählt" gegen Occams Rasiermesser. Ganz zu schweigen davon, dass es ziemlich nervig wäre, das Flag "ignore Any" durch den Subtyping-Algorithmus zu führen. Ich bin mir nicht einmal sicher, ob der resultierende Algorithmus kohärent ist.

Ich mag die Idee von Protokollen sehr (erinnert mich ein bisschen an CLUster), ich bin nur neugierig, wie würde das mit der neuen Untertypisierung, die von Jeff auf der JuliaCon diskutiert wurde, und mit den Merkmalen zusammenpassen? (zwei Dinge, die ich bei Julia noch sehr gerne sehen würde).

Dies würde eine neue Art von Typ mit eigenen Subtyping-Regeln hinzufügen (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). Auf den ersten Blick scheinen sie mit dem Rest des Systems kompatibel zu sein und können einfach eingesteckt werden.

Diese Protokolle sind so ziemlich die "Ein-Parameter"-Version der Eigenschaften von

Ihr Join ist genau die Schnittmenge der Protokolltypen.

Ich habe mich irgendwie davon überzeugt, dass ich mich vorhin geirrt habe, als ich sagte, es sei die Kreuzung. Obwohl wir immer noch eine Möglichkeit brauchen würden, Typen in einer Zeile zu überschneiden (wie Union ).

Bearbeiten:

Ich mag es auch immer noch, Protokolle und abstrakte Typen in einem System zu verallgemeinern und benutzerdefinierte Regeln für ihre Auflösung zuzulassen (zB für super , um das aktuelle abstrakte Typsystem zu beschreiben). Ich denke, wenn es richtig gemacht wird, könnten die Leute benutzerdefinierte Typsysteme und schließlich benutzerdefinierte Optimierungen für diese Typsysteme hinzufügen. Ich bin mir zwar nicht sicher, ob Protokoll das richtige Schlüsselwort wäre, aber zumindest könnten wir abstract in ein Makro verwandeln, das wäre cool.

von den Weizenfeldern: Besser Gemeinsamkeiten durch das Protokollierte und Abstrakte zu heben, als ihre Verallgemeinerung als Ziel zu suchen.

was?

Der Prozess der Verallgemeinerung von Absicht, Fähigkeit und Potenzial von Protokollen und abstrakten Typen ist weniger effektiv, um ihre qualitativ befriedigende Synthese zu lösen. Es funktioniert besser, zuerst ihre intrinsischen Gemeinsamkeiten von Zweck, Muster und Prozess zu erkennen. Und entwickeln Sie dieses Verständnis, indem Sie die Verfeinerung der eigenen Perspektive ermöglichen, um die Synthese zu bilden.

Was auch immer die fruchtbare Erkenntnis für Julia sein mag, sie ist auf dem Gerüst aufgebaut, das die Synthese bietet. Eine klarere Synthese ist konstruktive Stärke und induktive Kraft.

Was?

Ich denke, er sagt, wir sollten zuerst herausfinden, was wir von Protokollen erwarten und warum sie nützlich sind. Sobald wir diese und abstrakte Typen haben, wird es einfacher sein, eine allgemeine Synthese davon zu finden.

Bloße Protokolle

(1) befürworten

Ein Protokoll kann zu einem (ausgearbeiteteren) Protokoll erweitert werden.
Ein Protokoll kann zu einem (weniger ausgearbeiteten) Protokoll reduziert werden.
Ein Protokoll kann als konforme Schnittstelle [in Software] realisiert werden.
Ein Protokoll kann abgefragt werden, um die Konformität einer Schnittstelle zu bestimmen.

(2) vorschlagen

Protokolle sollten standardmäßig protokollspezifische Versionsnummern unterstützen.

Es wäre gut, dies in irgendeiner Weise zu unterstützen:
Wenn eine Schnittstelle einem Protokoll entspricht, antworten Sie mit true; wenn eine Schnittstelle
einer Teilmenge des Protokolls treu ist und bei Erweiterung konform wäre,
antworte unvollständig und antworte ansonsten falsch. Eine Funktion sollte alle auflisten
notwendige Erweiterung für eine Schnittstelle, die bezüglich eines Protokolls unvollständig ist.

(3) nachdenklich

Ein Protokoll könnte eine besondere Art von Modul sein. Seine Exporte würden dienen
als erster Komparand bei der Bestimmung, ob eine Schnittstelle konform ist.
Alle protokollspezifizierten [exportierten] Typen und Funktionen können mit . deklariert werden
@abstract , @type , @immutable und @function zur Unterstützung der angeborenen Abstraktion.

[pao: Wechseln Sie zu Code-Anführungszeichen, beachten Sie jedoch, dass das Pferd den Stall bereits verlassen hat, wenn Sie dies nachträglich tun...]

(Sie müssen @mentions zitieren!)

danke - repariert es

Am Mittwoch, 16. Dezember 2015 um 03:01 Uhr schrieb Mauro [email protected] :

(Sie müssen die @Erwähnungen zitieren!)


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

Entschuldigung, ich hätte deutlicher sein sollen: Code-Zitat mit ` und nicht "

Das Zitat-Fix wurde behoben.

danke - verzeiht meine vorherige Unwissenheit

Ich habe versucht, diese aktuelle Diskussion über das Hinzufügen eines Protokolltyps zu verstehen. Vielleicht verstehe ich etwas falsch, aber warum ist es notwendig, benannte Protokolle zu verwenden, anstatt nur den Namen des zugeordneten abstrakten Typs zu verwenden, den das Protokoll beschreiben wird?

Aus meiner Sicht ist es ganz natürlich, das aktuelle abstrakte Typsystem um eine Möglichkeit zu erweitern, das vom Typ erwartete Verhalten zu beschreiben. Ähnlich wie ursprünglich in diesem Thread vorgeschlagen, aber vielleicht mit Jeffs Syntax

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

Wenn Sie diesen Weg gehen, müssen Sie nicht speziell angeben, dass ein Untertyp die Schnittstelle implementiert. Dies würde implizit durch Subtyping erfolgen.

Das Hauptziel eines expliziten Schnittstellenmechanismus ist IMHO, bessere Fehlermeldungen zu erhalten und bessere Verifikationstests durchzuführen.

Also eine Typdeklaration wie:

type Foo <: Iterable
  ...
end

Definieren wir die Funktionen im Abschnitt ... ? Wenn nicht, wann machen wir Fehler bezüglich fehlender Funktionen (und der damit verbundenen Komplexität)? Und was passiert für Typen, die mehrere Protokolle implementieren, aktivieren wir die mehrfache abstrakte Vererbung? Wie gehen wir mit Supermethodenauflösung um? Was macht dies mit mehrfachem Versand (es scheint es einfach zu entfernen und ein Java-ähnliches Objektsystem darin einzufügen)? Wie definieren wir neue Typspezialisierungen für Methoden, nachdem der erste Typ definiert wurde? Wie definieren wir Protokolle, nachdem wir den Typ definiert haben?

Diese Fragen lassen sich alle leichter lösen, indem Sie einen neuen Typ erstellen (oder eine neue Typformulierung erstellen).

Es gibt nicht unbedingt einen verwandten abstrakten Typ für jedes Protokoll (es sollte wahrscheinlich keinen wirklich geben). Mehrere der aktuellen Schnittstellen können vom gleichen Typ implementiert werden. Was mit dem aktuellen abstrakten Typsystem nicht beschreibbar ist. Daher das Problem.

  • Die abstrakte Mehrfachvererbung (die mehrere Protokolle implementiert) ist orthogonal zu diesem Merkmal (wie von Jeff oben angegeben). Es ist also nicht so, dass wir diese Funktion erhalten, nur weil der Sprache Protokolle hinzugefügt werden.
  • In Ihrem nächsten Kommentar geht es um die Frage, wann die Schnittstelle überprüft werden soll. Ich denke, dies muss nicht mit blockinternen Funktionsdefinitionen verknüpft sein, die mir nicht julianisch vorkommen. Stattdessen gibt es drei einfache Lösungen:

    1. wie in #7025 implementiert, verwenden Sie eine verify_interface Methode, die entweder nach allen Funktionsdefinitionen oder in einem Unit-Test aufgerufen werden kann

    2. Man kann die Schnittstelle gar nicht verifizieren und auf eine verbesserte Fehlermeldung in "MethodError" zurückstellen. Eigentlich ist dies ein schöner Fallback für 1.

    3. Überprüfen Sie alle Schnittstellen entweder am Ende einer Kompilierzeiteinheit oder am Ende einer Modulladephase. Aktuell ist es auch möglich:

function a()
  b()
end

function b()
end

Daher glaube ich nicht, dass hier blockinterne Funktionsdefinitionen erforderlich sind.

  • Ihr letzter Punkt ist, dass es möglicherweise Protokolle gibt, die nicht mit abstrakten Typen verknüpft sind. Dies trifft derzeit sicherlich zu (zB das informelle "Iterable"-Protokoll). Aus meiner Sicht liegt dies jedoch nur an der fehlenden multiplen abstrakten Vererbung. Wenn dies der Fall ist, fügen Sie bitte einfach eine abstrakte Mehrfachvererbung hinzu, anstatt eine neue Sprachfunktion hinzuzufügen, die darauf abzielt, dieses Problem zu lösen. Ich denke auch, dass die Implementierung mehrerer Schnittstellen absolut entscheidend ist und dies in Java/C# absolut üblich ist.

Ich denke, der Unterschied zwischen einem "protokollartigen" Ding und der Mehrfachvererbung besteht darin, dass ein Typ zu einem Protokoll hinzugefügt werden kann, nachdem er definiert wurde. Dies ist nützlich, wenn Sie möchten, dass Ihr Paket (das Protokolle definiert) mit vorhandenen Typen funktioniert. Man könnte erlauben, die Supertypen eines Typs nach der Erstellung zu ändern, aber an diesem Punkt ist es wahrscheinlich besser, es "Protokoll" oder so zu nennen.

Hm, damit können alternative/erweiterte Schnittstellen zu bestehenden Typen definiert werden. Mir ist immer noch nicht klar, wo das wirklich nötig wäre. Wenn man einer bestehenden Schnittstelle etwas hinzufügen möchte (wenn wir dem im OP vorgeschlagenen Ansatz folgen), würde man einfach einen Untertyp erstellen und dem Untertyp zusätzliche Schnittstellenmethoden hinzufügen. Das ist das Schöne an diesem Ansatz. Es skaliert ganz gut.

Beispiel: Angenommen, ich hätte ein Paket, das Typen serialisiert. Für einen Typ muss eine Methode tobits implementiert werden, dann funktionieren alle Funktionen in diesem Paket mit dem Typ. Nennen wir dies das Serializer Protokoll (dh tobits ist definiert). Jetzt kann ich Array (oder einen anderen Typ) hinzufügen, indem ich tobits implementiere. Bei Mehrfachvererbung konnte ich Array mit Serialzer da ich nach seiner Definition keinen Supertyp zu Array hinzufügen kann. Ich denke, das ist ein wichtiger Anwendungsfall.

Okay, verstehe das. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 ist ein ähnliches Problem, bei dem die Lösung im Wesentlichen darin besteht, Duck-Typing zu verwenden. Wenn wir etwas haben könnten, das dieses Problem elegant löst, wäre das in der Tat schön. Dies muss jedoch auf der Dispatch-Ebene unterstützt werden. Wenn ich die obige Protokollidee richtig verstehe, könnte man entweder einen abstrakten Typ oder ein Protokoll als Typannotation in Funktion setzen. Hier wäre es schön, diese beiden Konzepte in einem einzigen Tool zu vereinen, das leistungsstark genug ist.

Ich stimme zu: Es wird sehr verwirrend sein, sowohl abstrakte Typen als auch Protokolle zu haben. Wenn ich mich richtig erinnere, wurde oben argumentiert, dass abstrakte Typen eine gewisse Semantik haben, die nicht mit Protokollen modelliert werden kann, dh abstrakte Typen haben einige Eigenschaften, die Protokolle nicht haben. Selbst wenn dies unbedingt der Fall ist (ich bin nicht überzeugt), wird es dennoch verwirrend sein, da sich die beiden Konzepte so stark überschneiden. Abstrakte Typen sollten daher zugunsten von Protokollen entfernt werden.

Soweit oben Konsens über Protokolle besteht, betonen sie die Spezifizierung von Schnittstellen. Abstrakte Typen können verwendet worden sein, um einige dieser fehlenden Protokolle auszuführen. Das bedeutet nicht, dass es ihre wichtigste Verwendung ist. Sagen Sie mir, was Protokolle sind und was nicht, dann könnte ich Ihnen sagen, wie sich abstrakte Typen unterscheiden und was sie bringen. Ich habe nie gedacht, dass abstrakte Typen so sehr mit Schnittstellen als mit Typologie zu tun haben. Das Wegwerfen einer natürlichen Herangehensweise an typologische Flexibilität ist kostspielig.

@JeffreySarnoff +1

Denken Sie an die Hierarchie des Zahlentyps. Die verschiedenen abstrakten Typen, zB Signed, Unsigned, werden nicht durch ihre Schnittstelle definiert. Es gibt keinen Methodensatz, der "Unsigned" definiert. Es ist einfach eine sehr nützliche Erklärung.

Ich sehe das Problem nicht wirklich. Wenn beide Typen Signed und Unsigned denselben Methodensatz unterstützen, können wir zwei Protokolle mit identischen Schnittstellen erstellen. Dennoch kann für den Versand verwendet werden, indem ein Typ als Signed und nicht als Unsigned deklariert wird (dh Methoden derselben Funktion verhalten sich anders). Der Schlüssel hier besteht darin, eine explizite Deklaration zu verlangen, bevor man bedenkt, dass ein Typ ein Protokoll implementiert, anstatt dies implizit basierend auf den von ihm implementierten Methoden zu erkennen.

Aber auch implizit verbundene Protokolle sind wichtig, wie in https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Protokolle können nicht nur Funktionen definieren, die aufgerufen werden können, sondern auch (entweder implizit oder maschinentestbar) Eigenschaften dokumentieren, die gehalten werden müssen. Wie zum Beispiel:

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

Dieser äußerlich sichtbare Verhaltensunterschied zwischen Signed und Unsigned macht diese Unterscheidung nützlich.

Wenn eine Unterscheidung zwischen Typen so "abstrakt" ist, dass sie von außen zumindest theoretisch nicht sofort überprüft werden kann, dann ist es wahrscheinlich, dass man die Implementierung eines Typs kennen muss, um die richtige Wahl zu treffen. Hier könnte das aktuelle abstract nützlich sein. Dies geht vermutlich in Richtung algebraischer Datentypen.

Es gibt keinen Grund, Protokolle nicht zur einfachen Gruppierung von Typen zu verwenden, dh ohne definierte Methoden zu benötigen (und ist beim "aktuellen" Design mit dem Trick möglich: https://github.com/JuliaLang/julia/issues/ 6975#issuecomment-161056795). (Beachten Sie auch, dass dies implizit definierte Protokolle nicht beeinträchtigt.)

Betrachten wir das (Un)signed Beispiel: Was würde ich tun, wenn ich einen Typ hätte, der Signed aber aus irgendeinem Grund auch ein Untertyp eines anderen abstrakten Typs sein muss? Dies wäre nicht möglich.

@eschnett : Abstrakte Typen haben im Moment nichts mit der Implementierung ihrer Untertypen zu tun. Obwohl das diskutiert wurde: #4935.

Algebraische Datentypen sind ein gutes Beispiel dafür, wo sukzessive Verfeinerung von Natur aus sinnvoll ist.
Jede Taxonomie ist viel natürlicher gegeben und als abstrakte Typhierarchie direkter nützlich als als Mischung von Protokollspezifikationen.

Der Hinweis, dass ein Typ ein Untertyp von mehr als einer abstrakten Typhierarchie ist, ist ebenfalls wichtig. Es gibt viel utilitaristische Macht, die mit der mehrfachen Vererbung von Abstraktionen einhergeht.

@mauro3 Ja, ich weiß. Ich dachte an etwas Äquivalentes zu diskriminierten Unions, das aber genauso effizient wie Tupel implementiert wurde, anstatt über das Typsystem (wie Unions derzeit implementiert werden). Dies würde Aufzählungen und nullable Typen subsumieren und möglicherweise einige andere Fälle effizienter behandeln als derzeit abstrakte Typen.

Zum Beispiel wie Tupel mit anonymen Elementen:

DiscriminatedUnion{Int16, UInt32, Float64}

oder mit benannten Elementen:

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

Ich wollte darauf hinweisen, dass abstrakte Typen eine gute Möglichkeit sind, Julia ein solches Konstrukt zuzuordnen.

Es gibt keinen Grund, Protokolle nicht dazu zu verwenden, Typen einfach zu gruppieren, dh ohne definierte Methoden zu benötigen (und ist beim "aktuellen" Design mit dem Trick möglich: #6975 (Kommentar)). (Beachten Sie auch, dass dies implizit definierte Protokolle nicht beeinträchtigt.)

Ich habe das Gefühl, dass Sie damit vorsichtig sein müssen, um Leistung zu erzielen, eine Überlegung, die anscheinend nicht oft genug in Betracht gezogen wird. In dem Beispiel möchte man anscheinend einfach die Nicht-Any-Version definieren, damit der Compiler die Funktion zur Kompilierzeit immer noch auswählen kann (anstatt eine Funktion aufrufen zu müssen, um zur Laufzeit die richtige auszuwählen, oder der Compiler Funktionen inspiziert) um ihre Ergebnisse zu bestimmen). Persönlich glaube ich, dass die Verwendung mehrerer abstrakter "Vererbung" als Tags eine bessere Lösung wäre.

Ich denke, wir sollten die erforderlichen Tricks und Kenntnisse des Typsystems auf ein Minimum beschränken (obwohl es in ein Makro eingeschlossen werden könnte, würde es sich wie ein seltsamer Hack eines Makros anfühlen; wenn wir Makros verwenden, um das Typsystem zu manipulieren, dann denke ich @ Die einheitliche Lösung von JeffBezanson würde dieses Problem besser beheben).

Betrachten wir das (Un)signed-Beispiel: Was würde ich tun, wenn ich einen Typ hätte, der Signed ist, aber aus irgendeinem Grund auch ein Untertyp eines anderen abstrakten Typs sein muss? Dies wäre nicht möglich.

Mehrfache abstrakte Vererbung.


Ich glaube, all dieses Thema wurde schon einmal behandelt, dieses Gespräch scheint sich im Kreis zu drehen (wenn auch jedes Mal engere Kreise). Ich glaube, es wurde erwähnt, dass ein Korpus oder Probleme bei der Verwendung von Protokollen erworben werden sollten. Dies würde es uns ermöglichen, Lösungen leichter zu beurteilen.

Während wir die Dinge wiederholen :) Ich möchte alle daran erinnern, dass abstrakte Typen nominal sind, während Protokolle strukturell sind, daher bevorzuge ich Designs, die sie als orthogonal behandeln, _es sei denn_ wir können tatsächlich eine akzeptable "Kodierung" abstrakter Typen in Protokollen finden (vielleicht mit einem geschickten Einsatz der zugehörigen Typen). Bonuspunkte natürlich, wenn es auch mehrfach abstrakte Vererbung gibt. Ich habe das Gefühl, dass dies möglich ist, aber wir sind noch nicht ganz am Ziel.

@JeffBezanson Unterscheiden sich "assoziierte Typen" von "konkreten Typen, die mit [a] Protokoll verbunden sind"?

Ja, ich glaube schon; Ich meine "assoziierte Typen" im technischen Sinne eines Protokolls, das ein Schlüssel-Wert-Paar angibt, bei dem der "Wert" ein Typ ist, genauso wie Protokolle Methoden angeben. zB "Typ Foo folgt dem Container-Protokoll, wenn es ein eltype " oder "Typ Foo folgt dem Matrix-Protokoll, wenn sein Parameter ndims 2 ist".

abstrakte Typen sind nominal, während Protokolle strukturell sind und
abstrakte Typen sind qualitativ, während Protokolle operativ sind und
abstrakte Typen (mit Mehrfachvererbung) orchestrieren, während Protokolle führen

Auch wenn es eine Kodierung des einen in das andere gäbe, das "hey, hallo.. wie geht es dir? los geht's!" von Julia muss beides klar darstellen – den allgemein zweckmäßigen Begriff des Protokolls und die mehrfach vererbbaren abstrakten Typen (ein Begriff des verallgemeinerten Zwecks). Wenn es eine kunstvolle Entfaltung gibt, die Julia beides, getrennt entfaltet, gibt, geschieht dies eher so als eines durch das eine und das andere.

@mason-bially: Also sollten wir auch Mehrfachvererbung hinzufügen? Damit bliebe immer noch das Problem, dass Supertypen nach der Erstellung eines Typs nicht hinzugefügt werden können (es sei denn, dies wäre auch erlaubt).

@JeffBezanson : Nichts würde uns davon abhalten, rein nominale Protokolle zuzulassen.

@mauro3 Warum sollte die Entscheidung, ob das Einfügen von Supertypen nach dem

Entschuldigung, wenn ich eine weitere Runde des Kreises eingeleitet habe. Das Thema ist ziemlich komplex und wir müssen wirklich darauf achten, dass die Lösung super einfach zu bedienen ist. Also müssen wir abdecken:

  • Allgemeine Lösung
  • kein Leistungsabfall
  • einfache Bedienung (und auch leicht verständlich!)

Da sich das, was ursprünglich in #6975 vorgeschlagen wurde, ziemlich von der später diskutierten Protokollidee unterscheidet, kann es gut sein, eine Art JEP zu haben, die beschreibt, wie die Protokolle aussehen könnten.

Ein Beispiel dafür, wie Sie eine formale Schnittstelle definieren und mit der aktuellen Version 0.4 (ohne Makros) validieren können: Dispatch basiert derzeit auf dem Traits-Stil Dispatch, es sei denn, es wurden Änderungen an gf.c vorgenommen. Dies verwendet generierte Funktionen für die Validierung, alle Typberechnungen werden im Typraum durchgeführt.

Ich fange an, dies als Laufzeitprüfung in einer DSL zu verwenden, bei der wir definieren, wo ich sicherstellen muss, dass der angegebene Typ ein Iterator von Datumsangaben ist.

Es unterstützt derzeit die Mehrfachvererbung von Supertypen, der Feldname _super wird von der Laufzeit nicht verwendet und kann ein beliebiges gültiges Symbol sein. Sie können n andere Typen an das _super Tupel übergeben.

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

Ich weise hier nur darauf hin, dass ich eine Diskussion von JuliaCon über mögliche Syntax für Merkmale unter https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040 nachverfolgt habe

Guy Steele hat einige gute Einblicke in Eigenschaften in einer multiplen Dispatch-Sprache (Fortress), siehe seine JuliaCon 2016 Keynote: https://youtu.be/EZD3Scuv02g .

Ein paar Highlights: großes Traits-System für algebraische Eigenschaften, Unit-Tests von Trait-Eigenschaften für Typen, die ein Trait implementieren, und dass das von ihnen implementierte System vielleicht zu kompliziert war und dass er jetzt etwas einfacheres machen würde.

Neuer Swift für Tensorflow-Compiler AD-Anwendungsfall für Protokolle:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy und @Keno könnten daran interessiert sein. Hat brandneue Inhalte

Ich denke, diese Präsentation verdient Aufmerksamkeit, wenn man den Gestaltungsraum für dieses Problem untersucht.

Zur Diskussion unspezifischer Ideen und Links zu einschlägiger Hintergrundarbeit wäre es besser, einen entsprechenden Diskursthread zu starten und dort zu posten und zu diskutieren.

Beachten Sie, dass fast alle Probleme, die bei der Forschung zur generischen Programmierung in statisch typisierten Sprachen aufgetreten und diskutiert wurden, für Julia irrelevant sind. Statische Sprachen beschäftigen sich fast ausschließlich mit dem Problem, genügend Ausdruckskraft bereitzustellen, um den gewünschten Code zu schreiben, während sie dennoch statisch typüberprüfen können, ob es keine Typsystemverletzungen gibt. Wir haben keine Probleme mit der Ausdruckskraft und benötigen keine statische Typprüfung, daher spielt das alles in Julia keine Rolle.

Was uns wichtig ist, ist es den Leuten zu ermöglichen, die Erwartungen an ein Protokoll strukturiert zu dokumentieren, die die Sprache dann dynamisch überprüfen kann (vorab, wenn möglich). Wir kümmern uns auch darum, dass die Leute Dinge wie Eigenschaften versenden können; es bleibt offen, ob die angeschlossen werden sollen.

Fazit: Während die akademische Arbeit an Protokollen in statischen Sprachen von allgemeinem Interesse sein mag, ist sie im Kontext von Julia nicht sehr hilfreich.

Was uns wichtig ist, ist es den Leuten zu ermöglichen, die Erwartungen an ein Protokoll strukturiert zu dokumentieren, die die Sprache dann dynamisch überprüfen kann (vorab, wenn möglich). Wir kümmern uns auch darum, dass die Leute Dinge wie Eigenschaften versenden können; es bleibt offen, ob die angeschlossen werden sollen.

_das ist das_ :Ticket:

Wäre die Eliminierung abstrakter Typen und die Einführung von impliziten Schnittstellen im Golang-Stil in Julia möglich, abgesehen von der Vermeidung von Breaking Changes?

Nein, würde es nicht.

Nun, geht es nicht um Protokolle/Eigenschaften? Es gab einige Diskussionen darüber, ob Protokolle implizit oder explizit sein müssen.

Ich denke, dass seit 0.3 (2014) die Erfahrung gezeigt hat, dass implizite Schnittstellen (dh nicht durch die Sprache / den Compiler erzwungen) gut funktionieren. Nachdem ich miterlebt habe, wie sich einige Pakete entwickelt haben, denke ich, dass die besten Schnittstellen organisch entwickelt und erst zu einem späteren Zeitpunkt formalisiert (= dokumentiert) wurden.

Ich bin mir nicht sicher, ob eine formale Beschreibung von Schnittstellen erforderlich ist, die von der Sprache irgendwie erzwungen wird. Aber während dies entschieden ist, wäre es großartig, Folgendes zu fördern (in der Dokumentation, den Tutorials und den Styleguides):

  1. "Schnittstellen" sind billig und leichtgewichtig, nur eine Reihe von Funktionen mit einem vorgeschriebenen Verhalten für eine Reihe von Typen (ja, Typen haben die richtige Granularität — für x::T sollte T ausreichen um zu entscheiden, ob x die Schnittstelle implementiert) . Wenn man also ein Paket mit erweiterbarem Verhalten definiert, ist es wirklich sinnvoll, die Schnittstelle zu dokumentieren .

  2. Schnittstellen müssen nicht durch Subtypbeziehungen beschrieben werden . Typen ohne einen gemeinsamen (nicht trivialen) Supertyp können dieselbe Schnittstelle implementieren. Ein Typ kann mehrere Schnittstellen implementieren.

  3. Die Weiterleitung/Komposition erfordert implizit Schnittstellen. "Wie man einen Wrapper dazu bringt, alle Methoden des Elternteils zu erben" ist eine Frage, die oft auftaucht, aber es ist nicht die richtige Frage. Die praktische Lösung besteht darin, eine Kernschnittstelle zu haben und diese einfach für den Wrapper zu implementieren.

  4. Eigenschaften sind billig und sollten großzügig verwendet werden. Base.IndexStyle ist ein ausgezeichnetes kanonisches Beispiel.

Folgendes würde von einer Klärung profitieren, da ich nicht sicher bin, was die beste Vorgehensweise ist:

  1. Soll das Interface eine Abfragefunktion haben, wie zB Tables.istable um zu entscheiden, ob ein Objekt das Interface implementiert? Ich denke, es ist eine gute Praxis, wenn ein Anrufer mit verschiedenen alternativen Schnittstellen arbeiten kann und die Liste der Fallbacks durchgehen muss.

  2. Was ist der beste Ort für eine Schnittstellendokumentation in einem Docstring? Ich würde sagen, die Abfragefunktion oben.

  1. Ja, Typen haben die richtige Granularität

Warum ist das so? Einige Aspekte von Typen können in Schnittstellen (für Dispatch-Zwecke) herausgerechnet werden, wie zum Beispiel Iteration. Andernfalls müssten Sie Code neu schreiben oder unnötige Strukturen auferlegen.

  1. Schnittstellen müssen nicht durch Subtypbeziehungen beschrieben werden .

Vielleicht ist es nicht notwendig, aber wäre es besser? Ich kann eine Funktion Dispatch für einen iterierbaren Typ haben. Sollte ein gekachelter iterierbarer Typ das nicht implizit erfüllen? Warum sollte der Benutzer diese um nominale Typen herum ziehen müssen, wenn er sich nur um die Schnittstelle kümmert?

Was ist der Sinn der nominalen Subtypisierung, wenn Sie sie im Wesentlichen nur als abstrakte Schnittstellen verwenden? Merkmale scheinen detaillierter und mächtiger zu sein, daher wäre eine Verallgemeinerung besser. Es sieht also so aus, als wären Typen fast Eigenschaften, aber wir müssen Eigenschaften haben, um ihre Grenzen zu umgehen (und umgekehrt).

Was ist der Sinn der nominalen Subtypisierung, wenn Sie sie im Wesentlichen nur als abstrakte Schnittstellen verwenden?

Versand – Sie können den nominalen Typ von etwas versenden. Wenn Sie nicht angeben müssen, ob ein Typ eine Schnittstelle implementiert oder nicht, können Sie ihn einfach ducken. Dafür verwenden Leute normalerweise Holy-Traits: Mit dem Trait können Sie eine Implementierung aufrufen, die davon ausgeht, dass eine Schnittstelle implementiert ist (zB "mit einer bekannten Länge"). Etwas, das die Leute anscheinend wollen, ist, diese indirekte Ebene zu vermeiden, aber es scheint, als wäre es nur eine Annehmlichkeit, keine Notwendigkeit.

Warum ist das so? Einige Aspekte von Typen können in Schnittstellen (für Dispatch-Zwecke) herausgerechnet werden, wie zum Beispiel Iteration. Andernfalls müssten Sie Code neu schreiben oder unnötige Strukturen auferlegen.

Ich glaube, @tpapp sagte, dass Sie nur den Typ benötigen, um festzustellen, ob etwas eine Schnittstelle implementiert oder nicht, und nicht, dass alle Schnittstellen mit

Nur ein Gedanke, während Sie MacroTools forward :

Es ist manchmal nervig, viele Methoden weiterzuleiten

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

Was wäre, wenn wir den Typ von Foo.x und eine Liste von Methoden verwenden könnten, um dann abzuleiten, welche weitergeleitet werden soll? Dies wird eine Art inheritance und kann mit bestehenden Features (Makros + generierte Funktion) implementiert werden, es sieht auch wie eine Art Schnittstelle aus, aber wir brauchen nichts anderes in der Sprache.

Ich weiß, wir könnten nie eine Liste erstellen, was erben wird (deshalb ist das statische class Modell auch weniger flexibel), manchmal braucht man nur wenige davon, aber es ist einfach praktisch für Kernfunktionen ( zB jemand möchte einen Wrapper (Subtyp von AbstractArray ) um Array , die meisten Funktionen werden nur weitergeleitet)

@datnamer: wie andere geklärt haben, Schnittstellen sollte nicht mehr granulare als Typen (dh die Umsetzung der Schnittstelle nie auf dem Wert abhängen sollte, die Art gegeben). Dies passt gut in das Optimierungsmodell des Compilers und ist in der Praxis keine Einschränkung.

Vielleicht war ich nicht klar, aber der Zweck meiner Antwort bestand darin, darauf hinzuweisen, dass wir in Julia bereits Schnittstellen in einem nützlichen Umfang haben, und sie sind leichtgewichtig, schnell und werden mit zunehmender Reife des Ökosystems allgegenwärtig.

Eine formale Spezifikation zum Beschreiben einer Schnittstelle bietet IMO wenig Wert: Sie würde lediglich auf die Dokumentation und die Überprüfung, ob einige Methoden verfügbar sind, hinauslaufen. Letzteres ist Teil einer Schnittstelle, aber der andere Teil ist die Semantik, die von diesen Methoden implementiert wird (zB wenn A ein Array ist, gibt mir axes(A) einen Koordinatenbereich, der für getindex gültig ist.

Was ich jedoch gerne sehen würde ist

  1. Dokumentation für immer mehr Schnittstellen (in einem Docstring),

  2. Testsuiten , um offensichtliche Fehler für ausgereifte Schnittstellen für neu definierte Typen abzufangen (zB viele T <: AbstractArray implementieren eltype(::T) und nicht eltype(::Type{T}) .

@tpapp Macht jetzt Sinn für mich, danke.

@StefanKarpinski verstehe ich nicht ganz. Traits sind keine Nominaltypen (oder?), dennoch können sie für den Versand verwendet werden.

Mein Punkt ist im Grunde der von @tknopp und @mauro3 hier: https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Dass durch Merkmale und abstrakte Typisierung zusätzliche Komplexität und Verwirrung entstehen, da zwei sehr ähnliche Konzepte verwendet werden.

Etwas, das die Leute anscheinend wollen, ist, diese indirekte Ebene zu vermeiden, aber es scheint, als wäre es nur eine Annehmlichkeit, keine Notwendigkeit.

Können Abschnitte der Merkmalshierarchie nach Gruppierung nach Dingen wie Vereinigungen und Schnittmengen mit Typparametern robust verteilt werden? Ich habe es nicht ausprobiert, aber es fühlt sich so an, als würde dies Sprachunterstützung erfordern. IE-Ausdrucksproblem in der Typdomäne.

Bearbeiten: Ich denke, das Problem war meine Verschmelzung von Schnittstellen und Eigenschaften, wie sie hier verwendet werden.

Es macht einfach Spaß, das hier zu posten: Concepts wurde definitiv akzeptiert und wird ein Teil von C++20 sein. Interessantes Zeug!

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

Ich denke, dass Eigenschaften ein wirklich guter Weg sind, dieses Problem zu lösen, und heilige Eigenschaften haben sicherlich einen langen Weg zurückgelegt. Ich denke jedoch, was Julia wirklich braucht, ist eine Möglichkeit, Funktionen zu gruppieren, die zu einem Merkmal gehören. Dies wäre aus Dokumentationsgründen aber auch aus Gründen der Lesbarkeit des Codes sinnvoll. Nach dem, was ich bisher gesehen habe, denke ich, dass eine Trait-Syntax wie in Rust der richtige Weg wäre.

Ich denke, dies ist sehr wichtig, und der wichtigste Anwendungsfall wäre die Indizierung von Iteratoren. Hier ist ein Vorschlag für die Art von Syntax, von der Sie hoffen, dass sie funktioniert. Entschuldigung, falls es schon vorgeschlagen wurde (langer Thread...).

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
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen