Julia: 抽象型のインターフェース

作成日 2014ĺš´05月26日  Âˇ  171コメント  Âˇ  ソース: JuliaLang/julia

この機能リクエストは、たとえば#5で説明されていますが、まだ独自の問題はないと思います。

抽象型のインターフェースを明示的に定義できたら素晴らしいと思います。 インターフェイスとは、抽象型の要件を満たすために実装する必要のあるすべてのメソッドを意味します。 現在、インターフェースは暗黙的に定義されているだけであり、複数のファイルに分散している可能性があるため、抽象型から派生するときに何を実装する必要があるかを判断するのは非常に困難です。

インターフェイスは主に2つのことを提供します。

  • 一箇所でのインターフェースの自己文書化
  • より良いエラーメッセージ

Base.graphicsには、フォールバック実装でエラーメッセージをエンコードすることにより、実際にインターフェイスを定義できるマクロがあります。 これはすでに非常に賢いと思います。 しかし、多分それに次の構文を与えることはさらにきちんとしています:

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

ここでは、さまざまな粒度を指定できれば便利です。 printおよびpush!宣言は、その名前のメソッド(および最初のパラメーターとしてMyType )が必要であるとだけ述べていますが、タイプを指定していません。 対照的に、 size宣言は完全に型指定されています。 これにより多くの柔軟性が得られ、型指定されていないインターフェイス宣言の場合でも、非常に具体的なエラーメッセージが表示される可能性があると思います。

#5で述べたように、このようなインターフェイスは基本的に、C ++ 14またはC ++ 17のConcept-lightとしてC ++で計画されているものです。 そして、かなりのC ++テンプレートプログラミングを行ったので、この領域での形式化もJuliaにとって良いことだと確信しています。

最も参考になるコメント

非特定のアイデアや関連する背景作業へのリンクについて話し合うには、対応する談話スレッドを開始し、そこに投稿して話し合う方がよいでしょう。

静的に型付けされた言語でのジェネリックプログラミングの研究で遭遇し議論された問題のほとんどすべては、ジュリアとは無関係であることに注意してください。 静的言語は、型システム違反がないことを静的に型チェックできる一方で、必要なコードを書くのに十分な表現力を提供するという問題にほぼ独占的に関わっています。 表現力に問題はなく、静的な型チェックも必要ないので、ジュリアではそれは本当に重要ではありません。

私たちが気にかけているのは、言語が動的に検証できる構造化された方法でプロトコルの期待を文書化できるようにすることです(可能な場合は事前に)。 私たちはまた、人々が特性のようなものに派遣できるようにすることにも関心を持っています。 それらを接続する必要があるかどうかは開いたままです。

結論:静的言語でのプロトコルに関する学術研究は一般的に興味深いかもしれませんが、Juliaのコンテキストではあまり役に立ちません。

全てのコメント171件

一般的に、これはより良いインターフェース指向プログラミングへの良い方向だと思います。

ただし、ここには何かが欠けています。 メソッドのシグニチャ(名前だけでなく)もインターフェイスにとって重要です。

これは実装が簡単なことではなく、多くの落とし穴があります。 これが、_Concepts_がC ++ 11で受け入れられなかった理由の1つであり、3年後、非常に限られた_lite_バージョンのみがC ++ 14に組み込まれます。

私の例のsizeメソッドには、署名が含まれていました。 Base.graphicsからのさらに@mustimplementも、署名を考慮に入れます。

Concept-light一部がすでにあることを付け加えておきます。これは、型を特定の抽象型のサブタイプに制限する機能です。 インターフェイスは他の部分です。

そのマクロはかなりクールです。 エラーをトリガーするフォールバックを手動で定義しましたが、インターフェイスの定義には非常にうまく機能しました。 たとえば、JuliaOptのMathProgBaseはこれを実行し、うまく機能します。 私は新しいソルバー(https://github.com/IainNZ/RationalSimplex.jl)をいじっていましたが、エラーが発生しなくなるまでインターフェイス関数を実装し続けなければなりませんでした。

あなたの提案も同じようなことをしますよね? しかし、インターフェース全体を実装する必要がありますか?

これは共変/反変パラメータをどのように処理しますか?

例えば、

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はい、提案は実際には@mustimplementをもう少し用途の広いものにすることであり、たとえば署名を提供することはできますが、提供する必要はありません。 そして、これは非常に「コア」であるため、独自の構文を取得する価値があると感じています。 すべてのメソッドが実際に実装されていることを強制するのは素晴らしいことですが、 @mustimplement行われる現在のランタイムチェックはすでに素晴らしいことであり、実装が簡単な場合があります。

@lindahuaそれは興味深い例です。 それについて考えなければなりません。

@lindahuaおそらく、あなたの例が@mustimplementは、より具体的なメソッドシグネチャを定義するため、機能しません。

したがって、これはコンパイラーでもう少し深く実装する必要があるかもしれません。 抽象型の定義では、インターフェース名/署名を追跡する必要があります。 そして、現在「...未定義」エラーがスローされたその時点で、適切なエラーメッセージを生成する必要があります。

情報を表現してアクセスするための構文とAPIがある場合、 MethodError印刷方法を変更するのは非常に簡単です。

これで得られるもう1つのことは、 base.Test関数で、型(すべての型?)が親型のインターフェイスを完全に実装していることを確認することです。 それは本当にきちんとしたユニットテストになるでしょう。

@ivarneに感謝します。 したがって、実装は次のようになります。

  1. 1つは、キーとして抽象型を、値として関数(+オプションの署名)を持つグローバルディクショナリを持っています。
  2. has宣言が解析されるときに、パーサーをdictに合わせるように調整する必要があります。
  3. MethodErrorは、現在の関数がグローバルディクショナリの一部であるかどうかを検索する必要があります。

その場合、ほとんどのロジックはMethodErrorます。

私はこれを少し実験していて、次の要点を使用していますhttps://gist.github.com/tknopp/ed53dc22b61062a2b283私ができること:

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

length定義するとき、エラーはスローされません。

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

現在、これは署名を考慮していないわけではありません。

関数のシグネチャを考慮できるように、要点のコードを少し更新しました。 それはまだ非常にハッキーですが、以下が機能するようになりました:

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

要旨のインターフェースキャッシュが関数ではなくシンボルで動作するようになり、後でインターフェースを追加して関数を宣言できるようになったことを追加する必要があります。 署名についても同じことをしなければならないかもしれません。

#2248がすでにインターフェースにいくつかの資料を持っているのを見たばかりです。

インターフェースのようなより投機的な機能についての考えを公開するのは、0.3が出るまで延期するつもりでしたが、あなたが議論を始めたので、少し前に書いたものがあります。


これは、インターフェース宣言とそのインターフェースの実装の構文のモックアップです。

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

これを細かく分割してみましょう。 まず、そこの関数型の構文: A --> B型のオブジェクトにマッピング関数の型であるA入力するB 。 この表記のタプルは明らかなことをします。 これとは別に、 f :: A --> Bは、 fがジェネリック関数であり、型Aを型Bにマッピングすることを宣言することを提案しています。 これが何を意味するのかは少し未解決の質問です。 A型の引数に適用すると、 fがB型の結果を返すことを意味しますか? fはタイプA引数にのみ適用できるということですか? 自動変換は、出力時、入力時のどこでも発生する必要がありますか? 今のところ、これはメソッドを追加せずに新しいジェネリック関数を作成することだけであり、型はドキュメント用であると想定できます。

次に、インターフェースIterable{T,S}の宣言があります。 これにより、 Iterableはモジュールに少し似たものになり、抽象型に少し似たものになります。 Iterable.start 、 Iterable.done 、 Iterable.nextと呼ばれるジェネリック関数へのバインディングがあるという点でモジュールに似ています。 これは、 IterableとIterable{T}とIterable{T,S}を抽象型が使用できる場所、特にメソッドディスパッチで使用できるという点で型に似ています。

第三に、 UnitRangeがIterableインターフェースを実装する方法を定義するimplementブロックがあります。 implementブロック内では、ユーザーがimport Iterable: start, done, next実行したかのように、 Iterable.start 、 Iterable.done 、およびIterable.next関数を使用できます。これらの関数へのメソッドの追加。 このブロックは、パラメトリック型の宣言と同じようにテンプレートです。ブロック内では、 UnitRangeは、アンブレラ型ではなく、特定のUnitRange意味します。

implementブロックの主な利点は、拡張したい明示的なimport関数が不要になることです。これらは暗黙的にインポートされます。これは、一般的にimportについて混乱しているため便利です。とにかくBaseほとんどのジェネリック関数は、何らかのインターフェースに属するべきだと思うので、これにより、 importの使用の大部分が排除されるはずです。 名前はいつでも完全に修飾できるので、完全に廃止できるかもしれません。

私が思いついたもう1つのアイデアは、インターフェイス関数の「内部」バージョンと「外部」バージョンを分離することです。 これが意味するのは、「内部」関数は、あるインターフェースを実装するためのメソッドを提供する関数であり、「外部」関数は、あるインターフェースに関して一般的な機能を実装するために呼び出す関数であるということです。 sort!関数のメソッド(非推奨のメソッドを除く)を見るときは、次のことを考慮してください。

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

これらのメソッドの一部は、パブリックコンシューマーを対象としていますが、その他のメソッドは、パブリックソートメソッドの内部実装の一部にすぎません。 実際、これが持つべき唯一のパブリックメソッドはこれです:

sort!(v::AbstractArray)

残りはノイズであり、「内部」に属します。 特に、

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)

種類のメソッドは、ソートアルゴリズムが一般的なソート機構にフックするために実装するものです。 現在、 Sort.Algorithmは抽象型であり、 InsertionSortAlg 、 QuickSortAlg 、およびMergeSortAlgはその具体的なサブタイプです。 インターフェイスでは、 Sort.Algorithm代わりにインターフェイスにすることができ、特定のアルゴリズムがそれを実装します。 このようなもの:

# 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

次に、次のように定義することで、必要な分離を実現できます。

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

我々は呼んでいることを除いて、これは私たちが現在やっていることに_very_近いAlgorithm.sort!の代わりにはちょうどsort!様々なソートアルゴリズムを実装する場合と、「内側」の定義をする方法である- Algorithm.sort!はsort!関数ではありません。 これには、 sort!の実装をその外部インターフェイスから分離する効果があります。

@StefanKarpinski記事をありがとうございました! これは確かに0.3のものではありません。 今回お持ちしましたのでごめんなさい。 0.3がすぐに起こるのか、半年後に起こるのかはわかりません;-)

一見したところ、実装セクションが独自のコードブロックで定義されているのが本当に(!)好きです。 これにより、型定義のインターフェースを直接検証できます。

心配はいりません。リリースを安定させようとしている間、将来の機能について推測しても実際には害はありません。

あなたのアプローチははるかに基本的であり、いくつかのインターフェースに依存しない問題も解決しようとします。 また、言語に新しい構成(つまり、インターフェース)を導入して、言語を少し複雑にします(これは必ずしも悪いことではありません)。

私は「インターフェース」を抽象型への注釈としてもっと見ています。 hasを入れると、インターフェースを指定できますが、指定する必要はありません。

私が言ったように、私はインターフェースがその宣言で直接検証されることができるかどうか本当に欲しいです。 ここで最も侵襲性の低いアプローチは、型宣言内でメソッドを定義できるようにすることかもしれません。 だからあなたの例をとると

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

型宣言の外で関数を定義することは引き続き許可されます。 唯一の違いは、内部関数宣言がインターフェイスに対して検証されることです。

しかし、繰り返しになりますが、私の「最も侵襲性の低いアプローチ」は近視眼的すぎるかもしれません。 本当にわからない。

これらの定義をタイプブロック内に配置する際の問題の1つは、これを行うには、少なくともインターフェイスの多重継承が実際に必要であり、異なるインターフェイス間で名前の衝突が発生する可能性があることです。 型を定義した後のある時点で、型がインターフェイスをサポートするという事実を追加することもできますが、それについてはよくわかりません。

