Julia: Interfaces para tipos abstractos

Creado en 26 may. 2014  ·  171Comentarios  ·  Fuente: JuliaLang/julia

Creo que esta solicitud de función aún no tiene su propio problema, aunque se ha discutido, por ejemplo, en el n. ° 5.

Creo que sería genial si pudiéramos definir explícitamente interfaces en tipos abstractos. Por interfaz me refiero a todos los métodos que deben implementarse para cumplir con los requisitos de tipo abstracto. Actualmente, la interfaz solo está definida implícitamente y puede estar dispersa en varios archivos, por lo que es muy difícil determinar qué se debe implementar cuando se deriva de un tipo abstracto.

Las interfaces nos darían principalmente dos cosas:

  • auto documentación de interfaces en un solo lugar
  • mejores mensajes de error

Base.graphics tiene una macro que realmente permite definir interfaces codificando un mensaje de error en la implementación de reserva. Creo que esto ya es muy inteligente. Pero tal vez darle la siguiente sintaxis sea aún más ordenado:

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

Aquí sería bueno si se pudieran especificar diferentes granularidades. Las declaraciones print y push! solo dicen que debe haber métodos con ese nombre (y MyType como primer parámetro) pero no especifican los tipos. En contraste, la declaración size está completamente escrita. Creo que esto da mucha flexibilidad y para una declaración de interfaz sin tipo, todavía se pueden dar mensajes de error bastante específicos.

Como dije en el n. ° 5, tales interfaces son básicamente lo que está planeado en C ++ como Concept-light para C ++ 14 o C ++ 17. Y habiendo hecho bastante programación de plantillas en C ++, estoy seguro de que cierta formalización en esta área también sería buena para Julia.

Comentario más útil

Para la discusión de ideas no específicas y enlaces a trabajos de fondo relevantes, sería mejor comenzar un hilo de discurso correspondiente y publicar y discutir allí.

Tenga en cuenta que casi todos los problemas encontrados y discutidos en la investigación sobre programación genérica en lenguajes de tipado estático son irrelevantes para Julia. Los lenguajes estáticos se ocupan casi exclusivamente del problema de proporcionar suficiente expresividad para escribir el código que desean y, al mismo tiempo, poder verificar de forma estática que no haya violaciones del sistema de tipos. No tenemos problemas con la expresividad y no requerimos una verificación de tipo estática, por lo que nada de eso realmente importa en Julia.

Lo que sí nos importa es permitir que las personas documenten las expectativas de un protocolo de una manera estructurada que el lenguaje pueda luego verificar dinámicamente (de antemano, cuando sea posible). También nos preocupamos por permitir que las personas se relacionen con cosas como rasgos; permanece abierto si deben conectarse.

En pocas palabras: si bien el trabajo académico sobre protocolos en lenguajes estáticos puede ser de interés general, no es muy útil en el contexto de Julia.

Todos 171 comentarios

En general, creo que esta es una buena dirección para mejorar la programación orientada a la interfaz.

Sin embargo, aquí falta algo. Las firmas de los métodos (no solo sus nombres) también son importantes para una interfaz.

Esto no es algo fácil de implementar y habrá muchos errores. Esa es probablemente una de las razones por las que _Concepts_ no fue aceptado por C ++ 11, y después de tres años, solo una versión muy limitada de _lite_ ingresa a C ++ 14.

El método size en mi ejemplo contenía la firma. Otros @mustimplement de Base.graphics también tienen en cuenta la firma.

Debo agregar que ya tenemos una parte de Concept-light que es la capacidad de restringir un tipo para que sea un subtipo de un cierto tipo abstracto. Las interfaces son la otra parte.

Esa macro es genial. He definido manualmente las alternativas de activación de errores y ha funcionado bastante bien para definir interfaces. por ejemplo, MathProgBase de JuliaOpt hace esto y funciona bien. Estaba jugando con un nuevo solucionador (https://github.com/IainNZ/RationalSimplex.jl) y solo tenía que seguir implementando funciones de interfaz hasta que dejara de generar errores para que funcionara.

Tu propuesta haría algo similar, ¿verdad? Pero, ¿tendrías que implementar toda la interfaz?

¿Cómo trata esto con los parámetros covariantes / contravariantes?

Por ejemplo,

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 Sí, la propuesta en realidad se trata de hacer @mustimplement un poco más versátil de modo que, por ejemplo, la firma pueda, pero no tenga que ser proporcionada. Y mi sensación es que este es un "núcleo" tal que vale la pena obtener su propia sintaxis. Sería genial hacer cumplir que todos los métodos están realmente implementados, pero la verificación del tiempo de ejecución actual como se hace en @mustimplement ya es una gran cosa y podría ser más fácil de implementar.

@lindahua Ese es un ejemplo interesante. Tengo que pensar en eso.

@lindahua Uno probablemente querría que tu ejemplo simplemente funcionara. @mustimplement no funcionaría ya que define firmas de método más específicas.

Por lo tanto, esto podría tener que implementarse un poco más en el compilador. En la definición de tipo abstracto, uno tiene que realizar un seguimiento de los nombres / firmas de la interfaz. Y en ese punto donde actualmente se lanza un error "... no definido", uno tiene que generar el mensaje de error apropiado.

Es muy fácil cambiar cómo se imprime MethodError , cuando tenemos una sintaxis y una API para expresar y acceder a la información.

Otra cosa que esto podría conseguirnos es una función en base.Test para verificar que un tipo (¿todos los tipos?) Implemente completamente las interfaces de los tipos principales. Esa sería una prueba unitaria realmente genial.

Gracias @ivarne. Entonces, la implementación podría verse de la siguiente manera:

  1. Uno tiene un diccionario global con tipos abstractos como claves y funciones (+ firmas opcionales) como valores.
  2. El analizador debe adaptarse para completar el diccionario cuando se analiza una declaración has .
  3. MethodError necesita buscar si la función actual es parte del diccionario global.

La mayor parte de la lógica estará entonces en MethodError .

He estado experimentando un poco con esto y usando la siguiente esencia https://gist.github.com/tknopp/ed53dc22b61062a2b283 puedo hacer:

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

al definir length no se lanza ningún error:

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

No es que esto actualmente no tenga en cuenta la firma.

Actualicé un poco el código en esencia para que se puedan tener en cuenta las firmas de funciones. Todavía es muy hacky, pero lo siguiente ahora funciona:

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

Debería haber agregado que la memoria caché de la interfaz en la esencia ahora opera con símbolos en lugar de funciones para que uno pueda agregar una interfaz y declarar la función después. Puede que tenga que hacer lo mismo con la firma.

Acabo de ver que # 2248 ya tiene algo de material sobre interfaces.

Iba a retrasar la publicación de pensamientos sobre características más especulativas, como interfaces, hasta después de que obtengamos 0.3, pero desde que comenzaron la discusión, aquí hay algo que escribí hace un tiempo.


Aquí hay una maqueta de la sintaxis para la declaración de la interfaz y la implementación de esa interfaz:

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 a romper esto en pedazos. Primero, está la sintaxis del tipo de función: A --> B es el tipo de función que mapea objetos de tipo A para escribir B . Las tuplas en esta notación hacen lo obvio. De forma aislada, propongo que f :: A --> B declararía que f es una función genérica, mapeando el tipo A para escribir B . Es una pregunta un poco abierta qué significa esto. ¿Significa que cuando se aplica a un argumento de tipo A , f dará un resultado de tipo B ? ¿Significa que f solo se puede aplicar a argumentos de tipo A ? ¿Debería producirse la conversión automática en cualquier lugar, en la salida, en la entrada? Por ahora, podemos suponer que todo lo que esto hace es crear una nueva función genérica sin agregarle ningún método, y los tipos son solo para documentación.

En segundo lugar, está la declaración de la interfaz Iterable{T,S} . Esto hace que Iterable un poco como un módulo y un poco como un tipo abstracto. Es como un módulo en el sentido de que tiene enlaces a funciones genéricas llamadas Iterable.start , Iterable.done y Iterable.next . Es como un tipo en el que Iterable y Iterable{T} y Iterable{T,S} pueden usarse siempre que los tipos abstractos puedan, en particular, en el envío de métodos.

En tercer lugar, está el bloque implement que define cómo UnitRange implementa la interfaz Iterable . Dentro del bloque implement , las funciones Iterable.start , Iterable.done y Iterable.next disponibles, como si el usuario hubiera hecho import Iterable: start, done, next , permitiendo la adición de métodos a estas funciones. Este bloque es similar a una plantilla de la forma en que lo son las declaraciones de tipo paramétrico: dentro del bloque, UnitRange significa un UnitRange específico, no el tipo de paraguas.

La principal ventaja del bloque implement es que evita la necesidad explícita de las funciones import que desea extender; se importan implícitamente, lo cual es bueno ya que la gente generalmente se confunde con import todos modos. Esta parece una forma mucho más clara de expresarlo. Sospecho que la mayoría de las funciones genéricas en Base que los usuarios querrán extender deberían pertenecer a alguna interfaz, por lo que esto debería eliminar la gran mayoría de usos de import . Dado que siempre puede calificar completamente un nombre, tal vez podríamos eliminarlo por completo.

Otra idea que se me ha ocurrido es la separación de las versiones "interna" y "externa" de las funciones de la interfaz. Lo que quiero decir con esto es que la función "interna" es aquella para la que proporcionas métodos para implementar alguna interfaz, mientras que la función "externa" es la que llamas para implementar una funcionalidad genérica en términos de alguna interfaz. Considere cuando observe los métodos de la función sort! (excluyendo los 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

Algunos de estos métodos están destinados al consumo público, pero otros son solo parte de la implementación interna de los métodos de clasificación públicos. Realmente, el único método público que esto debería tener es este:

sort!(v::AbstractArray)

El resto son ruido y pertenecen al "interior". En particular, el

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 son los que implementa un algoritmo de clasificación para conectarse a la maquinaria de clasificación genérica. Actualmente Sort.Algorithm es un tipo abstracto, y InsertionSortAlg , QuickSortAlg y MergeSortAlg son subtipos concretos del mismo. Con interfaces, Sort.Algorithm podría ser una interfaz en su lugar y los algoritmos específicos la implementarían. Algo como esto:

# 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

La separación que queremos podría lograrse definiendo:

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

Esto está _muy_ cerca de lo que estamos haciendo actualmente, excepto que llamamos Algorithm.sort! lugar de solo sort! - y cuando implementamos varios algoritmos de clasificación, la definición "interna" es un método de Algorithm.sort! no es la función sort! . Esto tiene el efecto de separar la implementación de sort! de su interfaz externa.

@StefanKarpinski ¡ Muchas gracias por tu reseña! Seguramente no se trata de 0,3 cosas. Lo siento mucho que mencioné esto en este momento. Simplemente no estoy seguro de si 0.3 sucederá pronto o en medio año ;-)

Desde un primer vistazo, realmente (!) Me gusta que la sección de implementación esté definida con su propio bloque de código. Esto permite verificar directamente la interfaz en la definición de tipo.

No se preocupe, no hay nada de malo en especular sobre funciones futuras mientras intentamos estabilizar un lanzamiento.

Su enfoque es mucho más fundamental e intenta resolver también algunos problemas independientes de la interfaz. También trae una nueva construcción (es decir, la interfaz) al lenguaje que hace que el lenguaje sea un poco más complejo (lo cual no es necesariamente algo malo).

Veo "la interfaz" más como una anotación de tipos abstractos. Si se le pone has se puede especificar una interfaz, pero no es necesario.

Como dije, me gustaría mucho que la interfaz pudiera validarse directamente en su declaración. El enfoque menos invasivo aquí podría ser permitir la definición de métodos dentro de una declaración de tipo. Tomando tu ejemplo, 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

Aún se podría definir la función fuera de la declaración de tipo. La única diferencia sería que las declaraciones de funciones internas se validan contra interfaces.

Pero, de nuevo, tal vez mi "enfoque menos invasivo" sea demasiado miope. Realmente no lo sé.

Un problema al poner esas definiciones dentro del bloque de tipos es que para hacer esto, realmente necesitaremos una herencia múltiple de interfaces al menos, y es posible que haya colisiones de nombres entre diferentes interfaces. También puede agregar el hecho de que un tipo admite una interfaz en algún momento _después_ de definir el tipo, aunque no estoy seguro de eso.

@StefanKarpinski Es genial ver que estás pensando en esto.

El paquete Graphs es el que más necesita el sistema de interfaz. Sería interesante ver cómo este sistema puede expresar las interfaces descritas aquí: http://graphsjl-docs.readthedocs.org/en/latest/interface.html.

@StefanKarpinski : No veo completamente el problema con las declaraciones de función de herencia y en bloque múltiples. Dentro del bloque de tipo, deberían verificarse todas las interfaces heredadas.

Pero entiendo que uno podría querer dejar la implementación de la interfaz "abierta". Y la declaración de función en tipo podría complicar demasiado el lenguaje. Quizás el enfoque que he implementado en el n. ° 7025 sea suficiente. Ponga verify_interface después de las declaraciones de la función (o en una prueba unitaria) o difiera a MethodError .

Este problema es que diferentes interfaces podrían tener una función genérica con el mismo nombre, lo que causaría una colisión de nombres y requeriría hacer una importación explícita o agregar métodos con un nombre completo. También deja menos claro qué definiciones de método pertenecen a qué interfaces, razón por la cual la colisión de nombres puede ocurrir en primer lugar.

Por cierto, estoy de acuerdo en que agregar interfaces como otra "cosa" en el lenguaje se siente un poco poco ortogonal. Después de todo, como mencioné en la propuesta, son un poco como módulos y un poco como tipos. Parece que podría ser posible una unificación de conceptos, pero no tengo claro cómo.

Prefiero el modelo de interfaz como biblioteca al modelo de interfaz como característica de lenguaje por algunas razones: mantiene el lenguaje más simple (ciertamente es una preferencia y no una objeción concreta) y significa que la característica sigue siendo opcional y puede ser fácilmente mejorado o reemplazado por completo sin ensuciar con el lenguaje actual.

Específicamente, creo que la propuesta (o al menos la forma de la propuesta) de @tknopp es mejor que la de @StefanKarpinski : proporciona una verificación del tiempo de definición sin requerir nada nuevo en el idioma. El principal inconveniente que veo es la falta de capacidad para tratar con variables de tipo; Creo que esto se puede manejar haciendo que la definición de la interfaz proporcione el tipo _predicates_ para los tipos de funciones requeridas.

Una de las principales motivaciones de mi propuesta es la gran confusión que genera tener que _importar_ funciones genéricas - pero no exportarlas - para poder agregarles métodos. La mayoría de las veces, esto sucede cuando alguien intenta implementar una interfaz no oficial, por lo que parece que eso es lo que está sucediendo.

Eso parece un problema ortogonal para resolver, a menos que desee restringir por completo los métodos para que pertenezcan a interfaces.

No, eso ciertamente no parece una buena restricción.

@StefanKarpinski mencionas que podrías enviar en una interfaz. También en la sintaxis implement la idea es que un tipo particular implemente la interfaz.

Esto parece un poco en desacuerdo con el envío múltiple, ya que en general los métodos no pertenecen a un tipo en particular, pertenecen a una tupla de tipos. Entonces, si los métodos no pertenecen a tipos, ¿cómo pueden las interfaces (que son básicamente conjuntos de métodos) pertenecer a un tipo?

Digamos que estoy usando la 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

ahora quiero escribir una función genérica que tome una A y una B

using M

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

En este ejemplo, la función f forma una interfaz ad-hoc que toma un A y un B , y quiero poder asumir que puedo llamar al f función en ellos. En este caso, no está claro cuál de ellos debe considerarse para implementar la interfaz.

Se espera que otros módulos que deseen proporcionar subtipos concretos de A y B proporcionen implementaciones de f . Para evitar la explosión combinatoria de métodos requeridos, esperaría que la biblioteca defina f contra los tipos abstractos:

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

Es cierto que este ejemplo se siente bastante artificial, pero espero que ilustre que (al menos en mi mente) parece que hay una falta de coincidencia fundamental entre el envío múltiple y el concepto de un tipo particular que implementa una interfaz.

Sin embargo, veo su punto sobre la confusión import . Me tomó un par de intentos con este ejemplo para recordar que cuando puse using M y luego intenté agregar métodos a f no hizo lo que esperaba, y tuve que agregar los métodos a M.f (o podría haber usado import ). Sin embargo, no creo que las interfaces sean la solución a ese problema. ¿Existe un tema aparte para pensar en formas de hacer que la adición de métodos sea más intuitiva?

@ abe-egnor También creo que un enfoque más abierto parece más factible. Mi prototipo # 7025 carece esencialmente de dos cosas:
a) una mejor sintaxis para definir interfaces
b) definiciones de tipo paramétrico

