Julia: Interfaces para tipos abstratos

Criado em 26 mai. 2014  ·  171Comentários  ·  Fonte: JuliaLang/julia

Acho que esta solicitação de recurso ainda não tem seu próprio problema, embora tenha sido discutido no exemplo # 5.

Acho que seria ótimo se pudéssemos definir explicitamente as interfaces em tipos abstratos. Por interface, entendo todos os métodos que devem ser implementados para cumprir os requisitos de tipo abstrato. Atualmente, a interface é definida apenas implicitamente e pode ser espalhada por vários arquivos de forma que é muito difícil determinar o que se deve implementar ao derivar de um tipo abstrato.

As interfaces nos dariam principalmente duas coisas:

  • auto documentação de interfaces em um único lugar
  • melhores mensagens de erro

Base.graphics tem uma macro que realmente permite definir interfaces codificando uma mensagem de erro na implementação de fallback. Acho que isso já é muito inteligente. Mas talvez fornecer a seguinte sintaxe seja ainda mais claro:

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

Aqui, seria ótimo se pudéssemos especificar diferentes granularidades. As declarações print e push! apenas dizem que deve haver quaisquer métodos com esse nome (e MyType como primeiro parâmetro), mas não especificam os tipos. Em contraste, a declaração size é completamente digitada. Acho que isso dá muita flexibilidade e, para uma declaração de interface não digitada, ainda se pode dar mensagens de erro bastante específicas.

Como eu disse no item 5, essas interfaces são basicamente o que é planejado em C ++ como Concept-light para C ++ 14 ou C ++ 17. E tendo feito bastante programação de template C ++, estou certo de que alguma formalização nesta área também seria bom para Julia.

Comentários muito úteis

Para a discussão de ideias não específicas e links para trabalhos de fundo relevantes, seria melhor iniciar um tópico de discurso correspondente, postar e discutir lá.

Observe que quase todos os problemas encontrados e discutidos em pesquisas sobre programação genérica em linguagens com tipagem estática são irrelevantes para Julia. As linguagens estáticas estão quase exclusivamente preocupadas com o problema de fornecer expressividade suficiente para escrever o código que desejam, enquanto ainda conseguem digitar estaticamente para verificar se não há violações do sistema de tipos. Não temos problemas com a expressividade e não exigimos verificação de tipo estática, então nada disso realmente importa em Julia.

O que nos interessa é permitir que as pessoas documentem as expectativas de um protocolo de uma forma estruturada que a linguagem possa então verificar dinamicamente (com antecedência, quando possível). Também nos preocupamos em permitir que as pessoas despachem coisas como características; permanece em aberto se aqueles devem ser conectados.

Conclusão: embora o trabalho acadêmico em protocolos em linguagens estáticas possa ser de interesse geral, não é muito útil no contexto de Julia.

Todos 171 comentários

Geralmente, acho que esta é uma boa direção para uma melhor programação orientada à interface.

Porém, algo está faltando aqui. As assinaturas dos métodos (não apenas seus nomes) também são significativas para uma interface.

Isso não é algo fácil de implementar e haverá muitas pegadinhas. Essa é provavelmente uma das razões pelas quais _Conceitos_ não foi aceito pelo C ++ 11, e depois de três anos, apenas uma versão muito limitada do _lite_ chega ao C ++ 14.

O método size em meu exemplo continha a assinatura. Além disso, @mustimplement de Base.graphics também leva a assinatura em consideração.

Devo acrescentar que já temos uma parte de Concept-light que é a capacidade de restringir um tipo a ser um subtipo de um certo tipo abstrato. As interfaces são a outra parte.

Essa macro é muito legal. Eu defini manualmente os fallbacks desencadeadores de erros e funcionou muito bem para definir interfaces. por exemplo, o MathProgBase de JuliaOpt faz isso e funciona bem. Eu estava brincando com um novo solucionador (https://github.com/IainNZ/RationalSimplex.jl) e só tive que continuar implementando funções de interface até que parasse de gerar erros para fazê-lo funcionar.

Sua proposta faria algo semelhante, certo? Mas você _teria_ que implementar a interface inteira?

Como isso lida com parâmetros covariantes / contravariantes?

Por exemplo,

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 Sim, a proposta é realmente sobre tornar @mustimplement um pouco mais versátil de forma que, por exemplo, a assinatura possa, mas não precise, ser fornecida. E meu sentimento é que este é um "núcleo" que vale a pena obter sua própria sintaxe. Seria ótimo garantir que todos os métodos sejam realmente implementados, mas a verificação de tempo de execução atual, conforme feita em @mustimplement já é uma coisa excelente e pode ser mais fácil de implementar.

@lindahua Esse é um exemplo interessante. Tenho que pensar sobre isso.

@lindahua Alguém provavelmente gostaria que seu exemplo funcionasse. @mustimplement não funcionaria, pois define assinaturas de método mais específicas.

Portanto, isso pode ter que ser implementado um pouco mais profundamente no compilador. Na definição de tipo abstrato, é necessário acompanhar os nomes / assinaturas da interface. E nesse ponto em que um erro "... não definido" é gerado, é necessário gerar a mensagem de erro apropriada.

É muito fácil mudar a forma como MethodError imprime , quando temos uma sintaxe e API para expressar e acessar as informações.

Outra coisa que isso poderia nos fornecer é uma função em base.Test para verificar se um tipo (todos os tipos?) Implementa totalmente as interfaces dos tipos pai. Isso seria um teste de unidade realmente legal.

Obrigado @ivarne. Portanto, a implementação pode ser a seguinte:

  1. Um deles tem um dicionário global com tipos abstratos como chaves e funções (+ assinaturas opcionais) como valores.
  2. O analisador precisa ser adaptado para preencher o dicionário quando uma declaração has é analisada.
  3. MethodError precisa verificar se a função atual faz parte do dicionário global.

A maior parte da lógica estará em MethodError .

Tenho experimentado um pouco com isso e usando a seguinte essência https://gist.github.com/tknopp/ed53dc22b61062a2b283 que posso fazer:

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

ao definir length nenhum erro é lançado:

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

Não que isso atualmente não leve em consideração a assinatura.

Eu atualizei um pouco o código na essência para que as assinaturas de função possam ser levadas em consideração. Ainda é muito hacky, mas agora funciona o seguinte:

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

Eu deveria ter acrescentado que o cache da interface na essência agora opera em símbolos em vez de funções para que se possa adicionar uma interface e declarar a função posteriormente. Posso ter que fazer o mesmo com a assinatura.

Acabei de ver que o # 2248 já traz algum material sobre interfaces.

Eu ia adiar a publicação de pensamentos sobre recursos mais especulativos, como interfaces, até depois de lançarmos o 0.3, mas desde que você começou a discussão, aqui está algo que escrevi um tempo atrás.


Aqui está uma maquete de sintaxe para declaração de interface e a implementação dessa interface:

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

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

Vamos quebrar isso em pedaços. Primeiro, há a sintaxe do tipo de função: A --> B é o tipo de uma função que mapeia objetos do tipo A para o tipo B . As tuplas nesta notação fazem o que é óbvio. Isoladamente, estou propondo que f :: A --> B declararia que f é uma função genérica, mapeando o tipo A para o tipo B . O que isso significa é uma questão ligeiramente aberta. Isso significa que quando aplicado a um argumento do tipo A , f dará um resultado do tipo B ? Isso significa que f só pode ser aplicado a argumentos do tipo A ? A conversão automática deve ocorrer em qualquer lugar - na saída, na entrada? Por enquanto, podemos supor que tudo o que isso faz é criar uma nova função genérica sem adicionar nenhum método a ela, e os tipos são apenas para documentação.

Em segundo lugar, existe a declaração da interface Iterable{T,S} . Isso torna Iterable um pouco como um módulo e um pouco como um tipo abstrato. É como um módulo, pois possui ligações para funções genéricas chamadas Iterable.start , Iterable.done e Iterable.next . É como um tipo em que Iterable e Iterable{T} e Iterable{T,S} podem ser usados ​​onde quer que os tipos abstratos possam - em particular, no envio de métodos.

Terceiro, há o bloco implement que define como UnitRange implementa a interface Iterable . Dentro do bloco implement , as funções Iterable.start , Iterable.done e Iterable.next disponíveis, como se o usuário tivesse feito import Iterable: start, done, next , permitindo a adição de métodos a essas funções. Este bloco é semelhante a um modelo da maneira que as declarações de tipo paramétrico são - dentro do bloco, UnitRange significa um UnitRange , não o tipo guarda-chuva.

A principal vantagem do bloco implement é que ele evita a necessidade das funções import explicitamente que você deseja estender - elas são importadas implicitamente para você, o que é bom, pois as pessoas geralmente ficam confusas sobre import qualquer maneira. Esta parece ser uma forma muito mais clara de expressar isso. Suspeito que a maioria das funções genéricas em Base que os usuários desejam estender devem pertencer a alguma interface, então isso deve eliminar a grande maioria dos usos de import . Como você sempre pode qualificar totalmente um nome, talvez possamos eliminá-lo completamente.

Outra ideia que tive oscilando é a separação das versões "interna" e "externa" das funções de interface. O que quero dizer com isso é que a função "interna" é aquela para a qual você fornece métodos para implementar alguma interface, enquanto a função "externa" é aquela que você chama para implementar a funcionalidade genérica em termos de alguma interface. Considere quando você olhar para os métodos da função sort! (excluindo métodos obsoletos):

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

Alguns desses métodos são destinados ao consumo público, mas outros são apenas parte da implementação interna dos métodos de classificação públicos. Na verdade, o único método público que isso deve ter é este:

sort!(v::AbstractArray)

O resto é ruído e pertence ao "interior". Em particular, o

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)

tipos de métodos são o que um algoritmo de classificação implementa para se conectar ao mecanismo de classificação genérico. Atualmente Sort.Algorithm é um tipo abstrato e InsertionSortAlg , QuickSortAlg e MergeSortAlg são subtipos concretos dele. Com interfaces, Sort.Algorithm poderia ser uma interface e os algoritmos específicos a implementariam. Algo assim:

# 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

A separação que desejamos poderia então ser realizada definindo:

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

Isso é _muito_ perto do que estamos fazendo atualmente, exceto que chamamos Algorithm.sort! vez de apenas sort! - e ao implementar vários algoritmos de classificação, a definição "interna" é um método de Algorithm.sort! não a função sort! . Isso tem o efeito de separar a implementação de sort! de sua interface externa.

@StefanKarpinski Muito obrigado pelo seu artigo! Certamente não é 0.3. Sinto muito por ter tocado nisso neste momento. Só não tenho certeza se 0.3 acontecerá em breve ou em meio ano ;-)

À primeira vista, eu realmente (!) Gosto que a seção de implementação seja definida com seu próprio bloco de código. Isso permite verificar diretamente a interface na definição do tipo.

Não se preocupe - realmente não há mal nenhum em especular sobre recursos futuros enquanto tentamos estabilizar uma versão.

Sua abordagem é muito mais fundamental e tenta também resolver alguns problemas independentes de interface. Também meio que traz uma nova construção (ou seja, a interface) para a linguagem que a torna um pouco mais complexa (o que não é necessariamente uma coisa ruim).

Eu vejo "a interface" mais como uma anotação para tipos abstratos. Se alguém colocar has nele, pode-se especificar uma interface, mas não é necessário.

Como disse, gostaria muito que a interface pudesse ser validada diretamente em sua declaração. A abordagem menos invasiva aqui pode ser permitir a definição de métodos dentro de uma declaração de tipo. Então, tomando o seu exemplo, algo como

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

Ainda seria permitido definir a função fora da declaração de tipo. A única diferença seria que as declarações de funções internas são validadas em relação às interfaces.

Mas, novamente, talvez minha "abordagem menos invasiva" seja míope demais. Realmente não sei.

Um problema em colocar essas definições dentro do bloco de tipo é que, para fazer isso, realmente precisaremos de herança múltipla de interfaces, pelo menos, e é possível que possa haver colisões de nomes entre interfaces diferentes. Você também pode querer adicionar o fato de que um tipo suporta uma interface em algum ponto _após_ a definição do tipo, embora eu não tenha certeza sobre isso.

@StefanKarpinski É ótimo ver que você está pensando sobre isso.