@StefanKarpinskiあなたがこれについて考えているのを見るのは素晴らしいことです。

Graphsパッケージは、インターフェイスシステムを最も必要とするパッケージです。 このシステムがここで概説されているインターフェースをどのように表現できるかを見るのは興味深いでしょう: http :

@StefanKarpinski :多重継承とブロック内関数宣言の問題は完全にはわかりません。 タイプブロック内では、継承されたすべてのインターフェイスをチェックする必要があります。

しかし、私は、インターフェースの実装を「オープン」にしたいと思うかもしれないことを理解しています。 また、型内関数宣言は言語を複雑にしすぎる可能性があります。 たぶん、私が#7025で実装したアプローチで十分です。 どちらかに置くverify_interface関数宣言の後(またはユニットテストで)、またはにそれ延期MethodError 。

この問題は、異なるインターフェイスが同じ名前のジェネリック関数を持つ可能性があることです。これにより、名前の衝突が発生し、明示的なインポートを実行するか、完全修飾名でメソッドを追加する必要があります。 また、どのメソッド定義がどのインターフェイスに属しているかが明確になりません。これが、名前の衝突が最初に発生する可能性がある理由です。

ところで、私は、言語の別の「もの」としてインターフェースを追加することは、少し非直交的すぎると感じることに同意します。 結局のところ、提案で述べたように、それらはモジュールに少し似ていて、タイプに少し似ています。 コンセプトの統一は可能かもしれない気がしますが、どうすればいいのかわかりません。

私はいくつかの理由から、interface-as-library-featureモデルよりもinterface-as-libraryモデルを好みます。それは言語をよりシンプルに保ち(確かに好みであり、具体的な異議ではありません)、機能がオプションのままで簡単にできることを意味します実際の言語をいじることなく、改善または完全に置き換えられました。

具体的には、 @ tknoppからの提案(または少なくとも提案の形)は思います。これは、言語に新しいものを必要とせずに定義時のチェックを提供します。 私が目にする主な欠点は、型変数を処理する能力がないことです。 これは、インターフェイス定義に必要な関数のタイプのタイプ_predicates_を提供させることで処理できると思います。

私の提案の主な動機の1つは、メソッドを追加するために総称関数を_インポート_する必要があるが、それらをエクスポートしないことによって引き起こされる大量の混乱です。 ほとんどの場合、これは誰かが非公式のインターフェースを実装しようとしているときに発生するため、これにより、それが起こっているように見えます。

メソッドをインターフェースに属するものに完全に制限したい場合を除いて、これは解決すべき直交問題のように思われます。

いいえ、それは確かに良い制限のようには思えません。

@StefanKarpinskiは、インターフェースでディスパッチできると言っています。 また、 implement構文では、特定のタイプがインターフェースを実装するという考え方があります。

一般にメソッドは特定の型に属さず、型のタプルに属するため、これは多重ディスパッチとは少し相反するようです。 では、メソッドが型に属していない場合、インターフェイス(基本的にはメソッドのセット)はどのようにして型に属することができますか?

ライブラリ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

今、私はAとBを取るジェネリック関数を書きたいです

using M

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

この例では、 f関数は、 AとB受け取るアドホックインターフェイスを形成し、 f呼び出すことができると想定できるようにしたいと思います。それらの

AとB具体的なサブタイプを提供したい他のモジュールは、 f実装を提供することが期待されます。 必要なメソッドの組み合わせ爆発を回避するために、ライブラリが抽象型に対してfを定義することを期待します。

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

確かに、この例はかなり不自然に感じますが、うまくいけば、(少なくとも私の心の中では)多重ディスパッチとインターフェイスを実装する特定のタイプの概念との間に根本的な不一致があるように感じることを示しています。

しかし、 import混乱についてのあなたの意見はわかります。 この例で、 using Mを入れてから、 fにメソッドを追加しようとすると、期待どおりに機能せず、メソッドを追加する必要があったことを覚えておくのに、数回の試行が必要M.f (またはimportを使用することもできます)。 しかし、インターフェースがその問題の解決策だとは思いません。 メソッドの追加をより直感的にする方法をブレインストーミングする別の問題はありますか?

@ abe-egnorまた、よりオープンなアプローチがより実現可能だと思います。 私のプロトタイプ#7025には、基本的に2つの点が欠けています。
a)インターフェースを定義するためのより良い構文
b)パラメトリックタイプの定義

私はあまりパラメトリックタイプの第一人者ではないので、b)はより深い経験を持つ誰かによって解決できると確信しています。
a)に関してはマクロで行くことができます。 個人的には、抽象型定義の一部としてインターフェースを直接定義するために、ある程度の言語サポートを費やすことができると思います。 hasアプローチは近視眼的すぎるかもしれません。 コードブロックはこれをより良くするかもしれません。 実際、これは#4935と非常に関連があり、「内部」インターフェースが定義されていますが、これはパブリックインターフェースに関するものです。 この問題は#4935よりもはるかに重要だと思うので、これらをバンドルする必要はありません。 ただし、構文的には、両方のユースケースを考慮に入れることをお勧めします。

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489は、私が考えていた種類の実装を最初に突き刺しました。 要するに、インターフェースは、そのインターフェースに必要な関数の名前とパラメーター型を定義する、型からdictまでの関数です。 @implementマクロは、指定された型の関数を呼び出してから、指定された関数定義に型を接続し、すべての関数が定義されていることを確認します。

良い点:

  • インターフェイスの定義と実装のための単純な構文。
  • 他の言語機能と直交しますが、うまく機能します。
  • インターフェースタイプの計算は任意に凝ったものにすることができます(それらはインターフェースタイプパラメーターに対する関数にすぎません)

悪い点:

  • パラメータをインターフェイスタイプとして使用する場合は、パラメータ化されたタイプではうまく機能しません。 これはかなり重大な欠点ですが、すぐに対処する方法がわかりません。

パラメータ化の問題に対する解決策があると思います。つまり、インターフェイス定義は、型の値に対する関数ではなく、型の式に対するマクロである必要があります。 @implementマクロは、型パラメーターを関数定義に拡張して、次のようなことを可能にします。

<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

その場合、型パラメーターはインターフェースで定義されたメソッドに拡張されるので、 stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x)に拡張されます。これは、まさに正しいことだと思います。

時間があれば、これを行うために最初の実装を作り直します。 おそらく一週間程度です。

私はインターネットを少し閲覧して、他のプログラミング言語がインターフェースや継承などについて何をしているのかを見て、いくつかのアイデアを思いつきました。 (誰かがここに興味がある場合は、私が取った非常に大まかなメモhttps://gist.github.com/mauro3/e3e18833daf49cdf8f60)

要するに、インターフェースは次の方法で実装できる可能性があります。

  • 抽象型の多重継承を許可し、
  • 抽象型のフィールドとしてジェネリック関数を許可します。

これにより、抽象型がインターフェースに変わり、そのインターフェースを実装するために具体的なサブタイプが必要になります。

長い話:

私が見つけたのは、いくつかの「現代」言語はサブタイプのポリモーフィズムを廃止していることです。つまり、型を直接グループ化することはなく、代わりにインターフェイス/特性/型クラスに属するものに基づいて型をグループ化します。 一部の言語では、インターフェース/特性/型クラスはそれらの間で順序を持ち、相互に継承することができます。 彼らはまた、その選択について(ほとんど)満足しているようです。 例: Go 、
Rust 、 Haskell 。
Goは、3つの中で最も厳密ではなく、そのインターフェイスを暗黙的に指定できます。つまり、型がインターフェイスの特定の関数セットを実装する場合、そのインターフェイスに属します。 Rustの場合、インターフェース(特性)はimplブロックに明示的に実装する必要があります。 GoもRustもマルチメソッドを持っていません。 Haskellにはマルチメソッドがあり、それらは実際にはインターフェース(型クラス)に直接リンクされています。

ある意味で、これはJuliaが行うことにも似ています。抽象型は(暗黙の)インターフェースのようなものです。つまり、フィールドではなく動作に関するものです。 これは、 @ StefanKarpinskiが上記の投稿の1つでも観察し、さらにインターフェイスを追加すると「少し非直交に感じる」と述べたものです。 したがって、Juliaにはタイプ階層(つまりサブタイプポリモーフィズム)がありますが、Go / Rust / Haskellにはありません。

階層None<: ... <:Anyすべての型を保持しながら、Juliaの抽象型をより多くのインターフェイス/トレイト/型クラスに変換するのはどうですか? これには以下が含まれます。
1)(抽象)型の多重継承を許可する(問題#5)
2)関数を抽象型に関連付けることを許可する(つまり、インターフェースを定義する)
3)抽象型(つまりデフォルトの実装)と具象型の両方に対して、そのインターフェースを指定できるようにします。

これにより、現在よりもきめの細かいタイプのグラフが作成され、段階的に実装できると思います。 たとえば、配列型は次のように結合されます。

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

したがって、基本的に抽象型はフィールドとしてジェネリック関数を持つことができます(つまり、インターフェイスになります)が、具象型は通常のフィールドだけを持ちます。 これは、たとえば、AbstractArrayから派生するのではなく、コンテナーに役立つ部分を選択するだけでよいため、AbstractArrayから派生するものが多すぎるという問題を解決する可能性があります。

これがまったく良い考えである場合、解決すべきことがたくさんあります(特に、型と型パラメーターを指定する方法)が、おそらく考える価値がありますか?

@ssfrrは、インターフェースと多重ディスパッチは互換性がないと上記でコメントしました。 たとえば、Haskellでは、マルチメソッドは型クラスを使用することによってのみ可能であるため、これは当てはまらないはずです。

また、 @ StefanKarpinskiの記事を読んでいるinterface代わりにabstractを直接使用するのが理にかなっていることがわかりました。 ただし、この場合、 abstractがinterface 1つの重要なプロパティを継承することが重要です。つまり、定義されたinterface _ after_のimplementへの型の可能性です。 次に、typAがalgoBに必要なインターフェイスを実装することをコードで宣言することにより、libAのタイプtypAとlibBのアルゴリズムalgoBを使用できます(これは、具象型が一種のオープンな多重継承を持っていることを意味すると思います)。

@ mauro3 、私は実際にあなたの提案が本当に好きです。 私にとって、それは非常に「ジュリアン」で自然な感じがします。 また、これは、インターフェイス、多重継承、および抽象型の「フィールド」のユニークで強力な統合だと思います(ただし、フィールドは値ではなくメソッド/関数のみであるため、実際にはそうではありません)。 abstract AlgorithmとAlgorithm.sort!宣言することでabstract Algorithm sort!例に対する彼の提案を実装できるので、これは@StefanKarpinskiの「内部」と「外部」のインターフェイスメソッドを区別するという考えともうまくAlgorithm.sort! 。

みんなごめんなさい