Como no soy tanto un gurú de tipo paramétrico, estoy seguro de que b) puede resolverlo alguien con una experiencia más profunda.
Respecto a a) se podría ir con una macro. Personalmente, creo que podríamos gastar algo de soporte de lenguaje para definir directamente la interfaz como parte de la definición de tipo abstracto. El enfoque has puede ser demasiado miope. Un bloque de código podría hacer que esto sea más agradable. En realidad, esto está muy relacionado con # 4935, donde se define una interfaz "interna", mientras que esta se trata de la interfaz pública. Estos no tienen que estar agrupados, ya que creo que este problema es mucho más importante que el número 4935. Pero aún así, en cuanto a la sintaxis, es posible que desee tener en cuenta ambos casos de uso.

https://gist.github.com/abe-egnor/503661eb4cc0d66b4489 tiene mi primer intento con el tipo de implementación que estaba pensando. En resumen, una interfaz es una función desde tipos hasta un dictado que define el nombre y los tipos de parámetros de las funciones requeridas para esa interfaz. La macro @implement simplemente llama a la función para los tipos dados y luego empalma los tipos en las definiciones de función dadas, verificando que todas las funciones hayan sido definidas.

Buenos puntos:

  • Sintaxis simple para la definición e implementación de interfaces.
  • Ortogonal a otras características del idioma, pero se adapta bien a ellas.
  • El cálculo del tipo de interfaz puede ser arbitrariamente sofisticado (son solo funciones sobre los parámetros del tipo de interfaz)

Puntos malos:

  • No funciona bien con tipos parametrizados si desea utilizar el parámetro como un tipo de interfaz. Este es un inconveniente bastante importante, pero no veo una forma inmediata de abordarlo.

Creo que tengo una solución al problema de la parametrización; en resumen, la definición de la interfaz debería ser una macro sobre las expresiones de tipo, no una función sobre los valores de tipo. La macro @implement puede extender los parámetros de tipo a las definiciones de funciones, permitiendo 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

En ese caso, los parámetros de tipo se extienden a los métodos definidos en la interfaz, por lo que se expande a stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x) , que creo que es exactamente lo correcto.

Reelaboraré mi implementación inicial para hacer esto a medida que tenga tiempo; probablemente en el orden de una semana.

Busqué un poco en Internet para ver qué hacen otros lenguajes de programación con respecto a las interfaces, la herencia y demás, y se me ocurrieron algunas ideas. (En caso de que alguien esté interesado aquí las notas muy aproximadas que tomé https://gist.github.com/mauro3/e3e18833daf49cdf8f60)

En resumen, es posible que las interfaces se puedan implementar mediante:

  • permitiendo herencia múltiple para tipos abstractos, y
  • permitiendo funciones genéricas como campos de tipos abstractos.

Esto convertiría los tipos abstractos en interfaces y los subtipos concretos serían necesarios para implementar esa interfaz.

La larga historia:

Lo que encontré es que algunos de los lenguajes "modernos" eliminan el polimorfismo de subtipos, es decir, no hay una agrupación directa de tipos, y en su lugar agrupan sus tipos basándose en que pertenecen a interfaces / rasgos / clases de tipos. En algunos lenguajes, las interfaces / rasgos / clases de tipos pueden tener un orden entre ellos y heredarse entre sí. También parecen (en su mayoría) felices con esa elección. Algunos ejemplos son: Go ,
Óxido , Haskell .
Go es el menos estricto de los tres y permite que sus interfaces se especifiquen implícitamente, es decir, si un tipo implementa el conjunto específico de funciones de una interfaz, entonces pertenece a esa interfaz. Para Rust, la interfaz (rasgos) debe implementarse explícitamente en un bloque impl . Ni Go ni Rust tienen métodos múltiples. Haskell tiene múltiples métodos y en realidad están directamente vinculados a la interfaz (clase de tipo).

En cierto sentido, esto es similar a lo que hace Julia también, los tipos abstractos son como una interfaz (implícita), es decir, se refieren al comportamiento y no a los campos. Esto es lo que @StefanKarpinski también observó en una de sus publicaciones anteriores y afirmó que, además, tener interfaces "se siente un poco demasiado no ortogonal". Entonces, Julia tiene una jerarquía de tipos (es decir, polimorfismo de subtipo) mientras que Go / Rust / Haskell no.

¿Qué tal convertir los tipos abstractos de Julia en más una clase de interfaz / rasgo / tipo, mientras se mantienen todos los tipos en la jerarquía None<: ... <:Any ? Esto implicaría:
1) permitir herencia múltiple para tipos (abstractos) (problema n. ° 5)
2) permitir asociar funciones con tipos abstractos (es decir, definir una interfaz)
3) Permitir especificar esa interfaz, tanto para tipos abstractos (es decir, una implementación predeterminada) como concretos.

Creo que esto podría llevar a un tipo de gráfico más detallado que el que tenemos ahora y podría implementarse paso a paso. Por ejemplo, un tipo de matriz se ensamblaría:

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

Entonces, los tipos básicamente abstractos pueden tener funciones genéricas como campos (es decir, convertirse en una interfaz) mientras que los tipos concretos solo tienen campos normales. Esto puede, por ejemplo, resolver el problema de que se deriven demasiadas cosas de AbstractArray, ya que la gente podría simplemente elegir las piezas útiles para su contenedor en lugar de derivar de AbstractArray.

Si esto es una buena idea, hay mucho que trabajar (en particular, cómo especificar tipos y parámetros de tipo), pero ¿tal vez valga la pena pensarlo?

@ssfrr comentó anteriormente que las interfaces y el envío múltiple son incompatibles. Ese no debería ser el caso ya que, por ejemplo, en Haskell los métodos múltiples solo son posibles mediante el uso de clases de tipos.

También descubrí al leer el artículo de @StefanKarpinski que usar directamente abstract lugar de interface podría tener sentido. Sin embargo, en este caso, es importante que abstract herede una propiedad crucial de interface : la posibilidad de que un tipo implement an interface _después_ se defina. Entonces puedo usar un tipo typA de lib A con un algoritmo algoB de lib B declarando en mi código que typA implementa la interfaz requerida por algoB (supongo que esto implica que los tipos concretos tienen una especie de herencia múltiple abierta).

@ mauro3 , de hecho me gusta mucho tu sugerencia. Para mí, se siente muy "juliano" y natural. También creo que es una integración única y poderosa de interfaces, herencia múltiple y "campos" de tipo abstracto (aunque, en realidad, no, ya que los campos solo serían métodos / funciones, no valores). También creo que esto combina bien con la idea de @StefanKarpinski de distinguir los métodos de interfaz "interno" y "externo", ya que podría implementar su propuesta para el ejemplo sort! declarando abstract Algorithm y Algorithm.sort! .

lo siento todo el mundo

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

@ mauro3 , de hecho me gusta mucho tu sugerencia. Para mí, se siente muy "juliano" y natural. También creo que es una integración única y poderosa de interfaces, herencia múltiple y "campos" de tipo abstracto (aunque, en realidad, no, ya que los campos solo serían métodos / funciones, no valores). También creo que esto encaja bien con la idea de @StefanKarpinski de distinguir los métodos de interfaz "internos" y "externos", ¡ya que podrías implementar su propuesta para el género! ejemplo declarando algoritmo abstracto y Algorithm.sort !.

-
Responda a este correo electrónico directamente o véalo en GitHub.

@implement Lo siento mucho; No estoy seguro de cómo te hicimos ping. Si aún no lo sabía, puede eliminarse de esas notificaciones mediante el botón "Cancelar suscripción" en el lado derecho de la pantalla.

No, solo quiero decirte que no puedo ayudarte mucho para decir sarry

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

@implement Lo siento mucho; No estoy seguro de cómo te hicimos ping. Si aún no lo sabía, puede eliminarse de esas notificaciones mediante el botón "Cancelar suscripción" en el lado derecho de la pantalla.

-
Responda a este correo electrónico directamente o véalo en GitHub.

¡No esperamos que lo haga! Fue un accidente, ya que estamos hablando de una macro de Julia con el mismo nombre que tu nombre de usuario. ¡Gracias!

Acabo de ver que hay algunas características potencialmente interesantes (tal vez relevantes para este problema) en las que se trabajó en Rust: http://blog.rust-lang.org/2014/09/15/Rust-1.0.html , en particular: https : //github.com/rust-lang/rfcs/pull/195