O pacote Graphs é o que mais precisa do sistema de interface. Seria interessante ver como esse sistema pode expressar as interfaces descritas aqui: http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : Não vejo totalmente o problema com herança múltipla e declarações de função em bloco. Dentro do bloco de tipo, todas as interfaces herdadas devem ser verificadas.

Mas eu meio que entendo que alguém possa querer deixar a implementação da interface "aberta". E a declaração de função in-type pode complicar muito a linguagem. Talvez a abordagem que implementei no # 7025 seja suficiente. Coloque verify_interface após as declarações da função (ou em um teste de unidade) ou adie-o para MethodError .

Esse problema é que interfaces diferentes podem ter funções genéricas com o mesmo nome, o que pode causar uma colisão de nomes e exigir uma importação explícita ou adição de métodos por um nome totalmente qualificado. Também torna menos claro quais definições de método pertencem a quais interfaces - é por isso que a colisão de nomes pode ocorrer em primeiro lugar.

Aliás, eu concordo que adicionar interfaces como outra "coisa" na linguagem parece um pouco não ortogonal. Afinal, como mencionei na proposta, eles são um pouco como módulos e um pouco como tipos. Parece que alguma unificação de conceitos pode ser possível, mas não tenho certeza de como.

Eu prefiro o modelo de interface como biblioteca ao modelo de interface como recurso de linguagem por alguns motivos: ele mantém a linguagem mais simples (reconhecidamente preferência e não uma objeção concreta) e significa que o recurso permanece opcional e pode ser facilmente melhorado ou totalmente substituído sem sujar com a linguagem real.

Especificamente, acho que a proposta (ou pelo menos a forma da proposta) de @tknopp é melhor do que a de @StefanKarpinski - fornece verificação de tempo de definição sem exigir nada de novo na linguagem. A principal desvantagem que vejo é a falta de habilidade para lidar com variáveis ​​de tipo; Acho que isso pode ser resolvido fazendo com que a definição de interface forneça o tipo _predicates_ para os tipos de funções necessárias.

Uma das principais motivações para minha proposta é a grande confusão causada por ter que _importar_ funções genéricas - mas não exportá-las - a fim de adicionar métodos a elas. Na maioria das vezes, isso acontece quando alguém está tentando implementar uma interface não oficial, então parece que é isso que está acontecendo.

Parece um problema ortogonal a ser resolvido, a menos que você queira restringir totalmente os métodos para pertencer a interfaces.

Não, isso certamente não parece uma boa restrição.

@StefanKarpinski você mencionou que seria capaz de despachar em uma interface. Também na sintaxe implement a ideia é que um tipo específico implemente a interface.

Isso parece um pouco diferente do dispatch múltiplo, já que em geral os métodos não pertencem a um tipo específico, eles pertencem a uma tupla de tipos. Portanto, se os métodos não pertencem a tipos, como as interfaces (que são basicamente conjuntos de métodos) podem pertencer a um tipo?

Digamos que estou usando a biblioteca 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

agora eu quero escrever uma função genérica que leva um A e B

using M

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

Neste exemplo, a função f forma uma interface ad-hoc que pega A e B , e eu quero assumir que posso chamar f função neles. Nesse caso, não está claro qual deles deve ser considerado para implementar a interface.

Outros módulos que desejam fornecer subtipos concretos de A e B devem fornecer implementações de f . Para evitar a explosão combinatória de métodos necessários, eu esperaria que a biblioteca definisse f relação aos tipos abstratos:

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

Reconhecidamente, este exemplo parece muito artificial, mas espero que ilustre que (pelo menos na minha mente) parece que há uma incompatibilidade fundamental entre o despacho múltiplo e o conceito de um tipo específico de implementação de uma interface.

Eu entendo o que você quis dizer sobre a confusão de import . Levei algumas tentativas neste exemplo para lembrar que quando coloquei using M e, em seguida, tentei adicionar métodos a f ele não fez o que eu esperava e tive que adicionar os métodos para M.f (ou poderia ter usado import ). Não acho que as interfaces sejam a solução para esse problema. Existe uma questão separada para debater maneiras de tornar a adição de métodos mais intuitiva?

@abe-egnor Eu também acho que uma abordagem mais aberta parece mais viável. Meu protótipo # 7025 carece essencialmente de duas coisas:
a) uma sintaxe melhor para definir interfaces
b) definições de tipo paramétrico

Como não sou tanto um guru do tipo paramétrico, estou certo de que b) pode ser resolvido por alguém com experiência mais profunda.
Em relação a), pode-se usar uma macro. Pessoalmente, acho que poderíamos gastar algum suporte de linguagem para definir diretamente a interface como parte da definição de tipo abstrato. A abordagem has pode ser muito míope. Um bloco de código pode tornar isso mais agradável. Na verdade, isso está altamente relacionado ao # 4935, onde uma interface "interna" é definida enquanto esta é sobre a interface pública. Eles não precisam ser agrupados, pois acho que esse problema é muito mais importante do que o # 4935. Mas, mesmo assim, em termos de sintaxe, convém levar os dois casos de uso em consideração.

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 fez minha primeira tentativa no tipo de implementação que eu estava pensando. Resumindo, uma interface é uma função de tipos a dict que define o nome e os tipos de parâmetro das funções necessárias para essa interface. A macro @implement apenas chama a função para os tipos fornecidos e, em seguida, une os tipos nas definições de função fornecidas, verificando se todas as funções foram definidas.

Bons pontos:

  • Sintaxe simples para definição e implementação de interfaces.
  • Ortogonal a, mas funciona bem com outros recursos da linguagem.
  • O cálculo do tipo de interface pode ser arbitrariamente sofisticado (são apenas funções sobre os parâmetros do tipo de interface)

Pontos ruins:

  • Não funciona bem com tipos parametrizados se você quiser usar o parâmetro como um tipo de interface. Esta é uma desvantagem bastante significativa, mas não consigo ver uma maneira imediata de resolver isso.

Acho que tenho uma solução para o problema de parametrização - em resumo, a definição da interface deve ser uma macro sobre expressões de tipo, não uma função sobre valores de tipo. A macro @implement pode então estender os parâmetros de tipo para definições de função, permitindo algo como:

<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

Nesse caso, os parâmetros de tipo são estendidos aos métodos definidos na interface, de forma que ele se expande para stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , o que acredito ser exatamente a coisa certa.

Vou refazer minha implementação inicial para fazer isso quando tiver tempo; provavelmente na ordem de uma semana.

Eu naveguei um pouco na internet para ver o que outras linguagens de programação fazem sobre interfaces, herança e coisas do gênero e tive algumas idéias. (Caso alguém esteja interessado aqui, as notas muito aproximadas que fiz https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

Resumindo, talvez as interfaces possam ser implementadas por:

  • permitindo herança múltipla para tipos abstratos, e
  • permitindo funções genéricas como campos de tipos abstratos.

Isso transformaria os tipos abstratos em interfaces e os subtipos concretos seriam então necessários para implementar essa interface.

A longa história:

O que descobri é que algumas das linguagens "modernas" eliminam o polimorfismo de subtipo, ou seja, não há agrupamento direto de tipos e, em vez disso, agrupam seus tipos com base neles pertencendo a interfaces / características / classes de tipo. Em algumas linguagens, as interfaces / características / classes de tipo podem ter ordem entre eles e herdar umas das outras. Eles também parecem (principalmente) felizes com essa escolha. Os exemplos são: Go ,
Ferrugem , Haskell .
Go é o menos rígido dos três e permite que suas interfaces sejam especificadas implicitamente, ou seja, se um tipo implementa o conjunto específico de funções de uma interface, ele pertence a essa interface. Para Rust, a interface (características) deve ser implementada explicitamente em um bloco impl . Nem Go nem Rust têm métodos multimétodos. Haskell possui multimétodos e eles estão diretamente vinculados à interface (classe de tipo).

Em certo sentido, isso é semelhante ao que Julia também faz, os tipos abstratos são como uma interface (implícita), ou seja, eles tratam do comportamento e não dos campos. Isso é o que @StefanKarpinski também observou em uma de suas postagens acima e afirmou que, além disso, ter interfaces "parece um pouco não ortogonal". Portanto, Julia tem uma hierarquia de tipos (ou seja, polimorfismo de subtipo), enquanto Go / Rust / Haskell não.

Que tal transformar os tipos abstratos de Julia em mais de uma interface / característica / classe de tipo, enquanto mantém todos os tipos na hierarquia None<: ... <:Any ? Isso implicaria:
1) permitir herança múltipla para tipos (abstratos) (questão 5)
2) permitir a associação de funções com tipos abstratos (ou seja, definir uma interface)
3) Permitir a especificação dessa interface, tanto para os tipos abstratos (ou seja, uma implementação padrão) quanto para os concretos.

Acho que isso pode levar a um gráfico de tipo mais refinado do que temos agora e pode ser implementado passo a passo. Por exemplo, um tipo de matriz seria montado:

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

Assim, basicamente os tipos abstratos podem ter funções genéricas como campos (ou seja, tornar-se uma interface), enquanto os tipos concretos têm apenas campos normais. Isso pode, por exemplo, resolver o problema de muitas coisas serem derivadas de AbstractArray, já que as pessoas poderiam apenas escolher as peças úteis para seu contêiner em vez de derivar de AbstractArray.

Se isso for uma boa ideia, há muito a ser trabalhado (em particular como especificar tipos e parâmetros de tipo), mas talvez valha a pena pensar?

@ssfrr comentou acima que interfaces e envio múltiplo são incompatíveis. Isso não deveria ser o caso, pois, por exemplo, nos multimétodos Haskell só são possíveis usando classes de tipo.

Eu também descobri enquanto lia o artigo de @StefanKarpinski que usar diretamente abstract vez de interface pode fazer sentido. No entanto, neste caso, é importante que abstract herde uma propriedade crucial de interface : a possibilidade de um tipo implement an interface _after_ ser definido. Então, posso usar um tipo typA da lib A com um algoritmo algoB da lib B, declarando em meu código que typA implementa a interface exigida pelo algoB (acho que isso implica que os tipos concretos têm um tipo de herança múltipla aberta).

@ mauro3 , na verdade gostei muito da sua sugestão. Para mim, parece muito "juliano" e natural. Também acho que é uma integração única e poderosa de interfaces, herança múltipla e "campos" de tipo abstrato (embora, na verdade, não, já que os campos seriam apenas métodos / funções, não valores). Eu também acho que isso combina bem com a ideia de @StefanKarpinski de distinguir métodos de interface "internos" vs. "externos", já que você poderia implementar sua proposta para o exemplo sort! declarando abstract Algorithm e Algorithm.sort! .

desculpa a todos