------------------原始邮件------------------
ρ件人: "Jacob Quinn" [email protected];
ρ送時間:2014年9月12日(星期五)上午6:23
収件人: "JuliaLang / julia "
抄送: "Implement" [email protected];
主题:Re:[julia]抽象型のインターフェース(#6975)

@ mauro3 、私は実際にあなたの提案が本当に好きです。 私にとって、それは非常に「ジュリアン」で自然な感じがします。 また、これは、インターフェイス、多重継承、および抽象型の「フィールド」のユニークで強力な統合だと思います(ただし、フィールドは値ではなくメソッド/関数のみであるため、実際にはそうではありません)。 また、これは@StefanKarpinskiの「内部」と「外部」のインターフェイスメソッドを区別するというアイデアと

—
このメールに直接返信するか、GitHubで表示してください。

@implement大変申し訳ありません。 どのようにpingしたかわかりません。 まだ知らなかった場合は、画面の右側にある[登録解除]ボタンを使用して、これらの通知から自分自身を削除できます。

いいえ、私はあなたがサリーと言うのをあまり助けることができないと言いたいだけです

------------------原始邮件------------------
ρ件人: "pao" [email protected];
ρ送時間:2014年9月13日(星期六)晚上9:50
収件人: "JuliaLang / julia "
抄送: "Implement" [email protected];
主题:Re:[julia]抽象型のインターフェース(#6975)

@implement大変申し訳ありません。 どのようにpingしたかわかりません。 まだ知らなかった場合は、画面の右側にある[登録解除]ボタンを使用して、これらの通知から自分自身を削除できます。

—
このメールに直接返信するか、GitHubで表示してください。

私たちはあなたがそうすることを期待していません! ユーザー名と同じ名前のJuliaマクロについて話しているので、これは偶然でした。 ありがとう!

Rustで取り組んでいる潜在的に興味深い機能(おそらくこの問題に関連する)があることをちょうど見ました: http : https ://github.com/rust-lang/rfcs/pull/195

THTT ( "Tim Holy Trait Trick")を見た後、私は過去数週間にわたってインターフェース/特性についてもう少し考えました。 私はいくつかのアイデアと実装を思いついた: Traits.jl 。 まず、(私が思うに)特性は、1つまたは複数のタイプを含む契約と見なされるべきです。 これは、私や他の人が上で示唆したように、インターフェイスの機能を1つの抽象型にアタッチするだけでは機能しないことを意味します(少なくとも、複数の型を含む特性の一般的なケースでは)。 そして第二に、 @ StefanKarpinskiが上で提案したように、メソッドはディスパッチに特性を使用できる必要があります。

ナフは言った、ここに私のパッケージ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

これは、 EqとCmpがタイプXとY間のコントラクトであることを宣言します。 Cmpは、スーパートレイトとしてEqがあります。つまり、 EqとCmp両方を満たす必要があります。 @traitdef本体では、関数シグネチャはどのメソッドを定義する必要があるかを指定します。 現時点では、リターンタイプは何もしません。 タイプは、トレイトを明示的に実装する必要はありません。関数を実装するだけで十分です。 たとえば、 Cmp{Int,Float64}が実際に特性であるかどうかを確認できます。

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

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

明示的なトレイトの実装はまだパッケージに含まれていませんが、追加するのはかなり簡単です。

_trait-dispatch_を使用する関数は次のように定義できます

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

これは、関数ft1を宣言します。この関数Cmp{X,Y}を満たす必要があるという制約を伴う2つの引数を取ります。 別のトレイトにディスパッチする別のメソッドを追加できます。

<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

これらのトレイト関数は、通常の関数と同じように呼び出すことができます。

julia> ft1(4,5)
6

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

後で他のタイプをトレイトに追加するのは簡単です(ft1にUnionを使用する場合はそうではありません)。

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

トレイト関数の_実装_とそのディスパッチは、 Timのトリックとステージングされた関数に基づいています。以下を参照してください。 特性の定義は比較的簡単です。すべての手動実装については、ここを参照してください。

簡単に言えば、特性ディスパッチは変わります

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

このようなものに(少し単純化された)

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

パッケージでは、 checkfnはstagedfuncitonsによって自動化されています。 ただし、詳細については、Traits.jlのREADMEを参照してください。

_パフォーマンス_単純な特性関数の場合、生成されるマシンコードは、ダックタイプの対応するものと同じです。つまり、最高の状態です。 より長い関数の場合、最大20%の長さの違いがあります。 これをすべてインライン化する必要があると思ったので、理由はわかりません。

( Traits.jlマイナーな変更を反映するために10月27日に編集されました)

Traits.jlパッケージを探索する準備はできていますか? readmeには、「 @ traitimplとのインターフェースの実装(まだ完了していません...)」と

探索する準備ができています(バグを含む:-)。 @traitimplないということは、

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

関数を手動で定義するだけです

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

T1とT2 2つのタイプの場合。

@traitimplマクロを追加したので、上記の例が機能するようになりました。 また、使用法の詳細でREADMEを更新しました。 そして、 @ lindahuaGraphs.jlインターフェースの一部を実装する例を追加しました。
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

これは本当にクールです。 私は特に、インターフェイスが一般に個々のタイプではなく、タイプのタプルのプロパティであることを認識しているのが好きです。

これもとてもかっこいいと思います。 このアプローチについては、好きなことがたくさんあります。 よくやった。

:+1:

良いフィードバックをありがとう! コードを少し更新/リファクタリングしましたが、バグがなく、遊んでみるのに適しているはずです。
この時点で、人々がこれを試して、ユースケースに適合するかどうかを確認できれば、おそらく良いでしょう。

これは、自分のコードを新しい観点から見ることができるパッケージの1つです。 とてもかっこいい。

申し訳ありませんが、これを真剣に検討する時間はまだありませんが、一度検討したら、いくつかのものをリファクタリングしたいと思います...

パッケージもリファクタリングします:)

不思議に思っていたのですが、トレイトが利用可能である場合(そして、上記の提案のように複数のディスパッチが可能である場合)、抽象型階層メカニズムや抽象型はまったく必要ないようです。 これでいいの?

トレイトが実装された後、ベース内のすべての関数が実装され、後でエコシステム全体で、トレイトのみに基づいたパブリックAPIが公開され、抽象型が消えます。 もちろん、このプロセスは、抽象型を廃止することによって触媒される可能性があります

これについてもう少し考えてみると、抽象型を特性に置き換えるには、次のような型をパラメーター化する必要があります。

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

私は上記のmauro3の点に同意します。これは、特性(彼の定義によれば、非常に良いと思います)を持つことは、抽象型と同等です。

  • 多重継承を許可し、
  • フィールドとしてジェネリック関数を許可する

また、定義後に特性を型に割り当てることができるようにするには、「レイジー継承」も許可する必要があります。つまり、型が定義された後、ある抽象型から継承することをコンパイラに通知する必要があります。

したがって、全体として、抽象型の外でいくつかの特性/インターフェースの概念を開発すると、いくつかの重複が誘発され、同じことを達成するためのさまざまな方法が導入されるように思われます。 これらの概念を導入する最良の方法は、抽象型に機能をゆっくりと追加することだと思います。

編集:もちろん、ある時点で抽象型から具象型を継承することは非推奨になり、最終的には禁止されなければなりません。 タイプ特性は暗黙的または明示的に決定されますが、継承によって決定されることはありません

抽象型は、特性の単なる「退屈な」例ではありませんか?

もしそうなら、現在の構文を維持し、その意味を特性に変更することは可能でしょうか(ユーザーが望む場合は直交の自由などを与える)?

_これでPoint{Float64} <: Pointy{Real}例にも対応できるのではないかと思います(問題番号があるかどうかはわかりません)?_

はい、あなたは正しいと思います。 特性機能は、現在のジュリア抽象型を拡張することで実現できます。 彼らが必要とする
1)多重継承
2)関数シグネチャ
3)「レイジー継承」、すでに定義されたタイプに新しい特性を明示的に与える

大変な作業のように思えますが、おそらくこれはコミュニティに大きな打撃を与えることなくゆっくりと成長させることができます。 少なくとも私たちはそれを手に入れました;)

私たちが選択するものは何でも大きな変化になると思います。0.4で作業を開始する準備ができていません。 推測しなければならないのであれば、従来の多重継承を追加する方向よりも、特性の方向に移動する可能性が高いと思います。 しかし、私の水晶玉はフリッツの上にあるので、何かを試さずに何が起こるかを確認するのは難しいです。

FWIW、私は以下の講演でサイモンペイトンジョーンズの型クラスに関する議論がサブタイピングの代わりに特性のようなものを使用する方法について本当に有益であることに気づきました: http :

うん、ワームの缶全体!

@johnmyleswhite 、リンクをありがとう、非常に興味深い。 ここにそのビデオへのリンクがあります。これはギャップを埋めるために見る価値があります。 そのプレゼンテーションは、私たちがここで得た多くの質問に触れているようです。 そして興味深いことに、型クラスの実装は、Traits.jlにあるものと非常に似ています(Timのトリック、特性はデータ型です)。 Haskellのhttps://www.haskell.org/haskellwiki/Multi-parameter_type_classは、Traits.jlによく似ています。 講演での彼の質問の1つは、「ジェネリックスを心から採用した後でも、サブタイピングが本当に必要なのか」というものです。 (ジェネリックスはパラメトリック多相関数だと思います。参照してください)これは、 @ skarielと@haydが上記で

@skarielと@haydを参照すると、単一のパラメーター特性(Traits.jlのように)は、別の階層、つまり多重継承を持つことができることを除いて、実際に抽象型に非常に近いと思います。

しかし、マルチパラメータの特性は少し異なっているように見えます。少なくとも私の頭の中にはありました。 私が見たように、抽象型の型パラメーターは、ほとんどの場合、型内に含まれる他の型に関するもののようです。たとえば、 Associative{Int,String}は、dictにIntキーとStringが含まれていることを示します。 Tr{Associative,Int,String}...は、 Associative 、 Int 、およびStrings間に何らかの「契約」があることを示しています。 しかし、おそらくAssociative{Int,String}もそのように読む必要があります。つまり、 getindex(::Associative, ::Int) -> String 、 setindex!(::Associative, ::Int, ::String)などのメソッドがあります...

@ mauro3重要なことは、関数の引数としてAssociative型のオブジェクトを渡すことです。これにより、関数自体がAssociative{Int,String}作成できるようになります。

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

これをたとえばf(Dict)と呼びます。

@eschnett 、申し訳ありませんが、あなたが何を意味するのかわかりません。

@ mauro3私はあまりにも複雑な方法で考えていたと思います。 私を無視。

Traits.jlを次のように更新しました:

  • 特性のあいまいさの解決
  • 関連するタイプ
  • ヘルプに@docを使用する
  • 特性仕様メソッドのより良いテスト

詳細については、 https://github.com/mauro3/Traits.jl/blob/master/NEWS.mdを参照してください。 フィードバックを歓迎します!

@ Rory-Finneganはインターフェースパッケージをまとめましたhttps://github.com/Rory-Finnegan/Interfaces.jl

私は最近@mdcfrancisとこれについて話し合いましたが、Clojureのプロトコルに似たものがシンプルで実用的だと思います。 基本的な機能は、(1)プロトコルは新しい種類のタイプであり、(2)いくつかのメソッドシグネチャをリストすることによってそれらを定義し、(3)他のタイプは、一致するメソッド定義を持つだけでそれらを暗黙的に実装します。 あなたは例えば書くでしょう

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

isa(Iterable, Protocol)とProtocol <: Typeます。 当然、これらにディスパッチすることができます。 タイプがT <: Iterableを使用してプロトコルを実装しているかどうかを確認できます。

サブタイピングのルールは次のとおりです。

P、Qをプロトコルタイプとします
Tを非プロトコルタイプとします

| 入力| 結果|
| --- | --- |
| P <:任意| 真|
| 下<:P | 真|
| (union、unionall、var)<:P | 通常のルールを使用します。 Pを基本型として扱う|
| P <:( union、unionall、var)| 通常のルールを使用する|
| P <:P | 真|
| P <:Q | メソッドのチェック(Q)<:メソッド(P)|
| P <:T | false |
| T <:P | Pのメソッドは、_の代わりにTを使用して存在します|

最後のものは大きなものです。T<:Pをテストするには、Pの定義で_をTに置き換え、署名ごとにmethod_existsをチェックします。 もちろん、これ自体は、「これを実装する必要があります」というエラーをスローするフォールバック定義が非常に悪いものになることを意味します。 うまくいけば、これは表面的な問題です。

もう1つの問題は、たとえばstart(::Iterable)が定義されている場合、この定義が循環的であることです。 そのような定義は実際には意味がありません。 どういうわけかこれを防ぐか、サブタイプチェック中にこのサイクルを検出することができます。 単純な循環検出で修正できるかどうかは100%わかりませんが、もっともらしいと思われます。

タイプ交差の場合、次のようになります。

| 入力| 結果|
| --- | --- |
| P∩(union、unionall、tvar)| 通常のルールを使用する|
| P∊Q| P |
| P∊T| T |

P∩Qにはいくつかのオプションがあります。

  1. PまたはQを返すことによって(たとえば、辞書式順序で最初にある方)、近似しすぎます。 これは型推論に関しては健全ですが、他の場所では煩わしいかもしれません。
  2. PとQの署名の和集合を含む新しいアドホックプロトコルを返します。
  3. 交差型。 おそらくプロトコルのみに制限されています。