Después de ver THTT ("Tim Holy Trait Trick"), pensé un poco más en las interfaces / rasgos durante las últimas semanas. Se me ocurrieron algunas ideas y una implementación: Traits.jl . Primero, (creo) los rasgos deben verse como un contrato que involucra uno o varios tipos . Esto significa que simplemente adjuntar las funciones de una interfaz a un tipo abstracto, como yo y otros sugerimos anteriormente, no funciona (al menos no en el caso general de un rasgo que involucra varios tipos). Y en segundo lugar, los métodos deberían poder usar rasgos para el envío , como @StefanKarpinski sugirió anteriormente.

Nuff dijo, aquí un ejemplo usando mi paquete 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

Esto declara que Eq y Cmp son contratos entre los tipos X y Y . Cmp tiene Eq como un superretrato, es decir, tanto Eq como Cmp deben cumplirse. En el cuerpo @traitdef , las firmas de funciones especifican qué métodos deben definirse. Los tipos de devolución no hacen nada por el momento. Los tipos no necesitan implementar explícitamente un rasgo, basta con implementar las funciones. Puedo comprobar si, digamos, Cmp{Int,Float64} es realmente un rasgo:

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

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

La implementación de rasgos explícitos aún no está en el paquete, pero debería ser bastante sencillo de agregar.

Una función que usa _trait-dispatch_ se puede definir así

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

Esto declara una función ft1 que toma dos argumentos con la restricción de que sus tipos deben cumplir Cmp{X,Y} . Puedo agregar otro método de envío en otro rasgo:

<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

Estas funciones de rasgo ahora se pueden llamar como funciones normales:

julia> ft1(4,5)
6

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

Agregar otro tipo a un rasgo más adelante es fácil (lo que no sería el caso con Unions 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

La _Implementación_ de las funciones de rasgos y su distribución se basa en el truco de Tim y en funciones por etapas, ver más abajo. La definición de rasgo es relativamente trivial, consulte aquí una implementación manual de todo.

En resumen, el despacho de rasgos se convierte

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

en algo como esto (un poco 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

En el paquete, la generación de checkfn está automatizada por stagedfuncitons. Pero consulte el archivo README de Traits.jl para obtener más detalles.

_Performance_ Para funciones de rasgo simples, el código de máquina producido es idéntico a sus contrapartes tipo pato, es decir, tan bueno como parece. Para funciones más largas hay diferencias, hasta ~ 20% en longitud. No estoy seguro de por qué, como pensé, todo esto debería estar incluido.

(editado el 27 de octubre para reflejar cambios menores en Traits.jl )

¿Está el paquete Traits.jl listo para explorar? El archivo Léame dice "implementar interfaces con

Está listo para explorar (incluidos los errores :-). El @traitimpl que falta solo significa que en lugar de

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

simplemente define las funciones manualmente

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

para dos de sus tipos T1 y T2 .

Agregué la macro @traitimpl , por lo que el ejemplo anterior ahora funciona. También actualicé el archivo README con detalles sobre el uso. Y agregué un ejemplo que implementa parte de la interfaz @lindahua Graphs.jl:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl

Esto es realmente genial. Particularmente me gusta que reconoce que las interfaces en general son una propiedad de tuplas de tipos, no de tipos individuales.

También encuentro esto muy bueno. Hay mucho que me gusta de este enfoque. Buen trabajo.

: +1:

¡Gracias por los buenos comentarios! Actualicé / refactoricé el código un poco y debería estar razonablemente libre de errores y bueno para jugar.
En este punto, probablemente sería bueno, si la gente pudiera darle una vuelta a esto para ver si se ajusta a sus casos de uso.

Este es uno de esos paquetes que hace que uno vea su propio código con una nueva luz. Muy genial.

Lo siento, no he tenido tiempo de revisar esto en serio todavía, pero sé que una vez que lo haga, querré refactorizar algunas cosas ...

También refactorizaré mis paquetes :)

Me estaba preguntando, me parece que si los rasgos están disponibles (y permiten el envío múltiple, como la sugerencia anterior), entonces no hay necesidad de un mecanismo de jerarquía de tipos abstractos, o tipos abstractos en absoluto. ¿Puede ser esto?

Después de que los rasgos se implementan, cada función en la base y luego en todo el ecosistema eventualmente expondría una API pública basada únicamente en los rasgos, y los tipos abstractos desaparecerían. Por supuesto, el proceso podría catalizarse desaprobando los tipos abstractos.

Pensando en esto un poco más, reemplazar tipos abstractos por rasgos requeriría parametrizar tipos como este:

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

Estoy de acuerdo con el punto anterior de mauro3, que tener rasgos (por su definición, que creo que es muy bueno) es equivalente a tipos abstractos que

  • Permitir herencia múltiple, y
  • permitir funciones genéricas como campos

También agregaría que para permitir que los rasgos se asignen a tipos después de su definición, también se necesitaría permitir la "herencia perezosa", es decir, decirle al compilador que un tipo hereda de algún tipo abstracto después de haber sido definido.

así que, en general, me parece que desarrollar algún concepto de rasgo / interfaz fuera de los tipos abstractos induciría cierta duplicación, introduciendo diferentes formas de lograr lo mismo. Ahora creo que la mejor manera de introducir estos conceptos es agregando características lentamente a los tipos abstractos.

EDITAR : por supuesto, en algún momento, la herencia de tipos concretos de los abstractos tendría que ser desaprobada y finalmente rechazada. Los rasgos de tipo se determinarían implícita o explícitamente, pero nunca por herencia

¿No son los tipos abstractos sólo un ejemplo "aburrido" de rasgos?

Si es así, ¿sería posible mantener la sintaxis actual y simplemente cambiar su significado a rasgo (dando libertad ortogonal, etc. si el usuario lo desea)?

_Me pregunto si esto también podría abordar el ejemplo Point{Float64} <: Pointy{Real} (no estoy seguro de si hay un número de problema). _

Sí, creo que tienes razón. La funcionalidad de los rasgos se puede lograr mejorando los tipos abstractos de julia actuales. Necesitan
1) herencia múltiple
2) firmas de funciones
3) "herencia perezosa", para dar explícitamente un nuevo rasgo a un tipo ya definido

Parece mucho trabajo, pero tal vez esto se pueda desarrollar lentamente sin que la comunidad se rompa mucho. Así que al menos lo conseguimos;)

Creo que lo que elijamos será un gran cambio, uno en el que no estamos listos para comenzar a trabajar en 0.4. Si tuviera que adivinar, apostaría a que es más probable que nos movamos en la dirección de los rasgos que en la dirección de agregar la herencia múltiple tradicional. Pero mi bola de cristal está estropeada, por lo que es difícil estar seguro de lo que sucederá sin tan solo intentarlo.

FWIW, encontré la discusión de Simon Peyton-Jones sobre las clases de tipos en la charla a continuación realmente informativa sobre cómo usar algo como rasgos en lugar de subtipificar: http://research.microsoft.com/en-us/um/people/simonpj/ artículos / haskell-retrospective / ECOOP-July09.pdf

¡Sí, toda una lata de gusanos!

@johnmyleswhite , gracias por el enlace, muy interesante. Aquí un enlace al video, que vale la pena ver para llenar los vacíos. Esa presentación parece tocar muchas preguntas que tenemos aquí. Y curiosamente, la implementación de clases de tipos es bastante similar a lo que hay en Traits.jl (el truco de Tim, los rasgos son tipos de datos). Https://www.haskell.org/haskellwiki/Multi-parameter_type_class de Haskell se parece mucho a Traits.jl. Una de sus preguntas en la charla es: "una vez que hemos adoptado de todo corazón los genéricos, ¿realmente necesitamos subtipificar?". (Los genéricos son funciones paramétrico-polimórficas, creo, ver ) Que es algo sobre lo que @skariel y @hayd han estado reflexionando anteriormente.

Refiriéndome a @skariel y @hayd , creo que los rasgos de un solo parámetro (como en Traits.jl) están muy cerca de los tipos abstractos, excepto que pueden tener otra jerarquía, es decir, herencia múltiple.

Pero los rasgos de múltiples parámetros parecen ser un poco diferentes, al menos estaban en mi mente. Como los vi, los parámetros de tipo de los tipos abstractos parecen ser principalmente sobre qué otros tipos están contenidos dentro de un tipo, por ejemplo, Associative{Int,String} dice que el dict contiene Int claves y String valores. Mientras que Tr{Associative,Int,String}... dice que hay algún "contrato" entre Associative , Int sy Strings . Pero entonces, quizás Associative{Int,String} debería leerse de esa manera también, es decir, hay métodos como getindex(::Associative, ::Int) -> String , setindex!(::Associative, ::Int, ::String) ...

@ mauro3 Lo importante sería pasar objetos de tipo Associative como argumento a una función, para que luego pueda crear Associative{Int,String} ella misma:

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

Llamaría a esto, por ejemplo, f(Dict) .

@eschnett , lo siento, no entiendo a qué te refieres.

@ mauro3 Creo que estaba pensando de una manera demasiado complicada; Ignorame.

Actualicé Traits.jl con:

  • resolución de ambigüedades de rasgos
  • tipos asociados
  • usando @doc para obtener ayuda
  • mejores pruebas de los métodos de especificación de rasgos

Consulte https://github.com/mauro3/Traits.jl/blob/master/NEWS.md para obtener más detalles. ¡Comentarios bienvenidos!

@ Rory-Finnegan armó un paquete de interfaz https://github.com/Rory-Finnegan/Interfaces.jl

Recientemente hablé de esto con

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

y tenemos isa(Iterable, Protocol) y Protocol <: Type . Naturalmente, puede enviar en estos. Puede verificar si un tipo implementa un protocolo usando T <: Iterable .

Aquí están las reglas de subtipificación:

sean P, Q tipos de protocolo
sea ​​T un tipo sin protocolo

| entrada | resultado |
| --- | --- |
| P <: Cualquiera | verdadero |
| Inferior <: P | verdadero |
| (union, unionall, var) <: P | use la regla normal; tratar P como un tipo base |
| P <: (union, unionall, var) | utilizar la regla normal |
| P <: P | verdadero |
| P <: Q | comprobar métodos (Q) <: métodos (P) |
| P <: T | falso |
| T <: P | Los métodos de P existen con T sustituido por _ |

El último es el más grande: para probar T <: P, sustituye T por _ en la definición de P y marca method_exists para cada firma. Por supuesto, esto en sí mismo significa que las definiciones de respaldo que arrojan errores de "debe implementar esto" se convierten en algo muy malo. Con suerte, esto es más una cuestión estética.

Otro problema es que esta definición es circular si, por ejemplo, se define start(::Iterable) . Esta definición realmente no tiene sentido. De alguna manera podríamos prevenir esto, o detectar este ciclo durante la verificación de subtipos. No estoy 100% seguro de que la detección de ciclo simple lo solucione, pero parece plausible.

Para tipo intersección tenemos:

| entrada | resultado |
| --- | --- |
| P ∩ (union, unionall, tvar) | utilizar la regla normal |
| P ∩ Q | P |
| P ∩ T | T |

Hay un par de opciones para P ∩ Q:

  1. Realice una aproximación excesiva devolviendo P o Q (por ejemplo, lo que sea lexicográficamente primero). Esto es correcto con respecto a la inferencia de tipos, pero puede resultar molesto en otros lugares.
  2. Devuelve un nuevo protocolo ad-hoc que contiene la unión de las firmas en P y Q.
  3. Tipos de intersecciones. Quizás restringido solo a protocolos.

P ∩ T es complicado. T es una buena aproximación conservadora, ya que los tipos que no son de protocolo son "más pequeños" que los tipos de protocolo en el sentido de que lo restringen a una región de la jerarquía de tipos, mientras que los tipos de protocolo no lo hacen (ya que cualquier tipo puede implementar cualquier protocolo ). Hacerlo mejor que esto parece requerir tipos de intersección generales, que preferiría evitar en la implementación inicial, ya que eso requiere revisar el algoritmo de subtipificación y abre worm-can tras worm-can.

Especificidad: P solo es más específico que Q cuando P <: Q. pero dado que P ∩ Q siempre no está vacío, las definiciones con diferentes protocolos en la misma ranura son a menudo ambiguas, lo que parece ser lo que querría (por ejemplo, estaría diciendo "si x es Iterable, haga esto, pero si x es imprimible, haga ese").
Sin embargo, no existe una forma práctica de expresar la definición de eliminación de ambigüedades requerida, por lo que tal vez debería ser un error.

