Julia: Интерфейсы для абстрактных типов

Созданный на 26 мая 2014  ·  171Комментарии  ·  Источник: JuliaLang/julia

Я думаю, что этот запрос функции еще не является отдельной проблемой, хотя он обсуждался, например, в №5.

Я думаю, было бы здорово, если бы мы могли явно определять интерфейсы для абстрактных типов. Под интерфейсом я подразумеваю все методы, которые должны быть реализованы для выполнения требований абстрактного типа. В настоящее время интерфейс определяется только неявно и может быть разбросан по нескольким файлам, поэтому очень сложно определить, что нужно реализовать при наследовании от абстрактного типа.

Интерфейсы в первую очередь дадут нам две вещи:

  • самостоятельная документация интерфейсов в одном месте
  • улучшенные сообщения об ошибках

В Base.graphics есть макрос, который фактически позволяет определять интерфейсы путем кодирования сообщения об ошибке в резервной реализации. Я думаю, это уже очень умно. Но, возможно, еще лучше придать ему следующий синтаксис:

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

Было бы неплохо, если бы можно было указать разные степени детализации. В объявлениях print и push! только говорится, что должны быть какие-либо методы с таким именем (и MyType качестве первого параметра), но они не указывают типы. Напротив, объявление size полностью типизировано. Я думаю, что это дает большую гибкость, и для объявления нетипизированного интерфейса все еще можно выдавать довольно конкретные сообщения об ошибках.

Как я сказал в №5, такие интерфейсы - это в основном то, что запланировано в C ++ как Concept-light для C ++ 14 или C ++ 17. И после некоторого программирования шаблонов C ++ я уверен, что некоторая формализация в этой области также будет полезна для Джулии.

Самый полезный комментарий

Для обсуждения неспецифических идей и ссылок на соответствующие фоновые работы было бы лучше начать соответствующую ветку дискуссии и публиковать и обсуждать там.

Обратите внимание, что почти все проблемы, встречающиеся и обсуждаемые в исследованиях универсального программирования на статически типизированных языках, не имеют отношения к Джулии. Статические языки почти исключительно связаны с проблемой обеспечения достаточной выразительности для написания кода, который они хотят, при этом сохраняя возможность статической проверки типов на предмет отсутствия нарушений системы типов. У нас нет проблем с выразительностью и не требуется статическая проверка типов, так что в Джулии все это не имеет значения.

Что нас действительно волнует, так это возможность людям структурированно документировать ожидания протокола, которые затем язык может динамически проверять (заранее, когда это возможно). Мы также заботимся о том, чтобы люди могли рассуждать о таких вещах, как черты характера; остается открытым вопрос о том, следует ли их подключать.

Итог: хотя академическая работа над протоколами на статических языках может представлять общий интерес, она не очень полезна в контексте Джулии.

Все 171 Комментарий

В целом, я думаю, что это хорошее направление к лучшему интерфейсно-ориентированному программированию.

Однако здесь чего-то не хватает. Сигнатуры методов (а не только их имена) также важны для интерфейса.

Это непросто реализовать, и здесь будет много подводных камней. Вероятно, это одна из причин, почему _Concepts_ не был принят C ++ 11, и через три года только очень ограниченная _lite_ версия попадает в C ++ 14.

Метод size в моем примере содержал подпись. Далее @mustimplement из Base.graphics также учитывает подпись.

Я должен добавить, что у нас уже есть одна часть Concept-light которая является возможностью ограничить тип как подтип определенного абстрактного типа. Другая часть - это интерфейсы.

Этот макрос довольно крутой. Я вручную определил резервные варианты, вызывающие ошибки, и они очень хорошо работали для определения интерфейсов. например, MathProgBase от JuliaOpt делает это, и это хорошо работает. Я возился с новым решателем (https://github.com/IainNZ/RationalSimplex.jl), и мне просто приходилось продолжать реализовывать функции интерфейса, пока он не перестал вызывать ошибки, чтобы заставить его работать.

Ваше предложение сделало бы то же самое, не так ли? Но не могли бы вы реализовать весь интерфейс?

Как это работает с ковариантными / контравариантными параметрами?

Например,

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

type B <: A 
    ...
end

type C <: A
    ...
end

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

@IainNZ Да, предложение на самом деле состоит в том, чтобы сделать @mustimplement немного более универсальным, например, чтобы подпись могла, но не должна быть предоставлена. И мне кажется, что это такое «ядро», что стоит обзавестись собственным синтаксисом. Было бы здорово обеспечить, чтобы все методы действительно были реализованы, но текущая проверка времени выполнения, как это сделано в @mustimplement , уже является отличной вещью, и ее может быть проще реализовать.

@lindahua Это интересный пример. Надо подумать об этом.

@lindahua Возможно, вы захотите, чтобы ваш пример просто работал. @mustimplement не будет работать, поскольку он определяет более конкретные сигнатуры методов.

Так что это, возможно, придется реализовать в компиляторе чуть глубже. При определении абстрактного типа нужно отслеживать имена / подписи интерфейсов. И в тот момент, когда в настоящее время выдается ошибка «... не определено», необходимо сгенерировать соответствующее сообщение об ошибке.

Очень легко изменить способ печати MethodError , когда у нас есть синтаксис и API для выражения и доступа к информации.

Еще одна вещь, которую мы можем получить, - это функция в base.Test для проверки того, что тип (все типы?) Полностью реализует интерфейсы родительских типов. Это был бы действительно изящный модульный тест.

Спасибо @ivarne. Таким образом, реализация могла бы выглядеть так:

  1. У одного есть глобальный словарь с абстрактными типами в качестве ключей и функциями (+ необязательные подписи) в качестве значений.
  2. Синтаксический анализатор необходимо адаптировать для заполнения словаря при разборе объявления has .
  3. MethodError необходимо найти, является ли текущая функция частью глобального словаря.

Тогда большая часть логики будет в MethodError .

Я немного экспериментировал с этим и, используя следующую суть https://gist.github.com/tknopp/ed53dc22b61062a2b283, могу сделать:

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

при определении length ошибки не возникает:

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

Не то чтобы это в настоящее время не учитывает подпись.

Я немного обновил код по сути, чтобы можно было учесть сигнатуры функций. Он все еще очень хакерский, но теперь работает следующее:

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

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

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

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

Я должен был добавить, что кеш интерфейса в сущности теперь работает с символами, а не с функциями, чтобы можно было добавить интерфейс и впоследствии объявить функцию. Возможно, мне придется проделать то же самое с подписью.

Только что увидел, что в # 2248 уже есть материал по интерфейсам.

Я собирался отложить публикацию мыслей о более спекулятивных функциях, таких как интерфейсы, до тех пор, пока мы не выпустим 0.3, но, поскольку вы начали обсуждение, вот кое-что, что я написал некоторое время назад.


Вот макет синтаксиса для объявления интерфейса и его реализации:

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

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

Разобьем это на части. Во-первых, есть синтаксис типа функции: A --> B - это тип функции, которая сопоставляет объекты типа A с типом B . Кортежи в этой нотации делают очевидную вещь. В отдельности я предлагаю, чтобы f :: A --> B объявлял, что f является универсальной функцией, отображая тип A на тип B . Что это означает - вопрос немного открытый. Означает ли это, что при применении к аргументу типа A , f даст результат типа B ? Означает ли это, что f можно применять только к аргументам типа A ? Должно ли автоматическое преобразование происходить где угодно - на выходе, на входе? На данный момент мы можем предположить, что все, что это делает, - это создание новой универсальной функции без добавления к ней каких-либо методов, а типы предназначены только для документации.

Во-вторых, это объявление интерфейса Iterable{T,S} . Это делает Iterable немного похожим на модуль и немного на абстрактный тип. Это похоже на модуль в том смысле, что у него есть привязки к универсальным функциям, называемым Iterable.start , Iterable.done и Iterable.next . Это похоже на тип в том, что Iterable и Iterable{T} и Iterable{T,S} можно использовать везде, где это возможно, абстрактные типы - в частности, в диспетчере методов.

В-третьих, есть блок implement определяющий, как UnitRange реализует интерфейс Iterable . Внутри блока implement доступны функции Iterable.start , Iterable.done и Iterable.next , как если бы пользователь выполнил import Iterable: start, done, next , позволяя добавление методов к этим функциям. Этот блок подобен шаблону, как и объявления параметрического типа - внутри блока UnitRange означает конкретный UnitRange , а не зонтичный тип.

Основное преимущество блока implement заключается в том, что он позволяет избежать необходимости явно расширять функции import которые вы хотите расширить - они неявно импортируются для вас, что приятно, поскольку люди обычно не понимают import равно Base которые пользователи захотят расширить, должны принадлежать какому-либо интерфейсу, поэтому это должно исключить подавляющее большинство случаев использования import . Поскольку вы всегда можете полностью указать имя, возможно, мы сможем полностью от него отказаться.

Еще одна идея, которая у меня возникла, - это разделение «внутренней» и «внешней» версий интерфейсных функций. Под этим я подразумеваю, что «внутренняя» функция - это функция, для которой вы предоставляете методы для реализации некоторого интерфейса, в то время как «внешняя» функция - это функция, которую вы вызываете для реализации общих функций в терминах некоторого интерфейса. При рассмотрении методов функции sort! (исключая устаревшие методы) подумайте:

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

Некоторые из этих методов предназначены для публичного использования, но другие являются лишь частью внутренней реализации общедоступных методов сортировки. На самом деле, единственный общедоступный метод, который должен иметь это:

sort!(v::AbstractArray)

Остальное - это шум и принадлежит «внутренности». В частности,

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

виды методов - это то, что реализует алгоритм сортировки, чтобы подключиться к общему механизму сортировки. В настоящее время Sort.Algorithm - это абстрактный тип, а InsertionSortAlg , QuickSortAlg и MergeSortAlg - его конкретные подтипы. С интерфейсами Sort.Algorithm может быть интерфейсом, и конкретные алгоритмы будут его реализовывать. Что-то вроде этого:

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

Желаемое разделение может быть достигнуто путем определения:

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

Это _ очень_ близко к тому, что мы делаем в настоящее время, за исключением того, что мы вызываем Algorithm.sort! вместо просто sort! - и при реализации различных алгоритмов сортировки "внутреннее" определение - это метод Algorithm.sort! не функция sort! . Это имеет эффект отделения реализации sort! от его внешнего интерфейса.

@StefanKarpinski Большое спасибо за вашу рецензию! Это уж точно не 0.3. Мне очень жаль, что я поднял этот вопрос сейчас. Я просто не уверен, скоро ли будет 0.3 или через полгода ;-)

С первого взгляда мне действительно (!) Нравится, что секция реализации определяется своим собственным блоком кода. Это позволяет напрямую проверить интерфейс по определению типа.

Не беспокойтесь - на самом деле нет ничего плохого в предположениях о будущих функциях, пока мы пытаемся стабилизировать выпуск.

Ваш подход намного более фундаментален и также пытается решить некоторые проблемы, не зависящие от интерфейса. Это также как бы привносит в язык новую конструкцию (т.е. интерфейс), которая делает язык немного более сложным (что не обязательно плохо).

Я рассматриваю «интерфейс» больше как аннотацию к абстрактным типам. Если поместить туда has можно указать интерфейс, но это не обязательно.

Как я уже сказал, мне бы очень хотелось, чтобы интерфейс можно было напрямую проверять по его объявлению. Наименее инвазивный подход здесь может заключаться в том, чтобы разрешить определение методов внутри объявления типа. Итак, возьмем ваш пример что-то вроде

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

По-прежнему можно будет определять функцию вне объявления типа. Единственное отличие будет заключаться в том, что объявления внутренних функций проверяются по интерфейсам.

Но опять же, возможно, мой «наименее инвазивный подход» слишком близорук. На самом деле не знаю.