P∩Tはトリッキーです。 非プロトコルタイプは、タイプ階層の1つの領域に制限されるという意味でプロトコルタイプよりも「小さい」ので、Tは適切な控えめな近似ですが、プロトコルタイプは制限しません(どのタイプでも任意のプロトコルを実装できるため) )。 これよりもうまくやるには、一般的な交差型が必要なようです。これは、サブタイピングアルゴリズムのオーバーホールが必要であり、ワーム缶の後にワーム缶を開くため、最初の実装では避けたいと思います。

特異性:P <:Qの場合、PはQよりも特異的です。 しかし、P∩Qは常に空ではないため、同じスロット内の異なるプロトコルの定義はあいまいであることが多く、これはあなたが望むように見えます(たとえば、「xが反復可能である場合はこれを実行しますが、xが印刷可能である場合は実行しますそれ")。
ただし、必要な曖昧性解消の定義を表現する便利な方法がないため、これはエラーである可能性があります。

#13412以降、プロトコルは、タプルタイプのユニオン(各内部タプルの最初の要素が問題の関数のタイプ)を介してUnionAll _として「エンコード」できます。 これは、以前は思いもよらなかったそのデザインの利点です。 たとえば、プロトコルの構造的サブタイピングは自動的に失敗するように見えます。

もちろん、これらのプロトコルは「単一パラメータ」スタイルです。 私はこれの単純さが好きです、そして私はタイプのグループをT <: Iterableようにエレガントに扱う方法がわかりません。

過去にこのアイデアに関するコメントがいくつかありました。x-refhttps : //github.com/JuliaLang/julia/issues/5#issuecomment-37995516。

私たちはサポートしますか、例えば

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

うわー、私はこれが本当に好きです(特に@Kenoの拡張機能で)!

+1これはまさに私が欲しいものです!

@Kenoそれは間違いなくこの機能のための素晴らしいアップグレードパスですが、それを延期する理由があります。 もちろん、リターンタイプに関係するものはすべて非常に問題があります。 パラメータ自体は概念的には細かく、すばらしいでしょうが、実装するのは少し難しいです。 すべてのメソッドの存在をチェックするプロセスの周囲に型環境を維持する必要があります。

このスキームに(配列のようなタイプのO(1)線形インデックスのような)特性を取り入れることができるようです。 hassomeproperty(::T) = trueようなダミーメソッドを定義し(ただし、 hassomeproperty(::Any) = falseではありません)、

protocol MyProperty
hassomeproperty(::_)
end

_は、プロトコル定義の同じメソッドに複数回表示される可能性があります。

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

プロトコル定義の同じメソッドに_複数回出現する可能性があります

はい。 _すべてのインスタンスに候補タイプをドロップするだけです。

@JeffBezansonは本当にそれを楽しみにしています。 私にとって特に注目すべきは、プロトコルの「リモート性」です。 その点で、タイプの作成者がプロトコルの存在についての知識を持っていなくても、タイプの特定の/カスタムプロトコルを実装できます。

メソッドはいつでも動的に定義できる(たとえば@eval )という事実はどうですか? 次に、タイプが特定のプロトコルのサブタイプであるかどうかは、一般に静的に知ることができません。これは、多くの場合、動的ディスパッチを回避する最適化を無効にするように見えます。

はい、これは#265を悪化させます:)メソッドが追加されたときにディスパッチと生成されたコードを変更する必要があるのと同じ問題ですが、依存関係のエッジが増えています。

これが進むのを見るのは良いことです! もちろん、私はマルチパラメータ特性が前進の道であると主張する人になるでしょう。 しかし、特性の95%は、とにかく単一のパラメーターである可能性があります。 それは、彼らが複数のディスパッチにとてもうまく合うということだけです! これは、必要に応じて後で再検討される可能性があります。 十分に言った。

コメントのカップル:

@Kenoの提案(そしてJeffのオリジナルでは実際にはstate )は、関連する型として知られています。 リターンタイプがなくても便利であることに注意してください。 Rustにはまともな手動エントリがあります。 Rustほど必要ではありませんが、それらは良い考えだと思います。 しかし、それが特性のパラメーターであるべきではないと思います。 Iterableにディスパッチする関数を定義するとき、 Tが何であるかわかりません。

私の経験では、 method_existsは、現在の形式では使用できません(#8959)。 しかし、おそらくこれは#8974で(またはこれで)修正されるでしょう。 特にパラメーター化されたvararg関数を説明するために、Traits.jlを実行するときに、trait-siganturesに対してメソッドシグネチャを一致させることが最も難しい部分であることがわかりました(を参照)。

おそらく相続も可能でしょうか?

デフォルトの実装を定義できるメカニズムが本当に欲しいです。 古典的なものは、比較のために、 = 、 < 、 > 、 <= 、 >=うち2つを定義するだけでよいというものです。 多分これはジェフによって言及されたサイクルが実際に役立つところです。 上記の例を続けると、 start(::Indexable) = 1とdone(i::Indexable,state)=length(i)==stateを定義すると、これらがデフォルトになります。 したがって、多くのタイプはnextを定義するだけで済みます。

良い点。 関連するタイプは、 Iterable{T}パラメーターとは多少異なると思います。 私のエンコーディングでは、パラメータは内部のすべてを存在記号で定量化するだけです---「タイプFooがこのプロトコルを実装するようなTは存在しますか?」

はい、簡単にprotocol Foo <: Bar, Baz許可し、BarとBazからFooに署名をコピーするだけでよいようです。

マルチパラメータ特性は間違いなく強力です。 それらをサブタイピングと統合する方法を考えるのは非常に興味深いと思います。 TypePair{A,B} <: Traitようなものを使用することもできますが、それは正しくないようです。

あなたの提案(機能の点で)は、実際にはClojureというよりもSwiftに似ていると思います。

名目(タイプ)と構造(プロトコル)のサブタイピングを混在させるのは奇妙に思えます(そして将来の混乱の原因になると思います)(しかし、それは避けられないと思います)。

また、数学/行列演算のプロトコルの表現力にも少し懐疑的です。 より複雑な例(行列演算)を通して考えることは、明確に指定されたインターフェースを持つ反復よりも啓発的だと思います。 たとえば、 core.matrixライブラリを参照してください。

同意します; この時点で、プロトコルの例を収集し、それらが私たちが望むことを実行するかどうかを確認する必要があります。

あなたがこれを想像しているように、プロトコルはそれらのメソッドが属する名前空間でしょうか? つまり、あなたが書くとき

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

これがジェネリック関数start 、 done 、 nextを定義し、それらの完全修飾名がIterable.start 、 Iterable.doneになるのは自然なことのように思われます。 Iterable.next 。 タイプはIterable実装しますが、 Iterableプロトコルですべてのジェネリック関数を実装します。 私は少し前にこれに非常に似たものを提案しました(今は見つかりません)が、反対側では、プロトコルを実装したいときにこれを行います:

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

これは、 @ mdcfrancisが言及した「打ち消すものですが、それを理解していれば、プロトコルを「誤って」実装できることのメリットはあまりわかりません。 @mdcfrancisさん、なぜそれが有益だと思うのか詳しく説明していただけますか? Goがこれをたくさん行っていることは知っていますが、それはGoがダックタイピングを実行できないためと思われます。これはJuliaが実行できることです。 implementブロックを使用すると、 import代わりにusing importを使用する必要がほとんどなくなると思います。これは、大きなメリットになります。

少し前にこれに非常に似たものを提案しました(今は見つかりません)

おそらくhttps://github.com/JuliaLang/julia/issues/6975#issuecomment-44502467以前のhttps://github.com/quinnj/Datetime.jl/issues/27#issuecomment-31305128 ? (編集:https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021も参照してください。)

うん、それだけです。

@StefanKarpinskiクイックコメント、

  • 現在iterableを実装しているすべてのクラスは、提案どおりにプロトコルを明示的に実装するように変更する必要があります。現在の提案では、定義をbaseに追加するだけで、既存のすべてのクラスがプロトコルに「リフト」されます。
  • 反復可能な定義に追加の関数を追加するMyModule.MySuperIterableを定義する場合、1つの追加メソッドを追加するのではなく、クラスごとに大量のボイラープレートコードを作成する必要があります。
  • 私はあなたが提案するものが遠隔性を打ち消すとは思いません、それは私が同じ目標を達成するためにたくさんの追加のコードを書かなければならないことを意味します。

プロトコルのある種の継承が許可された場合、MySuperIterabe、
既存のメソッドを再利用するために、Base.Iterableを拡張できます。

問題は、メソッドの選択だけが必要な場合です。
プロトコルですが、それは元のプロトコルが
最初から複合プロトコルである。

@mdcfrancis –最初のポイントは良いものですが、私が提案しているのは既存のコードを壊すことはありませんが、ディスパッチを頼りにする前に、人々のコードがタイプのプロトコルに「オプトイン」する必要があることを意味します働く。

MyModule.MySuperIterableポイントを拡張できますか? 余分な冗長性がどこから来ているのかわかりません。 たとえば、次のようなものがあります。

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

これは本質的に@ivarneが言ったことです。

上記の私の特定の設計では、プロトコルは名前空間ではなく、他のタイプや関数に関するステートメントにすぎません。 しかし、これはおそらく私がコア型システムに焦点を合わせているためです。 モジュールとプロトコルの組み合わせに拡張されるシンタックスシュガーを想像できます。

module Iterable

function start end
function done end
function next end

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

end

次に、Iterableが型として扱われるコンテキストでは、 Iterable.the_protocolを使用します。

jeff / mdcfrancisプロトコルは、ここにある他のすべてのものと非常に直交していると感じるので、私はこの視点が好きです。 「ジュリアン」と感じたくないのなら、「XはプロトコルYを実装している」と言う必要がないという軽量感。

なぜこの号を購読したのか、いつ購読したのかわかりません。 しかし、このプロトコルの提案は、私がここで提起した質問を解決できることがあり

技術的に追加するものは何もありませんが、Julia(一種)で実際に使用されている「プロトコル」の例として、ソルバーの機能を決定するJuMPがあります。

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

かっこいい、それは便利です。 m.internalModelがプロトコルを実装するものであるだけで十分ですか、それとも両方の引数が重要ですか?

はい、プロトコルを実装するにはm.internalModel十分です。 他の引数はほとんどが単なるベクトルです。

はい、 m.internalModelがプロトコルを実装するのに十分です

実際のプロトコルの例を見つける良い方法は、おそらくapplicableとmethod_exists呼び出しを検索することです。

Elixirもプロトコルを実装しているようですが、標準ライブラリ内のプロトコルの数(定義から外れている)はかなり制限されているようです。

プロトコルと抽象型の関係はどうなるでしょうか? 元の問題の説明では、プロトコルを抽象型にアタッチするようなものが提案されていました。 確かに、そこにある(現在は非公式の)プロトコルのほとんどは、現在抽象型として実装されているように思われます。 プロトコルのサポートが追加された場合、抽象型は何に使用されますか? APIを宣言する方法がない型階層は、あまり有用ではないように思われます。

とても良い質問です。 そこにはたくさんのオプションがあります。 まず、抽象型とプロトコルはどちらもオブジェクトをグループ化する方法ですが、非常に直交していることを指摘することが重要です。 抽象型は純粋に名目上のものです。 それらは、オブジェクトをセットに属するものとしてタグ付けします。 プロトコルは純粋に構造的です。 オブジェクトが特定のプロパティを持っている場合、そのオブジェクトはセットに属します。 したがって、いくつかのオプションは

  1. 両方持っているだけです。
  2. プロトコルを抽象型に関連付けることができます。たとえば、型がそれ自体をサブタイプとして宣言するときに、プロトコルに準拠しているかどうかがチェックされます。
  3. 抽象型を完全に削除します。

(2)のようなものがある場合、それは実際には単一の機能ではなく、名目上の型付けと構造型の組み合わせであることを認識することが重要だと思います。

抽象型が役立つと思われることの1つは、パラメーターです。たとえば、 convert(AbstractArray{Int}, x)記述します。 AbstractArrayがプロトコルである場合、要素タイプIntは必ずしもプロトコル定義で言及される必要はありません。 これは、メソッドが必要なタイプに関する追加情報です。 したがって、 AbstractArray{T}とAbstractArray{S}は、同じメソッドを指定しているにもかかわらず、異なるタイプのままであるため、記名的型付けを再導入しました。 したがって、この型パラメーターの使用には、ある種の名目的型付けが必要なようです。

では、2。複数の抽象継承を提供しますか?

では、2。複数の抽象継承を提供しますか?

いいえ。これは機能を統合または結合する方法ですが、各機能には現在のプロパティがあります。

複数の抽象継承を許可することは、もう1つのほぼ直交する設計上の決定であることを付け加えておきます。 いずれにせよ、抽象記名型を多用することの問題は、(1)プロトコルの事後実装を失う可能性があることです(人物Aが型を定義し、人物BがプロトコルとそのAの実装を定義します)、(2)プロトコルの構造的なサブタイピングが失われる可能性があります。

現在のシステムの型パラメーターは、どういうわけか暗黙のインターフェースの一部ではありませんか? たとえば、この定義はそれに依存しています: ndims{T,n}(::AbstractArray{T,n}) = nそして多くのユーザー定義関数もそうします。

したがって、新しいプロトコル+抽象継承システムでは、 AbstractArray{T,N}とProtoAbstractArrayます。 ここで、名目上AbstractArrayではなかったタイプは、おそらくeltypeをハードコーディングすることにより、 TおよびNパラメーターが何であるかを指定できる必要があります。およびndims 。 次に、 AbstractArrayのすべてのパラメーター化された関数は、パラメーターの代わりにeltypeとndimsを使用するように書き直す必要があります。 したがって、プロトコルにパラメーターも含ませる方が理にかなっているので、関連する型は結局のところ非常に役立つ可能性があります。 (具象型にはまだパラメーターが必要であることに注意してください。)

また、 @ malmaudのトリックを使用したプロトコルへのタイプのグループ化: https :

はい、抽象型のパラメーターは確かに一種のインターフェースであり、 eltypeとndimsある程度冗長です。 主な違いは、追加のメソッド呼び出しなしで直接ディスパッチできることです。 関連する型を使用すると、抽象型をプロトコル/特性に置き換えることにはるかに近づくことに同意します。 構文はどのようになりますか? サブタイピングとメソッド呼び出しの間に循環依存関係を持たせたくないので、理想的にはメソッド呼び出しよりも弱いでしょう。

残りの質問は、関連する抽象型の一部になることなくプロトコルを実装することが有用かどうかです。 例としては、反復可能で索引付け可能な文字列がありますが、多くの場合、コンテナーではなく「スカラー」量として扱われます。 これがどのくらいの頻度で発生するかはわかりません。

私はあなたの「メソッド呼び出し」ステートメントを完全には理解していないと思います。 したがって、この構文の提案は、あなたが求めていたものではない可能性があります。

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

プロトコルタイプのサブタイピングがどのように定義されているかに応じて、これは機能する可能性があります。 たとえば、与えられた

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

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

PAbstractArray{Int,1} <: Indexable{Int}ますか? パラメータが名前で一致していれば、これは非常にうまくいくと思います。 eltype(x)がxのタイプのeltypeパラメーターを返すようにする定義を自動化することもできます。

implブロック内にメソッド定義を配置するのは特に好きではありません。これは主に、単一のメソッド定義が複数のプロトコルに属している可能性があるためです。

したがって、このようなメカニズムでは、抽象型は不要になります。 AbstractArray{T,N}がプロトコルになる可能性があります。 次に、(プロトコルの)多重継承を自動的に取得します。 また、プロトコルの継承のみがサポートされるため、具象型から継承できないこと(これは、新参者から時々耳にする不満です)は明らかです。

余談ですが、 Callable特性を表現できると本当にいいでしょう。 次のようになります。

protocol Callable
    ::TupleCons{_, Bottom}
end

ここで、 TupleConsは、タプルの最初の要素と残りの要素に個別に一致します。 _のメソッドテーブルが空でない限り、これは一致するという考え方です(Bottomはすべての引数タプルタイプのサブタイプです)。 実際、 TupleCons{a, TupleCons{b, EmptyTuple}}に対してTuple{a,b}構文を作成したい場合があります(#11242も参照)。

私はそれが真実だとは思いません。すべての型パラメーターは存在記号で_制約付き_で定量化されるため、抽象型とプロトコルを直接置き換えることはできません。

@jakebolewski例を

たぶん私は要点を見逃しているかもしれませんが、プロトコルはどのようにして次のような制約のある適度に複雑な抽象型をエンコードできますか?

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

名目上すべての可能性を列挙する必要はありませんか?

提案されたProtocol提案は、私が強調しようとしていたすべての抽象的なサブタイピングと比較して、厳密に表現力が劣っています。

私は次のことを想像することができました(当然、設計を実用的な限界まで伸ばします):

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

既存の抽象型の表現力と一致させるには、関連する型や名前付き型のプロパティなどが必要であるという観察に沿って進みます。 これにより、ほぼ互換性を持つ可能性があります。

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

オブジェクトのデータフィールドの構造的なサブタイピングは、私にはあまり役に立たないように思われましたが、代わりに_types_のプロパティに適用されたため、非常に理にかなっているようです。

また、これにより、あいまいさの問題からの脱出ハッチが提供される可能性があることにも気付きました。2つのタイプの共通部分は、一部のパラメーターの値が競合している場合は空です。 したがって、明確なNumberタイプが必要な場合は、

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

これは、 superを単なる別のタイプのプロパティとして表示しています。

私は提案されたプロトコル構文が好きですが、いくつかのメモがあります。

しかし、それなら私はすべてを誤解しているかもしれません。 ジュリアを自分がやりたいこととして本当に調べ始めたのはつい最近のことで、型システムについてはまだ完全には理解していません。

(a) @ mauro3が上記で取り組んだ特性機能を使用するとさらに興味深いと思います。 特に、複数のディスパッチプロトコルを使用できない場合は、複数のディスパッチが適しているためです。 実世界の例が何であるかについての私の見解を後で書きます。 しかし、その一般的な要点は、「これら2つのオブジェクトが相互作用することを可能にする動作はありますか」に帰着します。 私は間違っているかもしれません、そしてそれはすべてプロトコルに包まれている可能性があります、例えば:

protocol Foo{bar}
    ...
end

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

また、Fooプロトコルが同じ定義のBarプロトコルを参照できないという重要な問題も明らかになります。

(b)

PAbstractArray {Int、1} <:Indexable {Int}はありますか? パラメータが名前で一致していれば、これは非常にうまくいくと思います。

なぜ_name_でパラメータを一致させる必要があるのか​​わかりません(私はそれをeltype名前と見なしています。誤解した場合は、このセクションを無視してください)。 潜在的な関数シグネチャと一致させないのはなぜですか。 ネーミングを使用する私の主な問題は、次のことを防ぐためです。

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

一方、プロトコルが必要な特定のタイプ階層のみを公開するようにします。 名前がIterable一致しない場合、反復可能を実装するメリットは得られません(また、依存関係に優位性を持たせることもありません)。 しかし、次のことを実行する機能に加えて、ユーザーがそれから何を得るのかわかりません...

(c)それで、私は何かが欠けているかもしれませんが、名前付きタイプが役立つ主な目的は、スーパーセットのさまざまな部分がどのように動作するかを説明することではありませんか? Number階層と抽象型SignedとUnsignedを考えてみましょう。どちらも、 Integerプロトコルを実装しますが、動作がまったく異なる場合があります。 それらを区別するために、 Signedタイプのみに特別なnegateを定義する必要があります(特に、実際にUnsignedタイプを否定したいリターンタイプがないと難しい)?

これは、 super = Number例で説明した問題だと思います。 bitstype Int16 <: Signedを宣言すると(私の他の質問は、型プロパティを持つプロトコルとしてのNumberまたはSignedが具体的な型にどのように適用されるかということです)、 Signed ( super = Signed )プロトコルは、 Unsignedプロトコルでマークされたタイプとは異なるものとしてマークしますか? これは私の見解では奇妙な解決策であり、名前付きの型パラメーターが奇妙だと思ったからではありません。 スーパーに配置されたタイプを除いて2つのプロトコルが完全に一致する場合、とにかくそれらはどのように異なりますか? そして、より大きな型(プロトコル)のサブセット間の動作に違いがある場合、抽象型の目的を再発明するだけではありませんか?

(d)問題は、抽象型で動作を区別し、プロトコルで特定の機能(多くの場合、他の動作に関係なく)を確保して、動作を表現する必要があることです。 しかし、私たちは、プロトコルが保証できる機能と、抽象型の動作がパーティション化されていることを理解しようとしています。

私たちがよくジャンプする解決策は、実装で問題となる「型に抽象クラスを実装する意図を宣言させ、コンプライアンスをチェックさせる」という方針に沿ったものです(循環参照、型ブロックまたはimpl内に関数定義を追加することにつながる)

しかし、さらに重要なことに、プロトコルは、複数の関数(反復など)にわたる複雑な機能を記述する動作を記述しません。その反復の動作は、抽象型(たとえば、ソートされているか、順序付けされているかどうか)によって記述されます。 一方、プロトコルと抽象型の組み合わせは、実際の型を手に入れることができれば、機能(機能ユーティリティメソッド)、動作(高レベルメソッド)、またはその両方(実装の詳細)からディスパッチできるため便利です。メソッド)。

(e)プロトコルが複数のプロトコル(とにかく基本的に構造的である)と具象型と同じ数の抽象型(たとえば、複数の抽象継承なし)を継承できるようにすると、純粋なプロトコル型、純粋な抽象型、およびプロトコル+抽象型。

これにより、上記のSignedとUnsigned問題が修正されると思います。

  • 2つのプロトコルを定義します。どちらも一般的なIntegerProtocol継承します(プロトコル構造、 NumberAddingProtocol 、 IntegerSteppingProtocolなどを継承します)一方はAbstractSignedIntegerから、もう一方はAbstractUnsignedInteger )。
  • 次に、 Signedタイプのユーザーは、機能(プロトコルから)と動作(抽象階層から)の両方が保証されます。
  • プロトコルのないAbstractSignedInteger具象型は、_とにかく_使用できません。
  • しかし、興味深いことに(そしてすでに前述した将来の機能として)、 IntegerSteppingProtocol (これは些細で基本的に単一の関数のエイリアス)だけが存在する場合、最終的に不足している機能を解決する機能を作成できます。具体的なAbstractUnsignedInteger与えられた場合、他のプロトコルを実装することでSignedを解くことができます。 たぶんconvertようなものでも。

それらのほとんどをプロトコル+抽象型に変換し、一部を純粋な抽象型として残すことにより、既存のすべての型を保持します。

編集:(f)構文例(パート(a)を含む)。

編集2 :いくつかの間違いを修正し( :<代わりに<: )、不適切な選択を修正しました( Foo代わりに::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}.

この構文の問題は次のように見えます。

  • プロトコルに対する匿名の内部型(状態変数など)。
  • タイプを返します。
  • セマンティクスを効率的に実装することは困難です。

_抽象型_はエンティティが何であるかを定義します。 _Protocol_は、エンティティが行うことを定義

私の意見では、プロトコルは単一のディスパッチされた抽象型です。 それはパッケージの拡張と協力を助けることができます。 したがって、エンティティが密接に関連している単一のパッケージ内で、抽象型を使用して開発を容易にします(複数のディスパッチから利益を得る)。 エンティティがより独立しているパッケージ間では、プロトコルを使用して実装の露出を減らします。

@ mason-bially

パラメータを名前で一致させる必要がある理由がわかりません

位置による一致とは対照的に、名前による一致を意味します。 これらの名前は、構造的にサブタイプ化されたレコードのように機能します。 私たちが持っている場合

protocol Collection{T}
    eltype = T
end

その場合、 eltypeというプロパティを持つものはすべて、 Collectionサブタイプになります。 これらの「パラメータ」の順序と位置は重要ではありません。

スーパーに配置されたタイプを除いて2つのプロトコルが完全に一致する場合、とにかくそれらはどのように異なりますか? そして、より大きな型(プロトコル)のサブセット間の動作に違いがある場合、抽象型の目的を再発明するだけではありませんか?

それは公正な点です。 名前付きパラメーターは、実際、抽象型のプロパティの多くを取り戻します。 私は、プロトコルと抽象型の両方が必要になるかもしれないという考えから始め、次に機能を統合して一般化しようとしました。 結局のところ、現在type Foo <: Barを宣言すると、あるレベルで実際に行ったことはFoo.super === Bar設定されます。 したがって、関連付けたい他のキーと値のペアとともに、それを直接サポートする必要があるかもしれません。

「型に抽象クラスを実装してコンプライアンスをチェックする意図を宣言させる」

はい、私はそのアプローチをコア機能にすることに反対しています。

プロトコルが複数のプロトコルを継承できるようにすると...そして同じ数の抽象型

これは、たとえば「Tがメソッドx、y、zを持ち、AbstractArrayのサブタイプであると宣言している場合、TはプロトコルPのサブタイプである」という意味ですか? この種の「プロトコル+抽象型」は、私のsuper = Tプロパティの提案で得られるものと非常に似ていると思います。 確かに、私のバージョンでは、現在のように階層をイントロにチェーンする方法をまだ理解していません(例: Integer <: Real <: Number )。

プロトコルに(名目上の)抽象型を継承させることは、プロトコルに対する非常に強い制約のようです。 プロトコルを実装しなかった抽象型のサブタイプはありますか? 私の直感は、プロトコルと抽象型を直交するものとして保持する方が良いということです。

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

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

この構文がわかりません。

  • このプロトコルには名前がありますか?
  • { }と( )ものは正確に
  • このプロトコルをどのように使用しますか? 派遣してもらえますか? もしそうなら、プロトコルが複数のタイプに関連しているとすると、 f(x::ThisProtocol)=...定義することはどういう意味ですか?

その場合、eltypeというプロパティを持つものはすべて、Collectionのサブタイプになります。 これらの「パラメータ」の順序と位置は重要ではありません。

ああ、私の誤解がありました、それはもっと理にかなっています。 つまり、割り当てる機能:

el1type = el_type
el2type = el_type

私の例の問題を解決するために。

したがって、関連付けたい他のキーと値のペアとともに、それを直接サポートする必要があるかもしれません。

そして、このキー/値機能は、abstractをそれに置き換えるため、すべてのタイプに適用されます。 それは素晴らしい一般的な解決策です。 あなたの解決策は今私にとってはるかに理にかなっています。

確かに、私のバージョンでは、現在のように階層をイントロにチェーンする方法をまだ理解していません(たとえば、整数<:実数<:数値)。

super (たとえば、 IntegerのスーパーをReal )、 super特別にして、名前付き型のように動作させるか、追加することができると思いますカスタム型解決コード(ala python)を追加し、 superパラメーターのデフォルトルールを作成する方法。

プロトコルに(名目上の)抽象型を継承させることは、プロトコルに対する非常に強い制約のようです。 プロトコルを実装しなかった抽象型のサブタイプはありますか? 私の直感は、プロトコルと抽象型を直交するものとして保持する方が良いということです。

そうそう、抽象制約は完全にオプションでした! 私の全体的なポイントは、プロトコルと抽象型は直交しているということでした。 抽象+プロトコルを使用して、特定の動作と関連する機能の組み合わせを確実に取得します。 機能のみ(ユーティリティ関数の場合)または動作のみが必要な場合は、それらを直交して使用します。

このプロトコルには名前がありますか?

1つのブロックに由来する2つの名前( FooとBar )を持つ2つのプロトコルですが、マクロを使用してそのような複数の定義を拡張することに慣れています。 私の構文のこの部分は、部分(a)を解決する試みでした。 それを無視すると、最初の行は単純にprotocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFooなる可能性があります( Barプロトコルの別の別個の定義を使用)。 また、通常の型定義のように、 Number 、 AbstractBar 、およびAbstractFooはすべてオプションです。

{}と()の中身は正確にはどういう意味ですか?

{}は、標準のパラメトリック型定義セクションです。 使用を可能にFoo{Float64}実装型説明するFoo使用してプロトコルをFloat64 、例えばします。 ()は基本的に、プロトコル本体の可変バインディングリストです(したがって、複数のプロトコルを一度に記述できます)。 オリジナルの<:ではなく:<入力を間違えたため、混乱が私のせいである可能性があります。 <<name>> <<parametric>> <<bindings>>構造を維持するためにそれらを交換することも価値があるかもしれません。ここで、 <<name>>はバインディングのリストになることがあります。

このプロトコルをどのように使用しますか? 派遣してもらえますか? もしそうなら、プロトコルが複数のタイプに関連しているとすると、 f(x::ThisProtocol)=...定義することはどういう意味ですか?

私の意見では、ディスパッチの例は構文的には正しいようです。実際、次の定義を検討してください。

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)