Después de # 13412, un protocolo se puede "codificar" como UnionAll _ sobre una Unión de tipos de tuplas (donde el primer elemento de cada tupla interna es el tipo de función en cuestión). Este es un beneficio de ese diseño que no se me ocurrió antes. Por ejemplo, la subtipificación estructural de protocolos parece fallar automáticamente.

Por supuesto, estos protocolos son del estilo de "parámetro único". Me gusta la simplicidad de esto, además no estoy seguro de cómo manejar grupos de tipos con tanta elegancia como T <: Iterable .

Hubo algunos comentarios sobre esta idea en el pasado, x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516.

¿Apoyaríamos, por ejemplo

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

¡Vaya, realmente me gusta esto (especialmente con la extensión de @Keno )!

+1 ¡Esto es exactamente lo que quiero!

@Keno Definitivamente es una buena ruta de actualización para esta función, pero hay razones para posponerla. Todo lo que involucre tipos de devolución es, por supuesto, muy problemático. El parámetro en sí es conceptualmente bueno y sería fantástico, pero es un poco difícil de implementar. Requiere mantener un entorno de tipos alrededor del proceso que verifique la existencia de todos los métodos.

Parece que podría calzar los rasgos (como la indexación lineal O (1) para tipos similares a matrices) en este esquema. Definiría un método ficticio como hassomeproperty(::T) = true (pero _no_ hassomeproperty(::Any) = false ) y luego tendría

protocol MyProperty
hassomeproperty(::_)
end

¿Podría _ aparecer varias veces en el mismo método en la definición del protocolo, como

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

Podría _ aparecer varias veces en el mismo método en la definición del protocolo

Si. Simplemente coloque el tipo de candidato para cada instancia de _ .

@JeffBezanson tiene muchas ganas de que

¿Qué pasa con el hecho de que los métodos se pueden definir dinámicamente (por ejemplo, con @eval ) en cualquier momento? Entonces, si un tipo es un subtipo de un protocolo dado, no se puede conocer estáticamente en general, lo que parecería anular las optimizaciones que evitan el envío dinámico en muchos casos.

Sí, esto empeora el # 265 :) Es el mismo problema en el que el envío y el código generado deben cambiar cuando se agregan métodos, solo que con más bordes de dependencia.

¡Es bueno ver que esto avanza! Por supuesto, yo sería el que argumentaría que los rasgos multiparamétricos son el camino a seguir. Pero el 95% de los rasgos probablemente serían un solo parámetro de todos modos. ¡Es solo que encajarían muy bien con el envío múltiple! Esto probablemente podría revisarse más tarde si es necesario. Basta de charla.

Un par de comentarios:

La sugerencia de @Keno (y realmente state en el original de Jeff) se conoce como tipos asociados. Tenga en cuenta que también son útiles sin tipos de devolución. Rust tiene una entrada manual decente. Creo que son una buena idea, aunque no tan necesarias como en Rust. Sin embargo, no creo que deba ser un parámetro del rasgo: al definir una función que se envía en Iterable , no sabría qué es T .

