Julia: Antarmuka untuk Tipe Abstrak

Dibuat pada 26 Mei 2014  ·  171Komentar  ·  Sumber: JuliaLang/julia

Saya pikir permintaan fitur ini belum menjadi masalahnya sendiri meskipun telah dibahas di misalnya #5.

Saya pikir akan lebih bagus jika kita dapat secara eksplisit mendefinisikan antarmuka pada tipe abstrak. Yang saya maksud dengan antarmuka adalah semua metode yang harus diimplementasikan untuk memenuhi persyaratan tipe abstrak. Saat ini, antarmuka hanya didefinisikan secara implisit dan dapat tersebar di beberapa file sehingga sangat sulit untuk menentukan apa yang harus diimplementasikan ketika diturunkan dari tipe abstrak.

Antarmuka utama akan memberi kita dua hal:

  • dokumentasi diri antarmuka di satu tempat
  • pesan kesalahan yang lebih baik

Base.graphics memiliki makro yang benar-benar memungkinkan untuk mendefinisikan antarmuka dengan mengkodekan pesan kesalahan dalam implementasi fallback. Saya pikir ini sudah sangat pintar. Tapi mungkin memberikan sintaks berikut ini bahkan lebih rapi:

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

Di sini akan rapi jika seseorang dapat menentukan perincian yang berbeda. Deklarasi print dan push! hanya mengatakan bahwa harus ada metode apa pun dengan nama itu (dan MyType sebagai parameter pertama) tetapi mereka tidak menentukan jenisnya. Sebaliknya deklarasi size benar-benar diketik. Saya pikir ini memberikan banyak fleksibilitas dan untuk deklarasi antarmuka yang tidak diketik, seseorang masih dapat memberikan pesan kesalahan yang cukup spesifik.

Seperti yang saya katakan di #5, antarmuka seperti itu pada dasarnya adalah apa yang direncanakan dalam C++ sebagai Concept-light untuk C++14 atau C++17. Dan setelah melakukan beberapa pemrograman template C++, saya yakin bahwa beberapa formalisasi di area ini juga akan baik untuk Julia.

Komentar yang paling membantu

Untuk diskusi tentang ide-ide non-spesifik dan tautan ke pekerjaan latar belakang yang relevan, akan lebih baik untuk memulai utas dan posting wacana yang sesuai dan berdiskusi di sana.

Perhatikan bahwa hampir semua masalah yang dihadapi dan dibahas dalam penelitian tentang pemrograman generik dalam bahasa yang diketik secara statis tidak relevan dengan Julia. Bahasa statis hampir secara eksklusif berkaitan dengan masalah penyediaan ekspresi yang cukup untuk menulis kode yang mereka inginkan sambil tetap dapat mengetik secara statis memeriksa bahwa tidak ada pelanggaran sistem tipe. Kami tidak memiliki masalah dengan ekspresif dan tidak memerlukan pemeriksaan tipe statis, jadi tidak ada yang benar-benar penting di Julia.

Yang kami pedulikan adalah mengizinkan orang untuk mendokumentasikan ekspektasi protokol dengan cara terstruktur yang kemudian dapat diverifikasi oleh bahasa secara dinamis (sebelumnya, jika memungkinkan). Kami juga peduli untuk mengizinkan orang mengirimkan hal-hal seperti sifat; itu tetap terbuka apakah itu harus terhubung.

Intinya: sementara pekerjaan akademis tentang protokol dalam bahasa statis mungkin menarik bagi umum, itu tidak terlalu membantu dalam konteks Julia.

Semua 171 komentar

Secara umum, saya pikir ini adalah arah yang baik untuk pemrograman berorientasi antarmuka yang lebih baik.

Namun, ada sesuatu yang hilang di sini. Tanda tangan metode (bukan hanya namanya) juga penting untuk sebuah antarmuka.

Ini bukan sesuatu yang mudah untuk diterapkan dan akan ada banyak kesalahan. Itu mungkin salah satu alasan mengapa _Concepts_ tidak diterima oleh C++ 11, dan setelah tiga tahun, hanya versi _lite_ yang sangat terbatas yang masuk ke C++ 14.

Metode size dalam contoh saya berisi tanda tangan. Selanjutnya @mustimplement dari Base.graphics juga memperhitungkan tanda tangan.

Saya harus menambahkan bahwa kita sudah memiliki satu bagian dari Concept-light yang merupakan kemampuan untuk membatasi tipe menjadi subtipe dari tipe abstrak tertentu. Antarmuka adalah bagian lain.

Makro itu cukup keren. Saya telah mendefinisikan fallback pemicu kesalahan secara manual, dan ini bekerja cukup baik untuk mendefinisikan antarmuka. misalnya MathProgBase JuliaOpt melakukan ini, dan itu bekerja dengan baik. Saya bermain-main dengan pemecah baru ( https://github.com/IainNZ/RationalSimplex.jl ) dan saya hanya harus terus mengimplementasikan fungsi antarmuka sampai berhenti meningkatkan kesalahan agar berfungsi.

Proposal Anda akan melakukan hal serupa, bukan? Tetapi apakah Anda _memiliki_ untuk mengimplementasikan seluruh antarmuka?

Bagaimana ini menangani parameter kovarian/kontravarian?

Sebagai contoh,

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 Ya, proposal sebenarnya tentang membuat @mustimplement sedikit lebih fleksibel sehingga misalnya tanda tangan dapat tetapi tidak harus disediakan. Dan perasaan saya adalah bahwa ini adalah "inti" yang layak untuk mendapatkan sintaksnya sendiri. Akan sangat bagus untuk menegakkan bahwa semua metode benar-benar diimplementasikan tetapi pemeriksaan runtime saat ini seperti yang dilakukan di @mustimplement sudah merupakan hal yang hebat dan mungkin lebih mudah untuk diterapkan.

@lindahua Itu contoh yang menarik. Harus berpikir tentang itu.

@lindahua Seseorang mungkin ingin contoh Anda berfungsi. @mustimplement tidak akan berfungsi karena mendefinisikan tanda tangan metode yang lebih spesifik.

Jadi ini mungkin harus diimplementasikan sedikit lebih dalam di kompiler. Pada definisi tipe abstrak, seseorang harus melacak nama/tanda tangan antarmuka. Dan pada titik di mana saat ini kesalahan "... tidak ditentukan" dilemparkan, seseorang harus menghasilkan pesan kesalahan yang sesuai.

Sangat mudah untuk mengubah cara MethodError print , ketika kita memiliki sintaks dan API untuk mengekspresikan dan mengakses informasi.

Hal lain yang bisa kita dapatkan adalah fungsi di base.Test untuk memverifikasi bahwa suatu tipe (semua tipe?) sepenuhnya mengimplementasikan antarmuka tipe induk. Itu akan menjadi tes unit yang sangat rapi.

Terima kasih @ivarne. Sehingga implementasinya bisa terlihat sebagai berikut:

  1. Seseorang memiliki kamus global dengan tipe abstrak sebagai kunci dan fungsi (+ tanda tangan opsional) sebagai nilai.
  2. Pengurai perlu diadaptasi untuk mengisi dict ketika deklarasi has diuraikan.
  3. MethodError perlu dicari jika fungsi saat ini adalah bagian dari kamus global.

Sebagian besar logika kemudian akan berada di MethodError .

Saya telah bereksperimen sedikit dengan ini dan menggunakan Intisari berikut https://Gist.github.com/tknopp/ed53dc22b61062a2b283 yang dapat saya lakukan:

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

saat mendefinisikan length tidak ada kesalahan yang muncul:

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

Bukan berarti ini saat ini tidak memperhitungkan tanda tangan.

Saya memperbarui kode di inti sedikit sehingga tanda tangan fungsi dapat diperhitungkan. Ini masih sangat hacky tetapi yang berikut ini sekarang berfungsi:

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

Saya seharusnya menambahkan bahwa cache antarmuka di Gist sekarang beroperasi pada simbol alih-alih fungsi sehingga seseorang dapat menambahkan antarmuka dan mendeklarasikan fungsi setelahnya. Saya mungkin harus melakukan hal yang sama dengan tanda tangan.

Baru saja melihat bahwa #2248 sudah memiliki beberapa materi di antarmuka.

Saya akan menunda penerbitan pemikiran tentang fitur yang lebih spekulatif seperti antarmuka sampai setelah kami mendapatkan 0,3 keluar dari pintu, tetapi karena Anda telah memulai diskusi, inilah sesuatu yang saya tulis beberapa waktu lalu.


Berikut adalah mockup sintaks untuk deklarasi antarmuka dan implementasi antarmuka itu:

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

Mari kita hancurkan ini menjadi beberapa bagian. Pertama, ada sintaks tipe fungsi: A --> B adalah tipe fungsi yang memetakan objek bertipe A untuk mengetik B . Tuple dalam notasi ini melakukan hal yang jelas. Secara terpisah, saya mengusulkan bahwa f :: A --> B akan mendeklarasikan bahwa f adalah fungsi umum, tipe pemetaan A untuk mengetik B . Ini adalah pertanyaan yang sedikit terbuka apa artinya ini. Apakah ini berarti bahwa ketika diterapkan pada argumen bertipe A , f akan memberikan hasil bertipe B ? Apakah itu berarti f hanya dapat diterapkan pada argumen bertipe A ? Haruskah konversi otomatis terjadi di mana saja – pada keluaran, pada masukan? Untuk saat ini, kita dapat menganggap bahwa semua ini adalah membuat fungsi generik baru tanpa menambahkan metode apa pun ke dalamnya, dan tipenya hanya untuk dokumentasi.

Kedua, ada deklarasi antarmuka Iterable{T,S} . Ini membuat Iterable sedikit seperti modul dan sedikit seperti tipe abstrak. Ini seperti modul yang memiliki ikatan dengan fungsi generik yang disebut Iterable.start , Iterable.done dan Iterable.next . Ini seperti tipe di Iterable dan Iterable{T} dan Iterable{T,S} dapat digunakan di mana pun tipe abstrak dapat – khususnya, dalam metode pengiriman.

Ketiga, ada blok implement mendefinisikan bagaimana UnitRange mengimplementasikan antarmuka Iterable . Di dalam blok implement , fungsi Iterable.start , Iterable.done dan Iterable.next tersedia, seolah-olah pengguna telah melakukan import Iterable: start, done, next , memungkinkan penambahan metode ke fungsi-fungsi ini. Blok ini seperti template seperti deklarasi tipe parametrik – di dalam blok, UnitRange berarti UnitRange spesifik, bukan tipe payung.

Keuntungan utama dari blok implement adalah menghindari kebutuhan eksplisit import fungsi yang ingin Anda perluas – mereka secara implisit diimpor untuk Anda, yang bagus karena orang umumnya bingung tentang import pula. Ini sepertinya cara yang lebih jelas untuk mengungkapkannya. Saya menduga bahwa sebagian besar fungsi generik dalam Base yang ingin diperluas pengguna harus dimiliki oleh beberapa antarmuka, jadi ini akan menghilangkan sebagian besar penggunaan import . Karena Anda selalu dapat sepenuhnya memenuhi syarat sebuah nama, mungkin kita bisa menghilangkannya sama sekali.

Gagasan lain yang saya pantulkan adalah pemisahan versi fungsi antarmuka "dalam" dan "luar". Yang saya maksud dengan ini adalah bahwa fungsi "dalam" adalah fungsi yang Anda berikan metode untuk mengimplementasikan beberapa antarmuka, sedangkan fungsi "luar" adalah yang Anda panggil untuk mengimplementasikan fungsionalitas generik dalam hal beberapa antarmuka. Pertimbangkan ketika Anda melihat metode fungsi sort! (tidak termasuk metode yang tidak digunakan lagi):

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

Beberapa dari metode ini dimaksudkan untuk konsumsi publik, tetapi yang lain hanyalah bagian dari implementasi internal metode penyortiran publik. Sungguh, satu-satunya metode publik yang seharusnya dimiliki ini adalah ini:

sort!(v::AbstractArray)

Sisanya adalah kebisingan dan termasuk di "dalam". Secara khusus,

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)

jenis metode adalah apa yang diimplementasikan oleh algoritme pengurutan untuk dihubungkan ke mesin penyortiran generik. Saat ini Sort.Algorithm adalah tipe abstrak, dan InsertionSortAlg , QuickSortAlg dan MergeSortAlg adalah subtipe konkretnya. Dengan antarmuka, Sort.Algorithm bisa menjadi antarmuka dan algoritme tertentu akan mengimplementasikannya. Sesuatu seperti ini:

# 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

Pemisahan yang kita inginkan kemudian dapat dicapai dengan mendefinisikan:

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

Ini _sangat_ dekat dengan apa yang kami lakukan saat ini, kecuali bahwa kami memanggil Algorithm.sort! bukan hanya sort! – dan ketika menerapkan berbagai algoritme pengurutan, definisi "dalam" adalah metode Algorithm.sort! bukan fungsi sort! . Ini memiliki efek memisahkan implementasi sort! dari antarmuka eksternalnya.

@StefanKarpinski Terima kasih banyak atas tulisan Anda! Ini pasti bukan barang 0,3. Sangat menyesal bahwa saya membawa ini saat ini. Saya hanya tidak yakin apakah 0,3 akan segera terjadi atau dalam setengah tahun ;-)

Dari pandangan pertama saya sangat (!) suka bahwa bagian pelaksana didefinisikan blok kodenya sendiri. Ini memungkinkan untuk secara langsung memverifikasi antarmuka pada definisi tipe.

Jangan khawatir – tidak ada salahnya berspekulasi tentang fitur masa depan saat kami mencoba menstabilkan rilis.

Pendekatan Anda jauh lebih mendasar dan mencoba juga memecahkan beberapa masalah independen antarmuka. Ini juga semacam membawa konstruksi baru (yaitu antarmuka) ke dalam bahasa yang membuat bahasa sedikit lebih kompleks (yang tidak perlu hal yang buruk).

Saya melihat "antarmuka" lebih sebagai anotasi untuk tipe abstrak. Jika seseorang memasukkan has ke dalamnya, seseorang dapat menentukan antarmuka tetapi tidak harus.

Seperti yang saya katakan, saya sangat ingin jika antarmuka dapat langsung divalidasi pada deklarasinya. Pendekatan paling tidak invasif di sini mungkin memungkinkan untuk mendefinisikan metode di dalam deklarasi tipe. Jadi ambil contoh Anda seperti

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

Seseorang masih diizinkan untuk mendefinisikan fungsi di luar deklarasi tipe. Satu-satunya perbedaan adalah bahwa deklarasi fungsi dalam divalidasi terhadap antarmuka.

Tetapi sekali lagi, mungkin "pendekatan paling tidak invasif" saya terlalu picik. Tidak benar-benar tahu.