Одна из проблем с размещением этого определения внутри блока типа заключается в том, что для этого нам действительно понадобится как минимум множественное наследование интерфейсов, и понятно, что между разными интерфейсами могут возникать конфликты имен. Вы также можете добавить тот факт, что тип поддерживает интерфейс в какой-то момент _ после_ определения типа, хотя я в этом не уверен.

@StefanKarpinski Приятно видеть, что вы думаете об этом.

Пакет Graphs больше всего нуждается в интерфейсной системе. Было бы интересно посмотреть, как эта система может выражать интерфейсы, описанные здесь: http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : Я не совсем понимаю проблему с множественным наследованием и объявлениями

Но я как бы понимаю, что можно позволить реализации интерфейса «открыться». А объявление функции внутри типа может слишком усложнить язык. Возможно, подход, который я реализовал в # 7025, достаточен. Либо поместите verify_interface после объявления функции (или в модульном тесте), либо отложите его до MethodError .

Эта проблема заключается в том, что разные интерфейсы могут иметь универсальную функцию с одним и тем же именем, что может вызвать конфликт имен и потребовать явного импорта или добавления методов по полностью определенному имени. Это также делает менее ясным, какие определения методов каким интерфейсам принадлежат - именно поэтому в первую очередь может произойти конфликт имен.

Кстати, я согласен с тем, что добавление интерфейсов как еще одной "вещи" в языке кажется слишком неортогональным. В конце концов, как я уже упоминал в предложении, они немного похожи на модули и немного на типы. Такое ощущение, что некоторая унификация концепций возможна, но я не понимаю, как это сделать.

Я предпочитаю модель интерфейса как библиотеки модели интерфейса как языка по нескольким причинам: она упрощает язык (по общему признанию, предпочтение, а не конкретное возражение), и это означает, что функция остается необязательной и может быть легко реализована. улучшены или полностью заменены без использования фактического языка.

В частности, я думаю, что предложение (или, по крайней мере, форма предложения) от @tknopp лучше, чем от @StefanKarpinski - оно обеспечивает проверку времени определения, не требуя ничего нового в языке. Главный недостаток, который я вижу, - это отсутствие возможности работать с переменными типа; Я думаю, что с этим можно справиться, указав в определении интерфейса тип _predicates_ для типов требуемых функций.

Одним из основных мотивов моего предложения является большая путаница, вызванная необходимостью _импортировать_ общие функции, но не экспортировать их, чтобы добавить к ним методы. В большинстве случаев это происходит, когда кто-то пытается реализовать неофициальный интерфейс, поэтому создается впечатление, что это происходит.

Это кажется ортогональной проблемой, которую необходимо решить, если только вы не хотите полностью ограничить методы принадлежностью к интерфейсам.

Нет, это определенно не кажется хорошим ограничением.

@StefanKarpinski, вы упомянули, что можете отправлять сообщения через интерфейс. Также в синтаксисе implement идея состоит в том, что определенный тип реализует интерфейс.

Это кажется немного противоречащим множественной отправке, поскольку в целом методы не принадлежат к определенному типу, они принадлежат к кортежу типов. Итак, если методы не принадлежат типам, как могут интерфейсы (которые в основном представляют собой наборы методов) принадлежать типу?

Скажем, я использую библиотеку M:

module M

abstract A
abstract B

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

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

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

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

теперь я хочу написать общую функцию, которая принимает A и B

using M

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

В этом примере функция f формирует специальный интерфейс, который принимает A и B , и я хочу предположить, что могу вызвать f функция на них. В этом случае неясно, какой из них следует рассматривать для реализации интерфейса.

Ожидается, что другие модули, которые хотят предоставить конкретные подтипы A и B будут предоставлять реализации f . Чтобы избежать комбинаторного взрыва требуемых методов, я ожидаю, что библиотека определит f для абстрактных типов:

module N

using M

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

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

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

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

export SpecialA, SpecialB

end # module N

По общему признанию, этот пример кажется довольно надуманным, но, надеюсь, он иллюстрирует, что (по крайней мере, на мой взгляд) кажется, что существует фундаментальное несоответствие между множественной отправкой и концепцией конкретного типа, реализующего интерфейс.

Я все же понимаю вашу точку зрения о путанице с import . Мне потребовалось несколько попыток в этом примере, чтобы вспомнить, что когда я поместил using M а затем попытался добавить методы в f он не сделал того, что я ожидал, и мне пришлось добавить методы на M.f (или я мог бы использовать import ). Я не думаю, что интерфейсы могут решить эту проблему. Есть ли отдельная проблема для мозгового штурма, чтобы сделать добавление методов более интуитивно понятным?

@ abe-egnor Я также считаю, что более возможен более открытый подход. В моем прототипе №7025, по сути, не хватает двух вещей:
а) лучший синтаксис для определения интерфейсов
б) определения параметрического типа

Поскольку я не столько гуру параметрического типа, то уверен, что решение б) может решить кто-то с более глубоким опытом.
Что касается а), можно использовать макрос. Лично я думаю, что мы могли бы потратить некоторую языковую поддержку для прямого определения интерфейса как части определения абстрактного типа. Подход has может быть слишком близоруким. Блок кода может сделать это лучше. На самом деле это очень связано с # 4935, где определен «внутренний» интерфейс, в то время как это касается открытого интерфейса. Их не нужно связывать, так как я думаю, что эта проблема гораздо важнее, чем # 4935. Но, тем не менее, с точки зрения синтаксиса можно принять во внимание оба варианта использования.

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 - это мой первый удар по реализации, о которой я думал. Короче говоря, интерфейс - это функция от типов к dict, которая определяет имя и типы параметров требуемых функций для этого интерфейса. Макрос @implement просто вызывает функцию для заданных типов, а затем вставляет типы в данные определения функций, проверяя, что все функции были определены.

Хорошие моменты:

  • Простой синтаксис для определения и реализации интерфейсов.
  • Ортогонален другим языковым функциям, но прекрасно сочетается с ними.
  • Вычисление типа интерфейса может быть произвольно сложным (это просто функции над параметрами типа интерфейса)

Плохие очки:

  • Не работает с параметризованными типами, если вы хотите использовать параметр в качестве типа интерфейса. Это довольно существенный недостаток, но я не вижу немедленного способа его решения.

Я думаю, что у меня есть решение проблемы параметризации - короче говоря, определение интерфейса должно быть макросом над выражениями типа, а не функцией над значениями типа. Макрос @implement затем может расширить параметры типа до определений функций, допуская что-то вроде:

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

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

В этом случае параметры типа расширяются до методов, определенных в интерфейсе, поэтому он расширяется до stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , что я считаю правильным.

Я переделаю свою первоначальную реализацию, чтобы сделать это, когда у меня будет время; наверное порядка недели.

Я немного просмотрел Интернет, чтобы узнать, что другие языки программирования делают с интерфейсами, наследованием и т. Д., И придумал несколько идей. (На случай, если кому-то интересно, я сделал очень приблизительные заметки https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

Короче говоря, интерфейсы могут быть реализованы с помощью:

  • разрешение множественного наследования для абстрактных типов и
  • разрешая универсальные функции как поля абстрактных типов.

Это превратит абстрактные типы в интерфейсы, и тогда для реализации этого интерфейса потребуются конкретные подтипы.

Длинная история:

Я обнаружил, что некоторые из «современных» языков избавляются от полиморфизма подтипов, т.е. нет прямого группирования типов, и вместо этого они группируют свои типы на основе их принадлежности к интерфейсам / чертам / классам типов. В некоторых языках классы интерфейсов / черт / типов могут иметь порядок между собой и наследовать друг от друга. Они также кажутся (в основном) счастливыми по поводу этого выбора. Примеры: Go ,
Ржавчина , Haskell .
Go является наименее строгим из трех и позволяет указывать его интерфейсы неявно, т. Е. Если тип реализует определенный набор функций интерфейса, то он принадлежит этому интерфейсу. Для Rust интерфейс (трейты) должен быть явно реализован в блоке impl . Ни в Go, ни в Rust нет мультиметодов. В Haskell есть мультиметоды, и они фактически напрямую связаны с интерфейсом (классом типа).

В некотором смысле это похоже на то, что делает и Джулия, абстрактные типы похожи на (неявный) интерфейс, то есть они касаются поведения, а не полей. Это то, что @StefanKarpinski также заметил в одном из своих постов выше и заявил, что наличие дополнительных интерфейсов «кажется слишком неортогональным». Итак, у Джулии есть иерархия типов (т.е. полиморфизм подтипов), тогда как в Go / Rust / Haskell ее нет.

Как насчет того, чтобы превратить абстрактные типы Джулии в нечто большее, чем класс интерфейса / черты / типа, сохранив при этом все типы в иерархии None<: ... <:Any ? Это повлечет за собой:
1) разрешить множественное наследование для (абстрактных) типов (проблема # 5)
2) разрешить связывать функции с абстрактными типами (т.е. определить интерфейс)
3) Разрешить указывать этот интерфейс как для абстрактных (т.е. реализация по умолчанию), так и для конкретных типов.

Я думаю, что это могло бы привести к более мелкозернистому графу типов, чем у нас сейчас, и могло бы быть реализовано шаг за шагом. Например, массив-тип будет собран вместе:

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

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

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

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

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

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

Таким образом, в основном абстрактные типы могут иметь общие функции как поля (т. Е. Становиться интерфейсом), тогда как конкретные типы имеют только обычные поля. Это может, например, решить проблему слишком большого количества вещей, производных от AbstractArray, поскольку люди могут просто выбирать полезные части для своего контейнера, а не наследовать от AbstractArray.

Если это вообще хорошая идея, есть еще много над чем поработать (в частности, как указать типы и параметры типа), но, может быть, стоит подумать?

@ssfrr прокомментировал выше, что интерфейсы и множественная отправка несовместимы. Такого быть не должно, поскольку, например, в Haskell мультиметоды возможны только с использованием классов типов.

Я также обнаружил, читая статью @StefanKarpinski , что использование abstract напрямую вместо interface может иметь смысл. Однако в этом случае важно, чтобы abstract наследует одно важное свойство interface : возможность определения типа implement и interface _after_. Затем я могу использовать тип typA из библиотеки A с алгоритмом algoB из библиотеки B, объявив в моем коде, что typA реализует интерфейс, требуемый algoB (я предполагаю, что это означает, что конкретные типы имеют своего рода открытое множественное наследование).

@ mauro3 , мне действительно нравится ваше предложение. Мне это кажется очень "юлианским" и естественным. Я также думаю, что это уникальная и мощная интеграция интерфейсов, множественного наследования и «полей» абстрактного типа (хотя, на самом деле, не совсем, поскольку поля будут только методами / функциями, а не значениями). Я также думаю, что это хорошо сочетается с идеей sort! , объявив abstract Algorithm и Algorithm.sort! .

извините всех