En mi experiencia, method_exists se puede utilizar en su forma actual para esto (# 8959). Pero presumiblemente esto se solucionará en # 8974 (o con esto). Encontré que las firmas de métodos coincidentes con firmas de rasgos son la parte más difícil al hacer Traits.jl, especialmente para tener en cuenta las funciones parametrizadas y vararg ( ver ).

¿Es de suponer que la herencia también sería posible?

Realmente me gustaría ver un mecanismo que permita la definición de implementaciones predeterminadas. El clásico es que, para comparar, solo necesita definir dos de = , < , > , <= , >= . Quizás aquí es donde el ciclo mencionado por Jeff es realmente útil. Continuando con el ejemplo anterior, definir start(::Indexable) = 1 y done(i::Indexable,state)=length(i)==state los convertiría en los valores predeterminados. Por lo tanto, muchos tipos solo necesitarían definir next .

Buenos puntos. Creo que los tipos asociados son algo diferentes del parámetro en Iterable{T} . En mi codificación, el parámetro cuantificaría existencialmente todo lo que hay dentro --- "¿existe una T tal que el tipo Foo implemente este protocolo?".

Sí, parece que fácilmente podríamos permitir protocol Foo <: Bar, Baz , y simplemente copiar las firmas de Bar y Baz en Foo.

Los rasgos de múltiples parámetros son definitivamente poderosos. Creo que es muy interesante pensar en cómo integrarlos con subtipos. Podría tener algo como TypePair{A,B} <: Trait , pero eso no parece del todo correcto.

Creo que su propuesta (en términos de características) en realidad se parece más a Swift que a Clojure.

Parece extraño (y creo que una fuente de confusión futura) mezclar subtipos nominales (tipos) y estructurales (protocolo) (pero supongo que es inevitable).

También soy un poco escéptico sobre el poder expresivo de los protocolos para operaciones matemáticas / matriciales. Creo que pensar en ejemplos más complicados (operaciones matriciales) sería más esclarecedor que la iteración, que tiene una interfaz claramente especificada. Consulte, por ejemplo, la biblioteca core.matrix .

Estoy de acuerdo; En este punto, deberíamos recopilar ejemplos de protocolos y ver si hacen lo que queremos.

De la forma en que se está imaginando esto, ¿serían los protocolos espacios de nombres a los que pertenecen sus métodos? Es decir, cuando escribes

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

Parecería natural que esto defina las funciones genéricas start , done y next y que sus nombres completos sean Iterable.start , Iterable.done y Iterable.next . Un tipo implementaría Iterable pero implementando todas las funciones genéricas en el protocolo Iterable . Propuse algo muy similar a esto hace algún tiempo (no puedo encontrarlo ahora), pero el otro lado es que cuando desea implementar un protocolo, haga esto:

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

Esto contrarrestaría la "lejanía" que mencionó @mdcfrancis , si lo entiendo, pero, de nuevo, no veo el beneficio de poder implementar "accidentalmente" un protocolo. ¿Puede explicar por qué cree que eso es beneficioso, @mdcfrancis? Sé que Go hace mucho de esto, pero eso parece deberse a que Go no puede escribir pato, lo que Julia sí. Sospecho que tener bloques implement eliminaría casi todas las necesidades de usar import lugar de using , lo que sería un gran beneficio.

Propuse algo muy similar a esto hace algún tiempo (no puedo encontrarlo ahora)

¿Quizás https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 y anterior https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (Editar: también https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021.)

SIP eso es.

@StefanKarpinski comentarios rápidos,

  • todas las clases que actualmente implementan iterables tendrán que ser modificadas para implementar explícitamente el protocolo si hacemos lo que usted propone, la propuesta actual simplemente agregando la definición a la base 'elevará' todas las clases existentes al protocolo.
  • si defino MyModule.MySuperIterable, que agrega una función adicional a la definición iterable, tendría que escribir una gran cantidad de código de placa de caldera para cada clase en lugar de agregar un método adicional.
  • No creo que lo que propongas contrarreste la lejanía, solo significa que tendría que escribir mucho código adicional para lograr el mismo objetivo.

Si se permitiera algún tipo de herencia en los protocolos, MySuperIterabe,
podría extender Base.Iterable, con el fin de reutilizar los métodos existentes.

El problema sería si solo quisiera una selección de los métodos en un
protocolo, pero eso parecería indicar que el protocolo original debería
ser un protocolo compuesto desde el principio.

@mdcfrancis : el primer punto es bueno, aunque lo que propongo no rompería ningún código existente, solo significaría que el código de las personas tendría que "optar" por los protocolos para sus tipos antes de poder contar con el envío. laboral.

¿Puede ampliar el punto MyModule.MySuperIterable? No veo de dónde viene la verbosidad adicional. Podría tener algo como esto, por ejemplo:

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

Que es esencialmente lo que dijo @ivarne .

En mi diseño específico anterior, los protocolos no son espacios de nombres, solo declaraciones sobre otros tipos y funciones. Sin embargo, esto probablemente se deba a que me estoy centrando en el sistema de tipos de núcleo. Podría imaginarme un azúcar sintáctico que se expande a una combinación de módulos y protocolos, p. Ej.

module Iterable

function start end
function done end
function next end

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

end

Luego, en contextos donde Iterable se trata como un tipo, usamos Iterable.the_protocol .

Me gusta esta perspectiva porque los protocolos jeff / mdcfrancis se sienten muy ortogonales a todo lo demás aquí. La sensación de ligereza de no tener que decir "X implementa el protocolo Y" a menos que quieras sentir "julian" para mí.

No sé por qué me suscribí a este número y cuándo lo hice. Pero sucede que esta propuesta de protocolo puede resolver la pregunta que planteé aquí .

No tengo nada que agregar sobre una base técnica, pero como un ejemplo de "protocolos" que se utilizan en la naturaleza en Julia (más o menos) sería JuMP determinando la funcionalidad de un solucionador, por ejemplo:

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

Genial, eso es útil. ¿Es suficiente que m.internalModel sea ​​lo que implemente el protocolo, o ambos argumentos son importantes?

Sí, es suficiente con m.internalModel para implementar el protocolo. Los otros argumentos son en su mayoría solo vectores.

Sí, suficiente para m.internalModel para implementar el protocolo

Una buena forma de encontrar ejemplos de protocolos en la naturaleza probablemente sea buscar llamadas applicable y method_exists .

Elixir también parece implementar protocolos, pero el número de protocolos en la biblioteca estándar (saliendo de la definición) parece bastante limitado.

¿Cuál sería la relación entre protocolos y tipos abstractos? La descripción del problema original proponía algo así como adjuntar un protocolo a un tipo abstracto. De hecho, me parece que la mayoría de los protocolos (ahora informales) que existen actualmente se implementan como tipos abstractos. ¿Para qué se usarían los tipos abstractos cuando se agrega soporte para protocolos? Una jerarquía de tipos sin ninguna forma de declarar su API no suena demasiado útil.

Muy buena pregunta. Hay muchas opciones ahí. Primero, es importante señalar que los tipos abstractos y los protocolos son bastante ortogonales, aunque ambas son formas de agrupar objetos. Los tipos abstractos son puramente nominales; etiquetan objetos como pertenecientes al conjunto. Los protocolos son puramente estructurales; un objeto pertenece al conjunto si tiene ciertas propiedades. Entonces algunas opciones son

  1. Solo ten ambos.
  2. Ser capaz de asociar protocolos con un tipo abstracto, por ejemplo, para que cuando un tipo se declare un subtipo, se verifique que cumpla con los protocolos.
  3. Elimina los tipos abstractos por completo.

Si tenemos algo como (2), creo que es importante reconocer que no es realmente una característica única, sino una combinación de tipificación nominal y estructural.

Una cosa para la que los tipos abstractos parecen útiles son sus parámetros, por ejemplo, escribir convert(AbstractArray{Int}, x) . Si AbstractArray fuera un protocolo, el tipo de elemento Int no necesariamente necesitaría ser mencionado en la definición del protocolo. Es información adicional sobre el tipo, además de, a partir del cual se requieren métodos. Entonces AbstractArray{T} y AbstractArray{S} seguirían siendo tipos diferentes, a pesar de especificar los mismos métodos, por lo que hemos reintroducido la escritura nominal. Por lo tanto, este uso de parámetros de tipo parece requerir una tipificación nominal de algún tipo.

Entonces, ¿nos daría una herencia abstracta múltiple?

Entonces, ¿nos daría una herencia abstracta múltiple?

No. Sería una forma de integrar o combinar las características, pero cada característica aún tendría las propiedades que tiene ahora.

Debo agregar que permitir la herencia abstracta múltiple es otra decisión de diseño casi ortogonal. En cualquier caso, el problema con el uso excesivo de tipos nominales abstractos es (1) es posible que pierda la implementación de protocolos después de los hechos (la persona A define el tipo, la persona B define el protocolo y su implementación para A), (2) podría perder el subtipo estructural de protocolos.

¿No son los parámetros de tipo en el sistema actual parte de alguna manera de la interfaz implícita? Por ejemplo, esta definición se basa en eso: ndims{T,n}(::AbstractArray{T,n}) = n y muchas funciones definidas por el usuario también lo hacen.

Entonces, en un nuevo protocolo + sistema de herencia abstracta, tendríamos AbstractArray{T,N} y ProtoAbstractArray . Ahora, un tipo que nominalmente no era AbstractArray necesitaría poder especificar cuáles son los parámetros T y N , presumiblemente a través de la codificación eltype y ndims . Entonces, todas las funciones parametrizadas en AbstractArray s tendrían que reescribirse para usar eltype y ndims lugar de parámetros. Entonces, tal vez tenga más sentido que el protocolo también lleve los parámetros, por lo que los tipos asociados podrían ser muy útiles después de todo. (Tenga en cuenta que los tipos concretos aún necesitarían parámetros).

Además, una agrupación de tipos en un protocolo utilizando @malmaud 's truco: https://github.com/JuliaLang/julia/issues/6975#issuecomment -161 056 795 es similar a la tipificación nominal: la agrupación se debe exclusivamente a recoger y tipos los tipos no comparten ninguna interfaz (utilizable). Entonces, ¿quizás los tipos abstractos y los protocolos se superponen bastante?

Sí, los parámetros de un tipo abstracto son definitivamente una especie de interfaz y, hasta cierto punto, redundantes con eltype y ndims . La principal diferencia parece ser que puede enviarlos directamente, sin una llamada de método adicional. Estoy de acuerdo en que con los tipos asociados estaríamos mucho más cerca de reemplazar los tipos abstractos con protocolos / rasgos. ¿Cómo sería la sintaxis? Idealmente, sería más débil que la llamada al método, ya que prefiero no tener una dependencia circular entre el subtipo y la llamada al método.

La pregunta restante es si es útil implementar un protocolo _sin_ convertirse en parte del tipo abstracto relacionado. Un ejemplo podrían ser las cadenas, que son iterables e indexables, pero a menudo se tratan como cantidades "escalares" en lugar de contenedores. No sé con qué frecuencia surge esto.

No creo que entiendo bien tu declaración de "llamada al método". Por lo tanto, esta sugerencia de sintaxis puede no ser lo que solicitó:

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

Eso podría funcionar, dependiendo de cómo se defina el subtipo de tipos de protocolo. Por ejemplo, dado

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

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

tenemos PAbstractArray{Int,1} <: Indexable{Int} ? Creo que esto podría funcionar muy bien si los parámetros coinciden con el nombre. Quizás también podríamos automatizar la definición que hace que eltype(x) devuelva el parámetro eltype del tipo x .

No me gusta particularmente poner definiciones de métodos dentro de un bloque impl , principalmente porque una única definición de método puede pertenecer a múltiples protocolos.

Entonces, parece que con tal mecanismo, ya no necesitaríamos tipos abstractos. AbstractArray{T,N} podría convertirse en un protocolo. Entonces automáticamente obtenemos herencia múltiple (de protocolos). Además, la imposibilidad de heredar de tipos concretos (que es una queja que a veces escuchamos de los recién llegados) es obvia, ya que solo se admitiría la herencia de protocolo.

Aparte: sería muy bueno poder expresar el rasgo Callable . Tendría que verse así:

protocol Callable
    ::TupleCons{_, Bottom}
end

donde TupleCons coincide por separado con el primer elemento de una tupla y el resto de los elementos. La idea es que esto coincida siempre que la tabla de métodos para _ no esté vacía (la parte inferior es un subtipo de cada tipo de tupla de argumento). De hecho, podríamos querer hacer una sintaxis Tuple{a,b} para TupleCons{a, TupleCons{b, EmptyTuple}} (ver también # 11242).

No creo que eso sea cierto, todos los parámetros de tipo están cuantificados existencialmente _con restricciones_ por lo que los tipos abstractos y los protocolos no son directamente sustituibles.

@jakebolewski, ¿puedes pensar en un ejemplo? Obviamente nunca volverán a ser exactamente lo mismo; Yo diría que la pregunta es más si podemos masajear uno de modo que podamos arreglárnoslas sin tener ambos.

Tal vez me estoy perdiendo el punto, pero ¿cómo pueden los protocolos codificar tipos abstractos moderadamente complejos con restricciones, como:

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

sin tener que enumerar nominalmente todas las posibilidades?

La propuesta Protocol propuesta es estrictamente menos expresiva en comparación con el subtipo abstracto, que es todo lo que estaba tratando de resaltar.

Podría imaginar lo siguiente (naturalmente, estirando el diseño hasta sus límites prácticos):

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

de acuerdo con la observación de que necesitamos algo así como tipos asociados o propiedades de tipo con nombre para que coincida con la expresividad de los tipos abstractos existentes. Con esto, podríamos tener una compatibilidad cercana:

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

El subtipo estructural para los campos de datos de los objetos nunca me pareció muy útil, pero aplicado a las propiedades de _tipos_ parece tener mucho sentido.

También me di cuenta de que esto puede proporcionar una trampilla de escape a los problemas de ambigüedad: la intersección de dos tipos está vacía si tienen valores en conflicto para algún parámetro. Entonces, si queremos un tipo Number inequívoco, podríamos tener

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

Esto está viendo super como solo otro tipo de propiedad.

Me gusta la sintaxis del protocolo propuesto, pero tengo algunas notas.

Pero entonces puede que esté malinterpretando todo. Recientemente comencé a considerar realmente a Julia como algo en lo que quiero trabajar, y aún no tengo una comprensión perfecta del sistema de tipos.

(a) Creo que sería más interesante con las características de características en las que @ mauro3 trabajó anteriormente. ¡Especialmente porque de qué sirve el envío múltiple si no puede tener varios protocolos de envío! Escribiré mi opinión sobre lo que es un ejemplo del mundo real más adelante. Pero la esencia general se reduce a "¿Existe un comportamiento que permita que estos dos objetos interactúen?". Puedo estar equivocado, y todo eso se puede incluir en protocolos, por ejemplo:

protocol Foo{bar}
    ...
end

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

Y eso también expone el problema clave de no permitir que el protocolo Foo haga referencia al protocolo Bar en la misma definición.

(B)

¿tenemos PAbstractArray {Int, 1} <: Indexable {Int}? Creo que esto podría funcionar muy bien si los parámetros coinciden con el nombre.

No estoy seguro de por qué tenemos que hacer coincidir los parámetros por _name_ (supongo que son los eltype nombres, si no entendí bien, ignore esta sección). ¿Por qué no simplemente hacer coincidir las posibles firmas de funciones? Mi principal problema al usar nombres es porque previene lo siguiente:

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 otro lado, se asegura de que su protocolo solo exponga la jerarquía de tipos específica que nosotros también queremos. Si no nombramos la coincidencia Iterable entonces no obtenemos los beneficios de implementar iterable (y tampoco dibujamos una ventaja en la dependencia). Pero no estoy seguro de qué gana un usuario con eso, además de la capacidad de hacer lo siguiente ...

(c) Entonces, puede que me esté perdiendo algo, pero ¿no es el propósito principal donde los tipos nombrados son útiles para describir cómo se comportan las diferentes partes de un superconjunto? Considere la jerarquía Number y los tipos abstractos Signed y Unsigned , ambos implementarían el protocolo Integer , pero a veces se comportarían de manera bastante diferente. Para distinguir entre ellos, ¿estamos ahora obligados a definir un negate solo en tipos Signed (especialmente difícil sin tipos de retorno en los que es posible que queramos negar un tipo Unsigned )?

Creo que este es el problema que describe en el ejemplo super = Number . Cuando declaramos bitstype Int16 <: Signed (mi otra pregunta es incluso cómo se aplican Number o Signed como protocolos con sus propiedades de tipo al tipo concreto?), ¿Eso adjunta las propiedades de tipo de el protocolo Signed ( super = Signed ) marcándolo como diferente de los tipos marcados por el protocolo Unsigned ? Porque esa es una solución extraña desde mi punto de vista, y no solo porque encuentro extraños los parámetros de tipo con nombre. Si dos protocolos coinciden exactamente excepto el tipo que colocaron en super, ¿en qué se diferencian de todos modos? Y si la diferencia está en los comportamientos entre subconjuntos de un tipo más grande (el protocolo), ¿no estamos realmente reinventando el propósito de los tipos abstractos?

(d) El problema es que queremos tipos abstractos para diferenciar entre comportamientos y queremos protocolos que aseguren ciertas capacidades (a menudo independientemente de otros comportamientos), a través de las cuales se expresan los comportamientos. Pero estamos tratando de completar las capacidades que los protocolos nos permiten asegurar y los comportamientos de partición de tipos abstractos.

La solución a la que estamos saltando a menudo es en la línea de "hacer que los tipos declaren su intención de implementar una clase abstracta y verificar el cumplimiento", lo cual es problemático en la implementación (referencias circulares, lo que lleva a agregar definiciones de funciones dentro del bloque de tipos o impl blocks) y elimina la buena calidad de los protocolos que se basan en el conjunto actual de métodos y los tipos en los que operan. De todos modos, estos problemas impiden poner protocolos en la jerarquía abstracta.

Pero lo que es más importante, los protocolos no describen el comportamiento, describen capacidades complejas en múltiples funciones (como la iteración), el comportamiento de esa iteración se describe mediante los tipos abstractos (ya sea que esté ordenada o incluso ordenada, por ejemplo). Por otro lado, la combinación de protocolo + tipo abstracto es útil una vez que podemos tener en nuestras manos un tipo real porque nos permite distribuir capacidades (métodos de utilidad de capacidad), comportamientos (métodos de alto nivel) o ambos (detalles de implementación). métodos).

(e) Si permitimos que los protocolos hereden múltiples protocolos (son básicamente estructurales de todos modos) y tantos tipos abstractos como tipos concretos (por ejemplo, sin herencia abstracta múltiple, uno) podemos permitir la creación de tipos de protocolo puros, tipos abstractos puros, y protocolo + tipos abstractos.

Creo que esto soluciona el problema Signed frente a Unsigned anterior:

  • Defina dos protocolos, ambos heredando del IntegerProtocol (heredando cualquier estructura de protocolo, NumberAddingProtocol , IntegerSteppingProtocol , etc.) uno del AbstractSignedInteger y el otro de AbstractUnsignedInteger ).
  • Entonces, un usuario del tipo Signed tiene garantizada tanto la funcionalidad (del protocolo) como el comportamiento (de la jerarquía abstracta).
  • Un tipo concreto AbstractSignedInteger sin los protocolos no se puede usar _ de todos modos_.
  • Pero curiosamente (y como característica futura ya mencionada anteriormente) podríamos eventualmente crear la capacidad para resolver las características faltantes, si solo existiera el IntegerSteppingProtocol (que es trivial y básicamente un alias para una sola función) para un dado el concreto AbstractUnsignedInteger podríamos intentar resolver para Signed implementando los otros protocolos en términos de él. Quizás incluso con algo como convert .

Manteniendo todos los tipos existentes convirtiendo la mayoría de ellos en protocolos + tipos abstractos, y dejando algunos como tipos abstractos puros.

Editar: (f) Ejemplo de sintaxis (incluida la parte (a) ).

Edición 2 : se corrigieron algunos errores ( :< lugar de <: ), se corrigió una mala elección ( Foo lugar 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}.

Veo problemas con esta sintaxis como:

  • Tipos internos anónimos del protocolo (por ejemplo, las variables de estado).
  • Tipos de devolución.
  • Difícil de implementar eficientemente la semántica.

Define Type_ _Abstract lo que es una entidad. Define _Protocol_ lo hace una entidad. Dentro de un solo paquete, estos dos conceptos son intercambiables: una entidad _es_ lo que _hace_. Y el "tipo abstracto" es más directo. Sin embargo, entre dos paquetes, hay una diferencia: no necesita lo que su cliente "es", pero sí requiere lo que su cliente "hace". Aquí, "tipo abstracto" no proporciona información al respecto.

En mi opinión, un protocolo es un solo tipo abstracto enviado. Puede ayudar a la extensión y cooperación del paquete. Por lo tanto, dentro de un solo paquete, donde las entidades están estrechamente relacionadas, use el tipo abstracto para facilitar el desarrollo (aprovechando el envío múltiple); entre paquetes, donde las entidades son más independientes, utilice el protocolo para reducir la exposición a la implementación.

@ mason-bially

No estoy seguro de por qué tenemos que hacer coincidir los parámetros por nombre

Me refiero a la coincidencia por nombre _ en contraposición a_ la coincidencia por posición. Estos nombres actuarían como registros con subtipos estructurales. Si tenemos

protocol Collection{T}
    eltype = T
end

entonces cualquier cosa con una propiedad llamada eltype es un subtipo de Collection . El orden y la posición de estos "parámetros" no importa.

Si dos protocolos coinciden exactamente excepto el tipo que colocaron en super, ¿en qué se diferencian de todos modos? Y si la diferencia está en los comportamientos entre subconjuntos de un tipo más grande (el protocolo), ¿no estamos realmente reinventando el propósito de los tipos abstractos?