------------------ 原始 邮件 ------------------
发件人: "Jacob Quinn" [email protected];
发送 时间: 2014 年 9 月 12 日 (星期五) 上午 6:23
收件人: "JuliaLang / julia" [email protected];
抄送: "Implementar" [email protected];
主题: Re: [julia] Interfaces para tipos abstratos (# 6975)

@ mauro3 , na verdade gostei muito da sua sugestão. Para mim, parece muito "juliano" e natural. Também acho que é uma integração única e poderosa de interfaces, herança múltipla e "campos" de tipo abstrato (embora, na verdade, não, já que os campos seriam apenas métodos / funções, não valores). Eu também acho que isso combina bem com a ideia de @StefanKarpinski de distinguir métodos de interface "internos" vs. "externos", já que você poderia implementar sua proposta para o tipo! exemplo, declarando Algorithm e Algorithm.sort! abstratos.

-
Responda a este e-mail diretamente ou visualize-o no GitHub.

@implement Sinto muito; não tenho certeza de como o pingamos. Se você ainda não sabia, pode remover a si mesmo dessas notificações usando o botão "Cancelar inscrição" no lado direito da tela.

Não, eu só quero dizer que não posso te ajudar muito a dizer sarry

------------------ 原始 邮件 ------------------
发件人: "pao" [email protected];
发送 时间: 2014 年 9 月 13 日 (星期六) 晚上 9:50
收件人: "JuliaLang / julia" [email protected];
抄送: "Implementar" [email protected];
主题: Re: [julia] Interfaces para tipos abstratos (# 6975)

@implement Sinto muito; não tenho certeza de como o pingamos. Se você ainda não sabia, pode remover a si mesmo dessas notificações usando o botão "Cancelar inscrição" no lado direito da tela.

-
Responda a este e-mail diretamente ou visualize-o no GitHub.

Não esperamos que você faça isso! Foi um acidente, já que estamos falando de uma macro Julia com o mesmo nome do seu nome de usuário. Obrigado!

Acabei de ver que existem alguns recursos potencialmente interessantes (talvez relevantes para este problema) trabalhados no Rust: http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , em particular: https :

Depois de ver THTT ("Tim Holy Trait Trick"), pensei mais sobre interfaces / características nas últimas semanas. Tive algumas ideias e uma implementação: Traits.jl . Em primeiro lugar, (acho) os traços devem ser vistos como um contrato envolvendo um ou vários tipos . Isso significa que apenas anexar as funções de uma interface a um tipo abstrato, como eu e outros sugerimos acima, não funciona (pelo menos não no caso geral de um traço envolvendo vários tipos). E segundo, os métodos devem ser capazes de usar traits para despacho , como @StefanKarpinski sugerido acima.

Nuff disse, aqui um exemplo usando meu pacote 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

Isso declara que Eq e Cmp são contratos entre os tipos X e Y . Cmp tem Eq como superestrito, ou seja, tanto Eq quanto Cmp precisam ser cumpridos. No corpo @traitdef , as assinaturas da função especificam quais métodos precisam ser definidos. Os tipos de retorno não fazem nada no momento. Os tipos não precisam implementar explicitamente uma característica, basta implementar as funções. Posso verificar se, digamos, Cmp{Int,Float64} é realmente uma característica:

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

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

A implementação explícita do traço ainda não está no pacote, mas deve ser bastante simples de adicionar.

Uma função usando _trait-dispatch_ pode ser definida assim

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

Isso declara uma função ft1 que recebe dois argumentos com a restrição de que seus tipos precisam cumprir Cmp{X,Y} . Posso adicionar outro método de despacho em outra característica:

<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

Essas funções de características agora podem ser chamadas como funções normais:

julia> ft1(4,5)
6

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

Adicionar outro tipo a uma característica posteriormente é fácil (o que não seria o caso usando Uniões para 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

_Implementação_ de funções trait e seu despacho é baseado no truque de Tim e nas funções encenadas, veja abaixo. A definição de traço é relativamente trivial, veja aqui uma implementação manual de tudo isso.

Em resumo, o despacho de traços transforma

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

em algo assim (um pouco simplificado)

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

No pacote, a geração de checkfn é automatizada por funções de estágio. Mas veja o README do Traits.jl para mais detalhes.

_Desempenho_ Para funções de características simples, o código de máquina produzido é idêntico às suas contrapartes digitadas em pato, ou seja, o melhor que pode acontecer. Para funções mais longas, existem diferenças, até ~ 20% no comprimento. Eu não tenho certeza porque eu pensei que tudo isso deveria ser destacado.

(editado em 27 de outubro para refletir pequenas alterações em Traits.jl )

O pacote Traits.jl está pronto para ser explorado? O readme diz "implemente interfaces com @traitimpl (não feito ainda ...)" - esta é uma lacuna importante?

Está pronto para ser explorado (incluindo bugs :-). O @traitimpl ausente significa apenas que em vez de

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

você apenas define a (s) função (ões) manualmente

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

para dois de seus tipos T1 e T2 .

Eu adicionei a macro @traitimpl , então o exemplo acima agora funciona. Eu também atualizei o README com detalhes sobre o uso. E eu adicionei um exemplo de implementação de parte da interface @lindahua Graphs.jl:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

Isso é muito legal. Gosto particularmente do fato de ele reconhecer que as interfaces em geral são propriedade de tuplas de tipos, não de tipos individuais.

Eu também acho isso muito legal. Há muito o que gostar nessa abordagem. Bom trabalho.

: +1:

Obrigado pelo bom feedback! Eu atualizei / refatorei o código um pouco e ele deve estar razoavelmente livre de bugs e bom para brincar.
Nesse ponto, provavelmente seria bom se as pessoas pudessem experimentar para ver se ele se encaixa em seus casos de uso.

Este é um daqueles pacotes que faz com que alguém olhe para seu próprio código sob uma nova luz. Muito legal.

Desculpe, ainda não tive tempo de examinar isso seriamente, mas sei que, assim que o fizer, vou querer refatorar algumas coisas ...

Vou refatorar meus pacotes também :)

Eu estava me perguntando, me parece que se os traços estão disponíveis (e permitem o envio múltiplo, como a sugestão acima), então não há necessidade de um mecanismo de hierarquia de tipo abstrato, ou de tipos abstratos. Este poderia ser?

Depois que as características são implementadas, cada função na base e posteriormente em todo o ecossistema acabaria por expor uma API pública baseada apenas nas características, e os tipos abstratos desapareceriam. Claro que o processo pode ser catalisado pela descontinuação de tipos abstratos

Pensando nisso um pouco mais, substituir tipos abstratos por características exigiria a parametrização de tipos como este:

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

Concordo com o ponto mauro3 acima, que ter traços (por sua definição, o que eu acho muito bom) é equivalente a tipos abstratos que

  • permitir herança múltipla, e
  • permite funções genéricas como campos

Eu também acrescentaria que, para permitir que os traços sejam atribuídos aos tipos após sua definição, também seria necessário permitir a "herança preguiçosa", isto é, dizer ao compilador que um tipo herda de algum tipo abstrato depois de definido.

então, de modo geral, parece-me que desenvolver algum conceito de traço / interface fora dos tipos abstratos induziria a alguma duplicação, introduzindo diferentes maneiras de alcançar a mesma coisa. Agora acho que a melhor maneira de introduzir esses conceitos é adicionando lentamente recursos aos tipos abstratos

EDITAR : é claro que em algum ponto herdar tipos concretos de abstratos teria que ser reprovado e, finalmente, proibido. As características de tipo seriam determinadas implícita ou explicitamente, mas nunca por herança

Os tipos abstratos não são apenas um exemplo "enfadonho" de características?

Em caso afirmativo, seria possível manter a sintaxe atual e simplesmente alterar seu significado para traço (dando liberdade ortogonal etc. se o usuário quiser)?

_Eu me pergunto se isso também pode resolver o exemplo Point{Float64} <: Pointy{Real} (não tenho certeza se há um número de problema)? _

Sim, acho que você está certo. A funcionalidade de traço pode ser alcançada aprimorando os tipos abstratos de julia atuais. Eles precisam
1) herança múltipla
2) assinaturas de função
3) "herança preguiçosa", para dar explicitamente a um tipo já definido uma nova característica

Parece muito trabalho, mas talvez isso possa ser desenvolvido lentamente, sem muitas interrupções para a comunidade. Então, pelo menos, conseguimos isso;)

Acho que o que escolhermos será uma grande mudança, que não estamos prontos para começar a trabalhar na 0.4. Se eu tivesse que adivinhar, apostaria que estamos mais propensos a nos mover na direção das características do que na direção de adicionar a herança múltipla tradicional. Mas minha bola de cristal está quebrada, então é difícil ter certeza do que vai acontecer sem apenas tentar as coisas.

FWIW, achei a discussão de Simon Peyton-Jones sobre typeclasses na palestra abaixo realmente informativa sobre como usar algo como traços em vez de subtipagem: http://research.microsoft.com/en-us/um/people/simonpj/ papers / haskell-retrospective / ECOOP-July09.pdf

Sim, uma lata inteira de minhocas!

@johnmyleswhite , obrigado pelo link, muito interessante. Aqui está um link para o vídeo dele, que vale a pena assistir para preencher as lacunas. Essa apresentação parece abordar muitas questões que recebemos aqui. E, curiosamente, a implementação de classes de tipo é muito semelhante ao que está em Traits.jl (truque de Tim, características sendo tipos de dados). A https://www.haskell.org/haskellwiki/Multi-parameter_type_class de Haskell é muito parecida com Traits.jl. Uma de suas perguntas na palestra é: "uma vez que adotamos os genéricos de todo o coração, ainda precisamos realmente de subtipagem." (genéricos são funções paramétricas-polimórficas, eu acho, veja ) Que é mais ou @skariel e @hayd estiveram refletindo acima.

Referindo-se a @skariel e @hayd , acho que os traços de parâmetro único (como em Traits.jl) são muito próximos aos tipos abstratos, exceto que eles podem ter outra hierarquia, ou seja, herança múltipla.

Mas os traços de multiparâmetros parecem um pouco diferentes, pelo menos eles estavam em minha mente. Como eu os vi, os parâmetros de tipo de tipos abstratos parecem ser principalmente sobre quais outros tipos estão contidos em um tipo, por exemplo, Associative{Int,String} diz que o dict contém Int keys e String valores. Considerando que Tr{Associative,Int,String}... diz que existe algum "contrato" entre Associative , Int Strings . Mas então, talvez Associative{Int,String} deva ser lido dessa forma também, ou seja, existem métodos como getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@ mauro3 O importante seria passar objetos do tipo Associative como argumento para uma função, para que ela pudesse criar a própria Associative{Int,String} :

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

Você chamaria isso, por exemplo, de f(Dict) .

@eschnett , desculpe, não entendo o que você quer dizer.

@ mauro3 acho que estava pensando de uma forma muito complicada; me ignore.

Eu atualizei Traits.jl com:

  • resolução de ambigüidades de traço
  • tipos associados
  • usando @doc para obter ajuda
  • melhores testes de métodos de especificações de características

Consulte https://github.com/mauro3/Traits.jl/blob/master/NEWS.md para obter detalhes. Feedback seja bem-vindo!

@ Rory-Finnegan criou um pacote de interface https://github.com/Rory-Finnegan/Interfaces.jl

Recentemente, discuti isso com @mdcfrancis e achamos que algo semelhante aos protocolos de Clojure seria simples e prático. Os recursos básicos são (1) os protocolos são um novo tipo de tipo, (2) você os define listando algumas assinaturas de método, (3) outros tipos os implementam implicitamente apenas por ter definições de método correspondentes. Você escreveria, por exemplo

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

e nós temos isa(Iterable, Protocol) e Protocol <: Type . Naturalmente, você pode despachar neles. Você pode verificar se um tipo implementa um protocolo usando T <: Iterable .

Aqui estão as regras de subtipagem:

sejam P, Q tipos de protocolo
seja T um tipo não protocolar

| entrada | resultado |
| --- | --- |
| P <: Qualquer | verdade |
| Inferior <: P | verdade |
| (união, união todos, var) <: P | usar regra normal; tratar P como um tipo de base |
| P <: (união, união, var) | usar regra normal |
| P <: P | verdade |
| P <: Q | métodos de verificação (Q) <: métodos (P) |
| P <: T | falso |
| T <: P | Os métodos de P existem com T substituído por _ |

O último é o maior: para testar T <: P, você substitui T por _ na definição de P e verifica method_exists para cada assinatura. É claro que, por si só, isso significa que as definições de fallback que geram erros do tipo "você deve implementar isso" tornam-se algo muito ruim. Esperançosamente, esta é uma questão mais cosmética.

Outro problema é que esta definição é circular se, por exemplo, start(::Iterable) for definido. Essa definição realmente não faz sentido. Poderíamos de alguma forma evitar isso ou detectar esse ciclo durante a verificação de subtipo. Não estou 100% certo de que a detecção de ciclo simples corrige isso, mas parece plausível.

Para interseção de tipo, temos:

| entrada | resultado |
| --- | --- |
| P ∩ (união, união, tvar) | usar regra normal |
| P ∩ Q | P |
| P ∩ T | T |

Existem algumas opções para P ∩ Q:

  1. Sobre-aproxime retornando P ou Q (por exemplo, o que for lexicograficamente primeiro). Isso é válido em relação à inferência de tipo, mas pode ser irritante em outro lugar.
  2. Retorne um novo protocolo ad-hoc que contém a união das assinaturas em P e Q.
  3. Tipos de interseção. Talvez restrito apenas a protocolos.

P ∩ T é complicado. T é uma boa aproximação conservadora, uma vez que os tipos não-protocolo são "menores" do que os tipos de protocolo no sentido de que eles restringem você a uma região da hierarquia de tipo, enquanto os tipos de protocolo não (uma vez que qualquer tipo pode implementar qualquer protocolo ) Fazer melhor do que isso parece exigir tipos gerais de interseção, o que eu prefiro evitar na implementação inicial, pois isso requer a revisão do algoritmo de subtipagem e abre worm-can após worm-can.