Salah satu masalah dengan menempatkan definisi tersebut di dalam blok tipe adalah bahwa untuk melakukan ini, kita akan benar-benar membutuhkan beberapa pewarisan antarmuka setidaknya, dan dapat dibayangkan bahwa mungkin ada tabrakan nama antara antarmuka yang berbeda. Anda mungkin juga ingin menambahkan fakta bahwa suatu tipe mendukung antarmuka di beberapa titik _setelah_ mendefinisikan tipe, meskipun saya tidak yakin tentang itu.

@StefanKarpinski Senang melihat Anda memikirkan hal ini.

Paket Graphs adalah salah satu yang paling membutuhkan sistem antarmuka. Akan menarik untuk melihat bagaimana sistem ini dapat mengekspresikan antarmuka yang diuraikan di sini: http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : Saya tidak sepenuhnya melihat masalah dengan pewarisan berganda dan deklarasi fungsi dalam blok. Di dalam blok tipe, semua antarmuka yang diwarisi harus diperiksa.

Tapi saya agak mengerti bahwa orang mungkin ingin membiarkan implementasi antarmuka "terbuka". Dan deklarasi fungsi dalam tipe mungkin terlalu memperumit bahasa. Mungkin pendekatan yang saya terapkan di #7025 sudah cukup. Letakkan verify_interface setelah deklarasi fungsi (atau dalam pengujian unit) atau tunda ke MethodError .

Masalah ini adalah bahwa antarmuka yang berbeda dapat memiliki fungsi generik dengan nama yang sama, yang akan menyebabkan tabrakan nama dan mengharuskan melakukan impor eksplisit atau menambahkan metode dengan nama yang sepenuhnya memenuhi syarat. Itu juga membuatnya kurang jelas definisi metode mana yang termasuk dalam antarmuka mana - itulah sebabnya tabrakan nama dapat terjadi sejak awal.

Btw, saya setuju bahwa menambahkan antarmuka sebagai "hal" lain dalam bahasa terasa agak terlalu non-ortogonal. Lagi pula, seperti yang saya sebutkan dalam proposal, mereka sedikit seperti modul dan sedikit seperti tipe. Rasanya seperti beberapa penyatuan konsep mungkin terjadi, tetapi saya tidak jelas bagaimana caranya.

Saya lebih suka model antarmuka-sebagai-perpustakaan daripada model antarmuka-sebagai-bahasa-fitur karena beberapa alasan: ini membuat bahasa lebih sederhana (diakui preferensi dan bukan keberatan konkret) dan itu berarti bahwa fitur tersebut tetap opsional dan dapat dengan mudah ditingkatkan atau diganti seluruhnya tanpa mengacaukan bahasa yang sebenarnya.

Secara khusus, saya pikir proposal (atau setidaknya bentuk proposal) dari @tknopp lebih baik daripada proposal dari

Salah satu motivasi utama proposal saya adalah banyaknya kebingungan yang disebabkan oleh keharusan _mengimpor_ fungsi generik – tetapi tidak mengekspornya – untuk menambahkan metode ke dalamnya. Sering kali, ini terjadi ketika seseorang mencoba mengimplementasikan antarmuka tidak resmi, jadi ini membuatnya terlihat seperti itulah yang terjadi.

Itu sepertinya masalah ortogonal untuk dipecahkan, kecuali jika Anda ingin sepenuhnya membatasi metode untuk menjadi milik antarmuka.

Tidak, itu jelas bukan larangan yang bagus.

@StefanKarpinski Anda menyebutkan bahwa Anda dapat mengirim pada antarmuka. Juga dalam sintaks implement idenya adalah bahwa tipe tertentu mengimplementasikan antarmuka.

Ini tampaknya agak bertentangan dengan beberapa pengiriman, karena pada umumnya metode tidak termasuk dalam tipe tertentu, mereka milik Tuple tipe. Jadi jika metode bukan milik tipe, bagaimana antarmuka (yang pada dasarnya adalah kumpulan metode) termasuk dalam tipe?

Katakanlah saya menggunakan perpustakaan 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

sekarang saya ingin menulis fungsi generik yang mengambil A dan B

using M

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

Dalam contoh ini fungsi f membentuk antarmuka ad-hoc yang mengambil A dan B , dan saya ingin mengasumsikan bahwa saya dapat memanggil f berfungsi pada mereka. Dalam hal ini tidak jelas mana yang harus dipertimbangkan untuk mengimplementasikan antarmuka.

Modul lain yang ingin menyediakan subtipe konkret A dan B diharapkan menyediakan implementasi f . Untuk menghindari ledakan kombinatorial dari metode yang diperlukan, saya berharap perpustakaan mendefinisikan f terhadap tipe abstrak:

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

Memang contoh ini terasa sangat dibuat-buat, tetapi mudah-mudahan ini menggambarkan bahwa (setidaknya dalam pikiran saya) rasanya ada ketidakcocokan mendasar antara beberapa pengiriman dan konsep tipe tertentu yang mengimplementasikan antarmuka.

Saya mengerti maksud Anda tentang kebingungan import . Saya perlu beberapa kali mencoba contoh ini untuk mengingat bahwa ketika saya memasukkan using M dan kemudian mencoba menambahkan metode ke f itu tidak melakukan apa yang saya harapkan, dan saya harus menambahkan metode ke M.f (atau saya bisa menggunakan import ). Saya tidak berpikir bahwa antarmuka adalah solusi untuk masalah itu. Apakah ada masalah terpisah untuk bertukar pikiran tentang cara membuat metode penambahan lebih intuitif?

@abe-egnor Saya juga berpikir bahwa pendekatan yang lebih terbuka tampaknya lebih layak. Prototipe saya #7025 pada dasarnya tidak memiliki dua hal:
a) sintaks yang lebih baik untuk mendefinisikan antarmuka
b) definisi tipe parametrik

Karena saya bukan guru tipe parametrik, saya agak yakin bahwa b) dapat dipecahkan oleh seseorang dengan pengalaman yang lebih dalam.
Mengenai a) orang bisa menggunakan makro. Secara pribadi saya pikir kita dapat menggunakan beberapa dukungan bahasa untuk secara langsung mendefinisikan antarmuka sebagai bagian dari definisi tipe abstrak. Pendekatan has mungkin terlalu picik. Blok kode mungkin membuat ini lebih bagus. Sebenarnya ini sangat terkait dengan #4935 di mana antarmuka "internal" didefinisikan sementara ini tentang antarmuka publik. Ini tidak harus dibundel karena saya pikir masalah ini jauh lebih penting daripada #4935. Tapi tetap saja sintaksis orang mungkin ingin mempertimbangkan kedua kasus penggunaan.

https://Gist.github.com/abe-egnor/503661eb4cc0d66b4489 memiliki tikaman pertama saya pada jenis implementasi yang saya pikirkan. Singkatnya, antarmuka adalah fungsi dari tipe ke dict yang mendefinisikan nama dan tipe parameter dari fungsi yang diperlukan untuk antarmuka itu. Makro @implement hanya memanggil fungsi untuk tipe yang diberikan dan kemudian menyambungkan tipe ke dalam definisi fungsi yang diberikan, memeriksa bahwa semua fungsi telah didefinisikan.

Poin bagus:

  • Sintaks sederhana untuk definisi dan implementasi antarmuka.
  • Ortogonal, tetapi cocok dengan, fitur bahasa lainnya.
  • Komputasi tipe antarmuka dapat menjadi mewah secara sewenang-wenang (mereka hanya berfungsi di atas parameter tipe antarmuka)

Poin buruk:

  • Tidak cocok dengan tipe berparameter jika Anda ingin menggunakan parameter sebagai tipe antarmuka. Ini adalah kerugian yang cukup signifikan, tetapi saya tidak dapat melihat cara langsung untuk mengatasinya.

Saya pikir saya punya solusi untuk masalah parameterisasi - singkatnya, definisi antarmuka harus berupa ekspresi makro di atas tipe, bukan fungsi di atas nilai tipe. Makro @implement kemudian dapat memperluas parameter tipe ke definisi fungsi, memungkinkan sesuatu seperti:

<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

Dalam hal ini, parameter tipe diperluas ke metode yang ditentukan dalam antarmuka, sehingga diperluas ke stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , yang saya yakini adalah hal yang tepat.

Saya akan mengerjakan ulang implementasi awal saya untuk melakukan ini saat saya mendapatkan waktu; mungkin dalam urutan seminggu.

Saya menjelajahi internet sedikit untuk melihat apa yang dilakukan bahasa pemrograman lain tentang antarmuka, pewarisan, dan semacamnya dan menemukan beberapa ide. (Jika ada yang tertarik di sini, catatan kasar yang saya ambil https://Gist.github.com/mauro3/e3e18833daf49cdf8f60)

Singkatnya adalah mungkin antarmuka dapat diimplementasikan oleh:

  • memungkinkan pewarisan berganda untuk tipe abstrak, dan
  • memungkinkan fungsi generik sebagai bidang tipe abstrak.

Ini akan mengubah tipe abstrak menjadi antarmuka dan subtipe konkret kemudian akan diminta untuk mengimplementasikan antarmuka itu.

Cerita panjangnya:

Apa yang saya temukan adalah bahwa beberapa bahasa "modern" menghilangkan polimorfisme subtipe, yaitu tidak ada pengelompokan tipe secara langsung, dan sebaliknya mereka mengelompokkan tipenya berdasarkan kelas antarmuka/sifat/tipe. Dalam beberapa bahasa, kelas antarmuka / sifat / tipe dapat memiliki urutan di antara mereka dan mewarisi satu sama lain. Mereka juga tampak (kebanyakan) senang dengan pilihan itu. Contohnya adalah: Pergi ,
Karat , Haskell .
Go adalah yang paling tidak ketat dari ketiganya dan memungkinkan antarmukanya ditentukan secara implisit, yaitu jika suatu tipe mengimplementasikan kumpulan fungsi spesifik dari suatu antarmuka, maka itu milik antarmuka itu. Untuk Rust, antarmuka (sifat) harus diimplementasikan secara eksplisit dalam blok impl . Baik Go maupun Rust tidak memiliki multimetode. Haskell memiliki multimetode dan mereka sebenarnya terhubung langsung ke antarmuka (kelas tipe).

Dalam beberapa hal ini mirip dengan apa yang Julia lakukan juga, tipe abstrak seperti antarmuka (implisit), yaitu tentang perilaku dan bukan tentang bidang. Inilah yang juga diamati oleh @StefanKarpinski di salah satu postingannya di atas dan menyatakan bahwa selain memiliki antarmuka "terasa agak terlalu non-ortogonal." Jadi, Julia memiliki hierarki tipe (yaitu polimorfisme subtipe) sedangkan Go/Rust/Haskell tidak.

Bagaimana dengan mengubah tipe abstrak Julia menjadi lebih dari kelas antarmuka / sifat / tipe, sambil menjaga semua tipe dalam hierarki None<: ... <:Any ? Ini akan memerlukan:
1) izinkan pewarisan berganda untuk tipe (abstrak) (masalah #5)
2) memungkinkan menghubungkan fungsi dengan tipe abstrak (yaitu mendefinisikan antarmuka)
3) Izinkan untuk menentukan antarmuka itu, baik untuk abstrak (yaitu implementasi default) dan tipe konkret.

Saya pikir ini dapat menghasilkan grafik tipe berbutir lebih halus daripada yang kita miliki sekarang dan dapat diimplementasikan selangkah demi selangkah. Misalnya, tipe array akan disatukan:

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

Jadi, pada dasarnya tipe abstrak kemudian dapat memiliki fungsi generik sebagai bidang (yaitu menjadi antarmuka) sedangkan tipe konkret hanya memiliki bidang normal. Ini mungkin misalnya memecahkan masalah terlalu banyak hal yang diturunkan dari AbstractArray, karena orang hanya bisa memilih bagian yang berguna untuk wadah mereka sebagai lawan dari AbstractArray.

Jika ini adalah ide yang bagus, ada banyak hal yang harus dikerjakan (khususnya cara menentukan tipe dan parameter tipe), tetapi mungkin perlu dipikirkan?

@ssfrr berkomentar di atas bahwa antarmuka dan beberapa pengiriman tidak kompatibel. Seharusnya tidak demikian karena, misalnya, dalam multimetode Haskell hanya dimungkinkan dengan menggunakan kelas tipe.

Saya juga menemukan saat membaca @StefanKarpinski menulis bahwa menggunakan langsung abstract alih-alih interface bisa masuk akal. Namun dalam kasus ini, penting bahwa abstract mewarisi satu properti penting dari interface : kemungkinan sebuah tipe ke implement dan interface _after_ sedang didefinisikan. Kemudian saya dapat menggunakan tipe typA dari lib A dengan algoritme algoB dari lib B dengan menyatakan dalam kode saya bahwa typA mengimplementasikan antarmuka yang diperlukan oleh algoB (saya kira ini menyiratkan bahwa tipe konkret memiliki semacam pewarisan berganda terbuka).

@ mauro3 , saya sebenarnya sangat menyukai saran Anda. Bagi saya, rasanya sangat "julian" dan natural. Saya juga berpikir ini adalah integrasi antarmuka yang unik dan kuat, pewarisan berganda, dan "bidang" tipe abstrak (meskipun, tidak juga, karena bidang hanya akan menjadi metode/fungsi, bukan nilai). Saya juga berpikir ini cocok dengan ide @StefanKarpinski untuk membedakan metode antarmuka "dalam" vs. "luar", karena Anda dapat mengimplementasikan proposalnya untuk contoh sort! dengan mendeklarasikan abstract Algorithm dan Algorithm.sort! .

maaf semuanya