Ese es un buen punto. Los parámetros nombrados, de hecho, recuperan muchas de las propiedades de los tipos abstractos. Comencé con la idea de que podríamos necesitar tanto protocolos como tipos abstractos, luego intenté unificar y generalizar las características. Después de todo, cuando declaras type Foo <: Bar actualmente, en algún nivel lo que realmente has hecho es establecer Foo.super === Bar . Entonces, tal vez deberíamos respaldar eso directamente, junto con cualquier otro par clave / valor que desee asociar.

"hacer que los tipos declaren su intención de implementar una clase abstracta y verificar el cumplimiento"

Sí, estoy en contra de que ese enfoque sea la característica principal.

Si permitimos que los protocolos hereden múltiples protocolos ... y tantos tipos abstractos

¿Significa esto decir, por ejemplo, "T es un subtipo de protocolo P si tiene los métodos x, y, z, y se declara un subtipo de AbstractArray"? Creo que este tipo de "protocolo + tipo abstracto" es muy similar a lo que obtendría con mi propuesta de propiedad super = T . Es cierto que en mi versión aún no he descubierto cómo encadenarlos en una jerarquía como la que tenemos ahora (por ejemplo, Integer <: Real <: Number ).

Tener un protocolo heredado de un tipo abstracto (nominal) parece ser una restricción muy fuerte. ¿Habría subtipos del tipo abstracto que _no_ implementaran el protocolo? Mi intuición es que es mejor mantener los protocolos y los tipos abstractos como elementos ortogonales.

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

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

No entiendo esta sintaxis.

  • ¿Tiene este protocolo un nombre?
  • ¿Qué significan exactamente las cosas dentro de { } y ( ) ?
  • ¿Cómo usas este protocolo? ¿Puedes enviarlo? Si es así, ¿qué significa definir f(x::ThisProtocol)=... , dado que el protocolo relaciona varios tipos?

entonces cualquier cosa con una propiedad llamada eltype es un subtipo de Collection. El orden y la posición de estos "parámetros" no importa.

Ajá, hubo un malentendido, eso tiene más sentido. A saber, la capacidad de asignar:

el1type = el_type
el2type = el_type

para resolver mi problema de ejemplo.

Entonces, tal vez deberíamos respaldar eso directamente, junto con cualquier otro par clave / valor que desee asociar.

Y esta característica clave / valor estaría en todos los tipos, ya que reemplazaríamos abstracto con ella. Esa es una buena solución general. Tu solución tiene mucho más sentido para mí ahora.

Es cierto que en mi versión aún no he descubierto cómo encadenarlos en una jerarquía como la que tenemos ahora (por ejemplo, Integer <: Real <: Number).

Creo que podría usar super (por ejemplo, con Integer super como Real ) y luego hacer super especial y actuar como un tipo con nombre o agregar una forma de agregar código de resolución de tipo personalizado (ala python) y hacer una regla predeterminada para el parámetro super .

Tener un protocolo heredado de un tipo abstracto (nominal) parece ser una restricción muy fuerte. ¿Habría subtipos del tipo abstracto que no implementaron el protocolo? Mi intuición es que es mejor mantener los protocolos y los tipos abstractos como elementos ortogonales.

¡Oh, sí, la restricción abstracta era completamente opcional! Mi punto era que los protocolos y los tipos abstractos son ortogonales. Usaría el protocolo abstracto + para asegurarse de obtener una combinación de cierto comportamiento _y_ capacidades asociadas. Si solo desea las capacidades (para funciones de utilidad) o solo el comportamiento, entonces las usa ortogonalmente.

¿Tiene este protocolo un nombre?

Dos protocolos con dos nombres ( Foo y Bar ) que provienen de un bloque, pero luego estoy acostumbrado a usar macros para expandir múltiples definiciones como esa. Esta parte de mi sintaxis fue un intento de resolver la parte (a) . Si ignora eso, la primera línea podría ser simplemente protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo (con otra definición separada para el protocolo Bar ). Además, todo Number , AbstractBar y AbstractFoo serían opcionales, como en las definiciones de tipo normales,

¿Qué significa exactamente el contenido de {} y ()?

{} es la sección de definición de tipo paramétrico estándar. Permitir el uso de Foo{Float64} para describir un tipo que implementa el protocolo Foo usando Float64 por ejemplo. () es básicamente una lista de vinculación de variables para el cuerpo del protocolo (por lo que se pueden describir varios protocolos a la vez). Es probable que tu confusión sea culpa mía porque escribí mal :< lugar de <: en mi original. También puede valer la pena intercambiarlos para mantener la estructura <<name>> <<parametric>> <<bindings>> , donde <<name>> veces puede ser una lista de enlaces.

¿Cómo usas este protocolo? ¿Puedes enviarlo? Si es así, ¿qué significa definir f(x::ThisProtocol)=... , dado que el protocolo relaciona varios tipos?

Su ejemplo de envío parece correcto por su sintaxis en mi opinión, de hecho considere las siguientes definiciones:

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)

Efectivamente, los protocolos están usando el tipo Top nombrado (Cualquiera) a menos que se le proporcione un tipo abstracto más específico para verificar la estructura. De hecho, puede valer la pena permitir algo como typealias Foo Intersect{FooProtocol, Foo} (_Editar: Intersect era el nombre incorrecto, quizás Join en su lugar Intersect era correcto la primera vez_) en lugar de usar la sintaxis del protocolo para hacerlo.

¡Ah, genial, eso tiene mucho más sentido para mí ahora! Es interesante definir varios protocolos juntos en el mismo bloque; Tendré que pensar un poco más en eso.

Limpié todos mis ejemplos hace unos minutos. Al principio del hilo, alguien mencionó la recopilación de un corpus de protocolos para probar ideas, creo que es una gran idea.

Los protocolos múltiples en el mismo bloque son una especie de molestia cuando trato de describir relaciones complejas entre objetos con anotaciones de tipo correctas en ambos lados en definir / compilar mientras carga lenguajes (por ejemplo, como Python; Java, por ejemplo, no tengo el problema). Por otro lado, la mayoría de ellos probablemente se arreglen fácilmente, en términos de usabilidad, con múltiples métodos de todos modos; pero las consideraciones de rendimiento pueden surgir de tener las características escritas correctamente dentro de los protocolos (optimizando los protocolos especializándolos en vtables, por ejemplo).

Mencionaste anteriormente que los protocolos podrían implementarse (falsamente) mediante métodos que usan ::Any . Creo que sería un caso bastante simple para simplemente ignorarlo si llegara el momento. El tipo concreto no se clasificaría como protocolo si el método de implementación se distribuyera en ::Any . Por otro lado, no estoy convencido de que esto sea necesariamente un problema.

Para empezar, si el método ::Any se agrega después del hecho (digamos, porque a alguien se le ocurrió un sistema más genérico para manejarlo), sigue siendo una implementación válida, y si usamos protocolos también como una característica de optimización, entonces Las versiones especializadas de los métodos enviados ::Any aún funcionan para mejorar el rendimiento. Así que al final estaría en contra de ignorarlos.

Pero podría valer la pena tener una sintaxis que permita al definidor del protocolo elegir entre las dos opciones (cualquiera que sea la predeterminada, permitir la otra). Para el primero, una sintaxis de reenvío para el método enviado ::Any , diga la palabra clave global (consulte también la siguiente sección). Para el segundo, una forma de requerir un método más específico, no puedo pensar en una palabra clave útil existente.

Editar: eliminó un montón de cosas inútiles.

Su Join es exactamente la intersección de los tipos de protocolo. En realidad, es un "encuentro". Y felizmente, el tipo Join no es necesario, porque los tipos de protocolo ya están cerrados en la intersección: para calcular la intersección, simplemente devuelva un nuevo tipo de protocolo con las dos listas de métodos concatenados.

No me preocupa demasiado que los protocolos se trivialicen con las definiciones de ::Any . Para mí, la regla "buscar definiciones coincidentes excepto Any no cuenta" va en contra de la navaja de Occam. Sin mencionar que pasar el indicador "ignorar Cualquiera" a través del algoritmo de subtipificación sería bastante molesto. Ni siquiera estoy seguro de que el algoritmo resultante sea coherente.

Me gusta mucho la idea de los protocolos (me recuerda un poco a los CLUsters), solo tengo curiosidad, ¿cómo encajaría esto con el nuevo subtipo que fue discutido por Jeff en JuliaCon, y con los rasgos? (dos cosas que todavía me gustaría ver en Julia).

Esto agregaría un nuevo tipo de tipo con sus propias reglas de subtipo (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877). A primera vista, parecen ser compatibles con el resto del sistema y simplemente se pueden enchufar.

Estos protocolos son prácticamente la versión de "un parámetro" de los rasgos de @ mauro3 .

Su Join es exactamente la intersección de los tipos de protocolo.

De alguna manera me convencí de que estaba equivocado antes cuando dije que era la intersección. Aunque todavía necesitaríamos una forma de Intersectar tipos en una línea (como Union ).

Editar:

También me sigue gustando generalizar protocolos y tipos abstractos en un sistema y permitir reglas personalizadas para su resolución (por ejemplo, para super para describir el sistema de tipos abstractos actual). Creo que si se hace bien, esto permitiría a las personas agregar sistemas de tipos personalizados y, finalmente, optimizaciones personalizadas para esos sistemas de tipos. Aunque no estoy seguro de que protocolo sea la palabra clave correcta, pero al menos podríamos convertir abstract en una macro, sería genial.

de los campos de trigo: es mejor levantar la comunalidad a través de lo protoculado y abstraído que buscar su generalización como destino.

¿Qué?

El proceso de generalizar la intención, la capacidad y el potencial de los protocolos y el de los tipos abstractos es una forma menos eficaz de resolver su síntesis cualitativamente más satisfactoria. Primero funciona mejor para recoger sus puntos en común intrínsecos de propósito, patrón, proceso. Y desarrollar esa comprensión, permitiendo que el refinamiento de la propia perspectiva forme la síntesis.

Cualquiera que sea la fructífera realización para Julia, está construida sobre el andamiaje que ofrece la síntesis. Una síntesis más clara es la fuerza constructiva y el poder inductivo.

¿Qué?

Creo que está diciendo que primero deberíamos averiguar qué queremos de los protocolos y por qué son útiles. Entonces, una vez que tengamos eso y tipos abstractos, será más fácil llegar a una síntesis general de ellos.

Simples protocolos

(1) abogando

Un protocolo puede extenderse para convertirse en un protocolo (más elaborado).
Un protocolo puede reducirse para convertirse en un protocolo (menos elaborado).
Un protocolo puede realizarse como una interfaz conforme [en software].
Se puede consultar un protocolo para determinar la conformidad de una interfaz.

(2) sugiriendo

Los protocolos deben admitir números de versión específicos del protocolo, por defecto.

Sería bueno apoyar alguna forma de hacer esto:
Cuando una interfaz se ajusta a un protocolo, responda verdadero; cuando una interfaz
es fiel a un subconjunto del protocolo y sería conforme si se aumentara,
responda incompleto, y responda falso en caso contrario. Una función debe enumerar todo
aumento necesario para una interfaz que está incompleta con un protocolo.

(3) meditando

Un protocolo podría ser un tipo de módulo distinguido. Sus exportaciones servirían
como comparador inicial al determinar si alguna interfaz se ajusta.
Cualquier protocolo especificado [exportado] tipos y funciones se puede declarar utilizando
@abstract , @type , @immutable y @function para apoyar la abstracción innata.

[pao: cambie a citas en código, aunque tenga en cuenta que el caballo ya ha abandonado el granero cuando está haciendo esto después del hecho ...]

(¡debe cotizar el @mentions !)

gracias - arreglándolo

El miércoles 16 de diciembre de 2015 a las 3:01 a. M., Mauro [email protected] escribió:

(¡debe citar las @menciones!)

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727.

lo siento, debí haber sido más claro: code-quote usando "and not"

Se corrigió la corrección de cotizaciones.

gracias - perdona mi ignorancia anterior

Traté de comprender esta discusión reciente sobre cómo agregar un tipo de protocolo. Tal vez estoy entendiendo mal algo, pero ¿por qué es necesario tener protocolos con nombre en lugar de usar el nombre del tipo abstracto asociado que el protocolo está a punto de describir?

En mi punto de vista, es bastante natural extender el sistema de tipos abstractos actual con alguna forma de describir el comportamiento que se espera del tipo. Al igual que se propuso inicialmente en este hilo, pero tal vez con la sintaxis de Jeffs

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