Especificidade: P é apenas mais específico do que Q quando P <: Q. mas uma vez que P ∩ Q é sempre não vazio, as definições com diferentes protocolos no mesmo slot são frequentemente ambíguas, o que parece o que você gostaria (por exemplo, você estaria dizendo "se x é Iterável faça isso, mas se x é Imprimível faça naquela").
No entanto, não há uma maneira prática de expressar a definição de eliminação de ambigüidade necessária, então isso pode ser um erro.

Depois de # 13412, um protocolo pode ser "codificado" como UnionAll _ em uma união de tipos de tupla (onde o primeiro elemento de cada tupla interna é o tipo da função em questão). Este é um benefício daquele design que não me ocorreu antes. Por exemplo, os subtipos estruturais de protocolos parecem simplesmente falhar automaticamente.

Obviamente, esses protocolos são do estilo de "parâmetro único". Eu gosto da simplicidade disso, além de não ter certeza de como lidar com grupos de tipos tão elegantemente quanto T <: Iterable .

Houve alguns comentários em torno dessa ideia no passado, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516.

Nós apoiaríamos, por exemplo

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

Nossa, eu realmente gosto disso (especialmente com a extensão do @Keno )!

+1 Isso é exatamente o que eu quero!

@Keno Esse é definitivamente um bom caminho de atualização para esse recurso, mas há razões para adiá-lo. Qualquer coisa que envolva tipos de retorno é obviamente muito problemática. O parâmetro em si é conceitualmente bom e seria incrível, mas é um pouco difícil de implementar. Requer a manutenção de um ambiente de tipos em torno do processo que verifica a existência de todos os métodos.

Parece que você poderia calçar os traços (como indexação linear O (1) para tipos do tipo array) neste esquema. Você definiria um método fictício como hassomeproperty(::T) = true (mas _não_ hassomeproperty(::Any) = false ) e então teria

protocol MyProperty
hassomeproperty(::_)
end

Pode _ aparecer várias vezes no mesmo método na definição do protocolo, como

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

Pode _ aparecer várias vezes no mesmo método na definição do protocolo

sim. Basta inserir o tipo de candidato para cada instância de _ .

@JeffBezanson realmente ansioso por isso. De particular interesse para mim é o 'afastamento' do protocolo. Nesse sentido, posso implementar um protocolo específico / personalizado para um tipo sem que o autor do tipo tenha qualquer conhecimento da existência do protocolo.

E quanto ao fato de que os métodos podem ser definidos dinamicamente (por exemplo, com @eval ) a qualquer momento? Então, se um tipo é um subtipo de um determinado protocolo não pode ser conhecido estaticamente em geral, o que parece frustrar as otimizações que evitam o despacho dinâmico em muitos casos.

Sim, isso torna # 265 pior :) É o mesmo problema em que o despacho e o código gerado precisam ser alterados quando os métodos são adicionados, apenas com mais bordas de dependência.

É bom ver esse avanço! Claro, eu seria o único a argumentar que as características de multiparâmetros são o caminho a seguir. Mas 95% das características provavelmente seriam parâmetros únicos de qualquer maneira. É que caberiam tão bem com envio múltiplo! Isso provavelmente poderia ser revisado mais tarde, se necessário. Disse o suficiente.

Alguns comentários:

A sugestão de @Keno (e realmente state no original de Jeff) é conhecida como tipos associados. Observe que eles também são úteis sem tipos de retorno. Rust tem uma entrada manual decente. Acho que são uma boa ideia, embora não tão necessários como em Rust. Eu não acho que deveria ser um parâmetro da característica: ao definir uma função despachada em Iterable eu não saberia o que T é.