------------------ 原始 邮件 ------------------
发件人: «Джейкоб Куинн» [email protected];
Дата: 2014 год, 9 декабря, 12 месяцев () 6:23
收件人: "JuliaLang / julia" [email protected];
抄送: « Орудие »
主题: Re: [julia] Интерфейсы для абстрактных типов (# 6975)

@ mauro3 , мне действительно нравится ваше предложение. Мне это кажется очень "юлианским" и естественным. Я также думаю, что это уникальная и мощная интеграция интерфейсов, множественного наследования и «полей» абстрактного типа (хотя, на самом деле, не совсем, поскольку поля будут только методами / функциями, а не значениями). Я также думаю, что это хорошо сочетается с идеей

-
Ответьте на это письмо напрямую или просмотрите его на GitHub.

@implement Очень жаль; не знаю, как мы вас связали. Если вы еще не знали, вы можете удалить себя из этих уведомлений, используя кнопку «Отказаться от подписки» в правой части экрана.

Нет, я просто хочу сказать, что не могу слишком сильно тебе помочь, чтобы сказать сарри

------------------ 原始 邮件 ------------------
发件人: "pao" [email protected];
Дата: 2014 год, 9 декабря, 13 месяцев (), 9:50
收件人: "JuliaLang / julia" [email protected];
抄送: « Орудие »
主题: Re: [julia] Интерфейсы для абстрактных типов (# 6975)

@implement Очень жаль; не знаю, как мы вас связали. Если вы еще не знали, вы можете удалить себя из этих уведомлений, используя кнопку «Отказаться от подписки» в правой части экрана.

-
Ответьте на это письмо напрямую или просмотрите его на GitHub.

Мы этого не ждем! Это была случайность, поскольку мы говорим о макросе Julia с тем же именем, что и ваше имя пользователя. Спасибо!

Я только что увидел, что в Rust работают некоторые потенциально интересные функции (возможно, имеющие отношение к этой проблеме): http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , в частности: https : //github.com/rust-lang/rfcs/pull/195

После просмотра THTT («Трюк Тима Holy Trait») я задумался над интерфейсами / чертами в последние несколько недель. Я придумал несколько идей и их реализацию: Traits.jl . Во-первых, (я думаю) черты следует рассматривать как контракт, включающий один или несколько типов . Это означает, что просто прикрепление функций интерфейса к одному абстрактному типу, как я и другие предложили выше, не работает (по крайней мере, не в общем случае признака, включающего несколько типов). И, во-вторых, методы должны иметь возможность использовать черты для отправки , как предложил @StefanKarpinski выше.

Нафф сказал, вот пример использования моего пакета Traits.jl:

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

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

Это объявляет, что Eq и Cmp являются контрактами между типами X и Y . Cmp имеет Eq в качестве суперпризнака, т.е. должны быть выполнены как Eq и Cmp . В теле @traitdef сигнатуры функций указывают, какие методы необходимо определить. В настоящий момент возвращаемые типы ничего не делают. Типы не должны явно реализовывать трейт, достаточно просто реализовать функции. Я могу проверить, действительно ли Cmp{Int,Float64} является признаком:

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

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

Явная реализация трейта еще не включена в пакет, но ее довольно просто добавить.

Функция, использующая _trait-dispatch_, может быть определена так

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

Это объявляет функцию ft1 которая принимает два аргумента с ограничением, которое их типы должны выполнять для выполнения Cmp{X,Y} . Я могу добавить еще один метод диспетчеризации по другому признаку:

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

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

Эти функции-черты теперь можно вызывать как обычные функции:

julia> ft1(4,5)
6

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

Позже добавить к трейту другой тип несложно (чего не было бы при использовании Unions для 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

_Реализация_ функций-признаков и их отправка основана на уловке Тима и на поэтапных функциях, см. Ниже. Определение черты относительно тривиально, см. Здесь ручную реализацию всего этого.

Короче говоря, отправка черт превращается

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

во что-то вроде этого (немного упрощено)

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

В пакете генерация checkfn автоматизирована поэтапными функциями. Но для получения более подробной информации см. README на Traits.jl.

_Performance_ Для простых функций-признаков созданный машинный код идентичен их аналогам с утиным типом, то есть настолько хорош, насколько это возможно. Для более длинных функций есть различия, до ~ 20% длины. Я не уверен, почему, как я думал, все это должно быть встроено.

(отредактировано 27 октября, чтобы отразить незначительные изменения в Traits.jl )

Готов ли пакет Traits.jl к изучению? В readme говорится: «реализовать интерфейсы с @traitimpl (еще не сделано ...)» - это важный недостаток?

Он готов к изучению (включая ошибки :-). Отсутствие @traitimpl просто означает, что вместо

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

вы просто определяете функции вручную

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

для двух из ваших типов T1 и T2 .

Я добавил макрос @traitimpl , так что теперь пример выше работает. Я также обновил README подробностями об использовании. И я добавил пример реализации части интерфейса @lindahua Graphs.jl:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

Это действительно круто. Мне особенно нравится, что он признает, что интерфейсы в целом являются свойством кортежей типов, а не отдельных типов.

Я тоже считаю это очень крутым. В этом подходе есть что нравится. Хорошо сделано.

: +1:

Спасибо за хороший отзыв! Я немного обновил / реорганизовал код, и он должен быть достаточно безошибочным и подходящим для экспериментов.
На этом этапе, вероятно, было бы хорошо, если бы люди могли попробовать это, чтобы увидеть, соответствует ли он их вариантам использования.

Это один из тех пакетов, которые заставляют по-новому взглянуть на собственный код. Очень круто.

Извините, у меня еще не было времени серьезно рассмотреть это, но я знаю, что как только я это сделаю, я захочу реорганизовать некоторые вещи ...

Я тоже реорганизую свои пакеты :)

Мне было интересно, мне кажется, что если черты доступны (и допускают множественную отправку, как в предложении выше), тогда нет необходимости в механизме иерархии абстрактных типов или абстрактных типах вообще. Могло ли это быть?

После того, как черты будут реализованы, каждая функция в базе, а затем и во всей экосистеме в конечном итоге представит общедоступный API, основанный исключительно на чертах, и абстрактные типы исчезнут. Конечно, этот процесс можно ускорить, отказавшись от абстрактных типов.

Если подумать об этом немного подробнее, замена абстрактных типов на черты потребует параметризации таких типов:

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

Я согласен с пунктом mauro3 выше, у которого есть черты (по его определению, которое я считаю очень хорошим) эквивалентно абстрактным типам, которые

  • разрешить множественное наследование и
  • разрешить общие функции как поля

Я бы также добавил, что для того, чтобы позволить присваивать черты типам после их определения, необходимо также разрешить «ленивое наследование», то есть сообщить компилятору, что тип наследуется от некоторого абстрактного типа после того, как он был определен.

так что в целом мне кажется, что разработка некоторой концепции черты / интерфейса вне абстрактных типов вызовет некоторое дублирование, вводя разные способы достижения того же самого. Теперь я думаю, что лучший способ представить эти концепции - это постепенно добавлять функции к абстрактным типам.

РЕДАКТИРОВАТЬ : конечно, в какой-то момент наследование конкретных типов от абстрактных должно быть устарело и, наконец, запрещено. Признаки типа будут определяться неявно или явно, но никогда не наследованием.

Разве абстрактные типы не являются просто «скучным» примером черт характера?

Если да, можно ли сохранить текущий синтаксис и просто изменить его значение на свойство (предоставление ортогональной свободы и т. Д., Если пользователь этого хочет)?

_Мне интересно, может ли это также помочь в примере Point{Float64} <: Pointy{Real} (не уверен, есть ли номер проблемы)? _

Да, думаю, вы правы. Функциональность черт может быть достигнута путем улучшения текущих абстрактных типов julia. Им нужно
1) множественное наследование
2) сигнатуры функций
3) "ленивое наследование", чтобы явно дать уже определенному типу новую черту

Похоже, много работы, но, возможно, это можно будет вырастить медленно, без особых проблем для сообщества. Так что, по крайней мере, мы получили это;)

Я думаю, что бы мы ни выбрали, это будет большим изменением, над которым мы не готовы начать работу в версии 0.4. Если бы мне пришлось угадывать, я бы поспорил, что у нас больше шансов двигаться в направлении черт, чем в направлении добавления традиционного множественного наследования. Но мой хрустальный шар в ужасе, так что трудно быть уверенным в том, что произойдет, просто не попробовав что-нибудь.

FWIW, я нашел обсуждение Саймоном Пейтон-Джонсом классов типов в приведенном ниже докладе действительно информативным о том, как использовать что-то вроде черт вместо подтипов: http://research.microsoft.com/en-us/um/people/simonpj/ документы / haskell-retrospective / ECOOP-July09.pdf

Ага, целая банка червей!

@johnmyleswhite , спасибо за ссылку, очень интересно. Вот ссылка на видео, которое стоит посмотреть, чтобы заполнить пробелы. Эта презентация, кажется, затрагивает множество вопросов, которые у нас возникли. И что интересно, реализация классов типов очень похожа на то, что находится в Traits.jl (трюк Тима, черты являются типами данных). Https://www.haskell.org/haskellwiki/Multi-parameter_type_class в Haskell очень похож на Traits.jl. Один из его вопросов в разговоре: «После того, как мы всем сердцем приняли дженерики, действительно ли нам нужно подтипирование». (дженерики - это параметрически-полиморфные функции, я думаю, понимаете ) Это вроде того, о чем @skariel и @hayd размышляли выше.

Что касается @skariel и @hayd , я думаю, что характеристики с одним параметром (как в Traits.jl) действительно очень близки к абстрактным типам, за исключением того, что они могут иметь другую иерархию, то есть множественное наследование.

Но многопараметрические черты кажутся немного другими, по крайней мере, на мой взгляд. Как я их видел, параметры типа абстрактных типов, по-видимому, в основном связаны с тем, какие другие типы содержатся в типе, например, Associative{Int,String} говорит, что dict содержит ключи Int и String values. В то время как Tr{Associative,Int,String}... говорит, что существует некий «контракт» между Associative , Int s и Strings . Но тогда, возможно, Associative{Int,String} тоже следует читать так, т.е. есть такие методы, как getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@ mauro3 Важно передать объекты типа Associative качестве аргумента функции, чтобы затем она могла создать саму Associative{Int,String} :

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

Вы могли бы назвать это, например, как f(Dict) .

@eschnett , извини, я не понимаю о чем ты.

@ mauro3 Я думаю, что думал слишком сложно; игнорируй меня.

Я обновил Traits.jl:

  • разрешение неоднозначности черт
  • связанные типы
  • используя @doc для помощи
  • лучшее тестирование методов определения характеристик

Подробнее см. Https://github.com/mauro3/Traits.jl/blob/master/NEWS.md . Обратная связь приветствуется!

@ Rory-Finnegan собрал пакет интерфейса https://github.com/Rory-Finnegan/Interfaces.jl

Я недавно обсуждал это с @mdcfrancis, и мы думаем, что что-то похожее на протоколы Clojure было бы простым и практичным. Основные функции: (1) протоколы представляют собой новый тип типа, (2) вы определяете их, перечисляя некоторые сигнатуры методов, (3) другие типы реализуют их неявно, просто имея соответствующие определения методов. Вы бы написали, например,

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

и у нас есть isa(Iterable, Protocol) и Protocol <: Type . Естественно, вы можете отправить их. Вы можете проверить, реализует ли тип протокол, используя T <: Iterable .

Вот правила выделения подтипов:

пусть P, Q - типы протоколов
пусть T будет непротокольным типом

| ввод | результат |
| --- | --- |
| P <: Любые | правда |
| Внизу <: P | правда |
| (union, unionall, var) <: P | используйте обычное правило; рассматривать P как базовый тип |
| P <: (объединение, объединение всех, var) | использовать обычное правило |
| P <: P | правда |
| P <: Q | методы проверки (Q) <: методы (P) |
| P <: T | ложь |
| T <: P | Методы P существуют с заменой T на _ |

Последний - самый большой: чтобы проверить T <: P, вы заменяете T на _ в определении P и проверяете method_exists для каждой подписи. Конечно, само по себе это означает, что определения резервных копий, которые выдают ошибку «вы должны реализовать это», становятся очень плохим явлением. Надеюсь, это скорее косметическая проблема.

Другая проблема заключается в том, что это определение является циклическим, если, например, определено start(::Iterable) . Такое определение не имеет смысла. Мы могли бы как-то предотвратить это или обнаружить этот цикл во время проверки подтипа. Я не уверен на 100%, что простое обнаружение цикла исправит это, но это кажется правдоподобным.

Для пересечения типов имеем:

| ввод | результат |
| --- | --- |
| P ∩ (объединение, объединение всех, тварь) | использовать обычное правило |
| P ∩ Q | P |
| P ∩ T | Т |

Есть несколько вариантов для P ∩ Q:

  1. Завышенное приближение, возвращая P или Q (например, в зависимости от того, что лексикографически первым). Это звучит в отношении вывода типов, но может раздражать в другом месте.
  2. Вернуть новый специальный протокол, содержащий объединение подписей в P и Q.
  3. Типы пересечений. Возможно, только для протоколов.