Al seguir esta ruta, no sería necesario indicar especialmente que un subtipo implementa la interfaz. Esto se haría implícitamente mediante el subtipo.

En mi humilde opinión, el objetivo principal de un mecanismo de interfaz explícito es obtener mejores mensajes de error y realizar una mejor prueba de verificación.

Entonces, una declaración de tipo como:

type Foo <: Iterable
  ...
end

¿Definimos las funciones en la sección ... ? Si no es así, ¿cuándo nos equivocamos acerca de las funciones que faltan (y las complejidades relacionadas con eso)? Además, ¿qué sucede con los tipos que implementan múltiples protocolos, habilitamos la herencia abstracta múltiple? ¿Cómo manejamos la resolución de supermétodo? ¿Qué hace esto con el envío múltiple (parece simplemente eliminarlo y pegar un sistema de objetos al estilo java allí)? ¿Cómo definimos nuevas especializaciones de tipos para métodos después de que se haya definido el primer tipo? ¿Cómo definimos los protocolos después de haber definido el tipo?

Todas estas preguntas son más fáciles de resolver creando un nuevo tipo (o creando una nueva formulación de tipo).

No hay necesariamente un tipo abstracto relacionado para cada protocolo (probablemente no debería haber ninguno). El mismo tipo puede implementar múltiples de las interfaces actuales. Lo cual no se puede describir con el sistema de tipos abstractos actual. De ahí el problema.

  • La herencia múltiple abstracta (que implementa múltiples protocolos) es ortogonal a esta característica (como lo indicó Jeff anteriormente). Entonces, no es que obtengamos esa característica solo porque se agregan protocolos al lenguaje.
  • Su próximo comentario es sobre la pregunta de cuándo verificar la interfaz. Creo que esto no tiene que estar vinculado a las definiciones de funciones en bloque, que no me parecen Julian. En cambio, hay tres soluciones simples:

    1. como se implementó en # 7025, use un método verify_interface que se puede llamar después de todas las definiciones de función o en una prueba unitaria

    2. Uno no puede verificar la interfaz en absoluto y diferirla a un mensaje de error mejorado en "MethodError". En realidad, esta es una buena alternativa para 1.

    3. Verifique todas las interfaces al final de una unidad de tiempo de compilación o al final de una fase de carga del módulo. Actualmente también es posible tener:

function a()
  b()
end

function b()
end

Por lo tanto, no creo que aquí se requieran definiciones de funciones en bloque.

  • El último punto es que puede haber protocolos que no estén vinculados a tipos abstractos. Esto es ciertamente cierto en la actualidad (por ejemplo, el protocolo informal "Iterable"). Sin embargo, desde mi punto de vista, esto se debe solo a la falta de herencia abstracta múltiple. Si este es el problema, agreguemos la herencia múltiple abstracta en lugar de agregar una nueva función de lenguaje que tiene como objetivo resolver esto. También creo que implementar múltiples interfaces es absolutamente crucial y esto es absolutamente común en Java / C #.

Creo que la diferencia entre una cosa similar a un "protocolo" y la herencia múltiple es que un tipo se puede agregar a un protocolo después de haber sido definido. Esto es útil si desea que su paquete (definición de protocolos) funcione con tipos existentes. Se podría permitir modificar los supertipos de un tipo después de su creación, pero en ese punto probablemente sea mejor llamarlo "protocolo" o algo así.

Hm, por lo que permite definir interfaces alternativas / mejoradas a los tipos existentes. Todavía no tengo claro dónde se requeriría esto realmente. Cuando uno quiere agregar algo a una interfaz existente (cuando seguimos el enfoque propuesto en el OP), simplemente se escribe un subtipo y se agregan métodos de interfaz adicionales al subtipo. Esto es lo bueno de ese enfoque. Escala bastante bien.

Ejemplo: digamos que tengo un paquete que serializa tipos. Se debe implementar un método tobits para un tipo, luego todas las funciones de ese paquete funcionarán con el tipo. Llamemos a esto el protocolo Serializer (es decir, tobits está definido). Ahora puedo agregar Array (o cualquier otro tipo) implementando tobits . Con herencia múltiple no pude hacer que Array funcione con Serialzer ya que no puedo agregar un supertipo a Array después de su definición. Creo que este es un caso de uso importante.

Ok, entiende esto. https://github.com/JuliaLang/IterativeSolvers.jl/issues/2 es un problema similar, donde la solución es básicamente usar la escritura de pato. Si pudiéramos tener algo que resuelva este problema de manera elegante, sería realmente bueno. Pero esto es algo que debe apoyarse a nivel de despacho. Si entiendo correctamente la idea del protocolo anterior, se podría poner un tipo abstracto o un protocolo como anotación de tipo en la función. Aquí sería bueno fusionar estos dos conceptos con una sola herramienta que sea lo suficientemente poderosa.

Estoy de acuerdo: será muy confuso tener tanto tipos abstractos como protocolos. Si mal no recuerdo, se argumentó anteriormente que los tipos abstractos tienen alguna semántica que no se puede modelar con protocolos, es decir, los tipos abstractos tienen alguna característica que los protocolos no tienen. Incluso si ese es necesariamente el caso (no estoy convencido), seguirá siendo confuso ya que existe una gran superposición entre los dos conceptos. Por lo tanto, los tipos abstractos deben eliminarse en favor de los protocolos.

En la medida en que haya consenso arriba sobre los protocolos, enfatizan la especificación de interfaces. Es posible que se hayan utilizado tipos abstractos para hacer algunos de los protocolos ausentes. Eso no significa que sea su uso más importante. Dígame qué son y qué no son los protocolos, luego podría decirle en qué se diferencian los tipos abstractos y algo de lo que aportan. Nunca he considerado que los tipos abstractos tengan tanto que ver con la interfaz como con la tipología. Desechar un enfoque natural de la flexibilidad tipológica es costoso.

@JeffreySarnoff +1

Piense en la jerarquía de tipos de números. Los diferentes tipos abstractos, por ejemplo, firmados, no firmados, no están definidos por su interfaz. No existe un conjunto de métodos que defina "Sin firmar". Es simplemente una declaración muy útil.

Realmente no veo el problema. Si los tipos Signed y Unsigned admiten el mismo conjunto de métodos, podemos crear dos protocolos con interfaces idénticas. Aún así, declarar un tipo como Signed lugar de Unsigned puede usarse para el envío (es decir, los métodos de la misma función actúan de manera diferente). La clave aquí es requerir una declaración explícita antes de considerar que un tipo implementa un protocolo, en lugar de detectarlo implícitamente basándose en los métodos que implementa.

Pero tener protocolos asociados implícitamente también es importante, como en https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775

Los protocolos no solo pueden definir funciones a las que se puede llamar, sino que también pueden documentar (ya sea implícitamente o en formas comprobables por máquina) propiedades que deben mantenerse. Tal como:

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

Esta diferencia de comportamiento visible desde el exterior entre Signed y Unsigned es lo que hace que esta distinción sea útil.

Si existe una distinción entre tipos que es tan "abstracta" que no se puede verificar inmediatamente, al menos teóricamente, desde el exterior, entonces es probable que uno necesite conocer la implementación de un tipo para tomar la decisión correcta. Aquí es donde el actual abstract podría resultar útil. Esto probablemente va en la dirección de los tipos de datos algebraicos.