Na minha experiência, method_exists ser usado em sua forma atual para isso (# 8959). Mas provavelmente isso será corrigido em # 8974 (ou com isso). Eu descobri que a correspondência de assinaturas de método com características-siganturas é a parte mais difícil ao fazer Traits.jl, especialmente para contabilizar funções parametrizadas e vararg ( consulte Recursos).

Presumivelmente, a herança também seria possível?

Eu realmente gostaria de ver um mecanismo que permite a definição de implementações padrão. O clássico é que, para comparação, você só precisa definir dois de = , < , > , <= , >= . Talvez seja aqui que o ciclo mencionado por Jeff seja realmente útil. Continuando o exemplo acima, definir start(::Indexable) = 1 e done(i::Indexable,state)=length(i)==state tornaria esses os padrões. Assim, muitos tipos precisariam apenas definir next .

Bons pontos. Acho que os tipos associados são um pouco diferentes do parâmetro em Iterable{T} . Na minha codificação, o parâmetro apenas quantificaria existencialmente sobre tudo dentro --- "existe um T tal que o tipo Foo implementa este protocolo?".

Sim, parece que poderíamos facilmente permitir protocol Foo <: Bar, Baz e simplesmente copiar as assinaturas de Bar e Baz para Foo.

Traços de multiparâmetros são definitivamente poderosos. Acho muito interessante pensar em como integrá-los à subtipagem. Você poderia ter algo como TypePair{A,B} <: Trait , mas isso não parece muito certo.

Acho que sua proposta (em termos de recursos) é na verdade mais parecida com Swift do que com Clojure.

Parece estranho (e acho que é uma fonte de confusão futura) misturar subtipos nominais (tipos) e estruturais (protocolo) (mas acho que isso é inevitável).

Também sou um pouco cético quanto ao poder expressivo dos protocolos para operações matemáticas / matriciais. Acho que pensar em exemplos mais complicados (operações de matriz) seria mais esclarecedor do que Iteração, que tem uma interface claramente especificada. Veja, por exemplo, a biblioteca core.matrix .

Eu concordo; neste ponto, devemos coletar exemplos de protocolos e ver se eles fazem o que queremos.

Da maneira como você está imaginando, os protocolos seriam namespaces aos quais seus métodos pertencem? Ou seja, quando você escreve

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

pareceria natural para isso definir as funções genéricas start , done e next e seus nomes totalmente qualificados serem Iterable.start , Iterable.done e Iterable.next . Um tipo implementaria Iterable mas implementaria todas as funções genéricas no protocolo Iterable . Eu propus algo muito semelhante a isso há algum tempo (não consigo encontrar agora), mas com o outro lado sendo que quando você quer implementar um protocolo, você faz o seguinte:

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

Isso neutralizaria o "afastamento" que @mdcfrancis mencionou, se estou entendendo, mas, novamente, não vejo realmente o benefício de ser capaz de implementar um protocolo "acidentalmente". Você pode explicar por que acha isso benéfico, @mdcfrancis? Sei que Go faz muito isso, mas parece que é porque Go não consegue digitar como o pato, o que Julia faz. Suspeito que ter blocos de implement eliminaria quase todas as necessidades de usar import vez de using , o que seria um grande benefício.

Propus algo muito semelhante a isto há algum tempo (não consigo encontrar agora)

Talvez https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 e anterior https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Editar: também https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

Sim, é isso.

Comentários rápidos de

  • todas as classes que atualmente implementam iterável terão que ser modificadas para implementar explicitamente o protocolo se fizermos como você propõe, a proposta atual, simplesmente adicionando a definição à base, 'elevará' todas as classes existentes ao protocolo.
  • se eu definir MyModule.MySuperIterable, que adiciona uma função extra à definição iterável, terei que escrever um monte de código padrão para cada classe, em vez de adicionar um método adicional.
  • Não acho que o que você propõe neutraliza o afastamento, apenas significa que eu teria que escrever um monte de código adicional para atingir o mesmo objetivo.

Se algum tipo de herança em protocolos fosse permitido, MySuperIterabe,
poderia estender Base.Iterable, a fim de reutilizar os métodos existentes.

O problema seria se você quisesse apenas uma seleção dos métodos em um
protocolo, mas isso parece indicar que o protocolo original deve
ser um protocolo composto desde o início.

@mdcfrancis - o primeiro ponto é bom, embora o que estou propondo não quebrasse nenhum código existente, significaria apenas que o código das pessoas teria que "aceitar" os protocolos de seus tipos antes de poderem contar com o despacho trabalhando.

Você pode expandir no ponto MyModule.MySuperIterable? Não estou vendo de onde vem a verbosidade extra. Você poderia ter algo assim, por exemplo:

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

Que é essencialmente o que @ivarne disse.

Em meu projeto específico acima, os protocolos não são namespaces, apenas declarações sobre outros tipos e funções. No entanto, isso provavelmente se deve ao fato de que estou focando no sistema de tipo principal. Eu poderia imaginar a sintaxe de açúcar que se expande para uma combinação de módulos e protocolos, por exemplo

module Iterable

function start end
function done end
function next end

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

end

Então, em contextos onde Iterable é tratado como um tipo, usamos Iterable.the_protocol .

Gosto dessa perspectiva porque os protocolos jeff / mdcfrancis parecem muito ortogonais a tudo o mais aqui. A sensação leve de não precisar dizer "X implementa o protocolo Y", a menos que você queira parecer "juliano" para mim.

Não sei por que me inscrevi nesta edição e quando o fiz. Mas acontece que esta proposta de protocolo pode resolver a questão que coloquei aqui .

Não tenho nada a acrescentar em uma base técnica, mas como um exemplo de "protocolos" sendo usados ​​em estado selvagem em Julia (mais ou menos) seria o JuMP determinar a funcionalidade de um solucionador, por exemplo:

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

Legal, isso é útil. É suficiente que m.internalModel seja o que implementa o protocolo ou os dois argumentos são importantes?

Sim, é suficiente para m.internalModel implementar o protocolo. Os outros argumentos são, em sua maioria, apenas vetores.

Sim, o suficiente para m.internalModel implementar o protocolo

Uma boa maneira de encontrar exemplos de protocolos em uso é provavelmente procurando por chamadas applicable e method_exists .

Elixir também parece implementar protocolos, mas o número de protocolos na biblioteca padrão (saindo da definição) parece bastante limitado.

Qual seria a relação entre protocolos e tipos abstratos? A descrição do problema original propunha algo como anexar um protocolo a um tipo abstrato. Na verdade, parece-me que a maioria dos protocolos (agora informais) que existem atualmente são implementados como tipos abstratos. Para que os tipos abstratos seriam usados ​​quando o suporte para protocolos fosse adicionado? Uma hierarquia de tipos sem nenhuma maneira de declarar sua API não parece muito útil.

Ótima pergunta. Existem muitas opções lá. Em primeiro lugar, é importante ressaltar que os tipos e protocolos abstratos são bastante ortogonais, embora ambos sejam formas de agrupar objetos. Os tipos abstratos são puramente nominais; eles marcam objetos como pertencentes ao conjunto. Os protocolos são puramente estruturais; um objeto pertence ao conjunto se tiver certas propriedades. Então, algumas opções são

  1. Basta ter os dois.
  2. Ser capaz de associar protocolos a um tipo abstrato, por exemplo, de modo que quando um tipo se declara um subtipo, ele é verificado quanto à conformidade com o (s) protocolo (s).
  3. Remova os tipos abstratos completamente.

Se tivermos algo como (2), acho importante reconhecer que não é realmente um único recurso, mas uma combinação de tipagem nominal e estrutural.

Uma coisa para a qual os tipos abstratos parecem úteis são seus parâmetros, por exemplo, escrever convert(AbstractArray{Int}, x) . Se AbstractArray fosse um protocolo, o tipo de elemento Int não precisaria necessariamente ser mencionado na definição do protocolo. É uma informação extra sobre o tipo, _além_ do qual os métodos são necessários. Portanto, AbstractArray{T} e AbstractArray{S} ainda seriam tipos diferentes, apesar de especificar os mesmos métodos, portanto, reintroduzimos a digitação nominal. Portanto, esse uso de parâmetros de tipo parece exigir algum tipo de digitação nominal.

Então, 2. nos daria herança abstrata múltipla?

Então, 2. nos daria herança abstrata múltipla?

Não. Seria uma forma de integrar ou combinar os recursos, mas cada recurso ainda teria as propriedades que possui agora.

Devo acrescentar que permitir a herança abstrata múltipla é outra decisão de projeto quase ortogonal. Em qualquer caso, o problema com o uso excessivo de tipos nominais abstratos é (1) você pode perder a implementação de protocolos após o fato (a pessoa A define o tipo, a pessoa B define o protocolo e sua implementação para A), (2) você pode perder subtipos estruturais de protocolos.

Os parâmetros de tipo no sistema atual não fazem parte da interface implícita? Por exemplo, esta definição se baseia em: ndims{T,n}(::AbstractArray{T,n}) = n e muitas funções definidas pelo usuário também.

Portanto, em um novo protocolo + sistema de herança abstrata, teríamos AbstractArray{T,N} e ProtoAbstractArray . Agora, um tipo que nominalmente não fosse AbstractArray precisaria ser capaz de especificar quais são os parâmetros T e N , presumivelmente por meio de codificação rígida eltype e ndims . Então, todas as funções parametrizadas em AbstractArray s precisariam ser reescritas para usar eltype e ndims vez de parâmetros. Então, talvez faça mais sentido ter o protocolo carregando os parâmetros também, então os tipos associados podem ser muito úteis afinal. (Observe que os tipos concretos ainda precisam de parâmetros.)

Além disso, um agrupamento de tipos em um protocolo utilizando @malmaud 's truque: https://github.com/JuliaLang/julia/issues/6975#issuecomment -161056795 é semelhante a tipagem nominal: o agrupamento é exclusivamente devido a colheita e tipos os tipos não compartilham nenhuma interface (utilizável). Então, talvez os tipos e protocolos abstratos se sobreponham um pouco?

Sim, os parâmetros de um tipo abstrato são definitivamente um tipo de interface e, até certo ponto, redundantes com eltype e ndims . A principal diferença parece ser que você pode despachá-los diretamente, sem uma chamada de método extra. Concordo que, com tipos associados, estaríamos muito mais próximos de substituir tipos abstratos por protocolos / características. Qual seria a aparência da sintaxe? Idealmente, seria mais fraco do que a chamada de método, uma vez que prefiro não ter uma dependência circular entre subtipagem e chamada de método.

A questão restante é se é útil implementar um protocolo _sem_ tornar-se parte do tipo abstrato relacionado. Um exemplo pode ser strings, que são iteráveis ​​e indexáveis, mas geralmente são tratadas como quantidades "escalares" em vez de contêineres. Não sei com que frequência isso surge.

Acho que não entendo muito bem sua declaração de "chamada de método". Portanto, esta sugestão de sintaxe pode não ser o que você pediu:

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

Isso pode funcionar, dependendo de como os subtipos dos tipos de protocolo são definidos. Por exemplo, dado

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

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

nós temos PAbstractArray{Int,1} <: Indexable{Int} ? Acho que isso poderia funcionar muito bem se os parâmetros fossem combinados por nome. Nós também poderíamos talvez automatizar a definição que torna eltype(x) devolver o eltype parâmetro de x 's tipo.

Eu particularmente não gosto de colocar definições de método dentro de um bloco impl , principalmente porque uma única definição de método pode pertencer a vários protocolos.

Portanto, parece que, com esse mecanismo, não precisaríamos mais de tipos abstratos. AbstractArray{T,N} pode se tornar um protocolo. Então, obtemos automaticamente herança múltipla (de protocolos). Além disso, a impossibilidade de herdar de tipos concretos (que é uma reclamação que às vezes ouvimos de novatos) é óbvia, já que apenas a herança de protocolo seria suportada.

À parte: seria muito bom ser capaz de expressar o traço Callable . Teria que ser parecido com isto:

protocol Callable
    ::TupleCons{_, Bottom}
end

onde TupleCons corresponde separadamente ao primeiro elemento de uma tupla e ao restante dos elementos. A ideia é que isso corresponda desde que a tabela de métodos para _ não esteja vazia (Bottom é um subtipo de todo tipo de tupla de argumento). Na verdade, podemos querer fazer Tuple{a,b} sintaxe para TupleCons{a, TupleCons{b, EmptyTuple}} (veja também # 11242).

Eu não acho que seja verdade, todos os parâmetros de tipo são quantificados existencialmente _com restrições_ de modo que os tipos e protocolos abstratos não são diretamente substituíveis.

@jakebolewski você pode pensar em um exemplo? Obviamente, eles nunca serão exatamente a mesma coisa; Eu diria que a questão é mais se podemos massagear um de forma que possamos sobreviver sem ter os dois.

Talvez eu esteja perdendo o ponto, mas como os protocolos podem codificar tipos abstratos moderadamente complexos com restrições, como:

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

sem ter que enumerar nominalmente todas as possibilidades?

A proposta Protocol proposta é estritamente menos expressiva em comparação com a subtipagem abstrata, que é tudo o que eu estava tentando destacar.

Eu poderia imaginar o seguinte (naturalmente, estendendo o design até seus limites práticos):

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

indo junto com a observação de que precisamos de algo como tipos associados ou propriedades de tipo nomeado para corresponder à expressividade dos tipos abstratos existentes. Com isso, podemos ter quase compatibilidade potencial:

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

Subtipagem estrutural para os campos de dados de objetos nunca me pareceu muito útil, mas aplicada às propriedades de _tipos_ em vez disso, parece fazer muito sentido.

Eu também percebi que isso pode fornecer uma saída para problemas de ambigüidade: a interseção de dois tipos está vazia se eles tiverem valores conflitantes para algum parâmetro. Então, se quisermos um tipo Number inequívoco, poderíamos ter

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

Isso está vendo super apenas como outro tipo de propriedade.

Eu gosto da sintaxe do protocolo proposto, mas tenho algumas notas.

Mas então posso estar entendendo mal tudo. Só recentemente comecei a olhar para Julia como algo em que quero trabalhar, e ainda não tenho uma compreensão perfeita do sistema de tipos.

(a) Eu acho que seria mais interessante com os recursos de trait @ mauro3 trabalhados acima. Especialmente porque de que serve o despacho múltiplo, se você não pode ter vários protocolos de despacho! Vou escrever minha visão do que é um exemplo do mundo real mais tarde. Mas o resumo geral disso se resume a "Existe um comportamento que permite que esses dois objetos interajam". Posso estar enganado, e tudo isso pode ser encerrado em protocolos, digamos por:

protocol Foo{bar}
    ...
end

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

E isso também expõe o problema principal de não permitir que o protocolo Foo faça referência ao protocolo Bar na mesma definição.

(b)

temos PAbstractArray {Int, 1} <: Indexable {Int}? Acho que isso poderia funcionar muito bem se os parâmetros fossem combinados por nome.

Não sei por que temos que combinar os parâmetros por _nome_ (estou assumindo que são os eltype nomes, se não entendi, ignore esta seção). Por que não apenas combinar as assinaturas de funções potenciais. Meu principal problema com a nomenclatura é porque ela evita o seguinte:

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

Por outro lado, ele garante que seu protocolo exponha apenas a hierarquia de tipo específica que desejamos. Se não correspondermos com o nome Iterable , não obteremos os benefícios de implementar iterável (e também não desenharemos uma borda na dependência). Mas não tenho certeza do que um usuário _ganha_s disso, além da capacidade de fazer o seguinte ...

(c) Então, posso estar perdendo algo, mas o objetivo principal em que os tipos nomeados são úteis não é descrever como as diferentes partes de um superconjunto se comportam? Considere a hierarquia Number e os tipos abstratos Signed e Unsigned , ambos implementariam o protocolo Integer , mas às vezes se comportariam de maneira bem diferente. Para distinguir entre eles, somos agora forçados a definir um negate em apenas Signed tipos (especialmente difícil sem tipos de retorno onde podemos realmente querer negar um tipo Unsigned )?

Acho que esse é o problema que você descreve no exemplo super = Number . Quando declaramos bitstype Int16 <: Signed (minha outra pergunta é até mesmo como Number ou Signed como protocolos com suas propriedades de tipo são aplicados ao tipo concreto?) Isso anexa as propriedades de tipo de o protocolo Signed ( super = Signed ) marcando-o como sendo diferente dos tipos marcados pelo protocolo Unsigned ? Porque essa é uma solução estranha do meu ponto de vista, e não apenas porque acho estranhos os parâmetros de tipo nomeados. Se dois protocolos correspondem exatamente, exceto o tipo que eles colocaram em super, como eles são diferentes de qualquer maneira? E se a diferença está nos comportamentos entre subconjuntos de um tipo maior (o protocolo), então não estamos realmente apenas reinventando o propósito dos tipos abstratos?

(d) O problema é que queremos tipos abstratos para diferenciar o comportamento e queremos protocolos para garantir certas capacidades (frequentemente independentemente de outro comportamento), por meio das quais os comportamentos são expressos. Mas estamos tentando complementar as capacidades que os protocolos nos permitem garantir e a partição de tipos abstratos de comportamento.

A solução para a qual estamos frequentemente pulando é ao longo das linhas de "fazer com que os tipos declarem sua intenção de implementar uma classe abstrata e verificar a conformidade", que é problemática na implementação (referências circulares, levando à adição de definições de função dentro do bloco de tipo ou impl blocos), e remove a boa qualidade dos protocolos baseados no conjunto atual de métodos e os tipos nos quais eles operam. De qualquer forma, esses problemas impedem a colocação de protocolos na hierarquia abstrata.

Porém, o mais importante é que os protocolos não descrevem o comportamento, eles descrevem recursos complexos em várias funções (como iteração). O comportamento dessa iteração é descrito pelos tipos abstratos (seja classificado ou mesmo ordenado, por exemplo). Por outro lado, a combinação de protocolo + tipo abstrato é útil uma vez que podemos colocar as mãos em um tipo real porque nos permite despachar recursos (métodos de utilitário de capacidade), comportamentos (métodos de alto nível) ou ambos (detalhes de implementação métodos).

(e) Se permitirmos que os protocolos herdem vários protocolos (eles são basicamente estruturais de qualquer maneira) e tantos tipos abstratos quanto tipos concretos (por exemplo, sem herança abstrata múltipla, um), podemos permitir a criação de tipos de protocolo puros, tipos abstratos puros, e protocolo + tipos abstratos.

Acredito que isso corrige o problema Signed vs. Unsigned acima:

  • Defina dois protocolos, ambos herdando do IntegerProtocol (herdando qualquer estrutura de protocolo, NumberAddingProtocol , IntegerSteppingProtocol , etc), um do AbstractSignedInteger e o outro do AbstractUnsignedInteger ).
  • Então, um usuário do tipo Signed tem garantia de funcionalidade (do protocolo) e comportamento (da hierarquia abstrata).
  • Um tipo de concreto AbstractSignedInteger sem os protocolos não pode ser usado _de modo algum_.
  • Mas, curiosamente (e como um recurso futuro já mencionado acima), poderíamos eventualmente criar a capacidade de resolver os recursos ausentes, se apenas o IntegerSteppingProtocol (que é trivial e basicamente apenas um apelido para uma única função) existisse para um dado AbstractUnsignedInteger concreto, poderíamos tentar resolver Signed implementando os outros protocolos em seus termos. Talvez até com algo como convert .

Enquanto mantém todos os tipos existentes, transformando a maioria deles em protocolo + tipos abstratos, e deixando alguns como tipos abstratos puros.

Editar: (f) Exemplo de sintaxe (incluindo a parte (a) ).

Edição 2 : corrigiu alguns erros ( :< vez de <: ), corrigiu uma escolha ruim ( Foo vez de ::Foo )

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

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

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

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

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

Vejo problemas com esta sintaxe como:

  • Tipos internos anônimos para o protocolo (por exemplo, as variáveis ​​de estado).
  • Tipos de retorno.
  • Difícil de implementar a semântica de forma eficiente.

Define type_ _Abstract que uma entidade está. _Protocol_ define o que uma entidade faz . Dentro de um único pacote, esses dois conceitos são intercambiáveis: uma entidade _é_ o que _faz_. E o "tipo abstrato" é mais direto. Porém, entre dois pacotes, há uma diferença: você não exige o que seu cliente "é", mas exige o que seu cliente "faz". Aqui, "tipo abstrato" não fornece informações sobre ele.

Em minha opinião, um protocolo é um único tipo abstrato despachado. Pode ajudar na extensão e cooperação do pacote. Assim, dentro de um único pacote, onde as entidades estão intimamente relacionadas, use o tipo abstrato para facilitar o desenvolvimento (lucrando com o despacho múltiplo); entre os pacotes, onde as entidades são mais independentes, use o protocolo para reduzir a exposição da implementação.

@ mason-bially

Não tenho certeza por que temos que combinar os parâmetros por nome

Quero dizer correspondência por nome _em oposição a_ correspondência por posição. Esses nomes atuariam como registros com subtipos estruturais. Se tiver-mos

protocol Collection{T}
    eltype = T
end

então, qualquer coisa com uma propriedade chamada eltype é um subtipo de Collection . A ordem e a posição desses "parâmetros" não importa.

Se dois protocolos correspondem exatamente, exceto o tipo que eles colocaram em super, como eles são diferentes de qualquer maneira? E se a diferença está nos comportamentos entre subconjuntos de um tipo maior (o protocolo), então não estamos realmente apenas reinventando o propósito dos tipos abstratos?

Esse é um ponto justo. Os parâmetros nomeados, de fato, trazem de volta muitas das propriedades dos tipos abstratos. Eu estava começando com a ideia de que talvez precisássemos ter protocolos e tipos abstratos, tentando unificar e generalizar os recursos. Afinal, quando você declara type Foo <: Bar atualmente, em algum nível o que você realmente fez é definir Foo.super === Bar . Então, talvez devêssemos oferecer suporte a isso diretamente, junto com quaisquer outros pares de chave / valor que você queira associar.

"fazer com que os tipos declarem sua intenção de implementar uma classe abstrata e verificar a conformidade"

Sim, sou contra fazer dessa abordagem o recurso principal.

Se permitirmos que os protocolos herdem vários protocolos ... e tantos tipos abstratos

Isso significa dizer, por exemplo, "T é um subtipo do protocolo P se tiver os métodos x, y, z e se declarar um subtipo de AbstractArray"? Acho que esse tipo de "protocolo + tipo abstrato" é muito semelhante ao que você obteria com minha proposta de propriedade super = T . Admito que, em minha versão, ainda não descobri como encadea-los para introduzir uma hierarquia como a que temos agora (por exemplo, Integer <: Real <: Number ).

Ter um protocolo herdado de um tipo abstrato (nominal) parece ser uma restrição muito forte para ele. Haveria subtipos do tipo abstrato que _não_ implementariam o protocolo? Meu pressentimento é que é melhor manter os protocolos e tipos abstratos como coisas ortogonais.

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

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

Eu não entendo essa sintaxe.

  • Este protocolo tem um nome?
  • O que as coisas dentro de { } e ( ) significam exatamente?
  • Como você usa este protocolo? Você pode despachar nele? Em caso afirmativo, o que significa definir f(x::ThisProtocol)=... , visto que o protocolo relaciona vários tipos?

então, qualquer coisa com uma propriedade chamada eltype é um subtipo de Collection. A ordem e a posição desses "parâmetros" não importa.

Aha, aí foi meu mal-entendido, isso faz mais sentido. A saber, a capacidade de atribuir:

el1type = el_type
el2type = el_type

para resolver meu problema de exemplo.

Então, talvez devêssemos oferecer suporte a isso diretamente, junto com quaisquer outros pares de chave / valor que você queira associar.

E esse recurso de chave / valor estaria em todos os tipos, pois substituiríamos abstrato por ele. Essa é uma boa solução geral. Sua solução faz muito mais sentido para mim agora.

Admito que em minha versão ainda não descobri como encadea-los em uma hierarquia como a que temos agora (por exemplo, Integer <: Real <: Number).

Eu acho que você poderia usar super (por exemplo, com Integer super como Real ) e então tornar super especial e agir como um tipo nomeado ou adicionar uma maneira de adicionar código de resolução de tipo personalizado (ala python) e criar uma regra padrão para o parâmetro super .

Ter um protocolo herdado de um tipo abstrato (nominal) parece ser uma restrição muito forte para ele. Haveria subtipos do tipo abstrato que não implementassem o protocolo? Meu pressentimento é que é melhor manter os protocolos e tipos abstratos como coisas ortogonais.

Sim, a restrição abstrata era totalmente opcional! Meu ponto principal é que os protocolos e tipos abstratos são ortogonais. Você usaria o protocolo abstrato + para ter certeza de obter uma combinação de certos comportamentos _e_ recursos associados. Se você quiser apenas os recursos (para funções utilitárias) ou apenas o comportamento, use-os ortogonalmente.

Este protocolo tem um nome?

Dois protocolos com dois nomes ( Foo e Bar ) que vêm de um bloco, mas estou acostumado a usar macros para expandir várias definições como essa. Esta parte da minha sintaxe foi uma tentativa de resolver a parte (a) . Se você ignorar isso, a primeira linha pode ser simplesmente protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (com outra definição separada para o protocolo Bar ). Além disso, todos de Number , AbstractBar e AbstractFoo seriam opcionais, como nas definições de tipo normal,

O que as coisas dentro de {} e () significam exatamente?

O {} é a seção de definição de tipo paramétrico padrão. Permitindo o uso de Foo{Float64} para descrever um tipo que implementa o protocolo Foo usando Float64 por exemplo. O () é basicamente uma lista de ligação variável para o corpo do protocolo (portanto, vários protocolos podem ser descritos de uma vez). Provavelmente a sua confusão é minha porque eu digitei :< incorretamente em vez de <: no meu original. Também pode valer a pena trocá-los para manter a estrutura <<name>> <<parametric>> <<bindings>> , onde <<name>> pode às vezes ser uma lista de ligações.

Como você usa este protocolo? Você pode despachar nele? Em caso afirmativo, o que significa definir f(x::ThisProtocol)=... , visto que o protocolo relaciona vários tipos?

Na minha opinião, seu exemplo de envio parece correto para sua sintaxe; na verdade, considere as seguintes definições:

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)