構造をチェックするためのより具体的な抽象型が指定されていない限り、プロトコルは事実上、名前付きのトップ型(Any)を使用しています。 実際、プロトコル構文を使用するのではなく、 typealias Foo Intersect{FooProtocol, Foo} (_ Edit: Intersectは間違った名前でした。おそらく、 Intersectが最初に正しいのではなくJoin_)のようなものを許可する価値がある

ああ、それは今私にとってはるかに理にかなっています! 同じブロックで複数のプロトコルを一緒に定義するのは興味深いことです。 それについてもう少し考えなければなりません。

数分前にすべての例をクリーンアップしました。 スレッドの前半で、アイデアをテストするためのプロトコルのコーパスを収集することについて誰かが言及しましたが、それは素晴らしいアイデアだと思います。

同じブロック内の複数のプロトコルは、言語をロードするときに定義/コンパイルで両側に正しい型アノテーションを使用してオブジェクト間の複雑な関係を記述しようとすると、一種のペットのようです(たとえば、Pythonのように; Javaはそうではありません)問題があります)。 一方、それらのほとんどは、とにかくマルチメソッドを使用して、使いやすさの点でおそらく簡単に修正できます。 ただし、パフォーマンスの考慮事項は、プロトコル内に機能を正しく入力することから生じる場合があります(vtablesなどに特化してプロトコルを最適化する)。