------------------ ------------------
: "Jacob Quinn" [email protected];
: 2014年9月12日(星期五) 6:23
: "JuliaLang/julia" [email protected];
Alamat: "Implementasikan" [email protected];
Jawaban: Re: [julia] Antarmuka untuk Tipe Abstrak (#6975)

@ mauro3 , saya sebenarnya sangat menyukai saran Anda. Bagi saya, rasanya sangat "julian" dan natural. Saya juga berpikir ini adalah integrasi antarmuka yang unik dan kuat, pewarisan berganda, dan "bidang" tipe abstrak (meskipun, tidak juga, karena bidang hanya akan menjadi metode/fungsi, bukan nilai). Saya juga berpikir ini cocok dengan ide @StefanKarpinski untuk membedakan metode antarmuka "dalam" vs. "luar", karena Anda dapat mengimplementasikan proposalnya untuk semacam itu! contoh dengan mendeklarasikan Abstract Algorithm dan Algorithm.sort!.


Balas email ini secara langsung atau lihat di GitHub.

@implement Sangat menyesal; tidak yakin bagaimana kami melakukan ping kepada Anda. Jika Anda belum mengetahuinya, Anda dapat menghapus diri Anda dari notifikasi tersebut menggunakan tombol "Berhenti Berlangganan" di sisi kanan layar.

Tidak, saya hanya ingin mengatakan bahwa saya tidak dapat membantu Anda terlalu banyak untuk mengatakan sarry

------------------ ------------------
: notifikasi "pao"@github.com
: 2014年9月13日(星期六) 9:50
: "JuliaLang/julia" [email protected];
Alamat: "Implementasikan" [email protected];
Jawaban: Re: [julia] Antarmuka untuk Tipe Abstrak (#6975)

@implement Sangat menyesal; tidak yakin bagaimana kami melakukan ping kepada Anda. Jika Anda belum mengetahuinya, Anda dapat menghapus diri Anda dari notifikasi tersebut menggunakan tombol "Berhenti Berlangganan" di sisi kanan layar.


Balas email ini secara langsung atau lihat di GitHub.

Kami tidak mengharapkan Anda! Itu adalah kecelakaan, karena kita berbicara tentang makro Julia dengan nama yang sama dengan nama pengguna Anda. Terima kasih!

Saya baru saja melihat bahwa ada beberapa fitur yang berpotensi menarik (mungkin relevan dengan masalah ini) yang dikerjakan di Rust: http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , khususnya: https ://github.com/rust-lang/rfcs/pull/195

Setelah melihat THTT ("Tim Holy Trait Trick"), saya memikirkan beberapa antarmuka/sifat selama beberapa minggu terakhir. Saya datang dengan beberapa ide dan implementasi: Traits.jl . Pertama, (saya pikir) sifat harus dilihat sebagai kontrak yang melibatkan satu atau beberapa jenis . Ini berarti bahwa hanya melampirkan fungsi antarmuka ke satu tipe abstrak, seperti yang saya dan yang lain disarankan di atas, tidak berfungsi (setidaknya tidak dalam kasus umum sifat yang melibatkan beberapa tipe). Dan kedua, metode harus dapat menggunakan ciri-ciri untuk dispatch , seperti yang disarankan @StefanKarpinski di atas.

Nuff berkata, berikut contoh penggunaan paket Traits.jl saya:

<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

Ini menyatakan bahwa Eq dan Cmp adalah kontrak antara tipe X dan Y . Cmp memiliki Eq sebagai supertrait, yaitu Eq dan Cmp harus dipenuhi. Di badan @traitdef , tanda tangan fungsi menentukan metode apa yang perlu didefinisikan. Jenis pengembalian tidak melakukan apa-apa saat ini. Tipe tidak perlu secara eksplisit mengimplementasikan suatu sifat, cukup mengimplementasikan fungsi yang akan dilakukan. Saya dapat memeriksa apakah, katakanlah, Cmp{Int,Float64} memang suatu sifat:

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

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

Implementasi sifat eksplisit belum ada dalam paket tetapi harus cukup mudah untuk ditambahkan.

Sebuah fungsi menggunakan _trait-dispatch_ dapat didefinisikan seperti ini

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

Ini mendeklarasikan fungsi ft1 yang mengambil dua argumen dengan batasan yang harus dipenuhi tipenya Cmp{X,Y} . Saya dapat menambahkan metode pengiriman lain pada sifat lain:

<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

Fungsi-sifat ini sekarang dapat dipanggil seperti fungsi normal:

julia> ft1(4,5)
6

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

Menambahkan tipe lain ke suatu sifat nanti mudah (yang tidak akan terjadi menggunakan Unions untuk ft1):

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

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

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

_Implementasi_ fungsi sifat dan pengirimannya didasarkan pada trik Tim dan fungsi bertahap, lihat di bawah. Definisi sifat relatif sepele, lihat di sini untuk implementasi manual dari semuanya.

Singkatnya, pengiriman sifat berubah

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

menjadi sesuatu seperti ini (sedikit disederhanakan)

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

Dalam paket, pembuatan checkfn diotomatisasi oleh fungsi bertahap. Tapi lihat README dari Traits.jl untuk lebih jelasnya.

_Performance_ Untuk fungsi-sifat sederhana, kode mesin yang dihasilkan identik dengan rekan-rekan mereka yang diketik bebek, yaitu sebaik yang didapatnya. Untuk fungsi yang lebih panjang ada perbedaan, hingga ~20% panjangnya. Saya tidak yakin mengapa karena saya pikir ini semua harus digarisbawahi.

(diedit 27 Okt untuk mencerminkan perubahan kecil pada Traits.jl )

Apakah paket Traits.jl sudah siap untuk dijelajahi? Readme mengatakan "menerapkan antarmuka dengan @traitimpl (belum selesai...)" -- apakah ini kekurangan penting?

Ini siap untuk dijelajahi (termasuk bug :-). @traitimpl hilang hanya berarti bahwa alih-alih

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

Anda cukup mendefinisikan fungsinya secara manual

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

untuk dua tipe Anda T1 dan T2 .

Saya menambahkan makro @traitimpl , jadi contoh di atas sekarang berfungsi. Saya juga memperbarui README dengan detail tentang penggunaan. Dan saya menambahkan contoh implementasi bagian dari antarmuka @lindahua Graphs.jl:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

Ini benar-benar keren. Saya sangat suka bahwa ia mengakui bahwa antarmuka secara umum adalah properti dari tipe tupel, bukan tipe individu.

Saya juga menemukan ini sangat keren. Ada banyak hal yang disukai tentang pendekatan ini. Kerja bagus.

:+1:

Terima kasih atas umpan balik yang baik! Saya memperbarui/memfaktorkan ulang kode sedikit dan seharusnya cukup bebas bug dan bagus untuk bermain-main.
Pada titik ini mungkin akan baik, jika orang dapat mencoba ini untuk melihat apakah itu cocok dengan kasus penggunaan mereka.

Ini adalah salah satu paket yang membuat seseorang melihat kodenya sendiri dengan cara baru. Sangat keren.

Maaf saya belum punya waktu untuk membahas ini dengan serius, tetapi saya tahu bahwa begitu saya melakukannya, saya ingin memperbaiki beberapa hal ...

Saya akan refactor paket saya juga :)

Saya bertanya-tanya, menurut saya jika ciri-ciri tersedia (dan memungkinkan pengiriman ganda, seperti saran di atas) maka tidak perlu mekanisme hierarki tipe abstrak, atau tipe abstrak sama sekali. Mungkinkah ini?

Setelah sifat diimplementasikan, setiap fungsi di basis dan kemudian di seluruh ekosistem pada akhirnya akan mengekspos api publik hanya berdasarkan sifat, dan tipe abstrak akan menghilang. Tentu saja prosesnya dapat dikatalisasi dengan mencela tipe abstrak

Memikirkan ini sedikit lagi, mengganti tipe abstrak dengan sifat akan membutuhkan tipe parametrizing seperti ini:

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

Saya setuju dengan poin mauro3 di atas, yang memiliki ciri-ciri (menurut definisinya, yang menurut saya sangat bagus) setara dengan tipe abstrak yang

  • memungkinkan pewarisan berganda, dan
  • izinkan fungsi generik sebagai bidang

Saya juga akan menambahkan bahwa untuk memungkinkan ciri-ciri ditugaskan ke tipe setelah definisinya, seseorang juga perlu mengizinkan "warisan malas" yaitu untuk memberi tahu kompiler bahwa suatu tipe mewarisi dari beberapa tipe abstrak setelah didefinisikan.

jadi bagi saya secara keseluruhan tampaknya mengembangkan beberapa konsep sifat/antarmuka di luar tipe abstrak akan menyebabkan beberapa duplikasi, memperkenalkan berbagai cara untuk mencapai hal yang sama. Saya sekarang berpikir cara terbaik untuk memperkenalkan konsep-konsep ini adalah dengan perlahan menambahkan fitur ke tipe abstrak

EDIT : tentu saja pada titik tertentu mewarisi tipe konkret dari yang abstrak harus ditinggalkan dan akhirnya dianulir. Ciri-ciri tipe akan ditentukan secara implisit atau eksplisit tetapi tidak pernah dengan pewarisan

Bukankah tipe abstrak hanyalah contoh sifat yang "membosankan"?

Jika demikian, mungkinkah untuk mempertahankan sintaks saat ini dan cukup mengubah artinya menjadi sifat (memberikan kebebasan ortogonal dll. jika pengguna menginginkannya)?

_Saya ingin tahu apakah ini mungkin juga dapat mengatasi contoh Point{Float64} <: Pointy{Real} (tidak yakin apakah ada nomor masalah)?_

Ya, saya pikir Anda benar. Fungsionalitas sifat dapat dicapai dengan meningkatkan tipe abstrak julia saat ini. Mereka butuh
1) pewarisan berganda
2) tanda tangan fungsi
3) "warisan malas", untuk secara eksplisit memberikan tipe yang sudah ditentukan sifat baru

Sepertinya banyak pekerjaan, tapi Mungkin ini bisa berkembang perlahan tanpa banyak kerusakan bagi masyarakat. Jadi setidaknya kita mengerti ;)

Saya pikir apa pun yang kami pilih akan menjadi perubahan besar, yang belum siap kami kerjakan di 0.4. Jika saya harus menebak, saya berani bertaruh bahwa kita lebih cenderung bergerak ke arah sifat daripada ke arah penambahan pewarisan berganda tradisional. Tapi bola kristal saya ada di fritz, jadi sulit untuk memastikan apa yang akan terjadi tanpa hanya mencoba sesuatu.

FWIW, saya menemukan diskusi kelas tipe Simon Peyton-Jones dalam pembicaraan di bawah ini sangat informatif tentang bagaimana menggunakan sesuatu seperti ciri-ciri sebagai pengganti subtipe: http://research.microsoft.com/en-us/um/people/simonpj/ makalah/haskell-retrospective/ECOOP-July09.pdf

Ya, sekaleng cacing utuh!

@johnmyleswhite , terima kasih atas tautannya, sangat menarik. Berikut tautan ke videonya, yang layak ditonton untuk mengisi kekosongan. Presentasi itu tampaknya menyentuh banyak pertanyaan yang kami dapatkan di sini. Dan yang menarik, implementasi kelas tipe sangat mirip dengan apa yang ada di Traits.jl (trik Tim, ciri menjadi tipe data). https://www.haskell.org/haskellwiki/Multi-parameter_type_class Haskell sangat mirip dengan Traits.jl. Salah satu pertanyaannya dalam pembicaraan tersebut adalah: "setelah kita dengan sepenuh hati mengadopsi obat generik, apakah kita masih benar-benar membutuhkan subtipe." (generik adalah fungsi parametrik-polimorfik, saya pikir, lihat ) Yang kira- kira seperti yang @hayd renungkan di atas.

Mengacu pada @skariel dan @hayd , saya pikir sifat parameter tunggal (seperti pada Traits.jl) memang sangat mirip dengan tipe abstrak, kecuali bahwa mereka dapat memiliki hierarki lain, yaitu pewarisan berganda.

Tetapi sifat multi-parameter tampaknya sedikit berbeda, setidaknya ada dalam pikiran saya. Seperti yang saya lihat, parameter tipe dari tipe abstrak tampaknya sebagian besar tentang tipe lain yang terkandung dalam suatu tipe, misalnya, Associative{Int,String} mengatakan bahwa dict berisi kunci Int dan String nilai. Sedangkan Tr{Associative,Int,String}... mengatakan bahwa ada beberapa "kontrak" antara Associative , Int s dan Strings . Tapi kemudian, mungkin Associative{Int,String} harus dibaca seperti itu juga, yaitu ada metode seperti getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@mauro3 Yang penting adalah meneruskan objek bertipe Associative sebagai argumen ke suatu fungsi, sehingga kemudian dapat membuat Associative{Int,String} sendiri:

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

Anda akan menyebutnya misalnya sebagai f(Dict) .

@eschnett , maaf, saya tidak mengerti apa yang Anda maksud.

@ mauro3 Saya pikir saya berpikir dengan cara yang terlalu rumit; abaikan saya.

Saya memperbarui Traits.jl dengan:

  • resolusi ambiguitas sifat
  • jenis terkait
  • menggunakan @doc untuk bantuan
  • pengujian yang lebih baik dari metode sifat-spesifikasi

Lihat https://github.com/mauro3/Traits.jl/blob/master/NEWS.md untuk detailnya. Umpan balik selamat datang!

@Rory-Finnegan menyusun paket antarmuka https://github.com/Rory-Finnegan/Interfaces.jl

Saya baru-baru ini mendiskusikan ini dengan @mdcfrancis dan kami pikir sesuatu yang mirip dengan protokol Clojure akan sederhana dan praktis. Fitur dasarnya adalah (1) protokol adalah tipe baru, (2) Anda mendefinisikannya dengan mendaftar beberapa tanda tangan metode, (3) tipe lain mengimplementasikannya secara implisit hanya dengan memiliki definisi metode yang cocok. Anda akan menulis misalnya

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

dan kami memiliki isa(Iterable, Protocol) dan Protocol <: Type . Secara alami, Anda dapat mengirimkan ini. Anda dapat memeriksa apakah suatu tipe mengimplementasikan protokol menggunakan T <: Iterable .

Berikut adalah aturan subtipe:

biarkan P, Q menjadi tipe protokol
biarkan T menjadi tipe non-protokol

| masukan | hasil |
| --- | --- |
| P <: Apa saja | benar |
| Bawah <: P | benar |
| (serikat,serikat,var) <: P | gunakan aturan biasa; perlakukan P sebagai tipe dasar |
| P <: (serikat,serikat,var) | gunakan aturan biasa |
| P <: P | benar |
| P <: T | periksa metode(Q) <: metode(P) |
| P <: T | salah |
| T <: P | Metode P ada dengan T diganti untuk _ |

Yang terakhir adalah yang besar: untuk menguji T <: P, Anda mengganti T untuk _ dalam definisi P dan memeriksa method_exists untuk setiap tanda tangan. Tentu saja, dengan sendirinya ini berarti definisi fallback yang melemparkan kesalahan "Anda harus mengimplementasikan ini" menjadi hal yang sangat buruk. Semoga ini lebih merupakan masalah kosmetik.

Masalah lain adalah bahwa definisi ini melingkar jika misalnya start(::Iterable) didefinisikan. Definisi seperti itu sebenarnya tidak masuk akal. Kami entah bagaimana bisa mencegah ini, atau mendeteksi siklus ini selama pemeriksaan subtipe. Saya tidak 100% yakin deteksi siklus sederhana memperbaikinya, tetapi tampaknya masuk akal.

Untuk persimpangan tipe kita memiliki:

| masukan | hasil |
| --- | --- |
| P (union,unionall,tvar) | gunakan aturan biasa |
| P Q | P |
| P T | T |

Ada beberapa opsi untuk P Q:

  1. Over-approximate dengan mengembalikan P atau Q (mis. mana yang lebih dulu secara leksikografis). Ini terdengar sehubungan dengan inferensi tipe, tetapi mungkin mengganggu di tempat lain.
  2. Kembalikan protokol ad-hoc baru yang berisi gabungan tanda tangan di P dan Q.
  3. Jenis persimpangan. Mungkin terbatas pada protokol saja.