Efetivamente, os protocolos estão usando o tipo Top nomeado (Any), a menos que seja fornecido um tipo abstrato mais específico para verificar a estrutura. De fato, pode valer a pena permitir algo como typealias Foo Intersect{FooProtocol, Foo} (_Edit: Intersect era o nome errado, talvez Join, em vez de Intersect, estava certo da primeira vez_) em vez de usar a sintaxe de protocolo para fazer isso.

Ah, ótimo, isso faz muito mais sentido para mim agora! Definir vários protocolos juntos no mesmo bloco é interessante; Terei que pensar um pouco mais sobre isso.

Limpei todos os meus exemplos alguns minutos atrás. No início do tópico, alguém mencionou a coleta de um corpus de protocolos para testar ideias, acho que é uma ótima ideia.

O protocolo múltiplo no mesmo bloco é uma espécie de implicância quando tento descrever relacionamentos complexos entre objetos com anotações de tipo corretas em ambos os lados em definir / compilar conforme você carrega linguagens (por exemplo, como python; Java, por exemplo, não tenho o problema). Por outro lado, a maioria deles provavelmente são facilmente corrigidos, em termos de usabilidade, com vários métodos de qualquer maneira; mas as considerações de desempenho podem resultar de ter os recursos digitados corretamente nos protocolos (otimizando os protocolos por meio da especialização deles em vtables, digamos).

Você mencionou anteriormente que os protocolos podem ser (espúrios) implementados por métodos usando ::Any . Acho que seria um caso muito simples de simplesmente ignorar se fosse necessário. O tipo concreto não seria classificado como um protocolo se o método de implementação fosse despachado em ::Any . Por outro lado, não estou convencido de que isso é necessariamente um problema.

Para começar, se o método ::Any for adicionado após o fato (digamos, porque alguém surgiu com um sistema mais genérico para lidar com ele) ainda é uma implementação válida, e se usarmos protocolos como um recurso de otimização, então versões especializadas de ::Any métodos despachados ainda funcionam para ganhos de desempenho. Então, no final, eu realmente seria contra ignorá-los.

Mas pode valer a pena ter uma sintaxe que permita ao definidor do protocolo escolher entre as duas opções (qualquer que seja a padrão, permite a outra). Para o primeiro, uma sintaxe de encaminhamento para o método ::Any despachado, diga a palavra-chave global (consulte também a próxima seção). Para o segundo, uma maneira de exigir um método mais específico, não consigo pensar em uma palavra-chave útil existente.

Edit: Removido um monte de coisas inúteis.

Seu Join é exatamente a interseção dos tipos de protocolo. Na verdade, é um "encontro". E felizmente, o tipo Join não é necessário, porque os tipos de protocolo já estão fechados na interseção: para calcular a interseção, basta retornar um novo tipo de protocolo com as duas listas de métodos concatenados.

Não estou muito preocupado com os protocolos sendo banalizados por ::Any definições. Para mim, a regra "procure por definições correspondentes, exceto Any não conta" entra em conflito com a navalha de Occam. Sem mencionar que encadear o sinalizador "ignore Any" por meio do algoritmo de subtipagem seria muito chato. Nem tenho certeza se o algoritmo resultante é coerente.

Eu gosto muito da ideia de protocolos (me lembra um pouco CLUsters), estou apenas curioso, como isso se encaixaria com a nova subtipagem que foi discutida por Jeff na JuliaCon, e com traços? (duas coisas que eu ainda gostaria muito de ver na Julia).

Isso adicionaria um novo tipo de tipo com suas próprias regras de subtipagem (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). À primeira vista, eles parecem ser compatíveis com o resto do sistema e podem simplesmente ser plugados.

Esses protocolos são basicamente a versão de "um parâmetro" das características de @ mauro3 .

Seu Join é exatamente a interseção dos tipos de protocolo.

De alguma forma, me convenci de que estava errado antes, quando disse que era o cruzamento. Embora ainda precisemos de uma maneira de cruzar os tipos em uma linha (como Union ).

Editar:

Também gosto de generalizar protocolos e tipos abstratos em um sistema e permitir regras personalizadas para sua resolução (por exemplo, para super para descrever o sistema de tipo abstrato atual). Eu acho que se feito da maneira certa, isso permitiria às pessoas adicionar sistemas de tipos personalizados e, eventualmente, otimizações personalizadas para esses sistemas de tipos. Embora eu não tenha certeza se protocolo seria a palavra-chave certa, mas pelo menos poderíamos transformar abstract em uma macro, isso seria legal.

dos campos de trigo: melhor eliminar a semelhança por meio do protocolado e abstraído do que buscar sua generalização como destino.

que?

O processo de generalizar a intenção, capacidade e potencial dos protocolos e dos tipos abstratos é uma forma menos eficaz de resolver sua síntese qualitativamente mais satisfatória. Funciona melhor primeiro para coletar suas semelhanças intrínsecas de propósito, padrão, processo. E desenvolver essa compreensão, permitindo o refinamento da perspectiva de alguém para formar a síntese.

Qualquer que seja a realização fecunda para Julia, ela é construída sobre o andaime que a síntese oferece. A síntese mais clara é a força construtiva e o poder indutivo.

Que?

Acho que ele está dizendo que devemos primeiro descobrir o que queremos dos protocolos e por que eles são úteis. Então, uma vez que tivermos isso e os tipos abstratos, será mais fácil chegar a uma síntese geral deles.

Meros protocolos

(1) advogando

Um protocolo pode ser estendido para se tornar um protocolo (mais elaborado).
Um protocolo pode ser reduzido para se tornar um protocolo (menos elaborado).
Um protocolo pode ser realizado como uma interface em conformidade [no software].
Um protocolo pode ser consultado para determinar a conformidade de uma interface.

(2) sugerindo

Os protocolos devem oferecer suporte a números de versão específicos de protocolo, com o padrão.

Seria bom apoiar alguma maneira de fazer isso:
Quando uma interface está em conformidade com um protocolo, responda verdadeiro; quando uma interface
é fiel a um subconjunto do protocolo e estaria em conformidade se aumentado,
responda incompleta e falsa caso contrário. Uma função deve listar todos
aumento necessário para uma interface que está incompleta em um protocolo.

(3) meditando

Um protocolo pode ser um tipo distinto de módulo. Suas exportações serviriam
como a comparação inicial ao determinar se alguma interface está em conformidade.
Qualquer protocolo especificado [exportado] tipos e funções podem ser declarados usando
@abstract , @type , @immutable e @function para apoiar a abstração inata.

[pao: mude para aspas de código, embora observe que o cavalo já saiu do celeiro quando você fez isso depois do fato ...]

(você precisa citar @mentions !)

obrigado - consertando

Na quarta-feira, 16 de dezembro de 2015 às 3:01, Mauro [email protected] escreveu:

(você precisa citar as @ menções!)

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