先ほど、プロトコルは::Anyを使用するメソッドによって(誤って)実装される可能性があるとおっしゃいましたが、それは非常に単純なケースであり、無視するのは非常に簡単なケースだと思います。 実装メソッドが::Anyにディスパッチされた場合、具象型はプロトコルとして分類されません。 一方で、これが必ずしも問題になるとは言えません。

手始めに、 ::Anyメソッドが事後に追加された場合(たとえば、誰かがそれを処理するためのより一般的なシステムを思いついたため)、それはまだ有効な実装であり、最適化機能としてプロトコルを使用する場合も同様です::Anyディスパッチされたメソッドの特殊なバージョンは、パフォーマンスを向上させるために引き続き機能します。 だから結局、私は実際にそれらを無視することに反対するでしょう。

ただし、プロトコル定義者が2つのオプションから選択できる構文を使用することは価値があるかもしれません(デフォルトにする場合は、もう一方を許可します)。 最初に、 ::Anyディスパッチされたメソッドの転送構文、たとえばグローバルキーワード(次のセクションも参照)。 より具体的な方法を要求する2番目の方法として、私は既存の有用なキーワードを考えることができません。

編集:無意味なものの束を削除しました。

Joinは、まさにプロトコルタイプの共通部分です。 それは実際には「出会い」です。 そして幸いなことに、プロトコルタイプは交差点の下ですでに閉じられているため、 Joinタイプは必要ありません。交差点を計算するには、2つのメソッドリストが連結された新しいプロトコルタイプを返すだけです。

プロトコルが::Any定義によって単純化されることについてはあまり心配していません。 私にとって、「 Anyがカウントされないことを除いて、一致する定義を探す」というルールは、オッカムの剃刀に反するものです。 言うまでもなく、サブタイピングアルゴリズムに「ignoreAny」フラグをスレッド化するのはかなり面倒です。 結果として得られるアルゴリズムが一貫しているかどうかさえわかりません。

私はプロトコルのアイデアがとても好きです(少しCLUsterを思い出させます)、私はただ興味があります、これはJuliaConでJeffによって議論された新しいサブタイピングとどのように適合しますか? (私がまだジュリアで本当に見たい2つのこと)。

これにより、独自のサブタイピングルール(https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877)を持つ新しい種類のタイプが追加されます。 一見すると、システムの他の部分と互換性があるように見え、プラグインするだけで済みます。

これらのプロトコルは、 @ mauro3の特性のほとんど「1つのパラメーター」バージョンです。

Joinは、まさにプロトコルタイプの共通部分です。

交差点だと言ったとき、私はどういうわけか自分が間違っていると確信しました。 ただし、1行で型を交差させる方法が必要です( Union )。

編集:

また、プロトコルと抽象型を1つのシステムに一般化し、それらの解決のためのカスタムルールを許可することも好きです(たとえば、現在の抽象型システムを記述するためのsuper )。 これが正しく行われれば、人々はカスタム型システムを追加し、最終的にはそれらの型システムのカスタム最適化を行うことができると思います。 プロトコルが正しいキーワードになるかどうかはわかりませんが、少なくともabstractをマクロに変換することはできますが、それはすばらしいことです。

麦畑から:目的地としての一般化を求めるよりも、プロトコル化され抽象化されたものを通じて共通性を高める方がよい。

何?

プロトコルと抽象型の意図、機能、可能性を一般化するプロセスは、質的に最も満足のいく合成を解決する方法としてはあまり効果的ではありません。 最初に、目的、パターン、プロセスの本質的な共通点を収集する方がうまく機能します。 そして、その理解を深め、自分の視点を洗練させて統合を形成できるようにします。

ジュリアにとって実り多い実現が何であれ、それは合成が提供する足場の上に構築されています。 より明確な合成は、建設的な強さと誘導力です。

何?

彼は、最初にプロトコルから何が欲しいのか、そしてなぜそれらが有用なのかを理解する必要があると言っていると思います。 次に、それと抽象型を取得すると、それらの一般的な合成を思い付くのが簡単になります。

単なるプロトコル

(1)提唱

プロトコルを拡張して、(より複雑な)プロトコルにすることができます。
プロトコルは、(あまり複雑でない)プロトコルになるように縮小される場合があります。
プロトコルは、[ソフトウェアで]準拠するインターフェースとして実現できます。
インターフェースの適合性を判断するために、プロトコルを照会することができます。

(2)提案

プロトコルは、デフォルトでプロトコル固有のバージョン番号をサポートする必要があります。

これを行うための何らかの方法をサポートするのは良いことです:
インターフェイスがプロトコルに準拠している場合は、trueと応答します。 インターフェイスの場合
プロトコルのサブセットに忠実であり、拡張された場合は準拠します。
不完全な応答、それ以外の場合はfalseの応答。 関数はすべてをリストする必要があります
プロトコルに対して不完全なインターフェイスに必要な拡張。

(3)ミュージング

プロトコルは、優れた種類のモジュールである可能性があります。 その輸出は役立つだろう
一部のインターフェイスが準拠しているかどうかを判断するときの最初の比較対象として。
[エクスポートされた]タイプと関数で指定されたプロトコルは、次を使用して宣言できます。
@abstract 、 @type 、 @immutable 、および@function 、固有の抽象化をサポートします。

[pao:コード引用符に切り替えますが、事後にこれを行っているとき、馬はすでに納屋を離れていることに注意してください...]

( @mentionsを引用する必要があります!)

ありがとう-それを修正する

3時01分AMで水曜日、2015ĺš´12月16日には、マウロの[email protected]は書きました:

(@メンションを引用する必要があります!)