P T rumit. T adalah perkiraan konservatif yang baik, karena tipe non-protokol "lebih kecil" daripada tipe protokol dalam arti bahwa mereka membatasi Anda ke satu wilayah hierarki tipe, sedangkan tipe protokol tidak (karena tipe apa pun dapat mengimplementasikan protokol apa pun ). Melakukan lebih baik dari ini tampaknya memerlukan jenis persimpangan umum, yang saya lebih suka hindari dalam implementasi awal karena itu memerlukan perombakan algoritma subtyping, dan membuka worm-can setelah worm-can.

Spesifisitas: P hanya lebih spesifik daripada Q ketika P<:Q. tetapi karena P Q selalu kosong, definisi dengan protokol berbeda di slot yang sama sering kali ambigu, yang sepertinya seperti yang Anda inginkan (mis. Anda akan mengatakan "jika x Iterable lakukan ini, tetapi jika x Dapat Dicetak lakukan itu").
Namun tidak ada cara praktis untuk mengekspresikan definisi disambiguasi yang diperlukan, jadi ini mungkin merupakan kesalahan.

Setelah #13412, sebuah protokol dapat "dikodekan" sebagai UnionAll _ melalui Union of tuple types (di mana elemen pertama dari setiap inner tuple adalah tipe dari fungsi yang dimaksud). Ini adalah manfaat dari desain yang tidak terpikirkan oleh saya sebelumnya. Misalnya subtipe struktural protokol tampaknya jatuh begitu saja secara otomatis.

Tentu saja, protokol ini adalah gaya "parameter tunggal". Saya suka kesederhanaan ini, ditambah lagi saya tidak yakin bagaimana menangani grup tipe dengan elegan seperti T <: Iterable .

Ada beberapa komentar seputar ide ini di masa lalu, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516 .

Apakah kami akan mendukung, misalnya

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

Wow, saya sangat suka ini (terutama dengan ekstensi @Keno )!

+1 Inilah yang saya inginkan!

@Keno Itu jelas merupakan jalur peningkatan yang bagus untuk fitur ini, tetapi ada alasan untuk menundanya. Apa pun yang melibatkan tipe pengembalian tentu saja sangat bermasalah. Parameter itu sendiri secara konseptual baik-baik saja dan akan luar biasa, tetapi agak sulit untuk diterapkan. Ini membutuhkan pemeliharaan lingkungan tipe di sekitar proses yang memeriksa keberadaan semua metode.

Sepertinya Anda bisa memasukkan ciri-ciri (seperti pengindeksan linier O(1) untuk tipe seperti array) ke dalam skema ini. Anda akan mendefinisikan metode dummy seperti hassomeproperty(::T) = true (tetapi _not_ hassomeproperty(::Any) = false ) dan kemudian

protocol MyProperty
hassomeproperty(::_)
end

Bisakah _ muncul beberapa kali dalam metode yang sama dalam definisi protokol, seperti

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

Bisa _ muncul beberapa kali dalam metode yang sama dalam definisi protokol

Ya. Anda cukup memasukkan jenis kandidat untuk setiap instance _ .

@JeffBezanson sangat menantikannya. Catatan khusus bagi saya adalah 'keterpencilan' protokol. Dalam hal itu saya dapat mengimplementasikan protokol khusus/kustom untuk suatu tipe tanpa penulis tipe tersebut memiliki pengetahuan tentang keberadaan protokol tersebut.

Bagaimana dengan fakta bahwa metode dapat didefinisikan secara dinamis (misalnya dengan @eval ) kapan saja? Kemudian apakah suatu tipe adalah subtipe dari protokol yang diberikan tidak dapat diketahui secara statis secara umum, yang tampaknya mengalahkan pengoptimalan yang menghindari pengiriman dinamis dalam banyak kasus.

Ya, ini membuat #265 lebih buruk :) Ini adalah masalah yang sama di mana pengiriman dan kode yang dihasilkan perlu diubah ketika metode ditambahkan, hanya dengan lebih banyak tepi ketergantungan.

Senang melihat ini maju! Tentu saja, saya akan menjadi orang yang berpendapat bahwa sifat multi-parameter adalah jalan ke depan. Tetapi 95% sifat mungkin akan menjadi parameter tunggal. Hanya saja mereka akan sangat cocok dengan banyak pengiriman! Ini mungkin bisa ditinjau kembali nanti jika perlu. Cukup kata.

Beberapa komentar:

Saran dari @Keno (dan benar-benar state dalam Jeff's original) dikenal sebagai tipe terkait. Perhatikan bahwa mereka berguna tanpa tipe pengembalian juga. Rust memiliki entri manual yang layak. Saya pikir itu adalah ide yang bagus, meskipun tidak sepenting di Rust. Saya tidak berpikir itu harus menjadi parameter sifat: ketika mendefinisikan fungsi yang dikirim pada Iterable Saya tidak akan tahu apa itu T .

Dalam pengalaman saya, method_exists tidak dapat digunakan dalam bentuk saat ini untuk ini (#8959). Tapi mungkin ini akan diperbaiki di #8974 (atau dengan ini). Saya menemukan pencocokan metode tanda tangan terhadap ciri-tanda tangan bagian tersulit ketika melakukan Traits.jl, terutama untuk memperhitungkan fungsi parameter & vararg ( lihat ).

Agaknya warisan juga mungkin?

Saya benar-benar ingin melihat mekanisme yang memungkinkan definisi implementasi default. Yang klasik adalah bahwa untuk perbandingan Anda hanya perlu mendefinisikan dua dari = , < , > , <= , >= . Mungkin di sinilah siklus yang disebutkan Jeff sebenarnya bermanfaat. Melanjutkan contoh di atas, mendefinisikan start(::Indexable) = 1 dan done(i::Indexable,state)=length(i)==state akan menjadikannya default. Jadi banyak tipe hanya perlu mendefinisikan next .

Poin bagus. Saya pikir tipe terkait agak berbeda dari parameter di Iterable{T} . Dalam penyandian saya, parameter hanya akan secara eksistensial mengukur semua yang ada di dalamnya --- "apakah ada T sedemikian rupa sehingga tipe Foo mengimplementasikan protokol ini?".

Ya, sepertinya kita dapat dengan mudah mengizinkan protocol Foo <: Bar, Baz , dan cukup salin tanda tangan dari Bar dan Baz ke Foo.

Sifat multi-parameter pasti sangat kuat. Saya pikir sangat menarik untuk memikirkan bagaimana mengintegrasikannya dengan subtipe. Anda dapat memiliki sesuatu seperti TypePair{A,B} <: Trait , tetapi sepertinya itu tidak benar.

Saya pikir proposal Anda (dalam hal fitur) sebenarnya lebih mirip Swift daripada Clojure.

Tampaknya aneh (dan saya pikir sumber kebingungan di masa depan) untuk mencampur subtipe nominal (tipe) dan struktural (protokol) (tapi saya rasa itu tidak dapat dihindari).

Saya juga agak skeptis dengan kekuatan ekspresif protokol untuk operasi Matematika/Matriks. Saya pikir memikirkan contoh yang lebih rumit (operasi matriks) akan lebih mencerahkan daripada Iterasi yang memiliki antarmuka yang ditentukan dengan jelas. Lihat misalnya perpustakaan core.matrix .

Saya setuju; pada titik ini kita harus mengumpulkan contoh protokol dan melihat apakah mereka melakukan apa yang kita inginkan.

Seperti yang Anda bayangkan, apakah protokol akan menjadi ruang nama yang menjadi milik metode mereka? Yaitu ketika Anda menulis

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

tampaknya wajar untuk mendefinisikan fungsi generik start , done dan next dan untuk nama yang sepenuhnya memenuhi syarat menjadi Iterable.start , Iterable.done dan Iterable.next . Sebuah tipe akan mengimplementasikan Iterable tetapi mengimplementasikan semua fungsi generik dalam protokol Iterable . Saya mengusulkan sesuatu yang sangat mirip dengan ini beberapa waktu lalu (tidak dapat menemukannya sekarang), tetapi dengan sisi lain adalah ketika Anda ingin menerapkan protokol, Anda melakukan ini:

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

Ini akan melawan "keterpencilan" yang disebutkan @mdcfrancis , jika saya memahaminya, tetapi sekali lagi, saya tidak benar-benar melihat manfaat dari dapat "secara tidak sengaja" mengimplementasikan protokol. Bisakah Anda menjelaskan mengapa Anda merasa itu bermanfaat, @mdcfrancis? Saya tahu Go membuat banyak hal ini, tapi sepertinya karena Go tidak bisa mengetik bebek, yang Julia bisa. Saya menduga bahwa memiliki blok implement akan menghilangkan hampir semua kebutuhan untuk menggunakan import alih-alih using , yang akan menjadi manfaat besar.

Saya mengusulkan sesuatu yang sangat mirip dengan ini beberapa waktu lalu (tidak dapat menemukannya sekarang)

Mungkin https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 dan sebelumnya https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Sunting: Juga https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

Ya, itu saja.

@StefanKarpinski komentar cepat,

  • semua kelas yang saat ini mengimplementasikan iterable harus dimodifikasi untuk mengimplementasikan protokol secara eksplisit jika kami melakukan seperti yang Anda usulkan, proposal saat ini hanya dengan menambahkan definisi ke basis akan 'mengangkat' semua kelas yang ada ke protokol.
  • jika saya mendefinisikan MyModule.MySuperIterable yang menambahkan fungsi tambahan ke definisi yang dapat diubah, saya harus menulis banyak kode pelat boiler untuk setiap kelas daripada menambahkan satu metode tambahan.
  • Saya tidak berpikir apa yang Anda usulkan melawan keterpencilan, itu hanya berarti bahwa saya harus menulis banyak kode tambahan untuk mencapai tujuan yang sama.

Jika semacam warisan pada protokol diizinkan, MySuperIterabe,
dapat memperluas Base.Iterable, untuk menggunakan kembali metode yang ada.

Masalahnya adalah jika Anda hanya menginginkan pilihan metode dalam a
protokol, tetapi itu tampaknya menunjukkan bahwa protokol asli seharusnya
menjadi protokol komposit dari awal.

@mdcfrancis - poin pertama bagus, meskipun apa yang saya usulkan tidak akan merusak kode yang ada, itu hanya berarti bahwa kode orang harus "ikut serta" ke protokol untuk tipe mereka sebelum mereka dapat mengandalkan pengiriman bekerja.

Bisakah Anda memperluas poin MyModule.MySuperIterable? Saya tidak melihat dari mana verbositas ekstra itu berasal. Anda dapat memiliki sesuatu seperti ini, misalnya:

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

Yang pada dasarnya adalah apa yang dikatakan @ivarne .

Dalam desain khusus saya di atas, protokol bukanlah ruang nama, hanya pernyataan tentang jenis dan fungsi lain. Namun ini mungkin karena saya berfokus pada sistem tipe inti. Saya bisa membayangkan gula sintaksis yang berkembang menjadi kombinasi modul dan protokol, misalnya

module Iterable

function start end
function done end
function next end

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

end

Kemudian dalam konteks di mana Iterable diperlakukan sebagai tipe, kami menggunakan Iterable.the_protocol .

Saya suka perspektif ini karena protokol jeff/mdcfrancis terasa sangat ortogonal dengan semua yang lain di sini. Perasaan ringan tidak perlu mengatakan "X mengimplementasikan protokol Y" kecuali jika Anda ingin merasa "julian" bagi saya.

Saya tidak tahu mengapa saya berlangganan masalah ini dan kapan saya melakukannya. Tetapi ternyata proposal protokol ini dapat menyelesaikan pertanyaan yang saya ajukan di sini .

Saya tidak memiliki apa pun untuk ditambahkan secara teknis, tetapi sebagai contoh "protokol" yang digunakan di alam liar di Julia (semacam) adalah JuMP yang menentukan fungsionalitas pemecah, misalnya:

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

Keren, bermanfaat. Apakah m.internalModel untuk menjadi hal yang mengimplementasikan protokol, atau apakah kedua argumen itu penting?

Ya, m.internalModel untuk mengimplementasikan protokol. Argumen lain kebanyakan hanya vektor.

Ya, cukup untuk m.internalModel untuk mengimplementasikan protokol

Cara yang baik untuk menemukan contoh protokol di alam liar mungkin mencari panggilan applicable dan method_exists .

Elixir juga tampaknya mengimplementasikan protokol, tetapi jumlah protokol di perpustakaan standar (melampaui definisi) tampaknya cukup terbatas.

Apa hubungan antara protokol dan tipe abstrak? Deskripsi masalah asli mengusulkan sesuatu seperti melampirkan protokol ke tipe abstrak. Memang, menurut saya sebagian besar protokol (sekarang informal) di luar sana saat ini diimplementasikan sebagai tipe abstrak. Untuk apa tipe abstrak digunakan ketika dukungan untuk protokol ditambahkan? Hirarki tipe tanpa cara apa pun untuk mendeklarasikan API-nya tidak terdengar terlalu berguna.

Pertanyaan yang sangat bagus. Ada banyak pilihan di sana. Pertama, penting untuk menunjukkan bahwa tipe dan protokol abstrak cukup ortogonal, meskipun keduanya merupakan cara pengelompokan objek. Jenis abstrak murni nominal; mereka menandai objek sebagai milik set. Protokol adalah murni struktural; sebuah objek milik himpunan jika kebetulan memiliki properti tertentu. Jadi beberapa opsi adalah

  1. Hanya memiliki keduanya.
  2. Mampu mengasosiasikan protokol dengan tipe abstrak, misalnya sehingga ketika suatu tipe menyatakan dirinya sebagai subtipe, itu diperiksa untuk kepatuhan dengan protokol(s).
  3. Hapus tipe abstrak sepenuhnya.

Jika kita memiliki sesuatu seperti (2), saya pikir penting untuk menyadari bahwa itu bukan fitur tunggal, tetapi kombinasi pengetikan nominal dan struktural.

Satu hal yang tampaknya berguna bagi tipe abstrak adalah parameternya, misalnya menulis convert(AbstractArray{Int}, x) . Jika AbstractArray adalah sebuah protokol, tipe elemen Int tidak perlu disebutkan dalam definisi protokol. Ini adalah informasi tambahan tentang jenisnya, _selain_ dari metode mana yang diperlukan. Jadi AbstractArray{T} dan AbstractArray{S} akan tetap menjadi tipe yang berbeda, meskipun menentukan metode yang sama, jadi kami telah memperkenalkan kembali pengetikan nominal. Jadi penggunaan parameter tipe ini tampaknya memerlukan semacam pengetikan nominal.

Jadi akankah 2. memberi kita banyak warisan abstrak?

Jadi akankah 2. memberi kita banyak warisan abstrak?

Tidak. Ini akan menjadi cara untuk mengintegrasikan atau menggabungkan fitur, tetapi setiap fitur masih memiliki properti yang dimilikinya sekarang.

Saya harus menambahkan bahwa mengizinkan banyak pewarisan abstrak adalah keputusan desain yang hampir ortogonal lainnya. Bagaimanapun, masalah dengan menggunakan tipe nominal abstrak terlalu berat adalah (1) Anda mungkin kehilangan implementasi protokol setelah fakta (orang A mendefinisikan tipe, orang B mendefinisikan protokol dan implementasinya untuk A), (2) Anda mungkin kehilangan subtipe struktural protokol.

Bukankah parameter tipe dalam sistem saat ini entah bagaimana merupakan bagian dari antarmuka implisit? Misalnya definisi ini bergantung pada itu: ndims{T,n}(::AbstractArray{T,n}) = n dan banyak fungsi yang ditentukan pengguna juga melakukannya.

Jadi, dalam protokol baru + sistem pewarisan abstrak, kita akan memiliki AbstractArray{T,N} dan ProtoAbstractArray . Sekarang tipe yang secara nominal bukan AbstractArray harus dapat menentukan parameter T dan N , mungkin melalui hard-coding eltype dan ndims . Kemudian semua fungsi parameter pada AbstractArray s perlu ditulis ulang untuk menggunakan eltype dan ndims sebagai ganti parameter. Jadi, mungkin akan lebih masuk akal untuk memiliki protokol yang membawa parameter juga, jadi tipe terkait mungkin sangat berguna. (Perhatikan bahwa tipe beton masih membutuhkan parameter.)

Juga, pengelompokan jenis ke dalam protokol menggunakan @malmaud 's trik: https://github.com/JuliaLang/julia/issues/6975#issuecomment -161.056.795 ini mirip dengan mengetik nominal: pengelompokan ini semata-mata karena memilih jenis dan jenis tidak berbagi antarmuka (dapat digunakan). Jadi mungkin tipe dan protokol abstrak agak tumpang tindih?

Ya, parameter tipe abstrak jelas merupakan jenis antarmuka, dan sampai batas tertentu berlebihan dengan eltype dan ndims . Perbedaan utama tampaknya adalah Anda dapat mengirimkannya secara langsung, tanpa pemanggilan metode tambahan. Saya setuju bahwa dengan tipe terkait, kami akan lebih dekat untuk mengganti tipe abstrak dengan protokol/sifat. Seperti apa bentuk sintaksnya? Idealnya itu akan lebih lemah daripada pemanggilan metode, karena saya lebih suka tidak memiliki ketergantungan melingkar antara subtipe dan pemanggilan metode.

Pertanyaan selanjutnya adalah apakah berguna untuk mengimplementasikan protokol _tanpa_ menjadi bagian dari tipe abstrak terkait. Contohnya mungkin string, yang dapat diubah dan diindeks, tetapi sering diperlakukan sebagai jumlah "skalar" alih-alih wadah. Saya tidak tahu seberapa sering ini muncul.

Saya rasa saya tidak begitu memahami pernyataan "memanggil metode" Anda. Jadi saran untuk sintaks ini mungkin bukan yang Anda minta:

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

Itu bisa berhasil, tergantung pada bagaimana subtipe tipe protokol didefinisikan. Misalnya, diberikan

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

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

apakah kita punya PAbstractArray{Int,1} <: Indexable{Int} ? Saya pikir ini bisa bekerja dengan sangat baik jika parameternya dicocokkan dengan nama. Kami juga mungkin bisa mengotomatisasi definisi yang membuat eltype(x) mengembalikan eltype parameter x 's jenis.

Saya tidak terlalu suka menempatkan definisi metode di dalam blok impl , terutama karena definisi metode tunggal mungkin milik beberapa protokol.

Jadi sepertinya dengan mekanisme seperti itu, kita tidak lagi membutuhkan tipe abstrak. AbstractArray{T,N} bisa menjadi protokol. Kemudian kita secara otomatis mendapatkan multiple inheritance (dari protokol). Juga, ketidakmungkinan untuk mewarisi dari tipe konkret (yang merupakan keluhan yang terkadang kami dengar dari pendatang baru) sudah jelas, karena hanya pewarisan protokol yang akan didukung.

Selain: akan sangat menyenangkan untuk dapat mengekspresikan sifat Callable . Itu harus terlihat seperti ini:

protocol Callable
    ::TupleCons{_, Bottom}
end

di mana TupleCons secara terpisah cocok dengan elemen pertama dari tuple, dan elemen lainnya. Idenya adalah bahwa ini cocok selama tabel metode untuk _ tidak kosong (Bawah menjadi subtipe dari setiap tipe tuple argumen). Sebenarnya kita mungkin ingin membuat sintaks Tuple{a,b} untuk TupleCons{a, TupleCons{b, EmptyTuple}} (lihat juga #11242).

Saya rasa itu tidak benar, semua parameter tipe dikuantifikasi secara eksistensial _dengan batasan_ sehingga tipe dan protokol abstrak tidak dapat disubstitusikan secara langsung.

@jakebolewski dapatkah Anda memikirkan sebuah contoh? Jelas mereka tidak akan pernah menjadi hal yang sama persis; Saya akan mengatakan pertanyaannya adalah lebih apakah kita bisa memijat satu sehingga kita bisa bertahan tanpa keduanya.

Mungkin saya tidak mengerti maksudnya, tetapi bagaimana protokol dapat mengkodekan tipe abstrak yang cukup kompleks dengan batasan, seperti:

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

tanpa harus secara nominal menghitung setiap kemungkinan?

Usulan Protocol diusulkan benar-benar kurang ekspresif dibandingkan dengan subtipe abstrak yang saya coba soroti.

Saya dapat membayangkan hal berikut (secara alami, memperluas desain hingga batas praktisnya):

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

sejalan dengan pengamatan bahwa kita memerlukan sesuatu seperti tipe terkait atau properti tipe bernama agar sesuai dengan ekspresi tipe abstrak yang ada. Dengan ini, kami berpotensi memiliki kompatibilitas yang dekat:

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

Subtipe struktural untuk bidang data objek tidak pernah tampak sangat berguna bagi saya, tetapi diterapkan pada properti _types_ alih-alih tampaknya masuk akal.

Saya juga menyadari bahwa ini dapat memberikan jalan keluar dari masalah ambiguitas: persimpangan dua jenis kosong jika mereka memiliki nilai yang bertentangan untuk beberapa parameter. Jadi jika kita menginginkan tipe Number kita bisa memilikinya

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

Ini melihat super hanya sebagai properti tipe lain.

Saya suka sintaks protokol yang diusulkan, tetapi saya punya beberapa catatan.

Tapi kemudian saya mungkin salah paham segalanya. Saya baru-baru ini mulai benar-benar melihat Julia sebagai sesuatu yang ingin saya kerjakan, dan saya belum memiliki pemahaman yang sempurna tentang sistem tipe.

(a) Saya pikir akan lebih menarik dengan fitur ciri @mauro3 yang dikerjakan di atas. Terutama karena apa gunanya beberapa pengiriman jika Anda tidak dapat memiliki beberapa protokol pengiriman! Saya akan menulis pandangan saya tentang apa contoh dunia nyata nanti. Tetapi intisari umumnya adalah "Apakah ada perilaku yang memungkinkan kedua objek ini berinteraksi". Saya mungkin salah, dan semua itu dapat terbungkus dalam protokol, katakanlah dengan:

protocol Foo{bar}
    ...
end

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

Dan itu juga memperlihatkan masalah utama tidak mengizinkan protokol Foo untuk merujuk protokol Bar dalam definisi yang sama.

(B)

apakah kita memiliki PAbstractArray{Int,1} <: Indexable{Int} ? Saya pikir ini bisa bekerja dengan sangat baik jika parameternya dicocokkan dengan nama.

Saya tidak yakin mengapa kita harus mencocokkan parameter dengan _name_ (Saya menganggap itu sebagai nama eltype , jika saya salah paham, abaikan bagian ini). Mengapa tidak mencocokkan tanda tangan fungsi potensial saja. Masalah utama saya menggunakan penamaan adalah karena mencegah hal berikut:

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

Di sisi lain itu memastikan protokol Anda hanya memperlihatkan hierarki tipe tertentu yang kami inginkan juga. Jika nama kami tidak cocok dengan Iterable maka kami tidak mendapatkan manfaat dari penerapan iterable (dan juga tidak menarik keunggulan dalam ketergantungan). Tapi saya tidak yakin apa yang pengguna _gain_s dari itu, selain kemampuan untuk melakukan hal berikut...

(c) Jadi, saya mungkin melewatkan sesuatu, tetapi bukankah tujuan utama di mana tipe-tipe bernama berguna untuk menggambarkan bagaimana bagian-bagian yang berbeda dari sebuah superset berperilaku? Pertimbangkan hierarki Number dan tipe abstrak Signed dan Unsigned , keduanya akan mengimplementasikan protokol Integer , tetapi terkadang berperilaku sangat berbeda. Untuk membedakannya, apakah sekarang kita dipaksa untuk mendefinisikan negate hanya pada tipe Signed (terutama sulit tanpa tipe pengembalian di mana kita mungkin ingin meniadakan tipe Unsigned )?

Saya pikir ini adalah masalah yang Anda jelaskan dalam contoh super = Number . Ketika kami mendeklarasikan bitstype Int16 <: Signed (pertanyaan saya yang lain adalah bagaimana Number atau Signed sebagai protokol dengan properti tipenya diterapkan ke tipe konkret?) apakah itu melampirkan properti tipe dari protokol Signed ( super = Signed ) menandainya sebagai berbeda dari tipe yang ditandai oleh protokol Unsigned ? Karena itu solusi yang aneh dari pandangan saya, dan bukan hanya karena saya menemukan parameter tipe bernama aneh. Jika dua protokol sama persis kecuali tipe yang mereka tempatkan di super, apa bedanya? Dan jika perbedaannya terletak pada perilaku di antara himpunan bagian dari tipe yang lebih besar (protokol), bukankah kita benar-benar hanya menemukan kembali tujuan tipe abstrak?

(d) Masalahnya adalah kita ingin tipe abstrak untuk membedakan antara perilaku dan kita ingin protokol memastikan kemampuan tertentu (seringkali terlepas dari perilaku lain), di mana perilaku diekspresikan. Tetapi kami mencoba untuk melengkapi kemampuan yang memungkinkan kami untuk memastikan protokol dan partisi tipe abstrak perilaku.

Solusi yang sering kita lompati adalah dengan baris "memiliki tipe yang menyatakan niat mereka untuk mengimplementasikan kelas abstrak dan memeriksa kepatuhan" yang bermasalah dalam implementasi (referensi melingkar, yang mengarah ke penambahan definisi fungsi di dalam blok tipe atau impl blok), dan menghapus kualitas bagus dari protokol yang didasarkan pada kumpulan metode saat ini dan jenis yang mereka operasikan. Masalah-masalah ini menghalangi menempatkan protokol dalam hierarki abstrak.

Tetapi yang lebih penting, protokol tidak menggambarkan perilaku, mereka menggambarkan kemampuan kompleks di beberapa fungsi (seperti iterasi) perilaku iterasi itu dijelaskan oleh tipe abstrak (apakah itu diurutkan, atau bahkan dipesan, misalnya). Di sisi lain, kombinasi protokol + tipe abstrak berguna setelah kita mendapatkan tipe aktual karena memungkinkan kita untuk mengirimkan kemampuan (metode utilitas kemampuan), perilaku (metode tingkat tinggi), atau keduanya (detail implementasi metode).

(e) Jika kita mengizinkan protokol untuk mewarisi banyak protokol (pada dasarnya mereka tetap struktural) dan sebanyak tipe abstrak sebagai tipe konkret (misalnya tanpa banyak pewarisan abstrak, satu) kita dapat mengizinkan pembuatan tipe protokol murni, tipe abstrak murni, dan protokol + tipe abstrak.

Saya yakin ini memperbaiki masalah Signed vs. Unsigned atas:

  • Tentukan dua protokol, keduanya mewarisi dari IntegerProtocol (mewarisi struktur protokol apa pun, NumberAddingProtocol , IntegerSteppingProtocol , dll) satu dari AbstractSignedInteger dan yang lainnya dari AbstractUnsignedInteger ).
  • Kemudian pengguna tipe Signed dijamin fungsionalitas (dari protokol) dan perilaku (dari hierarki abstrak).
  • Jenis beton AbstractSignedInteger tanpa protokol tidak dapat digunakan _anyway_.
  • Tetapi yang menarik (dan sebagai fitur masa depan yang telah disebutkan di atas) pada akhirnya kami dapat membuat kemampuan untuk memecahkan fitur yang hilang, jika hanya IntegerSteppingProtocol (yang sepele dan pada dasarnya hanya alias untuk satu fungsi) ada untuk a diberikan beton AbstractUnsignedInteger kita bisa mencoba untuk memecahkan Signed dengan menerapkan protokol lain dalam hal itu. Mungkin bahkan dengan sesuatu seperti convert .

Sambil menjaga semua tipe yang ada dengan mengubah sebagian besar menjadi protokol + tipe abstrak, dan meninggalkan beberapa sebagai tipe abstrak murni.

Sunting: (f) Contoh sintaks (termasuk bagian (a) ).

Sunting 2 : Memperbaiki beberapa kesalahan ( :< alih-alih <: ), memperbaiki pilihan yang buruk ( Foo alih-alih ::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}.

Saya melihat masalah dengan sintaks ini sebagai:

  • Jenis internal anonim ke protokol (misalnya variabel status).
  • Jenis pengembalian.
  • Sulit untuk mengimplementasikan semantik secara efisien.

Mendefinisikan type_ _Abstract apa suatu entitas adalah. _Protocol_ mendefinisikan apa yang dilakukan entitas. Dalam satu paket, kedua konsep ini dapat dipertukarkan: entitas _adalah_ apa yang _dilakukannya_. Dan "tipe abstrak" lebih langsung. Namun, di antara dua paket, ada perbedaan: Anda tidak memerlukan "apa adanya" klien Anda, tetapi Anda memerlukan apa yang "dilakukan" klien Anda. Di sini, "tipe abstrak" tidak memberikan informasi tentangnya.

Menurut pendapat saya, protokol adalah tipe abstrak tunggal yang dikirim. Ini dapat membantu perpanjangan paket dan kerjasama. Jadi, dalam satu paket, di mana entitas terkait erat, gunakan tipe abstrak untuk memudahkan pengembangan (dengan mengambil untung dari beberapa pengiriman); antar paket, di mana entitas lebih independen, gunakan protokol untuk mengurangi eksposur implementasi.

@mason-bially

Saya tidak yakin mengapa kami harus mencocokkan parameter dengan nama

Maksud saya pencocokan dengan nama _sebagai lawan dari_ pencocokan dengan posisi. Nama-nama ini akan bertindak seperti catatan subtipe struktural. Jika kita punya

protocol Collection{T}
    eltype = T
end

maka apa pun dengan properti yang disebut eltype adalah subtipe dari Collection . Urutan dan posisi "parameter" ini tidak masalah.

Jika dua protokol sama persis kecuali tipe yang mereka tempatkan di super, apa bedanya? Dan jika perbedaannya terletak pada perilaku di antara himpunan bagian dari tipe yang lebih besar (protokol), bukankah kita benar-benar hanya menemukan kembali tujuan tipe abstrak?

Itu poin yang adil. Parameter bernama, pada kenyataannya, membawa kembali banyak properti dari tipe abstrak. Saya mulai dengan gagasan bahwa kita mungkin perlu memiliki protokol dan tipe abstrak, kemudian mencoba menyatukan dan menggeneralisasi fitur. Lagi pula, ketika Anda mendeklarasikan type Foo <: Bar saat ini, pada tingkat tertentu apa yang sebenarnya Anda lakukan adalah set Foo.super === Bar . Jadi mungkin kita harus mendukungnya secara langsung, bersama dengan pasangan kunci/nilai lainnya yang mungkin ingin Anda kaitkan.

"memiliki tipe yang menyatakan niat mereka untuk mengimplementasikan kelas abstrak dan memeriksa kepatuhan"

Ya, saya menentang menjadikan pendekatan itu sebagai fitur inti.

Jika kita mengizinkan protokol untuk mewarisi banyak protokol ... dan sebanyak mungkin tipe abstrak

Apakah ini berarti mengatakan misalnya "T adalah subtipe dari protokol P jika memiliki metode x, y, z, dan menyatakan dirinya sebagai subtipe dari AbstractArray"? Saya pikir "protokol + tipe abstrak" semacam ini sangat mirip dengan apa yang akan Anda dapatkan dengan proposal properti super = T . Memang, dalam versi saya, saya belum menemukan cara untuk menghubungkan mereka ke dalam hierarki seperti yang kita miliki sekarang (mis. Integer <: Real <: Number ).

Memiliki protokol yang diwarisi dari tipe abstrak (nominal) tampaknya menjadi kendala yang sangat kuat. Apakah akan ada subtipe dari tipe abstrak yang _not_ mengimplementasikan protokol? Perasaan saya adalah lebih baik untuk menjaga protokol dan tipe abstrak sebagai hal yang ortogonal.

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

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

Saya tidak mengerti sintaks ini.

  • Apakah protokol ini memiliki nama?
  • Apa arti sebenarnya dari barang-barang di dalam { } dan ( ) ?
  • Bagaimana Anda menggunakan protokol ini? Bisakah Anda mengirimkannya? Jika demikian, apa artinya mendefinisikan f(x::ThisProtocol)=... , mengingat protokol tersebut menghubungkan beberapa jenis?

maka apa pun dengan properti yang disebut eltype adalah subtipe Koleksi. Urutan dan posisi "parameter" ini tidak masalah.

Aha, ada kesalahpahaman saya, itu lebih masuk akal. Yaitu kemampuan untuk menetapkan:

el1type = el_type
el2type = el_type

untuk memecahkan masalah contoh saya.

Jadi mungkin kita harus mendukungnya secara langsung, bersama dengan pasangan kunci/nilai lainnya yang mungkin ingin Anda kaitkan.

Dan fitur kunci/nilai ini akan ada di semua jenis, karena kami akan mengganti abstrak dengannya. Itu solusi umum yang bagus. Solusi Anda jauh lebih masuk akal bagi saya sekarang.

Memang, dalam versi saya, saya belum menemukan cara untuk menghubungkan mereka ke dalam hierarki seperti yang kita miliki sekarang (misalnya Integer <: Real <: Number).

Saya pikir Anda bisa menggunakan super (misalnya dengan Integer super sebagai Real ) dan kemudian membuat super spesial dan bertindak seperti tipe bernama atau menambahkan cara menambahkan kode resolusi tipe kustom (ala python) dan membuat aturan default untuk parameter super .

Memiliki protokol yang diwarisi dari tipe abstrak (nominal) tampaknya menjadi kendala yang sangat kuat. Apakah akan ada subtipe dari tipe abstrak yang tidak mengimplementasikan protokol? Perasaan saya adalah lebih baik untuk menjaga protokol dan tipe abstrak sebagai hal yang ortogonal.

Oh ya, batasan abstrak sepenuhnya opsional! Seluruh poin saya adalah bahwa protokol dan tipe abstrak adalah ortogonal. Anda akan menggunakan protokol + abstrak untuk memastikan Anda mendapatkan kombinasi perilaku tertentu _dan_ kemampuan terkait. Jika Anda hanya menginginkan kemampuan (untuk fungsi utilitas) atau hanya perilaku, maka Anda menggunakannya secara ortogonal.

Apakah protokol ini memiliki nama?

Dua protokol dengan dua nama ( Foo , dan Bar ) yang berasal dari satu blok, tetapi kemudian saya terbiasa menggunakan makro untuk memperluas beberapa definisi seperti itu. Bagian sintaks saya ini adalah upaya untuk menyelesaikan bagian (a) . Jika Anda mengabaikannya, maka baris pertama dapat berupa protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (dengan definisi lain yang terpisah untuk protokol Bar ). Juga, semua Number , AbstractBar dan AbstractFoo akan menjadi opsional, seperti dalam definisi tipe normal,

Apa arti sebenarnya dari barang-barang di dalam {} dan ( )?

{} adalah bagian definisi tipe parametrik standar. Mengizinkan penggunaan Foo{Float64} untuk mendeskripsikan tipe yang mengimplementasikan protokol Foo menggunakan Float64 misalnya. () pada dasarnya adalah daftar pengikatan variabel untuk badan protokol (sehingga beberapa protokol dapat dijelaskan sekaligus). Kebingungan Anda kemungkinan adalah kesalahan saya karena saya salah mengetik :< bukannya <: dalam aslinya. Mungkin juga layak untuk menukarnya untuk mempertahankan struktur <<name>> <<parametric>> <<bindings>> , di mana <<name>> terkadang bisa menjadi daftar binding.

Bagaimana Anda menggunakan protokol ini? Bisakah Anda mengirimkannya? Jika demikian, apa artinya mendefinisikan f(x::ThisProtocol)=... , mengingat protokol tersebut menghubungkan beberapa jenis?

Contoh pengiriman Anda tampaknya benar untuk sintaks menurut pendapat saya, pertimbangkan definisi berikut:

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)

Secara efektif protokol menggunakan tipe Top bernama (Apa saja) kecuali jika diberikan tipe abstrak yang lebih spesifik untuk memeriksa struktur. Memang mungkin layak untuk mengizinkan sesuatu seperti typealias Foo Intersect{FooProtocol, Foo} (_Edit: Intersect adalah nama yang salah, mungkin Join bukan Intersect yang benar pertama kali_) daripada menggunakan sintaks protokol untuk melakukannya.

Ah bagus, itu jauh lebih masuk akal bagiku sekarang! Mendefinisikan beberapa protokol bersama-sama di blok yang sama menarik; Saya harus berpikir lebih banyak tentang itu.

Saya membersihkan semua contoh saya beberapa menit yang lalu. Sebelumnya di utas seseorang menyebutkan mengumpulkan kumpulan protokol untuk menguji ide, saya pikir itu ide bagus.

Beberapa protokol di blok yang sama adalah semacam pet peeve ketika saya mencoba menggambarkan hubungan kompleks antara objek dengan anotasi tipe yang benar di kedua sisi dalam definisi/kompilasi saat Anda memuat bahasa (misalnya seperti python; Java misalnya tidak' t memiliki masalah). Di sisi lain, kebanyakan dari mereka mungkin mudah diperbaiki, dari segi kegunaan, dengan multi-metode pula; tetapi pertimbangan kinerja mungkin muncul karena fitur-fitur yang diketik dalam protokol dengan benar (mengoptimalkan protokol dengan mengkhususkannya pada vtables katakan).

Anda menyebutkan sebelumnya bahwa protokol dapat (secara tidak sengaja) diimplementasikan dengan metode menggunakan ::Any Saya pikir itu akan menjadi kasus yang cukup sederhana untuk diabaikan begitu saja jika memang demikian. Jenis beton tidak akan diklasifikasikan sebagai protokol jika metode implementasi dikirim pada ::Any . Di sisi lain saya tidak setuju bahwa ini pasti masalah.

Sebagai permulaan jika metode ::Any ditambahkan setelah fakta (katakanlah karena seseorang datang dengan sistem yang lebih umum untuk menanganinya) itu masih merupakan implementasi yang valid, dan jika kita menggunakan protokol sebagai fitur pengoptimalan juga maka versi khusus dari metode pengiriman ::Any masih berfungsi untuk peningkatan kinerja. Jadi pada akhirnya saya sebenarnya menentang mengabaikan mereka.

Tetapi mungkin layak untuk memiliki sintaks yang memungkinkan penentu protokol memilih di antara dua opsi (yang mana pun yang kita jadikan default, izinkan yang lain). Untuk yang pertama, sintaks penerusan untuk metode pengiriman ::Any , ucapkan kata kunci global (lihat juga bagian berikutnya). Untuk cara kedua yang memerlukan metode yang lebih spesifik, saya tidak dapat memikirkan kata kunci berguna yang ada.

Sunting: Menghapus banyak hal yang tidak berguna.

Join persis persimpangan dari jenis protokol. Ini sebenarnya "bertemu". Dan untungnya, tipe Join tidak diperlukan, karena tipe protokol sudah ditutup di bawah persimpangan: untuk menghitung persimpangan, cukup kembalikan tipe protokol baru dengan dua daftar metode yang digabungkan.

Saya tidak terlalu khawatir tentang protokol yang diremehkan oleh definisi ::Any . Bagi saya aturan "cari definisi yang cocok kecuali Any tidak masuk hitungan" bertentangan dengan pisau cukur Occam. Belum lagi bahwa memasang tanda "abaikan Apa pun" melalui algoritme subtipe akan sangat mengganggu. Saya bahkan tidak yakin algoritma yang dihasilkan koheren.

Saya sangat menyukai gagasan tentang protokol (mengingatkan saya sedikit pada CLUsters), saya hanya ingin tahu, bagaimana ini cocok dengan subtipe baru yang dibahas oleh Jeff di JuliaCon, dan dengan ciri-ciri? (dua hal yang masih sangat ingin saya lihat di Julia).

Ini akan menambahkan jenis jenis baru dengan aturan subtipenya sendiri (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). Sepintas mereka tampaknya kompatibel dengan seluruh sistem dan hanya dapat dicolokkan.

Protokol-protokol ini cukup banyak merupakan versi "satu parameter" dari sifat-sifat @ mauro3 .

Join persis persimpangan dari jenis protokol.

Saya entah bagaimana meyakinkan diri sendiri bahwa saya salah sebelumnya ketika saya mengatakan itu adalah persimpangan. Meskipun kita masih membutuhkan cara untuk berpotongan tipe dalam satu baris (seperti Union ).

Sunting:

Saya masih juga suka menggeneralisasi protokol dan tipe abstrak ke dalam satu sistem dan mengizinkan aturan khusus untuk resolusinya (misalnya untuk super untuk menggambarkan sistem tipe abstrak saat ini). Saya pikir jika dilakukan dengan benar, ini akan memungkinkan orang untuk menambahkan sistem tipe khusus dan akhirnya optimasi kustom untuk sistem tipe tersebut. Walaupun saya tidak yakin protokol akan menjadi kata kunci yang tepat, tapi setidaknya kita bisa mengubah abstract menjadi makro, itu keren.

dari ladang gandum: lebih baik mengangkat kesamaan melalui protokol dan abstrak daripada mencari generalisasi mereka sebagai tujuan.

apa?

Proses generalisasi maksud, kemampuan dan potensi protokol dan tipe abstrak kurang efektif cara menyelesaikan sintesis yang paling memuaskan secara kualitatif. Ini bekerja lebih baik terlebih dahulu untuk mengumpulkan kesamaan intrinsik tujuan, pola, proses. Dan kembangkan pemahaman itu, yang memungkinkan penyempurnaan perspektif seseorang untuk membentuk sintesis.

Apa pun realisasi yang bermanfaat bagi Julia, itu dibangun di atas perancah yang ditawarkan sintesis. Sintesis yang lebih jelas adalah kekuatan konstruktif dan daya induktif.

Apa?

Saya pikir dia mengatakan kita harus terlebih dahulu mencari tahu apa yang kita inginkan dari protokol dan mengapa itu berguna. Kemudian begitu kita memilikinya dan tipe abstrak, akan lebih mudah untuk membuat sintesis umum dari mereka.

Hanya Protokol

(1) menganjurkan

Sebuah protokol dapat diperluas menjadi protokol (lebih rumit).
Sebuah protokol dapat direduksi menjadi protokol (kurang rumit).
Sebuah protokol dapat diwujudkan sebagai antarmuka yang sesuai [dalam perangkat lunak].
Sebuah protokol dapat ditanyakan untuk menentukan kesesuaian antarmuka.

(2) menyarankan

Protokol harus mendukung nomor versi khusus protokol, dengan default.

Akan lebih baik untuk mendukung beberapa cara melakukan ini:
Ketika sebuah antarmuka sesuai dengan protokol, respon benar; ketika sebuah antarmuka
setia pada subset dari protokol dan akan sesuai jika ditambah,
menjawab tidak lengkap, dan menjawab salah jika tidak. Suatu fungsi harus mencantumkan semua
augmentasi yang diperlukan untuk antarmuka yang tidak lengkap dengan protokol.

(3) merenung

Sebuah protokol bisa menjadi jenis modul yang berbeda. Ekspornya akan berfungsi
sebagai pembanding awal ketika menentukan apakah beberapa antarmuka sesuai.
Setiap jenis dan fungsi protokol yang ditentukan [diekspor] dapat dideklarasikan menggunakan
@abstract , @type , @immutable dan @function untuk mendukung abstraksi bawaan.

[pao: beralih ke kutipan kode, meskipun perhatikan bahwa kuda telah meninggalkan gudang ketika Anda melakukan ini setelah fakta ...]

(Anda perlu mengutip @mentions !)

terima kasih -- memperbaikinya

Pada Rabu, 16 Desember 2015 pukul 03:01, Mauro [email protected] menulis:

(Anda perlu mengutip @sebutan!)


Balas email ini secara langsung atau lihat di GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

maaf saya seharusnya lebih jelas: kutipan kode menggunakan `dan bukan "

Memperbaiki perbaikan kutipan.

terima kasih -- maafkan ketidaktahuan saya sebelumnya

Saya mencoba memahami diskusi baru-baru ini tentang menambahkan tipe protokol. Mungkin saya salah memahami sesuatu, tetapi mengapa perlu menamai protokol alih-alih hanya menggunakan nama tipe abstrak terkait yang akan dijelaskan oleh protokol?

Dalam sudut pandang saya, cukup alami untuk memperluas sistem tipe abstrak saat ini dengan beberapa cara untuk menggambarkan perilaku yang diharapkan dari tipe tersebut. Sama seperti awalnya diusulkan di utas ini tetapi mungkin dengan sintaks Jeffs

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

Saat menempuh rute ini, tidak perlu secara khusus menunjukkan bahwa subtipe mengimplementasikan antarmuka. Ini akan secara implisit dilakukan dengan subtipe.

Tujuan utama dari mekanisme antarmuka eksplisit adalah IMHO untuk mendapatkan pesan kesalahan yang lebih baik dan untuk melakukan uji verifikasi yang lebih baik.

Jadi deklarasi tipe seperti:

type Foo <: Iterable
  ...
end

Apakah kita mendefinisikan fungsi di bagian ... ? Jika tidak, kapan kita membuat kesalahan tentang fungsi yang hilang (dan kerumitan yang terkait dengan itu)? Juga, apa yang terjadi untuk tipe yang mengimplementasikan banyak protokol, apakah kami mengaktifkan banyak pewarisan abstrak? Bagaimana kita menangani resolusi super-metode? Apa yang dilakukan ini dengan beberapa pengiriman (sepertinya hanya menghapusnya dan menempelkan sistem objek Java-esque di sana)? Bagaimana kita mendefinisikan spesialisasi tipe baru untuk metode setelah tipe pertama didefinisikan? Bagaimana kita mendefinisikan protokol setelah kita mendefinisikan tipenya?

Semua pertanyaan ini lebih mudah dipecahkan dengan membuat tipe baru (atau membuat formulasi tipe baru).

Tidak selalu ada tipe abstrak terkait untuk setiap protokol (mungkin seharusnya tidak ada). Kelipatan antarmuka saat ini dapat diimplementasikan oleh tipe yang sama. Yang tidak dapat dijelaskan dengan sistem tipe abstrak saat ini. Oleh karena itu masalahnya.

  • Warisan berganda abstrak (menerapkan beberapa protokol) ortogonal untuk fitur ini (seperti yang dinyatakan oleh Jeff di atas). Jadi kami tidak mendapatkan fitur itu hanya karena protokol ditambahkan ke bahasa.
  • Komentar Anda berikutnya adalah tentang pertanyaan kapan harus memverifikasi antarmuka. Saya pikir ini tidak harus dikaitkan dengan definisi fungsi dalam blok, yang tidak terasa Julian bagi saya. Sebaliknya ada tiga solusi sederhana:

    1. seperti yang diimplementasikan di #7025 gunakan metode verify_interface yang dapat dipanggil setelah semua definisi fungsi atau dalam pengujian unit

    2. Seseorang tidak dapat memverifikasi antarmuka sama sekali dan menundanya ke pesan kesalahan yang ditingkatkan di "MethodError". Sebenarnya ini adalah mundur yang bagus untuk 1.

    3. Verifikasi semua antarmuka baik di akhir unit waktu kompilasi atau di akhir fase pemuatan modul. Saat ini juga dimungkinkan untuk memiliki:

function a()
  b()
end

function b()
end

Jadi, saya tidak berpikir definisi fungsi dalam blok akan diperlukan di sini.

  • Poin terakhir Anda adalah bahwa mungkin ada protokol yang tidak terkait dengan tipe abstrak. Saat ini memang benar (misalnya protokol informal "Iterable"). Namun, dalam sudut pandang saya, ini hanya karena kurangnya pewarisan abstrak berganda. Jika ini masalahnya, silakan tambahkan saja pewarisan berganda abstrak alih-alih menambahkan fitur bahasa baru yang bertujuan untuk menyelesaikan ini. Saya juga berpikir bahwa mengimplementasikan banyak antarmuka sangat penting dan ini sangat umum di Java/C#.

Saya pikir perbedaan antara hal seperti "protokol" dan pewarisan berganda adalah bahwa suatu tipe dapat ditambahkan ke protokol setelah ditentukan. Ini berguna jika Anda ingin membuat paket Anda (protokol pendefinisian) bekerja dengan tipe yang ada. Seseorang dapat mengizinkan untuk memodifikasi supertipe suatu tipe setelah pembuatan, tetapi pada saat itu mungkin lebih baik menyebutnya "protokol" atau semacamnya.

Hm sehingga memungkinkan untuk menentukan antarmuka alternatif/yang disempurnakan untuk tipe yang ada. Masih belum jelas bagi saya di mana ini akan benar-benar diperlukan. Ketika seseorang ingin menambahkan sesuatu ke antarmuka yang ada (ketika kita mengikuti pendekatan yang diusulkan dalam OP) ia hanya akan membuat subtipe dan menambahkan metode antarmuka tambahan ke subtipe. Ini adalah hal yang baik tentang pendekatan itu. Skalanya cukup baik.

Contoh: katakan saya punya beberapa paket yang membuat serialisasi tipe. Metode tobits perlu diimplementasikan untuk suatu tipe, maka semua fungsi dalam paket itu akan bekerja dengan tipe tersebut. Mari kita sebut ini protokol Serializer (yaitu tobits didefinisikan). Sekarang saya dapat menambahkan Array (atau jenis lainnya) ke dalamnya dengan menerapkan tobits . Dengan multiple inheritance saya tidak dapat membuat Array bekerja dengan Serialzer karena saya tidak dapat menambahkan supertype ke Array setelah definisinya. Saya pikir ini adalah kasus penggunaan yang penting.

Oke, pahami ini. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 adalah masalah serupa, di mana solusinya pada dasarnya adalah menggunakan pengetikan bebek. Jika kita bisa memiliki sesuatu yang memecahkan masalah ini dengan elegan, ini memang bagus. Tapi ini adalah sesuatu yang harus didukung di tingkat pengiriman. Jika saya memahami ide protokol di atas dengan benar, seseorang dapat menempatkan tipe abstrak atau protokol sebagai anotasi tipe dalam fungsi. Di sini alangkah baiknya untuk menggabungkan dua konsep ini dengan satu alat yang cukup kuat.

Saya setuju: akan sangat membingungkan memiliki tipe dan protokol abstrak. Jika saya ingat dengan benar, dikemukakan di atas bahwa tipe abstrak memiliki beberapa semantik yang tidak dapat dimodelkan dengan protokol, yaitu tipe abstrak memiliki beberapa fitur yang tidak dimiliki oleh protokol. Bahkan jika memang demikian (saya tidak yakin), itu masih akan membingungkan karena ada tumpang tindih yang besar antara kedua konsep tersebut. Jadi, tipe abstrak harus dihapus demi protokol.

Sejauh ada konsensus di atas tentang protokol, mereka menekankan menentukan antarmuka. Jenis abstrak mungkin telah digunakan untuk melakukan beberapa protokol yang tidak ada itu. Itu tidak berarti itu adalah penggunaan mereka yang paling penting. Beri tahu saya apa itu protokol dan bukan, maka saya dapat memberi tahu Anda bagaimana tipe abstrak berbeda dan beberapa dari apa yang mereka bawa. Saya tidak pernah menganggap tipe abstrak sebagai tentang antarmuka sebanyak tentang tipologi. Membuang pendekatan alami untuk fleksibilitas tipologis itu mahal.

@JeffreySarnoff +1

Pikirkan hierarki tipe Angka. Jenis abstrak yang berbeda misalnya Ditandatangani, Tidak Ditandatangani, tidak ditentukan oleh antarmukanya. Tidak ada kumpulan metode yang mendefinisikan "Tidak Ditandatangani". Ini hanyalah sebuah deklarasi yang sangat berguna.

Saya tidak melihat masalahnya, sungguh. Jika kedua tipe Signed dan Unsigned mendukung kumpulan metode yang sama, kita dapat membuat dua protokol dengan antarmuka yang identik. Namun, mendeklarasikan tipe sebagai Signed daripada Unsigned dapat digunakan untuk pengiriman (yaitu metode dari fungsi yang sama bekerja secara berbeda). Kuncinya di sini adalah meminta deklarasi eksplisit sebelum mempertimbangkan bahwa suatu tipe mengimplementasikan protokol, daripada mendeteksi ini secara implisit berdasarkan metode yang diimplementasikannya.

Tetapi memiliki protokol yang terkait secara implisit juga penting, seperti di https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Protokol tidak hanya dapat mendefinisikan fungsi yang dapat dipanggil, tetapi juga dapat mendokumentasikan (baik secara implisit, atau dengan cara yang dapat diuji mesin) properti yang perlu dipegang. Seperti:

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

Perbedaan perilaku yang terlihat secara eksternal antara Signed dan Unsigned inilah yang membuat perbedaan ini berguna.

Jika ada perbedaan antara tipe yang begitu "abstrak" sehingga tidak dapat segera diverifikasi, setidaknya secara teoritis, dari luar, maka kemungkinan seseorang perlu mengetahui implementasi suatu tipe untuk membuat pilihan yang tepat. Di sinilah abstract mungkin berguna. Ini mungkin mengarah pada tipe data aljabar.

Tidak ada alasan mengapa protokol tidak boleh digunakan hanya untuk mengelompokkan jenis, yaitu tanpa memerlukan metode yang ditentukan (dan dimungkinkan dengan desain "saat ini" menggunakan trik: https://github.com/JuliaLang/julia/issues/ 6975#issuecomment-161056795). (Perhatikan juga, ini tidak mengganggu protokol yang ditentukan secara implisit.)

Mempertimbangkan contoh (Un)signed : apa yang akan saya lakukan jika saya memiliki tipe Signed tetapi untuk beberapa alasan harus juga merupakan subtipe dari tipe abstrak lain? Ini tidak mungkin.

@eschnett : tipe abstrak, saat ini, tidak ada hubungannya dengan implementasi subtipe mereka. Meskipun yang telah dibahas: #4935.

Tipe data aljabar adalah contoh yang baik di mana penyempurnaan berturut-turut secara intrinsik bermakna.
Taksonomi apa pun jauh lebih alami diberikan, dan lebih berguna secara langsung sebagai hierarki tipe abstrak daripada sebagai campuran spesifikasi protokol.

Catatan tentang memiliki tipe yang merupakan subtipe dari lebih dari satu hierarki tipe abstrak juga penting. Ada banyak kekuatan utilitarian yang datang dengan banyak pewarisan abstraksi.

@ mauro3 Ya, saya tahu. Saya sedang memikirkan sesuatu yang setara dengan serikat pekerja yang didiskriminasi, tetapi diimplementasikan seefisien tupel alih-alih melalui sistem tipe (seperti serikat pekerja saat ini diimplementasikan). Ini akan menggolongkan enum, tipe nullable, dan mungkin dapat menangani beberapa kasus lain lebih efisien daripada tipe abstrak saat ini.

Misalnya, seperti tupel dengan elemen anonim:

DiscriminatedUnion{Int16, UInt32, Float64}

atau dengan elemen bernama:

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

Poin yang saya coba sampaikan adalah bahwa tipe abstrak adalah salah satu cara yang baik untuk memetakan konstruksi semacam itu ke Julia.

Tidak ada alasan mengapa protokol tidak boleh digunakan hanya untuk mengelompokkan jenis, yaitu tanpa memerlukan metode yang ditentukan (dan dimungkinkan dengan desain "saat ini" menggunakan trik: #6975 (komentar)). (Perhatikan juga, ini tidak mengganggu protokol yang ditentukan secara implisit.)

Saya merasa Anda harus berhati-hati dengan ini untuk mencapai kinerja, pertimbangan yang tampaknya tidak cukup sering dipertimbangkan oleh banyak orang. Dalam contoh, tampaknya seseorang hanya ingin mendefinisikan versi non-apa pun sehingga kompiler masih dapat memilih fungsi pada waktu kompilasi (daripada harus memanggil fungsi untuk memilih yang tepat saat runtime, atau kompiler memeriksa fungsi untuk menentukan hasilnya). Secara pribadi saya percaya menggunakan beberapa "warisan" abstrak sebagai tag akan menjadi solusi yang lebih baik.

Saya merasa kita harus menjaga trik dan pengetahuan yang diperlukan tentang sistem tipe seminimal mungkin (walaupun itu bisa dibungkus dengan makro, itu akan terasa seperti peretasan makro yang aneh; jika kita menggunakan makro untuk memanipulasi sistem tipe maka saya pikir @ Solusi terpadu JeffBezanson akan memperbaiki masalah ini dengan lebih baik).

Mempertimbangkan contoh (Tidak) yang ditandatangani: apa yang akan saya lakukan jika saya memiliki tipe yang Ditandatangani tetapi untuk beberapa alasan harus juga merupakan subtipe dari tipe abstrak lain? Ini tidak mungkin.

Warisan abstrak berganda.


Saya percaya semua alasan ini telah dibahas sebelumnya, percakapan ini tampaknya berputar-putar (walaupun semakin rapat setiap kali). Saya percaya telah disebutkan bahwa korpus atau masalah menggunakan protokol harus diperoleh. Ini akan memungkinkan kita untuk menilai solusi dengan lebih mudah.

Sementara kami mengulangi hal-hal :) Saya ingin mengingatkan semua orang bahwa tipe abstrak adalah nominal sementara protokol adalah struktural, jadi saya menyukai desain yang memperlakukan mereka sebagai ortogonal, _kecuali_ kita benar-benar dapat membuat "pengkodean" yang dapat diterima dari tipe abstrak dalam protokol (mungkin dengan penggunaan tipe terkait yang cerdas). Poin bonus, tentu saja, jika itu juga menghasilkan banyak pewarisan abstrak. Saya merasa ini mungkin, tetapi kami belum sampai di sana.

@JeffBezanson Apakah "tipe terkait" berbeda dari "tipe konkret yang terkait dengan protokol [a]"?

Ya saya percaya begitu; Maksud saya "tipe terkait" dalam pengertian teknis protokol yang menentukan beberapa pasangan nilai kunci di mana "nilai" adalah tipe, dengan cara yang sama seperti protokol menentukan metode. misalnya "ketik Foo mengikuti protokol Container jika memiliki eltype " atau "ketik Foo mengikuti protokol Matrix jika parameter ndims adalah 2".

tipe abstrak adalah nominal sedangkan protokol bersifat struktural dan
tipe abstrak bersifat kualitatif sedangkan protokol bersifat operatif dan
tipe abstrak (dengan banyak pewarisan) diatur saat protokol berjalan

Bahkan jika ada pengkodean satu sama lain, "hei, Hai.. apa kabar? ayo pergi!" Julia perlu menyajikan keduanya dengan jelas - gagasan protokol yang bertujuan umum dan tipe abstrak yang dapat diwariskan (gagasan tentang tujuan umum). Jika ada penyingkapan yang cerdik yang memberi Julia keduanya, terbungkus secara terpisah, kemungkinan besar itu dilakukan begitu saja daripada satu demi satu dan lainnya.

@ mason-bially: jadi kita harus menambahkan banyak warisan juga? Ini masih akan meninggalkan masalah bahwa supertipe tidak dapat ditambahkan setelah pembuatan tipe (kecuali jika itu juga diizinkan).

@JeffBezanson : tidak ada yang akan menghentikan kami untuk mengizinkan protokol nominal murni.

@ mauro3 Mengapa keputusan tentang apakah akan mengizinkan penyisipan supertipe post facto dikaitkan dengan pewarisan berganda? Dan ada berbagai jenis kreasi supertipe, beberapa di antaranya benar-benar tidak berbahaya dengan mengandaikan kemampuan untuk menyisipkan apa pun-mereka-yang baru: Saya ingin menambahkan tipe abstrak antara Real dan AbstractFloat, katakanlah ProtoFloat, sehingga saya dapat mengirimkan dua kali lipat pelampung ganda dan sistem Mengapung bersama-sama tanpa mengganggu sistem Mengapung hidup sebagai subtipe dari AbstractFloat. Mungkin kurang mudah untuk diizinkan, adalah kemampuan untuk membagi subtipe Integer saat ini, dan karenanya menghindari banyak pesan "ambigu dengan .. define f(Bool) before.."; atau untuk memperkenalkan supertipe dari Signed yang merupakan subtipe Integer dan membuka hierarki numerik untuk penanganan transparan, katakanlah Ordinal number.

Maaf jika saya memulai putaran lain dari lingkaran. Topiknya cukup kompleks dan kami benar-benar harus memastikan bahwa solusinya sangat mudah digunakan. Jadi kita perlu menutupi:

  • solusi umum
  • tidak ada penurunan kinerja
  • kemudahan penggunaan (dan juga mudah dimengerti!)

Karena apa yang awalnya diusulkan di #6975 sangat berbeda dengan ide protokol yang dibahas kemudian, mungkin ada baiknya untuk memiliki semacam JEP yang menjelaskan seperti apa bentuk protokol tersebut.

Contoh bagaimana Anda dapat mendefinisikan antarmuka formal dan memvalidasinya menggunakan 0,4 saat ini (tanpa makro), pengiriman saat ini bergantung pada pengiriman gaya sifat kecuali ada modifikasi yang dilakukan pada gf.c. Ini menggunakan fungsi yang dihasilkan untuk validasi, semua jenis komputasi dilakukan di ruang tipe.

Saya mulai menggunakan ini sebagai pemeriksaan runtime di DSL yang kami tentukan di mana saya harus memastikan bahwa jenis yang disediakan adalah iterator tanggal.

Saat ini mendukung banyak pewarisan tipe super, nama bidang _super tidak digunakan oleh runtime dan dapat berupa simbol apa pun yang valid. Anda dapat menyediakan n jenis lain ke _super Tuple.

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

Hanya menunjukkan di sini bahwa saya membuat tindak lanjut pada diskusi dari JuliaCon tentang kemungkinan sintaks pada sifat-sifat di https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040

Guy Steele memiliki beberapa wawasan bagus tentang sifat-sifat dalam beberapa bahasa pengiriman (Benteng), lihat keynote JuliaCon 2016-nya: https://youtu.be/EZD3Scuv02g .

Beberapa sorotan: sistem sifat besar untuk sifat aljabar, pengujian unit sifat-sifat untuk tipe yang menerapkan sifat, dan bahwa sistem yang mereka terapkan mungkin terlalu rumit dan dia akan melakukan sesuatu yang lebih sederhana sekarang.

Swift baru untuk tensorflow compiler AD usecase untuk protokol:
https://Gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy dan @Keno mungkin tertarik dengan ini. Memiliki konten baru

Saya pikir presentasi ini patut mendapat perhatian ketika menjelajahi ruang desain untuk masalah ini.

Untuk diskusi tentang ide-ide non-spesifik dan tautan ke pekerjaan latar belakang yang relevan, akan lebih baik untuk memulai utas dan posting wacana yang sesuai dan berdiskusi di sana.

Perhatikan bahwa hampir semua masalah yang dihadapi dan dibahas dalam penelitian tentang pemrograman generik dalam bahasa yang diketik secara statis tidak relevan dengan Julia. Bahasa statis hampir secara eksklusif berkaitan dengan masalah penyediaan ekspresi yang cukup untuk menulis kode yang mereka inginkan sambil tetap dapat mengetik secara statis memeriksa bahwa tidak ada pelanggaran sistem tipe. Kami tidak memiliki masalah dengan ekspresif dan tidak memerlukan pemeriksaan tipe statis, jadi tidak ada yang benar-benar penting di Julia.

Yang kami pedulikan adalah mengizinkan orang untuk mendokumentasikan ekspektasi protokol dengan cara terstruktur yang kemudian dapat diverifikasi oleh bahasa secara dinamis (sebelumnya, jika memungkinkan). Kami juga peduli untuk mengizinkan orang mengirimkan hal-hal seperti sifat; itu tetap terbuka apakah itu harus terhubung.

Intinya: sementara pekerjaan akademis tentang protokol dalam bahasa statis mungkin menarik bagi umum, itu tidak terlalu membantu dalam konteks Julia.

Yang kami pedulikan adalah mengizinkan orang untuk mendokumentasikan ekspektasi protokol dengan cara terstruktur yang kemudian dapat diverifikasi oleh bahasa secara dinamis (sebelumnya, jika memungkinkan). Kami juga peduli untuk mengizinkan orang mengirimkan hal-hal seperti sifat; itu tetap terbuka apakah itu harus terhubung.

_itulah_ :tiket:

Selain menghindari perubahan yang melanggar, apakah penghapusan tipe abstrak dan pengenalan antarmuka implisit gaya golang layak dilakukan di julia?

Tidak.

baik, bukankah itu semua tentang protokol/sifat? Ada beberapa diskusi apakah protokol perlu implisit atau eksplisit.

Saya pikir sejak 0,3 (2014), pengalaman telah menunjukkan bahwa antarmuka implisit (yaitu tidak ditegakkan oleh bahasa/kompiler) berfungsi dengan baik. Juga, setelah menyaksikan bagaimana beberapa paket berevolusi, saya pikir antarmuka terbaik dikembangkan secara organik, dan diformalkan (= didokumentasikan) hanya di kemudian hari.

Saya tidak yakin bahwa deskripsi formal antarmuka, yang entah bagaimana dipaksakan oleh bahasa, diperlukan. Tetapi sementara itu diputuskan, akan sangat bagus untuk mendorong yang berikut (dalam dokumentasi, tutorial, dan panduan gaya):

  1. "antarmuka" murah dan ringan, hanya sekelompok fungsi dengan perilaku yang ditentukan untuk sekumpulan tipe (ya, tipe adalah tingkat perincian yang tepat — untuk x::T , T seharusnya cukup untuk memutuskan apakah x mengimplementasikan antarmuka) . Jadi, jika seseorang mendefinisikan paket dengan perilaku yang dapat diperluas, masuk akal untuk mendokumentasikan antarmuka.

  2. Antarmuka tidak perlu dijelaskan dengan relasi subtipe . Tipe tanpa supertipe umum (nontrivial) dapat mengimplementasikan antarmuka yang sama. Suatu tipe dapat mengimplementasikan beberapa antarmuka.

  3. Penerusan/komposisi secara implisit membutuhkan antarmuka. "Bagaimana membuat pembungkus mewarisi semua metode induk" adalah pertanyaan yang sering muncul, tetapi itu bukan pertanyaan yang tepat. Solusi praktisnya adalah memiliki antarmuka inti dan hanya mengimplementasikannya untuk pembungkusnya.

  4. Sifat itu murah dan harus digunakan secara bebas. Base.IndexStyle adalah contoh kanonik yang sangat baik.

Berikut ini akan mendapat manfaat dari klarifikasi karena saya tidak yakin apa praktik terbaiknya:

  1. Haruskah antarmuka memiliki fungsi kueri, seperti misalnya Tables.istable untuk memutuskan apakah suatu objek mengimplementasikan antarmuka? Saya pikir ini adalah praktik yang baik, jika penelepon dapat bekerja dengan berbagai antarmuka alternatif dan perlu menelusuri daftar fallback.

  2. Apa tempat terbaik untuk dokumentasi antarmuka dalam docstring? Saya akan mengatakan fungsi kueri di atas.

  1. ya, jenis adalah tingkat perincian yang tepat

Mengapa begitu? Beberapa aspek tipe dapat dimasukkan ke dalam antarmuka (untuk tujuan pengiriman), seperti iterasi. Jika tidak, Anda harus menulis ulang kode atau memaksakan struktur yang tidak perlu.

  1. Antarmuka tidak perlu dijelaskan dengan relasi subtipe .

Mungkin tidak perlu, tetapi apakah itu lebih baik? Saya dapat memiliki pengiriman fungsi pada tipe yang dapat diubah. Bukankah seharusnya tipe iterable ubin memenuhi itu secara implisit? Mengapa pengguna harus menggambar ini di sekitar tipe nominal ketika mereka hanya peduli dengan antarmuka?

Apa gunanya subtipe nominal jika Anda pada dasarnya hanya menggunakannya sebagai antarmuka abstrak? Sifat tampaknya lebih terperinci dan kuat, jadi akan menjadi generalisasi yang lebih baik. Jadi sepertinya tipe hampir merupakan ciri, tetapi kita harus memiliki sifat untuk mengatasi keterbatasannya (dan sebaliknya).

Apa gunanya subtipe nominal jika Anda pada dasarnya hanya menggunakannya sebagai antarmuka abstrak?

Pengiriman—Anda dapat mengirimkan pada jenis nominal sesuatu. Jika Anda tidak perlu mengirimkan apakah suatu tipe mengimplementasikan antarmuka atau tidak, maka Anda cukup mengetiknya. Inilah yang biasanya digunakan orang untuk sifat Suci: sifat tersebut memungkinkan Anda mengirim untuk memanggil implementasi yang mengasumsikan bahwa beberapa antarmuka diimplementasikan (misalnya "memiliki panjang yang diketahui"). Sesuatu yang tampaknya diinginkan orang adalah untuk menghindari lapisan tipuan itu tetapi sepertinya itu hanya kenyamanan, bukan kebutuhan.

Mengapa begitu? Beberapa aspek tipe dapat dimasukkan ke dalam antarmuka (untuk tujuan pengiriman), seperti iterasi. Jika tidak, Anda harus menulis ulang kode atau memaksakan struktur yang tidak perlu.

Saya percaya @tpapp mengatakan bahwa Anda hanya perlu tipe untuk menentukan apakah sesuatu mengimplementasikan antarmuka atau tidak, bukan berarti semua antarmuka dapat diwakili dengan hierarki tipe.

Hanya sebuah pemikiran, saat menggunakan MacroTools 's forward :

Terkadang menjengkelkan untuk meneruskan banyak metode

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

bagaimana jika kita bisa menggunakan tipe Foo.x 's dan daftar metode kemudian menyimpulkan mana yang akan diteruskan? Ini akan menjadi semacam inheritance dan dapat diimplementasikan dengan fitur yang ada (makro + fungsi yang dihasilkan), sepertinya semacam antarmuka juga, tetapi kami tidak memerlukan hal lain dalam bahasa tersebut.

Saya tahu kami tidak akan pernah bisa membuat daftar apa yang akan diwarisi (ini juga mengapa model class statis kurang fleksibel), terkadang Anda hanya memerlukan beberapa dari mereka, tetapi itu hanya nyaman untuk fungsi inti ( misalnya seseorang ingin mendefinisikan pembungkus (subtipe AbstractArray ) sekitar Array , sebagian besar fungsi hanya diteruskan)

@datnamer : seperti yang telah diklarifikasi oleh orang lain, antarmuka tidak boleh lebih granular daripada tipe (yaitu mengimplementasikan antarmuka tidak boleh bergantung pada value , mengingat tipenya). Ini sangat cocok dengan model pengoptimalan kompiler dan bukan merupakan kendala dalam praktiknya.

Mungkin saya tidak jelas, tetapi tujuan tanggapan saya adalah untuk menunjukkan bahwa kita sudah memiliki antarmuka sejauh yang berguna di Julia , dan mereka ringan, cepat, dan menjadi meresap saat ekosistem matang.

Sebuah spesifikasi formal untuk menggambarkan sebuah antarmuka menambahkan sedikit nilai IMO: itu akan berarti hanya dokumentasi dan memeriksa bahwa beberapa metode tersedia. Yang terakhir adalah bagian dari antarmuka, tetapi bagian lainnya adalah semantik yang diimplementasikan oleh metode ini (misalnya jika A adalah array, axes(A) memberi saya rentang koordinat yang valid untuk getindex ). Spesifikasi formal antarmuka tidak dapat mengatasi ini secara umum, jadi saya berpendapat bahwa mereka hanya akan menambahkan boilerplate dengan sedikit nilai. Saya juga khawatir bahwa itu hanya akan meningkatkan hambatan masuk (kecil) untuk keuntungan kecil.

Namun, yang ingin saya lihat adalah

  1. dokumentasi untuk semakin banyak antarmuka (dalam docstring),

  2. test suite untuk menangkap kesalahan yang jelas untuk antarmuka dewasa untuk tipe yang baru ditentukan (misalnya banyak T <: AbstractArray implement eltype(::T) dan bukan eltype(::Type{T}) .

@tpapp Masuk akal bagi saya sekarang, terima kasih.

@StefanKarpinski Saya tidak begitu mengerti. Sifat bukanlah tipe nominal (kan?), namun, sifat dapat digunakan untuk pengiriman.

Maksud saya pada dasarnya adalah yang dibuat oleh @mauro3 di sini: https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Bahwa dengan memiliki sifat dan pengetikan abstrak, ada tambahan kerumitan dan kebingungan dengan memiliki dua konsep yang sangat mirip.

Sesuatu yang tampaknya diinginkan orang adalah untuk menghindari lapisan tipuan itu tetapi sepertinya itu hanya kenyamanan, bukan kebutuhan.

Bisakah bagian hierarki sifat dikirim setelah dikelompokkan berdasarkan hal-hal seperti serikat pekerja dan persimpangan, dengan parameter tipe, dengan kuat? Saya belum mencobanya, tetapi sepertinya itu membutuhkan dukungan bahasa. Masalah ekspresi IE dalam domain tipe.

Sunting: Saya pikir masalahnya adalah penggabungan antarmuka dan sifat saya, seperti yang digunakan di sini.

Hanya memposting ini di sini karena menyenangkan: sepertinya Konsep pasti telah diterima dan akan menjadi bagian dari C++ 20. Hal yang menarik!

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

Saya pikir sifat-sifat adalah cara yang sangat baik untuk menyelesaikan masalah ini dan sifat-sifat suci tentu saja telah berkembang jauh. Namun, menurut saya yang sebenarnya dibutuhkan Julia adalah cara mengelompokkan fungsi-fungsi yang termasuk dalam suatu sifat. Ini akan berguna untuk alasan dokumentasi tetapi juga untuk keterbacaan kode. Dari apa yang saya lihat sejauh ini, saya pikir sintaks sifat seperti di Rust akan menjadi cara yang tepat.

Saya pikir ini sangat penting, dan kasus penggunaan yang paling penting adalah untuk mengindeks iterator. Berikut adalah proposal untuk jenis sintaks yang Anda harapkan akan berhasil. Maaf jika sudah diusulkan (utas panjang ...).

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
Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

iamed2 picture iamed2  ·  3Komentar

yurivish picture yurivish  ·  3Komentar

StefanKarpinski picture StefanKarpinski  ·  3Komentar

omus picture omus  ·  3Komentar

wilburtownsend picture wilburtownsend  ·  3Komentar