desculpe, eu deveria ter sido mais claro: citar código usando `e não"

Corrigida a correção de cotação.

obrigado - perdoe minha ignorância anterior

Tentei entender essa discussão recente sobre como adicionar um tipo de protocolo. Talvez eu esteja entendendo mal alguma coisa, mas por que é necessário ter protocolos nomeados em vez de apenas usar o nome do tipo abstrato associado que o protocolo está prestes a descrever?

Em meu ponto de vista, é bastante natural estender o sistema de tipos abstratos atual de alguma forma para descrever o comportamento que é esperado do tipo. Muito parecido com o proposto inicialmente neste tópico, mas talvez com a sintaxe de Jeffs

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

Ao seguir essa rota, não haveria necessidade de indicar especialmente que um subtipo implementa a interface. Isso seria feito implicitamente por subtipagem.

O objetivo principal de um mecanismo de interface explícito é IMHO para obter melhores mensagens de erro e realizar melhores testes de verificação.

Portanto, uma declaração de tipo como:

type Foo <: Iterable
  ...
end

Definimos as funções na seção ... ? Se não, quando erramos sobre funções ausentes (e as complexidades relacionadas a isso)? Além disso, o que acontece com os tipos que implementam vários protocolos, habilitamos a herança abstrata múltipla? Como lidamos com a resolução de super-método? O que isso faz com envio múltiplo (parece apenas removê-lo e colocar um sistema de objetos java-esque lá)? Como definimos novas especializações de tipo para métodos após o primeiro tipo ter sido definido? Como definimos os protocolos depois de definir o tipo?

Essas questões são mais fáceis de resolver criando um novo tipo (ou criando uma nova formulação de tipo).

Não há necessariamente um tipo abstrato relacionado para cada protocolo (provavelmente não deveria haver nenhum). Múltiplos das interfaces atuais podem ser implementados pelo mesmo tipo. Que não é descritível com o sistema de tipo abstrato atual. Daí o problema.

  • A herança múltipla abstrata (implementando vários protocolos) é ortogonal a esse recurso (conforme declarado por Jeff acima). Portanto, não é que obtivemos esse recurso apenas porque os protocolos são adicionados à linguagem.
  • Seu próximo comentário é sobre a questão de quando verificar a interface. Acho que isso não precisa estar vinculado às definições de função em bloco, que não parecem Julianas para mim. Em vez disso, existem três soluções simples:

    1. como implementado em # 7025, use um método verify_interface que pode ser chamado após todas as definições de função ou em um teste de unidade

    2. Não se pode verificar a interface de forma alguma e adiá-la para uma mensagem de erro aprimorada em "MethodError". Na verdade, esta é uma boa alternativa para 1.

    3. Verifique todas as interfaces no final de uma unidade de tempo de compilação ou no final de uma fase de carregamento do módulo. Atualmente também é possível ter:

function a()
  b()
end

function b()
end

Portanto, não acho que as definições de função em bloco seriam necessárias aqui.

  • O último ponto é que pode haver protocolos que não estão vinculados a tipos abstratos. Isso certamente é verdade atualmente (por exemplo, o protocolo informal "Iterável"). No entanto, no meu ponto de vista, isso ocorre apenas por causa da falta de herança abstrata múltipla. Se esta é a preocupação, vamos apenas adicionar herança múltipla abstrata em vez de adicionar um novo recurso de linguagem que visa resolver isso. Também acho que implementar várias interfaces é absolutamente crucial e isso é absolutamente comum em Java / C #.

Acho que a diferença entre algo semelhante a um "protocolo" e a herança múltipla é que um tipo pode ser adicionado a um protocolo depois de definido. Isso é útil se você deseja fazer seu pacote (definição de protocolos) funcionar com os tipos existentes. Pode-se permitir a modificação dos supertipos de um tipo após a criação, mas nesse ponto é provavelmente melhor chamá-lo de "protocolo" ou algo parecido.

Hm então permite definir interfaces alternativas / aprimoradas para os tipos existentes. Ainda não está claro para mim onde isso seria realmente necessário. Quando alguém deseja adicionar algo a uma interface existente (quando seguimos a abordagem proposta no OP), seria simplesmente subtipo e adicionar métodos de interface adicionais ao subtipo. Isso é o que há de bom nessa abordagem. Escala muito bem.

Exemplo: digamos que eu tenha algum pacote que serializa tipos. Um método tobits precisa ser implementado para um tipo, então todas as funções naquele pacote funcionarão com o tipo. Vamos chamá-lo de protocolo Serializer (isto é, tobits é definido). Agora posso adicionar Array (ou qualquer outro tipo) implementando tobits . Com herança múltipla, eu não poderia fazer Array trabalhar com Serialzer porque não posso adicionar um supertipo a Array após sua definição. Acho que este é um caso de uso importante.

Ok, entenda isso. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 é um problema semelhante, onde a solução é basicamente usar a digitação de pato. Se pudéssemos ter algo que resolvesse esse problema com elegância, isso seria realmente bom. Mas isso é algo que deve ser apoiado no nível de despacho. Se eu entendi a ideia do protocolo acima corretamente, pode-se colocar um tipo abstrato ou um protocolo como a anotação de tipo em função. Aqui, seria bom mesclar esses dois conceitos com uma única ferramenta poderosa o suficiente.

Eu concordo: será muito confuso ter tipos e protocolos abstratos. Se bem me lembro, foi argumentado acima que os tipos abstratos têm alguma semântica que não pode ser modelada com protocolos, ou seja, os tipos abstratos têm alguns recursos que os protocolos não têm. Mesmo que seja necessariamente o caso (não estou convencido), ainda será confuso, pois há uma grande sobreposição entre os dois conceitos. Portanto, os tipos abstratos devem ser removidos em favor dos protocolos.

Desde que haja consenso acima sobre os protocolos, eles enfatizam a especificação de interfaces. Tipos abstratos podem ter sido usados ​​para fazer alguns desses protocolos ausentes. Isso não significa que seja seu uso mais importante. Diga-me o que os protocolos são e o que não são, então eu poderia dizer como os tipos abstratos diferem e um pouco do que eles trazem. Nunca considerei os tipos abstratos tão relacionados à interface quanto à tipologia. Descartar uma abordagem natural à flexibilidade tipológica é caro.

@JeffreySarnoff +1

Pense na hierarquia de tipo de número. Os diferentes tipos abstratos, por exemplo, assinado, não assinado, não são definidos por sua interface. Não existe um conjunto de métodos que defina "Unsigned". É simplesmente uma declaração muito útil.

Não vejo o problema, realmente. Se ambos os tipos Signed e Unsigned suportam o mesmo conjunto de métodos, podemos criar dois protocolos com interfaces idênticas. Ainda assim, declarar um tipo como Signed ao invés de Unsigned pode ser usado para despacho (isto é, métodos da mesma função agem de maneira diferente). A chave aqui é exigir uma declaração explícita antes de considerar que um tipo implementa um protocolo, em vez de detectar isso implicitamente com base nos métodos que implementa.

Mas ter protocolos implicitamente associados também é importante, como em https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Os protocolos podem não apenas definir funções que podem ser chamadas, mas também documentar (implicitamente ou de maneiras testáveis ​​por máquina) propriedades que precisam ser mantidas. Tal como:

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

Essa diferença de comportamento externamente visível entre Signed e Unsigned é o que torna essa distinção útil.

Se existe uma distinção entre os tipos que é tão "abstrata" que não pode ser verificada imediatamente, pelo menos teoricamente, de fora, então é provável que seja necessário saber a implementação de um tipo para fazer a escolha certa. É aqui que os abstract atuais podem ser úteis. Isso provavelmente vai na direção dos tipos de dados algébricos.

Não há razão para que os protocolos não devam ser usados ​​simplesmente para agrupar tipos, ou seja, sem exigir nenhum método definido (e é possível com o design "atual" usando o truque: https://github.com/JuliaLang/julia/issues/ 6975 # issuecomment-161056795). (Observe também que isso não interfere nos protocolos definidos implicitamente.)

Considerando o exemplo (Un)signed : o que eu faria se eu tivesse um tipo que é Signed mas por alguma razão tem que ser também um subtipo de outro tipo abstrato? Isso não seria possível.

@eschnett : tipos abstratos, no momento, não têm nada a ver com a implementação de seus subtipos. Embora isso tenha sido discutido: # 4935.

Os tipos de dados algébricos são um bom exemplo em que refinamentos sucessivos são intrinsecamente significativos.
Qualquer taxonomia é dada de maneira muito mais natural e mais diretamente útil como uma hierarquia de tipo abstrato do que como uma mistura de especificações de protocolo.

A observação sobre ter um tipo que é um subtipo de mais de uma hierarquia de tipo abstrato também é importante. Existe uma grande quantidade de poder utilitário que vem com a herança múltipla de abstrações.

@ mauro3 Sim, eu sei. Eu estava pensando em algo equivalente a sindicatos discriminados, mas implementado de forma tão eficiente quanto tuplas em vez de por meio do sistema de tipo (como os sindicatos são implementados atualmente). Isso incluiria enums, tipos anuláveis ​​e pode ser capaz de lidar com alguns outros casos de forma mais eficiente do que os tipos abstratos atualmente.

Por exemplo, como tuplas com elementos anônimos:

DiscriminatedUnion{Int16, UInt32, Float64}

ou com elementos nomeados:

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

O que eu estava tentando enfatizar é que os tipos abstratos são uma boa maneira de mapear tal construção para Julia.

Não há razão para que os protocolos não devam ser usados ​​simplesmente para agrupar tipos, ou seja, sem exigir nenhum método definido (e é possível com o design "atual" usando o truque: # 6975 (comentário)). (Observe também que isso não interfere nos protocolos definidos implicitamente.)

Eu sinto que você teria que ter cuidado com isso para alcançar o desempenho, uma consideração que poucos parecem estar considerando com freqüência suficiente. No exemplo, parece que se deseja simplesmente definir a versão não qualquer para que o compilador ainda possa escolher a função em tempo de compilação (ao invés de ter que chamar uma função para escolher a correta em tempo de execução, ou o compilador inspecionando funções para determinar seus resultados). Pessoalmente, acredito que usar "herança" abstrata múltipla como tags seria uma solução melhor.

Acho que devemos manter os truques necessários e o conhecimento do sistema de tipos a um mínimo (embora pudesse estar envolvido em uma macro, pareceria um hack estranho de uma macro; se estivermos usando macros para manipular o sistema de tipos, então acho @ A solução unificada de JeffBezanson resolveria melhor este problema).

Considerando o exemplo (não) assinado: o que eu faria se eu tivesse um tipo que é assinado, mas por alguma razão tem que ser também um subtipo de outro tipo abstrato? Isso não seria possível.

Herança abstrata múltipla.


Acredito que todo esse terreno já tenha sido percorrido antes, essa conversa parece estar andando em círculos (embora círculos mais estreitos a cada vez). Acredito que tenha sido mencionado que um corpus ou problemas com o uso de protocolos devam ser adquiridos. Isso nos permitiria julgar as soluções com mais facilidade.

Enquanto estamos reiterando as coisas :) Eu quero lembrar a todos que os tipos abstratos são nominais enquanto os protocolos são estruturais, então eu prefiro designs que os tratem como ortogonais, _a menos_ que possamos realmente chegar a uma "codificação" aceitável de tipos abstratos em protocolos (talvez com um uso inteligente de tipos associados). Pontos de bônus, é claro, se também produzir herança abstrata múltipla. Acho que isso é possível, mas ainda não chegamos lá.

@JeffBezanson Os "tipos associados" são diferentes dos "tipos concretos associados a [um] protocolo"?

Sim, eu acredito que sim; Quero dizer "tipos associados" no sentido técnico de um protocolo que especifica algum par de valores-chave onde o "valor" é um tipo, da mesma forma que os protocolos especificam métodos. por exemplo, "o tipo Foo segue o protocolo Container se tiver um eltype " ou "o tipo Foo segue o protocolo Matrix se seu parâmetro ndims for 2".

tipos abstratos são nominais, enquanto os protocolos são estruturais e
tipos abstratos são qualitativos, enquanto os protocolos são operativos e
tipos abstratos (com herança múltipla) orquestram enquanto os protocolos conduzem

Mesmo que houvesse uma codificação de um no outro, o "ei, oi .. como vai você? Vamos lá!" de Julia precisa apresentar ambos claramente - a noção geralmente propositada de protocolo e tipos abstratos multinheritable (uma noção de propósito generalizado). Se há um desdobramento engenhoso que dá a Julia ambos, envolvidos separadamente, é mais provável que seja feito apenas assim do que um por um e outro.

@mason-bially: então devemos adicionar herança múltipla também? Isso ainda deixaria o problema de que supertipos não podem ser adicionados após a criação de um tipo (a menos que isso também fosse permitido).

@JeffBezanson : nada nos impediria de permitir protocolos puramente nominais.

@ mauro3 Por que a decisão de permitir ou não a inserção de supertipo post facto deve ser vinculada à herança múltipla? E existem diferentes tipos de criação de supertipo, alguns são patentemente inofensivos, pressupondo a capacidade de interpor um novo seja o que for: eu queria adicionar um tipo abstrato entre Real e AbstractFloat, digamos ProtoFloat, para que eu pudesse despachar em duplo double floats e system Floats juntos sem interferir no sistema Floats vivendo como subtipos de AbstractFloat. Talvez menos fácil de permitir, seria a capacidade de subseção dos subtipos atuais de Inteiro e, assim, evitar muitas mensagens "ambíguas com .. definir f (Bool) antes .."; ou para introduzir um supertipo de Signed que é um subtipo de Integer e abrir a hierarquia numérica para manipulação transparente de, digamos, números ordinais.

Desculpe se iniciei outra rodada do círculo. O assunto é bastante complexo e realmente temos que nos certificar de que a solução seja super simples de usar. Portanto, precisamos cobrir:

  • solução geral
  • sem degradação de desempenho
  • facilidade de uso (e também fácil de entender!)

Uma vez que o que foi proposto inicialmente em # 6975 é bastante diferente da ideia de protocolo discutida posteriormente, pode ser bom ter algum tipo de JEP que descreva como os protocolos poderiam ser.

Um exemplo de como você pode definir uma interface formal e validá-la usando o 0.4 atual (sem macros), o despacho atualmente depende do estilo de despacho das características, a menos que haja modificações feitas em gf.c. Isso usa funções geradas para a validação, todo o cálculo de tipo é executado no espaço de tipo.

Estou começando a usar isso como uma verificação de tempo de execução em uma DSL que estamos definindo, onde tenho que garantir que o tipo fornecido é um iterador de datas.

Atualmente, ele suporta herança múltipla de supertipos, o nome do campo _super não é usado pelo tempo de execução e pode ser qualquer símbolo válido. Você pode fornecer n outros tipos para _super Tupla.

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

Apenas apontando aqui que fiz um acompanhamento em uma discussão da JuliaCon sobre a possível sintaxe nas características em https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040

Guy Steele tem alguns bons insights sobre as características em uma linguagem de despacho múltiplo (Fortaleza), consulte sua palestra JuliaCon 2016: https://youtu.be/EZD3Scuv02g .

Alguns destaques: sistema de grandes características para propriedades algébricas, teste de unidade de propriedades de características para tipos que implementam uma característica, e que o sistema que eles implementaram talvez seja muito complicado e que ele faria algo mais simples agora.

Novo Swift para tensorflow compilador AD usecase para protocolos:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy e @Keno podem estar interessados ​​nisso. Tem conteúdo novo

Acho que esta apresentação merece atenção ao explorar o espaço de design para este problema.

Para a discussão de ideias não específicas e links para trabalhos de fundo relevantes, seria melhor iniciar um tópico de discurso correspondente, postar e discutir lá.

Observe que quase todos os problemas encontrados e discutidos em pesquisas sobre programação genérica em linguagens com tipagem estática são irrelevantes para Julia. As linguagens estáticas estão quase exclusivamente preocupadas com o problema de fornecer expressividade suficiente para escrever o código que desejam, enquanto ainda conseguem digitar estaticamente para verificar se não há violações do sistema de tipos. Não temos problemas com a expressividade e não exigimos verificação de tipo estática, então nada disso realmente importa em Julia.

O que nos interessa é permitir que as pessoas documentem as expectativas de um protocolo de uma forma estruturada que a linguagem possa então verificar dinamicamente (com antecedência, quando possível). Também nos preocupamos em permitir que as pessoas despachem coisas como características; permanece em aberto se aqueles devem ser conectados.

Conclusão: embora o trabalho acadêmico em protocolos em linguagens estáticas possa ser de interesse geral, não é muito útil no contexto de Julia.

O que nos interessa é permitir que as pessoas documentem as expectativas de um protocolo de uma forma estruturada que a linguagem possa então verificar dinamicamente (com antecedência, quando possível). Também nos preocupamos em permitir que as pessoas despachem coisas como características; permanece em aberto se aqueles devem ser conectados.

_este é o_: ticket:

Além de evitar alterações significativas, a eliminação de tipos abstratos e a introdução de interfaces implícitas no estilo golang seriam viáveis ​​em julia?

Não, não seria.

bem, não é disso que tratam os protocolos / características? Houve alguma discussão se os protocolos precisam ser implícitos ou explícitos.

Acho que desde 0.3 (2014), a experiência tem mostrado que as interfaces implícitas (ou seja, não aplicadas pela linguagem / compilador) funcionam bem. Além disso, tendo testemunhado como alguns pacotes evoluíram, acho que as melhores interfaces foram desenvolvidas organicamente e foram formalizadas (= documentadas) apenas em um ponto posterior.

Não estou certo de que uma descrição formal das interfaces, imposta pela linguagem de alguma forma, seja necessária. Mas enquanto isso estiver decidido, seria ótimo encorajar o seguinte (na documentação, tutoriais e guias de estilo):

  1. "interfaces" são baratas e leves, apenas um monte de funções com um comportamento prescrito para um conjunto de tipos (sim, tipos são o nível certo de granularidade - para x::T , T deve ser suficiente para decidir se x implementa a interface). Portanto, se alguém está definindo um pacote com comportamento extensível, realmente faz sentido documentar a interface.

  2. As interfaces não precisam ser descritas por relações de subtipo . Tipos sem um supertipo comum (não trivial) podem implementar a mesma interface. Um tipo pode implementar várias interfaces.

  3. O encaminhamento / composição requer interfaces implicitamente. "Como fazer um invólucro herdar todos os métodos do pai" é uma questão que surge com frequência, mas não é a certa. A solução prática é ter uma interface central e apenas implementá-la para o wrapper.

  4. As características são baratas e devem ser usadas com liberalidade. Base.IndexStyle é um excelente exemplo canônico.

O seguinte se beneficiaria de um esclarecimento, pois não tenho certeza de qual é a prática recomendada:

  1. A interface deve ter uma função de consulta, como por exemplo Tables.istable para decidir se um objeto implementa a interface? Acho que é uma boa prática se um chamador pode trabalhar com várias interfaces alternativas e precisa percorrer a lista de substitutos.

  2. Qual é o melhor lugar para uma documentação de interface em uma docstring? Eu diria que a função de consulta acima.

  1. sim, os tipos têm o nível certo de granularidade

Por que? Alguns aspectos dos tipos podem ser fatorados em interfaces (para fins de despacho), como iteração. Caso contrário, você teria que reescrever o código ou impor uma estrutura desnecessária.

  1. As interfaces não precisam ser descritas por relações de subtipo .

Talvez não seja necessário, mas seria melhor? Posso ter uma função despachada em um tipo iterável. Um tipo iterável tiled não deveria cumprir isso implicitamente? Por que o usuário deve desenhá-los em torno dos tipos nominais quando eles se preocupam apenas com a interface?

Qual é o ponto de subtipagem nominal se você está basicamente usando-os apenas como interfaces abstratas? As características parecem ser mais granulares e poderosas, então seria uma generalização melhor. Portanto, parece que os tipos são quase características, mas precisamos ter características para contornar suas limitações (e vice-versa).

Qual é o ponto de subtipagem nominal se você está basicamente usando-os apenas como interfaces abstratas?

Despacho - você pode despachar no tipo nominal de algo. Se você não precisa despachar se um tipo implementa uma interface ou não, você pode simplesmente digitá-lo. É para isso que as pessoas normalmente usam os traços sagrados: o traço permite que você envie para chamar uma implementação que assume que alguma interface está implementada (por exemplo, "tendo um comprimento conhecido"). Algo que as pessoas parecem querer é evitar essa camada de indireção, mas parece que é apenas uma conveniência, não uma necessidade.

Por que? Alguns aspectos dos tipos podem ser fatorados em interfaces (para fins de despacho), como iteração. Caso contrário, você teria que reescrever o código ou impor uma estrutura desnecessária.

Acredito que @tpapp estava dizendo que você só precisa do tipo para determinar se algo implementa ou não uma interface, não que todas as interfaces possam ser representadas com hierarquias de tipo.

Apenas uma ideia, ao usar MacroTools forward :

Às vezes é irritante encaminhar muitos métodos

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

e se pudéssemos usar o tipo de Foo.x e uma lista de métodos e inferir qual deles encaminhar? Isso será uma espécie de inheritance e pode ser implementado com recursos existentes (macros + função gerada), parece algum tipo de interface também, mas não precisamos de mais nada na linguagem.

Eu sei que nunca poderíamos chegar a uma lista do que vai herdar (é também por isso que o modelo estático class é menos flexível), às vezes você só precisa de alguns deles, mas é conveniente apenas para funções básicas ( por exemplo, alguém deseja definir um wrapper (subtipo de AbstractArray ) em torno de Array , a maioria das funções são apenas encaminhadas)

@datnamer : como outros esclareceram, as interfaces não devem ser mais granulares do que os tipos (ou seja, a implementação da interface nunca deve depender do valor , dado o tipo). Isso combina bem com o modelo de otimização do compilador e não é uma restrição na prática.

Talvez eu não tenha sido claro, mas o objetivo da minha resposta foi apontar que já temos interfaces na medida em que é útil em Julia , e elas são leves, rápidas e se tornam generalizadas conforme o ecossistema amadurece.

Uma especificação formal para descrever uma interface agrega pouco valor IMO: seria apenas documentação e verificação de que alguns métodos estão disponíveis. O último é parte de uma interface, mas a outra parte é a semântica implementada por esses métodos (por exemplo, se A é um array, axes(A) me dá um intervalo de coordenadas que são válidas para getindex ). As especificações formais de interfaces não podem abordar isso em geral, então eu sou da opinião que eles apenas adicionariam clichês com pouco valor. Também estou preocupado com o fato de que isso apenas levantaria uma (pequena) barreira para a entrada de poucos benefícios.

No entanto, o que eu adoraria ver é

  1. documentação para mais e mais interfaces (em uma docstring),

  2. suítes de teste para detectar erros óbvios para interfaces maduras para tipos recém-definidos (por exemplo, muitos T <: AbstractArray implementam eltype(::T) e não eltype(::Type{T}) .

@tpapp Faz sentido para mim agora, obrigado.

@StefanKarpinski Não entendo muito bem. Traits não são tipos nominais (certo?), No entanto, eles podem ser usados ​​para despacho.

Meu ponto é basicamente aquele feito por @tknopp e @ mauro3 aqui: https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Por ter características e digitação abstrata, há complexidade e confusão adicionais por ter dois conceitos muito semelhantes.

Algo que as pessoas parecem querer é evitar essa camada de indireção, mas parece que é apenas uma conveniência, não uma necessidade.

As seções da hierarquia de traços podem ser despachadas agrupadas por coisas como uniões e interseções, com parâmetros de tipo, de maneira robusta? Eu não tentei, mas parece que requer suporte de idioma. Problema de expressão do IE no domínio de tipo.

Edit: Acho que o problema era minha fusão de interfaces e características, como são usadas aqui.

Só postar aqui porque é divertido: parece que Conceitos definitivamente foi aceito e fará parte do C ++ 20. Coisas interessantes!

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

Acho que os traços são uma maneira muito boa de resolver esse problema e os traços sagrados certamente já percorreram um longo caminho. No entanto, acho que o que Julia realmente precisa é uma maneira de agrupar funções que pertencem a uma característica. Isso seria útil por motivos de documentação, mas também para legibilidade do código. Pelo que tenho visto até agora, acho que uma sintaxe de trait como em Rust seria o caminho a percorrer.

Acho isso muito importante e o caso de uso mais importante seria para indexar iteradores. Aqui está uma proposta para o tipo de sintaxe que você espera que funcione. Desculpas se já foi proposto (longa discussão ...).

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
Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

StefanKarpinski picture StefanKarpinski  ·  3Comentários

sbromberger picture sbromberger  ·  3Comentários

arshpreetsingh picture arshpreetsingh  ·  3Comentários

yurivish picture yurivish  ·  3Comentários

musm picture musm  ·  3Comentários