—
このメールに直接返信するか、GitHubで表示してください
https://github.com/JuliaLang/julia/issues/6975#issuecomment-165026727 。

申し訳ありませんが、もっと明確にすべきでした:コード引用符は `ではなく`を使用しています

引用符の修正を修正しました。

ありがとう-私の以前の無知を許してください

プロトコルタイプの追加に関するこの最近の議論を理解しようとしました。 何かを誤解しているかもしれませんが、プロトコルが記述しようとしている関連する抽象型の名前を使用する代わりに、プロトコルに名前を付ける必要があるのはなぜですか?

私の見解では、現在の抽象型システムを、型から期待される動作を記述するための何らかの方法で拡張するのは非常に自然なことです。 このスレッドで最初に提案されたものとよく似ていますが、おそらくJeffs構文を使用します

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

このルートを進む場合、サブタイプがインターフェースを実装していることを特別に示す必要はありません。 これは、サブタイピングによって暗黙的に行われます。

明示的なインターフェイスメカニズムの主な目標は、IMHOがより適切なエラーメッセージを取得し、より適切な検証テストを実行することです。

したがって、次のような型宣言:

type Foo <: Iterable
  ...
end

...セクションで関数を定義しますか? そうでない場合、欠落している機能(およびそれに関連する複雑さ)についていつエラーが発生しますか? また、複数のプロトコルを実装する型ではどうなりますか?複数の抽象継承を有効にしますか? スーパーメソッドの解決をどのように処理しますか? これは多重ディスパッチで何をしますか(それを削除してJava風のオブジェクトシステムをそこに貼り付けるようです)? 最初の型が定義された後、メソッドの新しい型の特殊化をどのように定義しますか? タイプを定義した後、プロトコルをどのように定義しますか?

これらの質問はすべて、新しいタイプを作成する(または新しいタイプの定式化を作成する)ことで簡単に解決できます。

各プロトコルに関連する抽象型が必ずしも存在するわけではありません(おそらく実際には存在しないはずです)。 現在のインターフェースの倍数は、同じタイプで実装できます。 これは、現在の抽象型システムでは説明できません。 したがって、問題。

  • 抽象多重継承(複数のプロトコルを実装する)は、この機能に直交します(上記のJeffが述べたように)。 つまり、プロトコルが言語に追加されたからといって、その機能が利用できるわけではありません。
  • 次のコメントは、インターフェイスをいつ検証するかという質問についてです。 これは、ブロック内の関数定義にリンクする必要はないと思います。これは、私にはジュリアンを感じさせません。 代わりに、3つの簡単な解決策があります。

    1. #7025で実装されているように、 verify_interfaceメソッドを使用します。このメソッドは、すべての関数定義の後で、または単体テストで呼び出すことができます。

    2. インターフェイスをまったく確認して、「MethodError」の改善されたエラーメッセージに延期することはできません。 実際、これは1の優れたフォールバックです。

    3. コンパイル時単位の終了時またはモジュールのロードフェーズの終了時に、すべてのインターフェイスを確認します。 現在、次のことも可能です。

function a()
  b()
end

function b()
end

したがって、ここではブロック内関数の定義は必要ないと思います。

  • 最後のポイントは、抽象型にリンクされていないプロトコルが存在する可能性があるということです。 これは現在確かに真実です(例えば、非公式の「Iterable」プロトコル)。 ただし、私の見解では、これは複数の抽象継承がないためです。 これが懸念される場合は、これを解決することを目的とした新しい言語機能を追加するのではなく、抽象的な多重継承を追加してください。 また、複数のインターフェイスを実装することは絶対に重要であり、これはJava / C#では絶対に一般的だと思います。

「プロトコル」のようなものと多重継承の違いは、型が定義された後にプロトコルに追加できることだと思います。 これは、パッケージ(プロトコルの定義)を既存のタイプで機能させる場合に役立ちます。 作成後にタイプのスーパータイプを変更できるようにすることもできますが、その時点では、「プロトコル」などと呼ぶ方がよいでしょう。

うーん、それは既存のタイプへの代替/拡張インターフェースを定義することを可能にします。 これが本当に必要な場所はまだはっきりしていません。 既存のインターフェースに何かを追加したい場合(OPで提案されたアプローチに従う場合)、単にサブタイプを作成し、サブタイプに追加のインターフェースメソッドを追加します。 これは、そのアプローチの良いところです。 それは非常にうまくスケーリングします。

例:型をシリアル化するパッケージがあるとします。 メソッドtobits型に実装する必要があります。そうすれば、そのパッケージ内のすべての関数がその型で機能します。 これをSerializerプロトコルと呼びましょう(つまり、 tobitsが定義されています)。 これで、 tobits実装することで、 Array (またはその他のタイプ)を追加できます。 多重継承では、定義後にスーパータイプをArrayに追加できないため、 ArrayをSerialzerさせることができませんArrayた。 これは重要なユースケースだと思います。

わかりました、これを理解してください。 https://github.com/JuliaLang/IterativeSolvers.jl/issues/2も同様の問題であり、解決策は基本的にダックタイピングを使用することです。 この問題をエレガントに解決する何かがあれば、これは確かに素晴らしいことです。 しかし、これはディスパッチレベルでサポートする必要があるものです。 上記のプロトコルの考え方を正しく理解していれば、抽象型またはプロトコルを型注釈として機能させることができます。 ここでは、これら2つの概念を、十分に強力な1つのツールに統合するとよいでしょう。

私は同意します:抽象型とプロトコルの両方を持つことは非常に混乱するでしょう。 私が正しく思い出すと、抽象型にはプロトコルでモデル化できないいくつかのセマンティクスがある、つまり抽象型にはプロトコルにはない機能があるということが上で議論されました。 それが必然的に当てはまるとしても(私は確信していませんが)、2つの概念の間にそのような大きな重複があるため、それでも混乱を招きます。 したがって、プロトコルを優先して抽象型を削除する必要があります。

プロトコルについて上記のコンセンサスがある限り、それらはインターフェースの指定を強調しています。 抽象型は、その存在しないプロトコルのいくつかを実行するために使用された可能性があります。 それはそれが彼らの最も重要な用途であるという意味ではありません。 プロトコルとは何かを教えてください。それから、抽象型がどのように異なり、それらがもたらすもののいくつかを教えてください。 私は、抽象型が類型学と同じくらいインターフェースについてであると考えたことはありません。 類型的な柔軟性への自然なアプローチを捨てることは費用がかかります。

@JeffreySarnoff +1

数値タイプの階層について考えてみてください。 Signed、Unsignedなどのさまざまな抽象型は、それらのインターフェースによって定義されません。 「符号なし」を定義する一連のメソッドはありません。 これは単に非常に便利な宣言です。

本当に問題はわかりません。 SignedタイプとUnsignedタイプの両方が同じメソッドのセットをサポートしている場合、同じインターフェイスで2つのプロトコルを作成できます。 それでも、とタイプを宣言SignedではなくUnsignedディスパッチのために使用することができます(同じ機能のつまりの方法は異なる動作)。 ここで重要なのは、型がプロトコルを実装していることを考慮する前に、実装するメソッドに基づいて暗黙的にこれを検出するのではなく、明示的な宣言を要求することです。

ただし、 https: //github.com/JuliaLang/julia/issues/6975#issuecomment -168499775のように、暗黙的に関連付けられたプロトコルを持つことも重要です。

プロトコルは、呼び出すことができる関数を定義できるだけでなく、保持する必要のあるプロパティを(暗黙的に、またはマシンでテスト可能な方法で)文書化することもできます。 そのような:

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

SignedとUnsigned間のこの外部から見える動作の違いが、この区別を役立てています。

少なくとも理論的には外部からすぐに検証できないほど「抽象的」な型の区別が存在する場合、正しい選択を行うには型の実装を知る必要がある可能性があります。 これは、現在のabstractが役立つ可能性がある場所です。 これはおそらく代数的データ型の方向に進みます。

プロトコルを使用してタイプを単純にグループ化する必要がない理由はありません。つまり、定義されたメソッドを必要としません(そして、トリックを使用した「現在の」設計で可能です:https://github.com/JuliaLang/julia/issues/ 6975#issuecomment-161056795)。 (また、これは暗黙的に定義されたプロトコルに干渉しないことに注意してください。)

考えると(Un)signed例を:私はあるタイプを持っていた場合、私は何をするだろうSignedが、何らかの理由で別の抽象型のサブタイプでもなければなりませんか? これは不可能です。

@eschnett :現時点では、抽象型はそのサブタイプの実装とは何の関係もありません。 それは議論されていますが:#4935。

代数的データ型は、連続的な改良が本質的に意味のある良い例です。
分類法は、はるかに自然に与えられ、プロトコル仕様のメランジとしてよりも抽象型階層としてより直接的に役立ちます。

複数の抽象型階層のサブタイプである型を持つことについての注意も重要です。 抽象化の多重継承に伴う功利主義的な力はかなりあります。

@ mauro3はい、わかっています。 私は、識別された共用体と同等の何かを考えていましたが、型システムを介してではなく、タプルと同じくらい効率的に実装されました(共用体が現在実装されているため)。 これは列挙型、null許容型を包含し、現在の抽象型よりも効率的に他のいくつかのケースを処理できる可能性があります。

たとえば、匿名要素を持つタプルのように:

DiscriminatedUnion{Int16, UInt32, Float64}

または名前付き要素を使用:

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

私が言いたかったのは、抽象型はそのような構成をJuliaにマップする良い方法の1つであるということです。

プロトコルを使用してタイプを単純にグループ化する必要がない理由はありません。つまり、定義されたメソッドを必要としません(そして、トリックを使用した「現在の」設計で可能です:#6975(コメント))。 (また、これは暗黙的に定義されたプロトコルに干渉しないことに注意してください。)

パフォーマンスを達成するには、これに注意する必要があると思います。多くの人が十分に頻繁に検討しているようには見えません。 この例では、コンパイラがコンパイル時に関数を選択できるように、非任意のバージョンを単純に定義したいように思われます(実行時に正しい関数を選択するために関数を呼び出したり、コンパイラが関数を検査したりする必要はありません)。結果を決定するため)。 個人的には、タグとして複数の抽象的な「継承」を使用する方が良い解決策になると思います。

必要なトリックと型システムの知識を最小限に抑える必要があると思います(マクロにラップすることはできますが、マクロの奇妙なハックのように感じます。マクロを使用して型システムを操作している場合は、 @ JeffBezansonの統合ソリューションは、この問題をより適切に修正します)。

(Un)signedの例を考えると、Signed型であるが、何らかの理由で別の抽象型のサブ型でもある必要がある場合はどうすればよいでしょうか。 これは不可能です。

複数の抽象継承。


私はこのすべての地面が以前にカバーされたと信じています、この会話は円を描いて進んでいるように見えます(毎回より緊密な円ですが)。 コーパスやプロトコルを使った問題を取得する必要があると言われたと思います。 これにより、ソリューションをより簡単に判断できるようになります。

繰り返しになりますが:)プロトコルは構造的であるのに対し、抽象型は名目上のものであることを皆さんに思い出させたいので、プロトコルで抽象型の許容可能な「エンコーディング」を実際に思い付くことができない限り、それらを直交として扱う設計を好みます。 (おそらく、関連する型を巧みに使用することで)。 もちろん、複数の抽象継承も生成する場合は、ボーナスポイントが得られます。 これは可能だと思いますが、まだ十分ではありません。

@JeffBezanson 「関連付けられた型」は「[a]プロトコルに関連付けられた具体的な型」とは異なりますか?

はい、私はそう信じています。 プロトコルがメソッドを指定するのと同じように、「値」がタイプであるキーと値のペアを指定するプロトコルの技術的な意味での「関連付けられたタイプ」を意味します。 たとえば、「タイプFooはeltypeがある場合はコンテナプロトコルに従います」または「タイプFooはndimsパラメータが2の場合はマトリックスプロトコルに従います」。