No hay ninguna razón por la que los protocolos no deban usarse simplemente para agrupar tipos, es decir, sin requerir ningún método definido (y es posible con el diseño "actual" usando el truco: https://github.com/JuliaLang/julia/issues/ 6975 # issuecomment-161056795). (Tenga en cuenta también que esto no interfiere con los protocolos definidos implícitamente).

Teniendo en cuenta el ejemplo (Un)signed : ¿qué haría si tuviera un tipo que es Signed pero por alguna razón tiene que ser también un subtipo de otro tipo abstracto? Esto no sería posible.

@eschnett : los tipos abstractos, por el momento, no tienen nada que ver con la implementación de sus subtipos. Aunque eso se ha discutido: # 4935.

Los tipos de datos algebraicos son un buen ejemplo donde el refinamiento sucesivo es intrínsecamente significativo.
Cualquier taxonomía se da de forma mucho más natural y más directamente útil como una jerarquía de tipos abstractos que como una mezcla de especificaciones de protocolo.

La nota sobre tener un tipo que es un subtipo de más de una jerarquía de tipos abstractos también es importante. Hay una gran cantidad de poder utilitario que viene con la herencia múltiple de abstracciones.

@ mauro3 Sí, lo sé. Estaba pensando en algo equivalente a uniones discriminadas, pero implementado tan eficientemente como tuplas en lugar de a través del sistema de tipos (como se implementan actualmente los sindicatos). Esto incluiría enumeraciones, tipos que aceptan valores NULL y podría manejar algunos otros casos de manera más eficiente que los tipos abstractos actualmente.

Por ejemplo, como tuplas con elementos anónimos:

DiscriminatedUnion{Int16, UInt32, Float64}

o con elementos con nombre:

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

El punto que estaba tratando de hacer es que los tipos abstractos son una buena manera de mapear tal construcción a Julia.

No hay ninguna razón por la que los protocolos no deban usarse simplemente para agrupar tipos, es decir, sin requerir ningún método definido (y es posible con el diseño "actual" usando el truco: # 6975 (comentario)). (Tenga en cuenta también que esto no interfiere con los protocolos definidos implícitamente).

Siento que debería tener cuidado con esto para lograr el rendimiento, una consideración que no muchos parecen considerar con la suficiente frecuencia. En el ejemplo, parecería que uno quisiera simplemente definir la versión que no es cualquiera para que el compilador aún pueda elegir la función en tiempo de compilación (en lugar de tener que llamar a una función para elegir la correcta en tiempo de ejecución, o el compilador inspeccionando funciones para determinar sus resultados). Personalmente, creo que usar múltiples "herencias" abstractas como etiquetas sería una mejor solución.

Creo que deberíamos mantener los trucos requeridos y el conocimiento del sistema de tipos al mínimo (aunque podría estar envuelto en una macro, se sentiría como un truco extraño de una macro; si estamos usando macros para manipular el sistema de tipos, creo que @ La solución unificada de JeffBezanson solucionaría mejor este problema).

Teniendo en cuenta el ejemplo (sin) firmado: ¿qué haría si tuviera un tipo que está firmado pero que por alguna razón tiene que ser también un subtipo de otro tipo abstracto? Esto no sería posible.

Herencia abstracta múltiple.


Creo que todo este terreno se ha cubierto antes, esta conversación parece ir en círculos (aunque en círculos más cerrados cada vez). Creo que se mencionó que se debe adquirir un corpus o problemas en el uso de protocolos. Esto nos permitiría juzgar las soluciones con mayor facilidad.

Mientras reiteramos las cosas :) Quiero recordarles a todos que los tipos abstractos son nominales mientras que los protocolos son estructurales, así que prefiero los diseños que los tratan como ortogonales, a menos que podamos llegar a una "codificación" aceptable de los tipos abstractos en los protocolos. (quizás con un uso inteligente de tipos asociados). Puntos de bonificación, por supuesto, si también produce una herencia abstracta múltiple. Siento que esto es posible, pero aún no lo hemos logrado.

@JeffBezanson ¿Son los "tipos asociados" distintos de los "tipos concretos asociados con [un] protocolo"?

Sí, así lo creo; Me refiero a "tipos asociados" en el sentido técnico de un protocolo que especifica algún par clave-valor donde el "valor" es un tipo, de la misma manera que los protocolos especifican métodos. por ejemplo, "el tipo Foo sigue el protocolo Container si tiene un eltype " o "el tipo Foo sigue el protocolo Matrix si su parámetro ndims es 2".

Los tipos abstractos son nominales mientras que los protocolos son estructurales y
Los tipos abstractos son cualitativos mientras que los protocolos son operativos y
los tipos abstractos (con herencia múltiple) se orquestan mientras los protocolos conducen

Incluso si hubiera una codificación de uno en el otro, el "hey, hola ... ¿cómo estás? ¡Vamos a hacer!" de Julia debe presentar claramente: la noción generalmente intencionada de protocolo y los tipos abstractos multheredables (una noción de propósito generalizado). Si hay un despliegue ingenioso que le da a Julia ambos, envueltos por separado, es más probable que se haga así que uno a través de uno y otro.

@ mason-bially: ¿entonces deberíamos agregar herencia múltiple también? Esto todavía dejaría el problema de que los supertipos no se pueden agregar después de la creación de un tipo (a menos que eso también esté permitido).

@JeffBezanson : nada nos impediría permitir protocolos puramente nominales.

@ mauro3 ¿Por qué la decisión de permitir la inserción de supertipo post facto debe estar vinculada a la herencia múltiple? Y hay diferentes tipos de creación de supertipos, algunos son evidentemente inofensivos presuponiendo la capacidad de interponer un nuevo lo que sean: he querido agregar un tipo abstracto entre Real y AbstractFloat, digamos ProtoFloat, para poder enviar en doble flotadores dobles y flotadores del sistema juntos sin interferir con los flotadores del sistema que viven como subtipos de AbstractFloat. Quizás menos fácil de permitir, sería la capacidad de subseccionar los subtipos actuales de Integer, y así evitar muchos mensajes "ambiguos con .. define f (Bool) antes .."; o introducir un supertipo de Signed que es un subtipo de Integer y abrir la jerarquía numérica al manejo transparente de, digamos, números ordinales.

Lo siento si inicié otra ronda del círculo. El tema es bastante complejo y realmente tenemos que asegurarnos de que la solución sea súper simple de usar. Entonces necesitamos cubrir:

  • solución general
  • sin degradación del rendimiento
  • facilidad de uso (¡y también fácil de entender!)

Dado que lo que se propuso inicialmente en el n. ° 6975 es bastante diferente a la idea de protocolo que se discutirá más adelante, puede ser bueno tener algún tipo de JEP que describa cómo podrían verse los protocolos.

Un ejemplo de cómo puede definir una interfaz formal y validarla usando el 0.4 actual (sin macros), dispatch actualmente se basa en el estilo de rasgos dispatch a menos que se realicen modificaciones en gf.c. Esto utiliza funciones generadas para la validación, todo el cálculo de tipos se realiza en el espacio de tipos.

Estoy empezando a usar esto como una verificación de tiempo de ejecución en un DSL que estamos definiendo donde tengo que asegurarme de que el tipo proporcionado sea un iterador de fechas.

Actualmente admite herencia múltiple de súper tipos, el nombre de campo _super no es usado por el tiempo de ejecución y puede ser cualquier símbolo válido. Puede suministrar n otros tipos a _super Tuple.

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

Solo señalando aquí que hice un seguimiento de una discusión de JuliaCon sobre la posible sintaxis de los rasgos en https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040

Guy Steele tiene una buena comprensión de los rasgos en un lenguaje de envío múltiple (Fortaleza), vea su discurso de apertura de la JuliaCon 2016: https://youtu.be/EZD3Scuv02g .

Algunos aspectos destacados: sistema de rasgos grandes para propiedades algebraicas, pruebas unitarias de propiedades de rasgos para tipos que implementan un rasgo, y que el sistema que implementaron era quizás demasiado complicado y que ahora haría algo más simple.

Nuevo Swift para el caso de uso de AD del compilador de tensorflow para protocolos:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy y @Keno podrían estar interesados ​​en esto. Tiene contenido completamente nuevo

Creo que esta presentación merece atención al explorar el espacio de diseño para este problema.

Para la discusión de ideas no específicas y enlaces a trabajos de fondo relevantes, sería mejor comenzar un hilo de discurso correspondiente y publicar y discutir allí.

Tenga en cuenta que casi todos los problemas encontrados y discutidos en la investigación sobre programación genérica en lenguajes de tipado estático son irrelevantes para Julia. Los lenguajes estáticos se ocupan casi exclusivamente del problema de proporcionar suficiente expresividad para escribir el código que desean y, al mismo tiempo, poder verificar de forma estática que no haya violaciones del sistema de tipos. No tenemos problemas con la expresividad y no requerimos una verificación de tipo estática, por lo que nada de eso realmente importa en Julia.

Lo que sí nos importa es permitir que las personas documenten las expectativas de un protocolo de una manera estructurada que el lenguaje pueda luego verificar dinámicamente (de antemano, cuando sea posible). También nos preocupamos por permitir que las personas se relacionen con cosas como rasgos; permanece abierto si deben conectarse.

En pocas palabras: si bien el trabajo académico sobre protocolos en lenguajes estáticos puede ser de interés general, no es muy útil en el contexto de Julia.

Lo que sí nos importa es permitir que las personas documenten las expectativas de un protocolo de una manera estructurada que el lenguaje pueda luego verificar dinámicamente (de antemano, cuando sea posible). También nos preocupamos por permitir que las personas se relacionen con cosas como rasgos; permanece abierto si deben conectarse.

_ ese es el_: ticket:

Además de evitar cambios importantes, ¿sería factible en julia la eliminación de tipos abstractos y la introducción de interfaces implícitas al estilo golang?

No, no lo haría.

Bueno, ¿no es eso de lo que se tratan los protocolos / rasgos? Se debatió si los protocolos deben ser implícitos o explícitos.

Creo que desde 0.3 (2014), la experiencia ha demostrado que las interfaces implícitas (es decir, no impuestas por el lenguaje / compilador) funcionan bien. Además, habiendo sido testigo de cómo evolucionaron algunos paquetes, creo que las mejores interfaces se desarrollaron orgánicamente y se formalizaron (= documentaron) solo en un momento posterior.

No estoy seguro de que sea necesaria una descripción formal de las interfaces, reforzada por el lenguaje de alguna manera. Pero mientras eso se decide, sería genial fomentar lo siguiente (en la documentación, tutoriales y guías de estilo):

  1. Las "interfaces" son baratas y livianas, solo un montón de funciones con un comportamiento prescrito para un conjunto de tipos (sí, los tipos tienen el nivel correcto de granularidad, por x::T , T debería ser suficiente para decidir si x implementa la interfaz). Entonces, si uno está definiendo un paquete con comportamiento extensible, realmente tiene sentido documentar la interfaz.

  2. No es necesario que las interfaces

  3. El reenvío / composición requiere implícitamente interfaces. "Cómo hacer que una envoltura herede todos los métodos del padre" es una pregunta que surge a menudo, pero no es la pregunta correcta. La solución práctica es tener una interfaz central e implementarla para el contenedor.

  4. Los rasgos son baratos y deben usarse generosamente. Base.IndexStyle es un excelente ejemplo canónico.

Lo siguiente se beneficiaría de una aclaración, ya que no estoy seguro de cuál es la mejor práctica:

  1. ¿Debería la interfaz tener una función de consulta, como por ejemplo Tables.istable para decidir si un objeto implementa la interfaz? Creo que es una buena práctica si una persona que llama puede trabajar con varias interfaces alternativas y necesita recorrer la lista de alternativas.

  2. ¿Cuál es el mejor lugar para la documentación de una interfaz en una cadena de documentos? Yo diría que la función de consulta anterior.

  1. sí, los tipos tienen el nivel correcto de granularidad

¿Por qué es así? Algunos aspectos de los tipos se pueden tener en cuenta en las interfaces (con fines de envío), como la iteración. De lo contrario, tendría que reescribir el código o imponer una estructura innecesaria.

  1. No es necesario que las interfaces

Quizás no sea necesario, pero ¿sería mejor? Puedo tener un envío de función en un tipo iterable. ¿No debería un tipo iterable en mosaico cumplir eso implícitamente? ¿Por qué el usuario debería tener que dibujar estos en torno a tipos nominales cuando solo se preocupan por la interfaz?

¿Cuál es el punto de la subtipificación nominal si básicamente los está utilizando como interfaces abstractas? Los rasgos parecen ser más granulares y poderosos, por lo que sería una mejor generalización. Así que parece que los tipos son casi rasgos, pero tenemos que tener rasgos para sortear sus limitaciones (y viceversa).

¿Cuál es el punto de la subtipificación nominal si básicamente los está utilizando como interfaces abstractas?

Despacho: puede despachar en el tipo nominal de algo. Si no necesita enviar información sobre si un tipo implementa una interfaz o no, puede simplemente esquivarlo. Esto es para lo que la gente suele usar los rasgos sagrados: el rasgo le permite enviar para llamar a una implementación que asume que alguna interfaz está implementada (por ejemplo, "que tiene una longitud conocida"). Algo que la gente parece querer es evitar esa capa de indirecta, pero parece que es simplemente una conveniencia, no una necesidad.

¿Por qué es así? Algunos aspectos de los tipos se pueden tener en cuenta en las interfaces (con fines de envío), como la iteración. De lo contrario, tendría que reescribir el código o imponer una estructura innecesaria.

Creo que @tpapp estaba diciendo que solo necesita el tipo para determinar si algo implementa o no una interfaz, no que todas las interfaces se pueden representar con jerarquías de tipos.

Solo un pensamiento, mientras usas MacroTools 's forward :

A veces es molesto reenviar muchos métodos

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

¿Qué pasaría si pudiéramos usar el tipo de Foo.x y una lista de métodos y luego inferir cuál reenviar? Será una especie de inheritance y se puede implementar con las funciones existentes (macros + función generada), también parece una especie de interfaz, pero no necesitamos nada más en el idioma.

Sé que nunca podríamos crear una lista de lo que se heredará (esta es también la razón por la que el modelo estático class es menos flexible), a veces solo necesitas algunos de ellos, pero es conveniente para las funciones principales ( por ejemplo, alguien quiere definir una envoltura (subtipo de AbstractArray ) alrededor de Array , la mayoría de las funciones simplemente se reenvían)

@datnamer : como han aclarado otros, las interfaces no deben ser más granulares que los tipos (es decir, la implementación de la interfaz nunca debe depender del valor , dado el tipo). Esto encaja bien con el modelo de optimización del compilador y no es una restricción en la práctica.

Quizás no lo tuve claro, pero el propósito de mi respuesta fue señalar que ya tenemos interfaces en la medida en que es útil en Julia , y son livianas, rápidas y omnipresentes a medida que madura el ecosistema.

En mi opinión, una especificación formal para describir una interfaz agrega poco valor: sería solo documentación y verificación de que algunos métodos están disponibles. Este último es parte de una interfaz, pero la otra parte es la semántica implementada por estos métodos (por ejemplo, si A es una matriz, axes(A) me da un rango de coordenadas que son válidas para getindex ). Las especificaciones formales de las interfaces no pueden abordar estos en general, por lo que soy de la opinión de que simplemente agregarían un texto estándar con poco valor. También me preocupa que solo levante una (pequeña) barrera de entrada con pocos beneficios.

Sin embargo, lo que me encantaría ver es

  1. documentación para cada vez más interfaces (en una cadena de documentos),

  2. suites de prueba para detectar errores obvios para interfaces maduras para tipos recién definidos (por ejemplo, muchos T <: AbstractArray implementan eltype(::T) y no eltype(::Type{T}) .

@tpapp Tiene sentido para mí ahora, gracias.

@StefanKarpinski No lo entiendo del todo. Los rasgos no son tipos nominales (¿verdad?), Sin embargo, pueden usarse para el envío.

Mi punto es básicamente el hecho por @tknopp y @ mauro3 aquí: https://discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43?u=datnamer

Que al tener rasgos y mecanografía abstracta, hay complejidad y confusión adicionales al tener dos conceptos muy similares.

Algo que la gente parece querer es evitar esa capa de indirecta, pero parece que es simplemente una conveniencia, no una necesidad.

¿Se pueden distribuir secciones de la jerarquía de rasgos agrupadas por elementos como uniones e intersecciones, con parámetros de tipo, de manera robusta? No lo he probado, pero parece que requiere apoyo lingüístico. Problema de expresión de IE en el dominio de tipos.

Editar: Creo que el problema fue mi combinación de interfaces y rasgos, como se usan aquí.

Solo publico esto aquí porque es divertido: parece que Concepts definitivamente ha sido aceptado y será parte de C ++ 20. ¡Cosas interesantes!

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

Creo que los rasgos son una muy buena manera de resolver este problema y los rasgos sagrados ciertamente han recorrido un largo camino. Sin embargo, creo que lo que Julia realmente necesita es una forma de agrupar funciones que pertenecen a un rasgo. Esto sería útil por razones de documentación, pero también para la legibilidad del código. Por lo que he visto hasta ahora, creo que una sintaxis de rasgo como en Rust sería el camino a seguir.

Creo que esto es muy importante, y el caso de uso más importante sería para indexar iteradores. Aquí hay una propuesta para el tipo de sintaxis que podría esperar que funcione. Disculpas si ya se ha propuesto (hilo largo ...).

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
¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

omus picture omus  ·  3Comentarios

StefanKarpinski picture StefanKarpinski  ·  3Comentarios

iamed2 picture iamed2  ·  3Comentarios

StefanKarpinski picture StefanKarpinski  ·  3Comentarios

ararslan picture ararslan  ·  3Comentarios