P ∩ T сложно. T - хорошее консервативное приближение, поскольку типы, не являющиеся протоколами, «меньше», чем типы протоколов в том смысле, что они ограничивают вас одной областью иерархии типов, а типы протоколов - нет (поскольку любой тип вообще может реализовать любой протокол ). Для достижения большего, чем это, кажется, требуются общие типы пересечений, которых я бы предпочел избежать в первоначальной реализации, поскольку это требует пересмотра алгоритма подтипов и открывает червячную банку после червячной банки.

Специфичность: P более специфичен, чем Q, только когда P <: Q. но поскольку P ∩ Q всегда непусто, определения с разными протоколами в одном и том же слоте часто неоднозначны, что похоже на то, что вам нужно (например, вы бы сказали: «если x является Iterable, сделайте это, но если x - Printable, сделайте тот").
Однако нет удобного способа выразить требуемое определение, устраняющее неоднозначность, так что это может быть ошибкой.

После # 13412 протокол может быть «закодирован» как UnionAll _ по объединению типов кортежей (где первый элемент каждого внутреннего кортежа является типом рассматриваемой функции). Это преимущество того дизайна, которое раньше мне не приходило в голову. Например, структурные подтипы протоколов просто выпадают автоматически.

Конечно, эти протоколы выполнены в стиле «одного параметра». Мне нравится простота этого, к тому же я не уверен, как обрабатывать группы типов так же элегантно, как T <: Iterable .

В прошлом по этой идее было несколько комментариев, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516.

Поддержим ли мы, например,

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

Вау, мне это очень нравится (особенно с расширением @Keno )!

+1 Это именно то, что я хочу!

@Keno Это определенно хороший

Похоже, вы могли бы включить в эту схему черты (например, линейную индексацию O (1) для типов, подобных массиву). Вы должны определить фиктивный метод, например hassomeproperty(::T) = true (но _not_ hassomeproperty(::Any) = false ), а затем иметь

protocol MyProperty
hassomeproperty(::_)
end

Может ли _ появляться несколько раз в одном методе в определении протокола, например

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

Может _ появляться несколько раз в одном методе в определении протокола

да. Вы просто вставляете тип кандидата для каждого экземпляра _ .

@JeffBezanson очень этого жду. Особо следует отметить «удаленность» протокола. Таким образом, я могу реализовать конкретный / настраиваемый протокол для типа, при этом автор типа не знает о существовании протокола.

А как насчет того факта, что методы могут быть определены динамически (например, с помощью @eval ) в любое время? Тогда, является ли тип подтипом данного протокола, статически невозможно узнать в целом, что, казалось бы, препятствует оптимизации, избегающей динамической отправки во многих случаях.

Да, это делает # 265 хуже :) Это та же проблема, при которой диспетчерский и сгенерированный код должны изменяться при добавлении методов, только с большим количеством границ зависимости.

Приятно видеть это продвижение! Конечно, я буду первым, кто утверждал, что многопараметрические черты - это путь вперед. Но в любом случае 95% признаков, вероятно, будут одним параметром. Просто они так отлично подошли бы при многократной рассылке! При необходимости, возможно, можно будет вернуться к этому позже. Достаточно сказано.

Пара комментариев:

Предложение @Keno (и на самом деле state в оригинале Джеффа) известно как связанные типы. Обратите внимание, что они также полезны без возвращаемых типов. В Rust есть приличный ручной ввод . Я считаю, что это хорошая идея, хотя и не такая необходимая, как в Rust. Я не думаю, что это должен быть параметр признака: при определении диспетчеризации функции на Iterable я бы не знал, что такое T .

По моему опыту, method_exists в его текущей форме для этого непригоден (# 8959). Но, по-видимому, это будет исправлено в # 8974 (или с этим). Я обнаружил, что сопоставление сигнатур методов с сигнатурами признаков является самой сложной частью при работе с Traits.jl, особенно для учета параметризованных функций & vararg ( см . Раздел "Ресурсы").

Предположительно наследование тоже возможно?

Мне бы очень хотелось увидеть механизм, позволяющий определять реализации по умолчанию. Классический состоит в том, что для сравнения вам нужно определить только два из = , < , > , <= , >= . Может быть, здесь действительно пригодится цикл, упомянутый Джеффом. Продолжая приведенный выше пример, определение start(::Indexable) = 1 и done(i::Indexable,state)=length(i)==state сделает их значениями по умолчанию. Таким образом, для многих типов потребуется только определить next .

Хорошие моменты. Я думаю, что связанные типы несколько отличаются от параметра в Iterable{T} . В моей кодировке параметр просто экзистенциально оценивает все внутри --- «существует ли T, такой, что тип Foo реализует этот протокол?».

Да, похоже, мы могли бы легко разрешить protocol Foo <: Bar, Baz и просто скопировать подписи из Bar и Baz в Foo.

Многопараметрические черты определенно сильны. Думаю, очень интересно подумать о том, как объединить их с подтипами. У вас может быть что-то вроде TypePair{A,B} <: Trait , но это не совсем так.

Я думаю, что ваше предложение (с точки зрения возможностей) на самом деле больше похоже на Swift, чем на Clojure.

Кажется странным (и я думаю, что это источник путаницы в будущем) смешивать номинальные (типы) и структурные (протокол) подтипы (но я думаю, что это неизбежно).

Я также немного скептически отношусь к выразительной силе протоколов для математических / матричных операций. Я думаю, что обдумывание более сложных примеров (матричные операции) было бы более поучительным, чем Iteration с четко определенным интерфейсом. См., Например, библиотеку core.matrix .

Я согласен; на этом этапе мы должны собрать примеры протоколов и посмотреть, делают ли они то, что мы хотим.

Как вы это себе представляете, будут ли протоколы пространствами имен, которым принадлежат их методы? Т.е. когда ты пишешь

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

казалось бы естественным для этого определить общие функции start , done и next а их полные имена должны быть Iterable.start , Iterable.done и Iterable.next . Тип будет реализовывать Iterable но реализовывать все общие функции в протоколе Iterable . Некоторое время назад я предложил нечто очень похожее на это (сейчас не могу найти), но с другой стороны, когда вы хотите реализовать протокол, вы делаете это:

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

Это нейтрализует "удаленность", о которой упоминал implement устранит почти все потребности в использовании import вместо using , что будет огромным преимуществом.

Некоторое время назад я предлагал нечто очень похожее на это (сейчас не могу найти)

Возможно, https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 и ранее https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Изменить: также https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

Ага, вот и все.

@StefanKarpinski быстрые комментарии,

  • все классы, которые в настоящее время реализуют итерацию, должны быть изменены для явной реализации протокола, если мы сделаем то, что вы предлагаете, текущее предложение, просто добавив определение в базу, «поднимет» все существующие классы в протокол.
  • если я определю MyModule.MySuperIterable, который добавляет дополнительную функцию к итерируемому определению, мне пришлось бы написать целую часть кода шаблона для каждого класса, а не добавлять один дополнительный метод.
  • Я не думаю, что то, что вы предлагаете, противодействует удаленности, это просто означает, что мне пришлось бы написать много дополнительного кода для достижения той же цели.

Если какое-то наследование протоколов было разрешено, MySuperIterabe,
может расширить Base.Iterable, чтобы повторно использовать существующие методы.

Проблема была бы в том, если бы вы хотели просто выбрать методы в
протокол, но это, казалось бы, указывает на то, что исходный протокол должен
быть составным протоколом с самого начала.

@mdcfrancis - первый пункт хороший, хотя то, что я предлагаю, не нарушит какой-либо существующий код, это просто будет означать, что код людей должен будет «выбрать» протоколы для своих типов, прежде чем они смогут рассчитывать на отправку работающий.

Можете ли вы расширить точку MyModule.MySuperIterable? Я не понимаю, откуда взялось такое лишнее многословие. У вас могло быть что-то вроде этого, например:

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

По сути, это то, что сказал @ivarne .

В моем конкретном проекте выше протоколы - это не пространства имен, а просто утверждения о других типах и функциях. Однако это, вероятно, связано с тем, что я сосредотачиваюсь на системе основных типов. Я мог представить себе синтаксический сахар, который расширяется до комбинации модулей и протоколов, например

module Iterable

function start end
function done end
function next end

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

end

Затем в контекстах, где Iterable рассматривается как тип, мы используем Iterable.the_protocol .

Мне нравится эта перспектива, потому что протоколы jeff / mdcfrancis кажутся очень ортогональными ко всему остальному здесь. Легкое ощущение отсутствия необходимости говорить «X реализует протокол Y», если только вы не хотите, чтобы я чувствовал себя «юлианским».

Не знаю, зачем и когда я подписался на этот выпуск. Но бывает, что это предложение протокола может решить вопрос, который я здесь поставил.

Мне нечего добавить по техническим причинам, но в качестве примера «протоколов», используемых в «дикой природе» в Джулии (своего рода), будет JuMP, определяющий функциональность решателя, например:

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

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

Круто, это полезно. Достаточно ли m.internalModel быть тем, что реализует протокол, или оба аргумента важны?

Да, m.internalModel для реализации протокола. Остальные аргументы - это в основном просто векторы.

Да, достаточно m.internalModel для реализации протокола

Хороший способ найти примеры протоколов в дикой природе - это, вероятно, поиск вызовов applicable и method_exists .

Elixir, похоже, также реализует протоколы, но количество протоколов в стандартной библиотеке (выходит за рамки определения) кажется довольно ограниченным.

Каковы будут отношения между протоколами и абстрактными типами? В исходном описании проблемы предлагалось что-то вроде присоединения протокола к абстрактному типу. В самом деле, мне кажется, что большинство (теперь неофициальных) протоколов в настоящее время реализованы как абстрактные типы. Для чего будут использоваться абстрактные типы при добавлении поддержки протоколов? Иерархия типов без какого-либо способа объявления своего API не кажется слишком полезной.

Очень хороший вопрос. Там вариантов очень много. Во-первых, важно отметить, что абстрактные типы и протоколы довольно ортогональны, даже если они оба являются способами группировки объектов. Абстрактные типы чисто номинальные; они помечают объекты как принадлежащие набору. Протоколы чисто структурные; объект принадлежит к множеству, если он обладает определенными свойствами. Итак, некоторые варианты

  1. Просто имейте и то, и другое.
  2. Уметь связывать протоколы с абстрактным типом, например, чтобы когда тип объявлял себя подтипом, он проверялся на соответствие протоколу (ам).
  3. Полностью удалите абстрактные типы.

Если у нас есть что-то вроде (2), я думаю, важно признать, что на самом деле это не отдельная функция, а комбинация номинальной и структурной типизации.

Одна вещь, в которой абстрактные типы кажутся полезными, - это их параметры, например, написание convert(AbstractArray{Int}, x) . Если бы AbstractArray был протоколом, тип элемента Int не обязательно указывать в определении протокола. Это дополнительная информация о типе, _ помимо_ каких методов требуются. Итак, AbstractArray{T} и AbstractArray{S} все равно будут разными типами, несмотря на то, что указаны одни и те же методы, поэтому мы вновь ввели номинальную типизацию. Таким образом, такое использование параметров типа, похоже, требует какой-то номинальной типизации.

Итак, 2. даст ли нам множественное абстрактное наследование?

Итак, 2. даст ли нам множественное абстрактное наследование?

Нет. Это был бы способ интеграции или комбинирования функций, но каждая функция по-прежнему будет иметь те свойства, которые она имеет сейчас.

Я должен добавить, что разрешение множественного абстрактного наследования - это еще одно почти ортогональное дизайнерское решение. В любом случае проблема с чрезмерным использованием абстрактных номинальных типов заключается в (1) вы можете потерять постфактум реализацию протоколов (человек A определяет тип, человек B определяет протокол и его реализацию для A), (2) вы можете потерять структурные подтипы протоколов.

Разве параметры типа в текущей системе не являются частью неявного интерфейса? Например, это определение основывается на этом: ndims{T,n}(::AbstractArray{T,n}) = n и многие пользовательские функции тоже.

Итак, в новой системе протокол + абстрактное наследование у нас будут AbstractArray{T,N} и ProtoAbstractArray . Теперь тип, который номинально не был AbstractArray , должен иметь возможность указать параметры T и N , предположительно посредством жесткого кодирования eltype и ndims . Затем все параметризованные функции в AbstractArray s необходимо будет переписать, чтобы вместо параметров использовались eltype и ndims . Так что, возможно, было бы разумнее, чтобы протокол также передавал параметры, поэтому связанные типы могут быть очень полезны в конце концов. (Обратите внимание, что для конкретных типов по-прежнему требуются параметры.)

Кроме того, группировка типов в протокол с использованием уловки @malmaud : https://github.com/JuliaLang/julia/issues/6975#issuecomment -161056795 сродни номинальной типизации: группировка происходит исключительно из-за выбора типов и типы не имеют (пригодного для использования) интерфейса. Так, может быть, абстрактные типы и протоколы действительно частично перекрываются?

Да, параметры абстрактного типа определенно являются своего рода интерфейсом и в некоторой степени избыточны с eltype и ndims . Основное отличие, по-видимому, заключается в том, что вы можете отправлять их напрямую, без дополнительного вызова метода. Я согласен с тем, что со связанными типами мы были бы намного ближе к замене абстрактных типов протоколами / признаками. Как может выглядеть синтаксис? В идеале это было бы слабее, чем вызов метода, поскольку я бы предпочел не иметь циклической зависимости между подтипом и вызовом метода.

Остается вопрос, полезно ли реализовать протокол, не становясь частью связанного абстрактного типа. Примером могут быть строки, которые можно повторять и индексировать, но часто обрабатывают как «скалярные» величины, а не контейнеры. Я не знаю, как часто это возникает.

Я не думаю, что полностью понимаю ваше утверждение о "вызове метода". Таким образом, это предложение по синтаксису может быть не тем, о чем вы просили:

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

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

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

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


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

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

Это может сработать, в зависимости от того, как определены подтипы типов протоколов. Например, учитывая

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

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

у нас есть PAbstractArray{Int,1} <: Indexable{Int} ? Я думаю, что это могло бы сработать очень хорошо, если бы параметры совпадали по имени. Мы могли бы также автоматизировать определение, которое заставляет eltype(x) возвращать параметр eltype типа x .

Мне не очень нравится помещать определения методов внутри блока impl , в основном потому, что одно определение метода может принадлежать нескольким протоколам.

Похоже, что с таким механизмом нам больше не понадобятся абстрактные типы. AbstractArray{T,N} может стать протоколом. Тогда мы автоматически получаем множественное наследование (протоколов). Кроме того, очевидна невозможность наследования от конкретных типов (что является жалобой, которую мы иногда слышим от новичков), поскольку будет поддерживаться только наследование протокола.

Кроме того: было бы действительно неплохо иметь возможность выражать черту Callable . Это должно было бы выглядеть примерно так:

protocol Callable
    ::TupleCons{_, Bottom}
end

где TupleCons отдельно соответствует первому элементу кортежа и остальным элементам. Идея состоит в том, что это соответствует, пока таблица методов для _ не пуста (нижний является подтипом каждого типа кортежа аргументов). Фактически, мы могли бы захотеть сделать синтаксис Tuple{a,b} для TupleCons{a, TupleCons{b, EmptyTuple}} (см. Также # 11242).

Я не думаю, что это правда, все параметры типа экзистенциально количественно определены _с ограничениями_, поэтому абстрактные типы и протоколы не могут быть заменены напрямую.

@jakebolewski можешь придумать пример? Очевидно, они никогда не будут одинаковыми; Я бы сказал, что вопрос больше в том, можем ли мы сделать массаж одного, чтобы обойтись без обоих.

Возможно, я упускаю суть, но как протоколы могут кодировать умеренно сложные абстрактные типы с такими ограничениями, как:

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

без необходимости номинально перечислять все возможности?

Предлагаемое предложение Protocol строго менее выразительно по сравнению с абстрактным подтипом, и это все, что я пытался выделить.

Я мог представить себе следующее (естественно, растягивая дизайн до практических пределов):

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

вместе с наблюдением, что нам нужно что-то вроде связанных типов или свойств именованных типов, чтобы соответствовать выразительности существующих абстрактных типов. При этом у нас потенциально может быть почти совместимость:

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

Структурное выделение подтипов для полей данных объектов никогда не казалось мне очень полезным, но вместо этого в применении к свойствам _types_ оно, кажется, имеет большой смысл.

Я также понял, что это может обеспечить выход из проблемы неоднозначности: пересечение двух типов пусто, если у них есть конфликтующие значения для некоторого параметра. Итак, если нам нужен однозначный тип Number мы могли бы иметь

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

Это рассматривает super как свойство другого типа.

Мне нравится предложенный синтаксис протокола, но у меня есть несколько замечаний.

Но тогда я могу все неправильно понять. Я только недавно начал по-настоящему смотреть на Юлию как на то, над чем я хочу работать, и я еще не очень хорошо разбираюсь в системе типов.

(а) Думаю, было бы интереснее с особенностями черт, над которыми работал

protocol Foo{bar}
    ...
end

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

И это также обнажает ключевую проблему, заключающуюся в том, что протоколу Foo не разрешается ссылаться на протокол Bar в том же определении.

(б)

у нас есть PAbstractArray {Int, 1} <: Indexable {Int}? Я думаю, что это могло бы сработать очень хорошо, если бы параметры совпадали по имени.

Я не уверен, почему мы должны сопоставлять параметры по _name_ (я считаю, что это имена eltype , если я неправильно понял, проигнорируйте этот раздел). Почему бы просто не сопоставить потенциальные сигнатуры функций. Моя основная проблема с использованием именования заключается в том, что оно предотвращает следующее:

module SomeBigLibrary
  # Assuming required definitions

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

module SomeOtherLibrary
  # Assuming required definitions

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

module My
  # Assuming required definitions

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

С другой стороны, он гарантирует, что ваш протокол предоставляет только определенную иерархию типов, которую мы тоже хотим. Если мы не назовем match Iterable тогда мы не получим преимуществ от реализации итерации (а также не будем рисовать край в зависимости). Но я не уверен, что получает от этого пользователь, кроме возможности делать следующее ...

(c) Итак, я могу чего-то упустить, но разве основная цель использования именованных типов не в описании поведения различных частей надмножества? Рассмотрим иерархию Number и абстрактные типы Signed и Unsigned , оба реализуют протокол Integer , но иногда ведут себя совершенно иначе. Чтобы различать их, вынуждены ли мы теперь определять специальный negate только для типов Signed (особенно сложно без возвращаемых типов, когда мы действительно можем захотеть отрицать тип Unsigned )?

Я думаю, что это проблема, которую вы описываете в примере super = Number . Когда мы объявляем bitstype Int16 <: Signed (мой другой вопрос, даже как Number или Signed качестве протоколов со своими свойствами типа применяются к конкретному типу?), Присоединяются ли свойства типа из протокол Signed ( super = Signed ), помечающий его как отличный от типов, отмеченных протоколом Unsigned ? Потому что это, на мой взгляд, странное решение, и не только потому, что мне кажутся странными параметры именованного типа. Если два протокола точно совпадают, за исключением типа, который они поместили в супер, чем они вообще отличаются? И если разница заключается в поведении подмножеств более крупного типа (протокола), то разве мы не просто заново изобретаем цель абстрактных типов?

(d) Проблема в том, что мы хотим, чтобы абстрактные типы различали поведение, и мы хотим, чтобы протоколы обеспечивали определенные возможности (часто независимо от другого поведения), через которые выражается поведение. Но мы пытаемся объединить возможности, которые протоколы позволяют нам гарантировать, и разделение абстрактных типов поведения.

Решение, к которому мы часто прибегаем, похоже на «имеют типы, объявляющие о своем намерении реализовать абстрактный класс и проверять соответствие», что проблематично в реализации (циклические ссылки, ведущие к добавлению определений функций внутри блока типа или impl blocks), и удаляет хорошее качество протоколов, основанных на текущем наборе методов и типах, с которыми они работают. Эти проблемы в любом случае не позволяют поместить протоколы в абстрактную иерархию.

Но что более важно, протоколы не описывают поведение, они описывают сложные возможности нескольких функций (например, итерация), поведение этой итерации описывается абстрактными типами (например, отсортированными или даже упорядоченными). С другой стороны, комбинация протокола + абстрактного типа полезна, когда мы можем получить реальный тип, потому что она позволяет нам распределять возможности (методы утилиты возможностей), поведения (методы высокого уровня) или и то, и другое (детали реализации методы).

(e) Если мы позволяем протоколам наследовать несколько протоколов (они в основном структурные) и столько же абстрактных типов, сколько и конкретные типы (например, без множественного абстрактного наследования, один), мы можем разрешить создание чистых типов протоколов, чистых абстрактных типов, и протокол + абстрактные типы.

Я считаю, что это решает проблему Signed vs. Unsigned указанную выше:

  • Определите два протокола, оба наследуются от общего IntegerProtocol (наследуя любую структуру протокола, NumberAddingProtocol , IntegerSteppingProtocol и т. Д.), Один из AbstractSignedInteger а другой - из AbstractUnsignedInteger ).
  • Тогда пользователю типа Signed гарантируется как функциональность (из протокола), так и поведение (из абстрактной иерархии).
  • Конкретный тип AbstractSignedInteger без протоколов _в любом случае_ использовать нельзя.
  • Но что интересно (и в качестве будущей функции, уже упомянутой выше), мы могли бы в конечном итоге создать возможность поиска недостающих функций, если бы только IntegerSteppingProtocol (который является тривиальным и в основном просто псевдонимом для одной функции) существовал для учитывая конкретный AbstractUnsignedInteger мы могли бы попытаться решить для Signed , реализовав на его основе другие протоколы. Может быть, даже с чем-то вроде convert .

Сохранив все существующие типы, превратив большинство из них в типы протокол + абстрактные, а некоторые оставив как чистые абстрактные типы.

Изменить: (f) Пример синтаксиса (включая часть (a) ).

Изменить 2 : исправлены некоторые ошибки ( :< вместо <: ), исправлен плохой выбор ( Foo вместо ::Foo )

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

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

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

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

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

Я вижу проблемы с этим синтаксисом как:

  • Анонимные внутренние типы протокола (например, переменные состояния).
  • Типы возврата.
  • Трудно эффективно реализовать семантику.

_Abstract определяет type_ , что предприятие является. _Protocol_ определяет, что делает сущность. В рамках единого пакета эти две концепции взаимозаменяемы: сущность - это то, что она делает. А более прямой «абстрактный тип». Однако между двумя пакетами есть разница: вам не нужно то, что «есть» у вашего клиента, но вам нужно то, что ваш клиент «делает». Здесь «абстрактный шрифт» не дает об этом никакой информации.

На мой взгляд, протокол - это единственный отправленный абстрактный тип. Это может помочь расширению пакета и сотрудничеству. Таким образом, в одном пакете, где сущности тесно связаны, используйте абстрактный тип, чтобы упростить разработку (за счет получения выгоды от множественной отправки); между пакетами, где объекты более независимы, используйте протокол, чтобы уменьшить уязвимость реализации.

@ mason-bially

Я не уверен, почему мы должны сопоставлять параметры по имени

Я имею в виду сопоставление по имени _ в отличие от сопоставления по положению. Эти имена будут действовать как записи со структурными подтипами. Если у нас есть

protocol Collection{T}
    eltype = T
end

тогда все, что имеет свойство eltype является подтипом Collection . Порядок и положение этих «параметров» не имеет значения.

Если два протокола точно совпадают, за исключением типа, который они поместили в супер, чем они вообще отличаются? И если разница заключается в поведении подмножеств более крупного типа (протокола), то разве мы не просто заново изобретаем цель абстрактных типов?

Это справедливый момент. Именованные параметры действительно возвращают многие свойства абстрактных типов. Я начал с идеи, что нам могут понадобиться и протоколы, и абстрактные типы, а затем попытался унифицировать и обобщить функции. В конце концов, когда вы в настоящее время объявляете type Foo <: Bar , на каком-то уровне вы на самом деле устанавливаете Foo.super === Bar . Так что, возможно, нам следует поддерживать это напрямую, наряду с любыми другими парами ключ / значение, которые вы, возможно, захотите связать.

"иметь типы заявляют о своем намерении реализовать абстрактный класс и проверять соответствие"

Да, я против того, чтобы сделать такой подход основной функцией.

Если мы позволим протоколам наследовать несколько протоколов ... и столько же абстрактных типов

Означает ли это высказывание, например, «T является подтипом протокола P, если он имеет методы x, y, z и объявляет себя подтипом AbstractArray»? Я думаю, что такого рода «протокол + абстрактный тип» очень похож на то, что вы получили бы с моим предложением о собственности super = T . По общему признанию, в своей версии я еще не понял, как связать их в иерархию, как у нас сейчас (например, Integer <: Real <: Number ).

Наличие протокола, унаследованного от (номинального) абстрактного типа, кажется очень сильным ограничением. Существуют ли подтипы абстрактного типа, которые _не_ реализуют протокол? Мне кажется, что лучше хранить протоколы и абстрактные типы как ортогональные вещи.

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

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

Я не понимаю этого синтаксиса.

  • У этого протокола есть название?
  • Что именно означает содержимое внутри { } и ( ) ?
  • Как вы используете этот протокол? Можете ли вы отправить его? Если да, то что означает определение f(x::ThisProtocol)=... , учитывая, что протокол связывает несколько типов?

тогда все, что имеет свойство eltype, является подтипом Collection. Порядок и положение этих «параметров» не имеет значения.

Ага, это было мое недоразумение, в этом больше смысла. А именно возможность назначать:

el1type = el_type
el2type = el_type

чтобы решить мою примерную проблему.

Так что, возможно, нам следует поддерживать это напрямую, наряду с любыми другими парами ключ / значение, которые вы, возможно, захотите связать.

И эта функция "ключ-значение" будет для всех типов, поскольку мы заменим ею абстрактные. Это хорошее общее решение. Теперь ваше решение имеет для меня гораздо больше смысла.

По общему признанию, в своей версии я еще не понял, как связать их в иерархию, как у нас сейчас (например, Integer <: Real <: Number).

Я думаю, вы могли бы использовать super (например, с Integer super как Real ), а затем либо сделать super особенным и действовать как именованный тип, либо добавить способ добавить код разрешения пользовательского типа (ala python) и сделать правило по умолчанию для параметра super .

Наличие протокола, унаследованного от (номинального) абстрактного типа, кажется очень сильным ограничением. Существуют ли подтипы абстрактного типа, которые не реализуют протокол? Мне кажется, что лучше хранить протоколы и абстрактные типы как ортогональные вещи.

О да, абстрактное ограничение было совершенно необязательным! Вся моя точка зрения заключалась в том, что протоколы и абстрактные типы ортогональны. Вы должны использовать протокол abstract +, чтобы убедиться, что вы получаете комбинацию определенного поведения _и_ связанных возможностей. Если вам нужны только возможности (для служебных функций) или только поведение, вы используете их ортогонально.

У этого протокола есть название?

Два протокола с двумя именами ( Foo и Bar ), которые происходят из одного блока, но тогда я привык использовать макросы для расширения нескольких подобных определений. Эта часть моего синтаксиса была попыткой решить часть (а) . Если вы проигнорируете это, тогда первая строка может быть просто protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (с другим, отдельным определением для протокола Bar ). Кроме того, все Number , AbstractBar и AbstractFoo будут необязательными, как и в определениях обычных типов,

Что именно означают символы внутри {} и ()?

{} - это стандартная секция определения параметрического типа. Разрешение на использование Foo{Float64} для описания типа , реализующий Foo протокол , используя Float64 , например. () - это, по сути, список привязки переменных для тела протокола (поэтому можно описать сразу несколько протоколов). Вероятно, вы запутались по моей вине, потому что в оригинале я неправильно набрал :< вместо <: . Также может быть стоит поменять их местами, чтобы сохранить структуру <<name>> <<parametric>> <<bindings>> , где <<name>> иногда может быть списком привязок.

Как вы используете этот протокол? Можете ли вы отправить его? Если да, то что означает определение f(x::ThisProtocol)=... , учитывая, что протокол связывает несколько типов?

Ваш пример отправки кажется правильным для синтаксиса, на мой взгляд, действительно, рассмотрите следующие определения:

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

abstract FooAbstract

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

type Bar <: FooAbstract
  a
end

type Baz
  b
end

type Bax <: FooAbstract
  c
end

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

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

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

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

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

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

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

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

Фактически протоколы используют именованный тип Top (Any), если не указан более конкретный абстрактный тип для проверки структуры. На самом деле, возможно, стоит разрешить что-то вроде typealias Foo Intersect{FooProtocol, Foo} (_Edit: Intersect было неправильным именем, возможно, Join вместо Intersect был правильным в первый раз_) вместо того, чтобы использовать синтаксис протокола для этого.

Ах, отлично, теперь это имеет для меня гораздо больше смысла! Определение нескольких протоколов вместе в одном блоке интересно; Мне придется еще немного подумать об этом.

Несколько минут назад я убрал все свои примеры. Ранее в этой теме кто-то упомянул сбор корпуса протоколов для тестирования идей, я думаю, что это отличная идея.

Множественные протоколы в одном блоке - это своего рода раздражение, когда я пытаюсь описать сложные отношения между объектами с помощью правильных двусторонних аннотаций типов в определении / компиляции при загрузке языков (например, таких как python; Java, например, не делает) у меня есть проблема). С другой стороны, большинство из них, вероятно, легко исправить с точки зрения удобства использования, в любом случае с использованием нескольких методов; но соображения производительности могут возникать из-за правильного ввода функций в протоколах (например, оптимизации протоколов путем их специализации для vtables).

Вы упомянули ранее, что протоколы могут быть (ложно) реализованы методами, использующими ::Any Я думаю, что это было бы довольно простым случаем, чтобы просто проигнорировать, если бы дело дошло до него. Конкретный тип не был бы классифицирован как протокол, если бы реализующий метод был отправлен на ::Any . С другой стороны, я не уверен, что это обязательно проблема.

Для начала, если метод ::Any добавлен постфактум (скажем, потому что кто-то придумал более общую систему для его обработки), это все еще действительная реализация, и если мы действительно используем протоколы в качестве функции оптимизации, тогда специализированные версии отправленных методов ::Any прежнему работают для повышения производительности. Так что, в конце концов, я был бы против игнорировать их.

Но, возможно, стоит иметь синтаксис, который позволяет определителю протокола выбирать между двумя вариантами (когда мы выбираем значение по умолчанию, разрешаем другой). Во-первых, это синтаксис пересылки для диспетчерского метода ::Any , например, ключевое слово global (см. Также следующий раздел). Для второго способа, требующего более конкретного метода, я не могу вспомнить существующее полезное ключевое слово.

Изменить: убрано кучу бессмысленных вещей.

Ваш Join - это в точности пересечение типов протоколов. На самом деле это «встреча». И, к счастью, тип Join не нужен, потому что типы протоколов уже закрыты при пересечении: для вычисления пересечения просто верните новый тип протокола с двумя объединенными списками методов.

Меня не слишком беспокоит упрощение протоколов из-за ::Any определений. На мой взгляд, правило «искать подходящие определения, кроме того, что Any не считается» противоречит бритве Оккама. Не говоря уже о том, что установка флага «игнорировать любой» через алгоритм выделения подтипов была бы довольно неприятной. Я даже не уверен, что полученный алгоритм является последовательным.

Мне очень нравится идея протоколов (немного напоминает мне CLUsters), мне просто любопытно, как это согласуется с новым подтипом, который обсуждался Джеффом на JuliaCon, и с чертами? (две вещи, которые мне все еще очень хотелось бы видеть в Юлии).

Это добавит новый тип типа со своими собственными правилами выделения подтипов (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). На первый взгляд кажется, что они совместимы с остальной системой и их можно просто подключить.

Эти протоколы в значительной степени являются версией свойств @mauro3 с одним параметром.

Ваш Join - это в точности пересечение типов протоколов.

Я как-то убедил себя, что ошибался, когда сказал, что это перекресток. Хотя нам все равно понадобится способ пересекать типы в одну строку (например, Union ).

Редактировать:

Мне все еще нравится обобщать протоколы и абстрактные типы в одну систему и разрешать настраиваемые правила для их разрешения (например, для super для описания текущей системы абстрактных типов). Я думаю, если все будет сделано правильно, это позволит людям добавлять собственные системы типов и, в конечном итоге, настраивать оптимизацию для этих систем типов. Хотя я не уверен, что протокол будет правильным ключевым словом, но, по крайней мере, мы могли бы превратить abstract в макрос, это было бы круто.

с пшеничных полей: лучше поднимать общность через протоколированное и абстрагированное, чем искать их обобщение в качестве пункта назначения.

какие?

Процесс обобщения намерений, возможностей и потенциала протоколов и абстрактных типов является менее эффективным способом разрешения их наиболее качественно удовлетворительного синтеза. Лучше сначала выявить присущие им общие черты цели, модели, процесса. И развивайте это понимание, позволяя уточнять свою точку зрения на синтез.

Какой бы плодотворной ни была реализация для Джулии, она построена на строительных лесах, предлагаемых синтезом. Более ясный синтез - это конструктивная сила и индуктивная сила.

Какие?

Я думаю, он говорит, что мы должны сначала выяснить, чего мы хотим от протоколов и почему они полезны. Тогда, когда у нас будут этот и абстрактный типы, будет легче придумать их общий синтез.

Простые протоколы

(1) защита

Протокол может быть расширен, чтобы стать (более сложным) протоколом.
Протокол может быть сокращен до (менее детализированного) протокола.
Протокол может быть реализован как соответствующий интерфейс [в программном обеспечении].
Протокол может быть запрошен для определения соответствия интерфейса.

(2) предлагая

Протоколы должны поддерживать номера версий для конкретных протоколов по умолчанию.

Было бы хорошо поддержать такой способ:
Когда интерфейс соответствует протоколу, ответ истина; когда интерфейс
верен подмножеству протокола и будет соответствовать при расширении,
ответ неполный, в противном случае - ложный. Функция должна перечислить все
необходимое расширение для интерфейса, неполного по отношению к протоколу.

(3) размышления

Протокол может быть особенным модулем. Его экспорт будет служить
в качестве начального сравнения при определении того, соответствует ли какой-либо интерфейс.
Любой протокол, указанные [экспортируемые] типы и функции, могут быть объявлены с использованием
@abstract , @type , @immutable и @function для поддержки врожденной абстракции.

[пао: переключитесь на кодовые кавычки, но обратите внимание, что лошадь уже покинула сарай, когда вы делаете это постфактум ...]

(вам нужно указать @mentions !)

спасибо - исправляем

В среду, 16 декабря 2015 г., в 3:01, Mauro [email protected] написал:

(необходимо процитировать @ упоминания!)

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

извините, я должен был быть более ясным: код-цитата с использованием `а не"

Исправлено исправление цитирования.

спасибо - извините за предыдущее невежество

Я попытался разобраться в недавнем обсуждении добавления типа протокола. Возможно, я что-то неправильно понимаю, но почему необходимо иметь именованные протоколы вместо того, чтобы просто использовать имя связанного абстрактного типа, который протокол собирается описать?

С моей точки зрения, вполне естественно расширить существующую систему абстрактных типов с помощью некоторого способа описания поведения, которое ожидается от типа. Во многом похоже на то, что изначально было предложено в этой ветке, но, возможно, с синтаксисом Jeffs

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

При переходе по этому маршруту нет необходимости специально указывать, что подтип реализует интерфейс. Это будет неявно сделано путем выделения подтипов.

Основная цель явного механизма интерфейса - IMHO, чтобы получить более качественные сообщения об ошибках и выполнить лучший проверочный тест.

Итак, объявление типа вроде:

type Foo <: Iterable
  ...
end

Определяем ли мы функции в разделе ... ? Если нет, то когда мы ошибаемся по поводу отсутствующих функций (и связанных с этим сложностей)? Кроме того, что происходит с типами, реализующими несколько протоколов, включаем ли мы множественное абстрактное наследование? Как мы справляемся с разрешением супер-метода? Что это делает с множественной отправкой (кажется, просто удаляет ее и вставляет туда объектную систему в стиле Java)? Как мы определяем специализацию нового типа для методов после определения первого типа? Как мы определяем протоколы после определения типа?

Все эти вопросы легче решить, создав новый тип (или формулировку нового типа).

Не обязательно иметь связанный абстрактный тип для каждого протокола (на самом деле, вероятно, его не должно быть). Несколько текущих интерфейсов могут быть реализованы одним и тем же типом. Что не поддается описанию с помощью существующей системы абстрактных типов. Отсюда и проблема.

  • Абстрактное множественное наследование (реализация нескольких протоколов) ортогонально этой функции (как указано Джеффом выше). Так что дело не в том, что мы получаем эту функцию только потому, что к языку добавлены протоколы.
  • Ваш следующий комментарий касается вопроса, когда проверять интерфейс. Я думаю, это не должно быть связано с определениями функций внутри блока, которые мне не кажутся Джулианом. Вместо этого есть три простых решения:

    1. как это реализовано в # 7025, используйте метод verify_interface который может быть вызван либо после всех определений функций, либо в модульном тесте

    2. Нельзя вообще проверить интерфейс и отложить его до улучшенного сообщения об ошибке в "MethodError". На самом деле это хороший запасной вариант для 1.

    3. Проверьте все интерфейсы либо в конце единицы времени компиляции, либо в конце фазы загрузки модуля. В настоящее время также возможно наличие:

function a()
  b()
end

function b()
end

Таким образом, я не думаю, что здесь потребуются определения встроенных функций.

  • Последнее, что вы заметили, - это то, что могут быть протоколы, не связанные с абстрактными типами. В настоящее время это, безусловно, верно (например, неофициальный протокол «Iterable»). Однако, на мой взгляд, это просто из-за отсутствия множественного абстрактного наследования. Если это вызывает беспокойство, давайте просто добавим абстрактное множественное наследование вместо добавления новой языковой функции, которая призвана решить эту проблему. Я также считаю, что реализация нескольких интерфейсов абсолютно необходима, и это абсолютно обычное явление в Java / C #.

Я думаю, что разница между «протокольным» и множественным наследованием заключается в том, что тип может быть добавлен к протоколу после того, как он был определен. Это полезно, если вы хотите, чтобы ваш пакет (определяющий протоколы) работал с существующими типами. Можно было бы разрешить изменять супертипы типа после создания, но на этом этапе, вероятно, лучше назвать это «протоколом» или чем-то в этом роде.

Хм, так это позволяет определять альтернативные / расширенные интерфейсы для существующих типов. Мне все еще непонятно, где это действительно потребуется. Когда кто-то хочет добавить что-то к существующему интерфейсу (когда мы следуем подходу, предложенному в OP), нужно просто подтип и добавить дополнительные методы интерфейса к подтипу. Это хорошая черта такого подхода. Он неплохо масштабируется.

Пример: скажем, у меня есть пакет, который сериализует типы. Для типа должен быть реализован метод tobits , тогда все функции в этом пакете будут работать с этим типом. Назовем это протоколом Serializer (т.е. определено tobits ). Теперь я могу добавить к нему Array (или любой другой тип), реализовав tobits . При множественном наследовании я не мог заставить Array работать с Serialzer как я не могу добавить супертип к Array после его определения. Я думаю, что это важный вариант использования.

Хорошо, поймите это. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 - аналогичная проблема, решение которой в основном состоит в использовании утиной печати. Если бы у нас было что-то, что элегантно решает эту проблему, это было бы действительно хорошо. Но это то, что нужно поддерживать на уровне диспетчеризации. Если я правильно понимаю идею протокола, приведенную выше, можно было бы использовать абстрактный тип или протокол в качестве аннотации типа в функции. Здесь было бы неплохо объединить эти две концепции в одном достаточно мощном инструменте.

Я согласен: будет очень сложно иметь и абстрактные типы, и протоколы. Если я правильно помню, выше утверждалось, что абстрактные типы имеют некоторую семантику, которая не может быть смоделирована с помощью протоколов, т.е. абстрактные типы имеют некоторые особенности, которых нет у протоколов. Даже если это обязательно так (я не уверен), это все равно будет сбивать с толку, поскольку между этими двумя концепциями существует такое большое совпадение. Итак, абстрактные типы следует удалить в пользу протоколов.

Поскольку выше существует консенсус относительно протоколов, они делают упор на указание интерфейсов. Для некоторых из отсутствующих протоколов могли использоваться абстрактные типы. Это не значит, что это их самое важное использование. Скажите мне, какие протоколы есть, а какие нет, тогда я смогу рассказать вам, чем отличаются абстрактные типы и что они несут. Я никогда не считал, что абстрактные типы связаны столько с интерфейсом, сколько с типологией. Отказ от естественного подхода к типологической гибкости обходится дорого.

@JeffreySarnoff +1

Подумайте об иерархии числовых типов. Различные абстрактные типы, например Signed, Unsigned, не определяются их интерфейсом. Не существует набора методов, который определяет «Беззнаковый». Это просто очень полезное объявление.

На самом деле я не вижу в этом проблемы. Если оба типа Signed и Unsigned поддерживают один и тот же набор методов, мы можем создать два протокола с одинаковыми интерфейсами. Тем не менее, объявление типа как Signed вместо Unsigned может использоваться для диспетчеризации (т. Е. Методы одной и той же функции действуют по-разному). Ключевым моментом здесь является требование явного объявления перед рассмотрением того, что тип реализует протокол, а не обнаружение этого неявно на основе методов, которые он реализует.

Но также важно наличие неявно связанных протоколов, как в https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Протоколы могут не только определять функции, которые могут быть вызваны, но также могут задокументировать (неявно или тестируемыми на машине способами) свойства, которые необходимо сохранять. Такие как:

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

Эта внешне видимая разница в поведении между Signed и Unsigned и делает это различие полезным.

Если существует различие между типами, которое является настолько «абстрактным», что его нельзя сразу проверить, по крайней мере теоретически, извне, то, вероятно, нужно знать реализацию типа, чтобы сделать правильный выбор. Здесь может пригодиться текущий abstract . Вероятно, это идет в направлении алгебраических типов данных.

Нет причин, по которым протоколы не следует использовать для простой группировки типов, т.е. без каких-либо определенных методов (и это возможно с «текущим» дизайном, используя уловку: https://github.com/JuliaLang/julia/issues/ 6975 # issuecomment-161056795). (Также обратите внимание, что это не мешает неявно определенным протоколам.)

Рассмотрим пример (Un)signed : что бы я сделал, если бы у меня был тип, который равен Signed но по какой-то причине должен быть также подтипом другого абстрактного типа? Это было бы невозможно.

@eschnett : абстрактные типы на данный момент не имеют ничего общего с реализацией своих подтипов. Хотя это уже обсуждалось: # 4935.

Алгебраические типы данных - хороший пример, когда последовательное уточнение имеет внутреннее значение.
Любая таксономия гораздо более естественна и более полезна как иерархия абстрактных типов, чем как смесь спецификаций протоколов.

Также важно замечание о наличии типа, который является подтипом более чем одной иерархии абстрактных типов. Есть много утилитарной власти, которая приходит с множественным наследованием абстракций.

@ mauro3 Да, я знаю. Я думал о чем-то эквивалентном размеченным объединениям, но реализованным так же эффективно, как кортежи, а не через систему типов (поскольку объединения в настоящее время реализованы). Это будет включать перечисления, типы, допускающие значение NULL, и может обрабатывать несколько других случаев более эффективно, чем в настоящее время абстрактные типы.

Например, как кортежи с анонимными элементами:

DiscriminatedUnion{Int16, UInt32, Float64}

или с именованными элементами:

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

Я пытался подчеркнуть, что абстрактные типы - один из хороших способов сопоставить такую ​​конструкцию с Джулией.

Нет причин, по которым протоколы не следует использовать для простой группировки типов, т.е. без каких-либо определенных методов (и это возможно с «текущим» дизайном, использующим уловку: # 6975 (комментарий)). (Также обратите внимание, что это не мешает неявно определенным протоколам.)

Я чувствую, что вам нужно быть осторожным с этим, чтобы достичь производительности, - соображение, которое не многие, кажется, рассматривают достаточно часто. В этом примере может показаться, что нужно просто определить версию, отличную от любой, чтобы компилятор мог выбирать функцию во время компиляции (вместо того, чтобы вызывать функцию, чтобы выбрать правильную во время выполнения, или компилятор проверяет функции для определения их результатов). Лично я считаю, что использование множественного абстрактного «наследования» в качестве тегов было бы лучшим решением.

Я считаю, что мы должны свести к минимуму требуемые уловки и знания о системе типов (хотя это можно было бы обернуть в макрос, это было бы странным взломом макроса; если мы используем макросы для управления системой типов, то я думаю, что @ Единое решение JeffBezanson лучше решит эту проблему).

Учитывая (не) подписанный пример: что бы я сделал, если бы у меня был тип, который подписан, но по какой-то причине должен быть также подтипом другого абстрактного типа? Это было бы невозможно.

Множественное абстрактное наследование.


Я считаю, что все это было сделано раньше, этот разговор, кажется, идет кругами (хотя каждый раз все более узкими кругами). Я считаю, что было упомянуто, что следует приобрести корпус или проблемы с использованием протоколов. Это позволит нам легче судить о решениях.

Пока мы повторяем вещи :) Я хочу напомнить всем, что абстрактные типы являются номинальными, в то время как протоколы являются структурными, поэтому я предпочитаю проекты, которые рассматривают их как ортогональные, _ если_ мы действительно не сможем придумать приемлемое «кодирование» абстрактных типов в протоколах (возможно, с умным использованием связанных типов). Бонусные баллы, конечно, если это также приводит к множественному абстрактному наследованию. Я чувствую, что это возможно, но мы еще не совсем там.

@JeffBezanson Отличаются ли «связанные типы» от «конкретных типов, связанных с протоколом [a]»?

Да, я так считаю; Я имею в виду «связанные типы» в техническом смысле протокола, определяющего некоторую пару «ключ-значение», где «значение» - это тип, точно так же, как протоколы определяют методы. например, «тип Foo следует протоколу контейнера, если он имеет eltype » или «тип Foo следует протоколу матрицы, если его параметр ndims равен 2».

абстрактные типы являются номинальными, в то время как протоколы являются структурными и
абстрактные типы являются качественными, а протоколы действующими и
абстрактные типы (с множественным наследованием) оркестрируют, пока протоколы проводят

Даже если бы одно было закодировано в другом, "привет, привет .. как дела? Давай, давай!" Джулии необходимо четко представить как общее, целенаправленное понятие протокола, так и множественные наследуемые абстрактные типы (понятие общего назначения). Если есть искусное разворачивание, которое дает Джулии и то, и другое свернутым по отдельности, это, скорее, делается именно так, чем одно через одно и другое.

@ mason-bially: значит, мы должны добавить множественное наследование? Это по-прежнему оставит проблему, заключающуюся в том, что супертипы не могут быть добавлены после создания типа (если это также не было разрешено).

@JeffBezanson : ничто не помешает нам разрешить чисто номинальные протоколы.

@ mauro3 Почему решение о том, разрешить ли вставку супертипа постфактум, должно быть связано с множественным наследованием? И есть разные виды создания супертипов, некоторые из них явно безвредны, предполагая возможность вставлять новые, что бы они ни были: я хотел добавить абстрактный тип между Real и AbstractFloat, скажем ProtoFloat, чтобы я мог отправлять двойные сообщения. двойные поплавки и система. Поплавки вместе, не мешая системе. Поплавки существуют как подтипы AbstractFloat. Возможно, менее легко разрешить возможность разделить текущие подтипы Integer и, таким образом, избежать множества сообщений "неоднозначно с .. define f (Bool) before .."; или ввести супертип Signed, который является подтипом Integer, и открыть числовую иерархию для прозрачной обработки, скажем порядковых чисел.

Извините, если я инициировал еще один круг. Тема довольно сложная, и мы действительно должны убедиться, что решение очень простое в использовании. Итак, нам нужно покрыть:

  • общее решение
  • нет снижения производительности
  • простота использования (а также простота понимания!)

Поскольку то, что было изначально предложено в # 6975, сильно отличается от идеи протокола, обсуждаемой позже, было бы неплохо иметь какой-то JEP, который описывает, как могут выглядеть протоколы.

Пример того, как вы можете определить формальный интерфейс и проверить его, используя текущую версию 0.4 (без макросов), dispatch в настоящее время полагается на отправку стиля traits, если в gf.c. При этом используются сгенерированные функции для проверки, все вычисления типов выполняются в пространстве типов.

Я начинаю использовать это как проверку времени выполнения в DSL, который мы определяем, где я должен убедиться, что предоставленный тип является итератором дат.

В настоящее время он поддерживает множественное наследование супертипов, имя поля _super не используется средой выполнения и может быть любым допустимым символом. Вы можете указать n других типов в _super Tuple.

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

Просто укажу здесь, что я продолжил обсуждение на JuliaCon возможного синтаксиса черт на https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040

Гай Стил хорошо разбирается в особенностях языка множественной диспетчеризации (Fortress), см. Его доклад на JuliaCon 2016: https://youtu.be/EZD3Scuv02g .

Несколько основных моментов: большая система признаков для алгебраических свойств, модульное тестирование свойств признаков для типов, реализующих признак, и что реализованная ими система, возможно, была слишком сложной, и теперь он сделает что-то попроще.

Новый Swift для компилятора тензорного потока AD для протоколов:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy и @Keno могут быть заинтересованы в этом. Совершенно новый контент

Я думаю, что эта презентация заслуживает внимания при исследовании пространства дизайна для этой проблемы.

Для обсуждения неспецифических идей и ссылок на соответствующие фоновые работы было бы лучше начать соответствующую ветку дискуссии и публиковать и обсуждать там.

Обратите внимание, что почти все проблемы, встречающиеся и обсуждаемые в исследованиях универсального программирования на статически типизированных языках, не имеют отношения к Джулии. Статические языки почти исключительно связаны с проблемой обеспечения достаточной выразительности для написания кода, который они хотят, при этом сохраняя возможность статической проверки типов на предмет отсутствия нарушений системы типов. У нас нет проблем с выразительностью и не требуется статическая проверка типов, так что в Джулии все это не имеет значения.

Что нас действительно волнует, так это возможность людям структурированно документировать ожидания протокола, которые затем язык может динамически проверять (заранее, когда это возможно). Мы также заботимся о том, чтобы люди могли рассуждать о таких вещах, как черты характера; остается открытым вопрос о том, следует ли их подключать.

Итог: хотя академическая работа над протоколами на статических языках может представлять общий интерес, она не очень полезна в контексте Джулии.

Что нас действительно волнует, так это возможность людям структурированно документировать ожидания протокола, которые затем язык может динамически проверять (заранее, когда это возможно). Мы также заботимся о том, чтобы люди могли рассуждать о таких вещах, как черты характера; остается открытым вопрос о том, следует ли их подключать.

_это_: билет:

Возможно ли в julia исключение абстрактных типов и введение неявных интерфейсов в стиле golang, помимо предотвращения критических изменений?

Нет, не пойдет.

ну, разве это не то, о чем все протоколы / черты характера? Было некоторое обсуждение того, должны ли протоколы быть явными или неявными.

Я думаю, что, начиная с версии 0.3 (2014 г.), опыт показал, что неявные интерфейсы (т. Е. Не выполняемые языком / компилятором) работают нормально. Кроме того, увидев, как развивались некоторые пакеты, я думаю, что лучшие интерфейсы были разработаны органически и были формализованы (= задокументированы) только позже.

Я не уверен, что необходимо формальное описание интерфейсов, каким-то образом подкрепленное языком. Но пока это решено, было бы здорово поощрять следующее (в документации, руководствах и руководствах по стилю):

  1. "интерфейсы" дешевы и легковесны, это просто набор функций с заданным поведением для набора типов (да, типы - это правильный уровень детализации - для x::T T должно быть достаточно x интерфейс). Итак, если вы определяете пакет с расширяемым поведением, действительно имеет смысл задокументировать интерфейс.

  2. Интерфейсы не нужно описывать отношениями подтипов . Типы без общего (нетривиального) супертипа могут реализовывать один и тот же интерфейс. Тип может реализовывать несколько интерфейсов.

  3. Для пересылки / компоновки неявно требуются интерфейсы. «Как заставить оболочку унаследовать все методы родительского элемента» - это вопрос, который возникает часто, но это неправильный вопрос. Практическое решение - иметь основной интерфейс и просто реализовать его для оболочки.

  4. Черты дешевы и должны использоваться обильно. Base.IndexStyle - отличный канонический пример.

Следующее можно было бы получить от пояснения, поскольку я не уверен, что лучше всего:

  1. Должен ли интерфейс иметь функцию запроса, например Tables.istable для определения того, реализует ли объект интерфейс? Я думаю, что это хорошая практика, если вызывающий абонент может работать с различными альтернативными интерфейсами и должен просмотреть список резервных вариантов.

  2. Как лучше всего разместить документацию по интерфейсу в строке документации? Я бы сказал функцию запроса выше.

  1. да, типы - это правильный уровень детализации

Почему это так? Некоторые аспекты типов могут быть включены в интерфейсы (для целей диспетчеризации), например, итерация. В противном случае вам пришлось бы переписывать код или вводить ненужную структуру.

  1. Интерфейсы не нужно описывать отношениями подтипов .

Возможно, в этом нет необходимости, но что будет лучше? У меня может быть отправка функции для итеративного типа. Разве мозаичный итеративный тип не должен выполнять это неявно? Зачем пользователю рисовать их вокруг номинальных типов, когда им важен только интерфейс?

Какой смысл в номинальных подтипах, если вы, по сути, просто используете их как абстрактные интерфейсы? Черты кажутся более детальными и мощными, поэтому было бы лучше обобщить. Кажется, что типы - это почти черты характера, но у нас должны быть черты, чтобы обойти их ограничения (и наоборот).

Какой смысл в номинальных подтипах, если вы, по сути, просто используете их как абстрактные интерфейсы?

Отгрузка - вы можете отправить товар по номинальному типу. Если вам не нужно сообщать о том, реализует ли тип интерфейс или нет, вы можете просто уйти от этого типа. Это то, для чего люди обычно используют свойства Holy: эта черта позволяет вам диспетчеризировать вызов реализации, предполагающей, что какой-то интерфейс реализован (например, «имеющий известную длину»). Кажется, что люди хотят избежать этого уровня косвенности, но это кажется просто удобством, а не необходимостью.

Почему это так? Некоторые аспекты типов могут быть включены в интерфейсы (для целей диспетчеризации), например, итерация. В противном случае вам пришлось бы переписывать код или вводить ненужную структуру.

Я считаю, что @tpapp говорил, что вам нужен только тип, чтобы определить, реализует ли что-то интерфейс, а не то, что все интерфейсы могут быть представлены иерархиями типов.

Просто подумайте, используя MacroTools forward :

Иногда надоедает пересылка множества методов

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

что, если бы мы могли использовать тип Foo.x и список методов, а затем сделать вывод, какой из них пересылать? Это будет своего рода inheritance и может быть реализовано с помощью существующих функций (макросы + сгенерированная функция), это тоже похоже на какой-то интерфейс, но нам больше ничего не нужно в этом языке.

Я знаю, что мы никогда не смогли бы составить список того, что будет наследовать (именно поэтому статическая модель class менее гибкая), иногда вам нужно всего несколько из них, но это просто удобно для основных функций ( например, кто-то хочет определить оболочку (подтип AbstractArray ) около Array , большинство функций просто пересылаются)

@datnamer : как более детализированными, чем типы (т.е. реализация интерфейса никогда не должна зависеть от значения , учитывая тип). Это хорошо согласуется с оптимизационной моделью компилятора и на практике не является ограничением.

Возможно, я не совсем понял, но цель моего ответа состояла в том, чтобы указать, что у нас уже есть интерфейсы в той степени, в которой это полезно в Julia , и что они легкие, быстрые и становятся широко распространенными по мере развития экосистемы.

Формальная спецификация для описания интерфейса не имеет большого значения, ИМО: она сводится только к документации и проверке доступности некоторых методов. Последний является частью интерфейса, но другая часть - это семантика, реализованная этими методами (например, если A является массивом, axes(A) дает мне диапазон координат, которые действительны для getindex ). Официальные спецификации интерфейсов не могут решить эти проблемы в целом, поэтому я считаю, что они просто добавили бы шаблон с небольшой ценностью. Я также обеспокоен тем, что это просто поднимет (небольшой) барьер для входа с небольшой выгодой.

Однако я бы хотел увидеть

  1. документация по все большему количеству интерфейсов (в строке документации),

  2. наборы тестов для выявления очевидных ошибок для зрелых интерфейсов для вновь определенных типов (например, много T <: AbstractArray реализуют eltype(::T) а не eltype(::Type{T}) .

@tpapp Теперь для меня

@StefanKarpinski Я не совсем понимаю. Признаки не являются номинальными типами (правда?), Тем не менее, их можно использовать для рассылки.

Моя точка зрения в основном совпадает с высказали здесь @tknopp и @ mauro3 : https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Наличие черт и абстрактной типизации создает дополнительную сложность и путаницу из-за наличия двух очень похожих концепций.

Кажется, что люди хотят избежать этого уровня косвенности, но это кажется просто удобством, а не необходимостью.

Могут ли разделы иерархии признаков быть надежно сгруппированы по таким вещам, как объединения и пересечения, с параметрами типа? Я не пробовал, но похоже, что для этого нужна языковая поддержка. Проблема выражения IE в области типов.

Изменить: я думаю, что проблема заключалась в моем сочетании интерфейсов и черт, поскольку они здесь используются.

Просто разместите это здесь, потому что это весело: похоже, Concepts определенно принят и станет частью C ++ 20. Интересные штучки!

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

Я думаю, что черты характера - действительно хороший способ решить эту проблему, а святые черты определенно прошли долгий путь. Однако я думаю, что Джулии действительно нужен способ группировки функций, принадлежащих к определенному признаку. Это было бы полезно для документации, но также для удобочитаемости кода. Судя по тому, что я видел до сих пор, я думаю, что синтаксис черт, подобный Rust, будет подходящим вариантом.

Я думаю, что это очень важно, и наиболее важным вариантом использования будет индексирование итераторов. Вот предложение по синтаксису, который, как вы могли надеяться, сработает. Приносим извинения, если это уже было предложено (длинная цепочка ...).

import Base: Generator
<strong i="6">@require</strong> getindex(AbstractArray, Vararg{Int})
function getindex(container::Generator, index...)
    iterator = container.iter
    if <strong i="7">@works</strong> getindex(iterator, index...)
        container.f(getindex(iterator, index...))
    else
        <strong i="8">@interfaceerror</strong> getindex(iterator, index...)
    end
end
Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

StefanKarpinski picture StefanKarpinski  ·  3Комментарии

i-apellaniz picture i-apellaniz  ·  3Комментарии

manor picture manor  ·  3Комментарии

dpsanders picture dpsanders  ·  3Комментарии

TotalVerb picture TotalVerb  ·  3Комментарии