抽象型は名目上のものですが、プロトコルは構造的であり、
抽象型は定性的ですが、プロトコルは機能し、
抽象型(多重継承)は、プロトコルが実行している間にオーケストレーションを行います

一方のエンコーディングがもう一方にあったとしても、「こんにちは、こんにちは..お元気ですか?行こう!」 of Juliaは、プロトコルの一般的に目的のある概念と多継承可能な抽象型(一般化された目的の概念)の両方を明確に提示する必要があります。 ジュリアに別々に折りたたまれた両方を与える巧妙な展開がある場合、それは一方を他方を介して行うよりも、まさにそうする可能性が高いです。

@ mason-bially:では、多重継承も追加する必要がありますか? これでも、タイプの作成後にスーパータイプを追加できないという問題が残ります(許可されている場合を除く)。

@JeffBezanson :純粋に名目上のプロトコルを許可することを

@ mauro3事後スーパータイプの挿入を許可するかどうかの決定を、多重継承に

サークルの別のラウンドを開始した場合は申し訳ありません。 トピックは非常に複雑であり、ソリューションが非常に簡単に使用できることを確認する必要があります。 したがって、以下をカバーする必要があります。

  • 一般的な解決策
  • パフォーマンスの低下なし
  • 使いやすさ(そして理解しやすい!)

#6975で最初に提案されたものは、後で説明するプロトコルのアイデアとはまったく異なるため、プロトコルがどのように見えるかを説明するある種のJEPがあるとよいでしょう。

正式なインターフェースを定義し、現在の0.4(マクロなし)を使用して検証する方法の例として、ディスパッチは現在、gf.cに変更が加えられていない限り、トレイトスタイルのディスパッチに依存しています。 これは検証に生成された関数を使用し、すべての型計算は型空間で実行されます。

提供されたタイプが日付のイテレーターであることを確認する必要がある場所を定義しているDSLのランタイムチェックとしてこれを使用し始めています。

現在、スーパータイプの多重継承をサポートしています。_superフィールド名はランタイムでは使用されず、任意の有効なシンボルにすることができます。 _superタプルに他のn個のタイプを指定できます。

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

ここで、 https: //github.com/JuliaLang/julia/issues/5#issuecomment -230645040で、JuliaConからの特性に関する可能な構文についてのディスカッションをフォローアップしたことを指摘し

ガイ・スティールは、多重ディスパッチ言語(要塞)の特性についていくつかの優れた洞察を持っています。彼のJuliaCon 2016基調講演を参照してhttps ://youtu.be/EZD3Scuv02g。

いくつかのハイライト:代数プロパティの大きな特性システム、特性を実装する型の特性プロパティの単体テスト、およびそれらが実装したシステムは多分複雑すぎて、彼は今より簡単なことをするだろうということです。

プロトコルのテンソルフローコンパイラADユースケースの新しいSwift:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholyと@Kenoはこれに興味があるかもしれません。 まったく新しいコンテンツがあります

この問題の設計空間を探るとき、このプレゼンテーションは注目に値すると思います。

非特定のアイデアや関連する背景作業へのリンクについて話し合うには、対応する談話スレッドを開始し、そこに投稿して話し合う方がよいでしょう。

静的に型付けされた言語でのジェネリックプログラミングの研究で遭遇し議論された問題のほとんどすべては、ジュリアとは無関係であることに注意してください。 静的言語は、型システム違反がないことを静的に型チェックできる一方で、必要なコードを書くのに十分な表現力を提供するという問題にほぼ独占的に関わっています。 表現力に問題はなく、静的な型チェックも必要ないので、ジュリアではそれは本当に重要ではありません。

私たちが気にかけているのは、言語が動的に検証できる構造化された方法でプロトコルの期待を文書化できるようにすることです(可能な場合は事前に)。 私たちはまた、人々が特性のようなものに派遣できるようにすることにも関心を持っています。 それらを接続する必要があるかどうかは開いたままです。

結論:静的言語でのプロトコルに関する学術研究は一般的に興味深いかもしれませんが、Juliaのコンテキストではあまり役に立ちません。

私たちが気にかけているのは、言語が動的に検証できる構造化された方法でプロトコルの期待を文書化できるようにすることです(可能な場合は事前に)。 私たちはまた、人々が特性のようなものに派遣できるようにすることにも関心を持っています。 それらを接続する必要があるかどうかは開いたままです。

_それは_:ticket:

重大な変更を回避することは別として、抽象型の排除とgolangスタイルの暗黙的なインターフェースの導入はjuliaで実現可能でしょうか?

いいえ、そうではありません。

さて、それはどのプロトコル/特性がすべてについてであるかではありませんか? プロトコルを暗黙的または明示的にする必要があるかどうかについて、いくつかの議論がありました。

0.3(2014)以降、経験上、暗黙のインターフェース(つまり、言語/コンパイラーによって強制されない)が問題なく機能することが示されていると思います。 また、いくつかのパッケージがどのように進化したかを目の当たりにして、最高のインターフェースは有機的に開発され、後の時点でのみ形式化(=文書化)されたと思います。

どういうわけか言語によって強制されるインターフェースの正式な説明が必要かどうかはわかりません。 しかし、それが決定されている間、次のことを奨励するのは素晴らしいことです(ドキュメント、チュートリアル、およびスタイルガイドで):

  1. 「インターフェース」は安価で軽量であり、一連の型に対して所定の動作をする一連の関数です(はい、型は適切なレベルの粒度です。 x::T場合、 Tで十分です。 xがインターフェースを実装するかどうかを決定するため)。 したがって、拡張可能な動作を備えたパッケージを定義している場合は、インターフェースを文書化することは本当に理にかなっています。

  2. インターフェイスは、サブタイプの関係で記述する必要はありません。 共通の(重要な)スーパータイプのない型は、同じインターフェースを実装する場合があります。 タイプは複数のインターフェースを実装できます。

  3. 転送/構成には暗黙的にインターフェースが必要です。 「ラッパーに親のすべてのメソッドを継承させる方法」は頻繁に発生する質問ですが、正しい質問ではありません。 実用的な解決策は、コアインターフェイスを用意し、それをラッパーに実装することです。

  4. 特性は安価であり、自由に使用する必要があります。 Base.IndexStyleは、優れた標準的な例です。

ベストプラクティスが何であるかわからないので、以下は明確化の恩恵を受けるでしょう:

  1. インターフェイスには、オブジェクトがインターフェイスを実装するかどうかを決定するためのTables.istableなどのクエリ関数が必要ですか? 発信者がさまざまな代替インターフェイスを操作でき、フォールバックのリストをたどる必要がある場合は、これは良い習慣だと思います。

  2. docstringのインターフェースドキュメントに最適な場所はどこですか? 上記のクエリ関数と言えます。

  1. はい、タイプは適切なレベルの粒度です

どうしてこんなことに? 反復など、タイプの一部の側面は、(ディスパッチの目的で)インターフェースに組み込まれる場合があります。 そうしないと、コードを書き直すか、不要な構造を課す必要があります。

  1. インターフェイスは、サブタイプの関係で記述する必要はありません。

おそらくそれは必要ではありませんが、それはより良いでしょうか? 反復可能な型で関数ディスパッチを行うことができます。 タイル化された反復可能型は、暗黙的にそれを満たすべきではありませんか? インターフェースだけを気にするのに、なぜユーザーはこれらを名目型の周りに描画する必要があるのでしょうか。

基本的に抽象インターフェースとして使用している場合、名目上のサブタイピングのポイントは何ですか? 特性はよりきめ細かく強力であるように思われるので、より一般化する必要があります。 したがって、型はほとんど特性のように見えますが、それらの制限を回避するには特性が必要です(逆も同様です)。

基本的に抽象インターフェースとして使用している場合、名目上のサブタイピングのポイントは何ですか?

ディスパッチ—名目上のタイプのものにディスパッチできます。 型がインターフェースを実装しているかどうかをディスパッチする必要がない場合は、ダックタイピングするだけです。 これは、人々が通常聖なる特性を使用する目的です。この特性を使用すると、何らかのインターフェイスが実装されていることを前提とした実装を呼び出すためにディスパッチできます(たとえば、「既知の長さ」)。 人々が望んでいるように見えるのは、その間接参照の層を回避することですが、それは単なる便利であり、必要ではないように思われます。

どうしてこんなことに? 反復など、タイプの一部の側面は、(ディスパッチの目的で)インターフェースに組み込まれる場合があります。 そうしないと、コードを書き直すか、不要な構造を課す必要があります。

@tpappは、何かがインターフェイスを実装しているかどうかを判断するために必要なのは型だけであり、すべてのインターフェイスを型階層で表すことができるとは限らないと言っていたと思います。

MacroToolsのforwardを使用している間、考えてみてください。

多くのメソッドを転送するのは時々面倒です

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

Foo.xのタイプとメソッドのリストを使用して、どちらを転送するかを推測できるとしたらどうでしょうか。 これは一種のinheritanceあり、既存の機能(マクロ+生成された関数)で実装できます。ある種のインターフェースのようにも見えますが、言語には他に何も必要ありません。

継承するもののリストを思い付くことができなかったことは知っています(これが静的classモデルの柔軟性が低い理由でもあります)。必要なのはそのうちのいくつかだけですが、コア機能には便利です(たとえば、誰かがArray周りにラッパー( AbstractArrayサブタイプ)を定義したい場合、ほとんどの関数は転送されます)

@datnamer :他の人が明らかにしているように、インターフェースはタイプよりも細かくすべきではありません(つまり、インターフェースの実装は、タイプが与えられた場合、

はっきりしなかったかもしれませんが、私の回答の目的は、ジュリアで役立つ程度のインターフェースがすでにあり、それらは軽量で高速であり、生態系が成熟するにつれて普及することを指摘することでした。

インターフェイスを記述するための正式な仕様は、IMOの価値をほとんど追加しません。それは、ドキュメントを作成し、いくつかのメソッドが使用可能であることを確認するだけです。 後者はインターフェースの一部ですが、他の部分はこれらのメソッドによって実装されるセマンティクスです(たとえば、 Aが配列の場合、 axes(A)はgetindex有効な座標の範囲を提供します

しかし、私が見たいのは

  1. ますます多くのインターフェースのドキュメント(docstring内)、

  2. 新しく定義された型の成熟したインターフェースの明らかなエラーをキャッチするためのテストスイート(たとえば、多くのT <: AbstractArrayはeltype(::Type{T})ではなくeltype(::T)を実装します)。

@tpapp今、私には理にかなっています、ありがとう。

@StefanKarpinskiよくわかりません。 特性は名目的型ではありませんが(右?)、それでもディスパッチに使用できます。

私のポイントは基本的にここで@tknoppと@ mauro3によって作られたものです: https : //discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u = datnamer

特性と抽象的なタイピングを使用することにより、2つの非常に類似した概念を使用することにより、さらに複雑で混乱が生じます。

人々が望んでいるように見えるのは、その間接参照の層を回避することですが、それは単なる便利であり、必要ではないように思われます。

特性階層のセクションは、タイプパラメータを使用して、和集合や交差点などによってグループ化されたときに確実にディスパッチできますか? 試したことはありませんが、言語サポートが必要な気がします。 タイプドメインでのIE式の問題。

編集:問題は、ここで使用されているインターフェースと特性の混同であったと思います。

これをここに投稿するだけで楽しいのです。Conceptsは間違いなく受け入れられており、C ++ 20の一部になるようです。 面白いもの!

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

特性はこの問題を解決するための本当に良い方法だと思います、そして聖なる特性は確かに長い道のりを歩んできました。 ただし、Juliaが本当に必要としているのは、特性に属する関数をグループ化する方法だと思います。 これは、ドキュメントの理由だけでなく、コードの読みやすさにも役立ちます。 私がこれまで見てきたことから、Rustのような特性構文が進むべき道だと思います。

これは非常に重要だと思います。最も重要なユースケースは、イテレータのインデックス作成です。 これがあなたがうまくいくと思うかもしれない種類の構文の提案です。 すでに提案されている場合はお詫びします(長いスレッド...)。

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
このページは役に立ちましたか?
0 / 5 - 0 評価