我认为这个功能请求还没有它自己的问题,尽管它已经在例如#5 中讨论过。
我认为如果我们可以在抽象类型上显式定义接口,那就太好了。 我所说的接口是指为满足抽象类型要求而必须实现的所有方法。 目前,接口只是隐式定义的,它可以分散在几个文件中,因此很难确定从抽象类型派生时必须实现什么。
接口将主要给我们两件事:
Base.graphics 有一个宏,它实际上允许通过在回退实现中编码错误消息来定义接口。 我觉得这已经很聪明了。 但也许给它以下语法更简洁:
abstract MyType has print, size(::MyType,::Int), push!
如果可以指定不同的粒度,那就太好了。 print
和push!
声明只说必须有任何具有该名称的方法(并且MyType
作为第一个参数),但它们没有指定类型。 相比之下, size
声明是完全类型化的。 我认为这提供了很大的灵活性,对于无类型的接口声明,仍然可以给出非常具体的错误消息。
正如我在 #5 中所说的,这些接口基本上是 C++ 中为 C++14 或 C++17 设计的Concept-light
。 在完成了相当多的 C++ 模板编程之后,我确信这方面的一些形式化对 Julia 也有好处。
一般来说,我认为这是一个更好的面向接口编程的方向。
然而,这里缺少一些东西。 方法的签名(不仅仅是它们的名称)对于接口也很重要。
这不是一件容易实现的事情,会有很多问题。 这可能是 C++ 11 不接受 _Concepts_ 的原因之一,三年后,只有非常有限的 _lite_ 版本进入 C++ 14。
我示例中的size
方法包含签名。 Base.graphics 中的进一步@mustimplement
也考虑了签名。
我应该补充一点,我们已经有了Concept-light
一部分,它能够将类型限制为某个抽象类型的子类型。 接口是另一部分。
那个宏很酷。 我已经手动定义了触发错误的回退,它在定义接口方面效果很好。 例如,JuliaOpt 的 MathProgBase 就可以做到这一点,而且效果很好。 我正在玩弄一个新的求解器 (https://github.com/IainNZ/RationalSimplex.jl),我只需要继续实现接口函数,直到它停止引发错误以使其正常工作。
你的提议也会做类似的事情,对吧? 但是你_必须_实现整个接口吗?
这如何处理协变/逆变参数?
例如,
abstract A has foo(::A, ::Array)
type B <: A
...
end
type C <: A
...
end
# is it ok to let the arguments to have more general types?
foo(x::Union(B, C), y::AbstractArray) = ....
@IainNZ是的,该提案实际上是关于使@mustimplement
更加通用,例如可以但不必提供签名。 我的感觉是,这是一个“核心”,值得拥有自己的语法。 强制所有方法都真正实现会很棒,但是在@mustimplement
完成的当前运行时检查已经是一件很棒的事情并且可能更容易实现。
@lindahua这是一个有趣的例子。 必须考虑一下。
@lindahua One 可能希望您的示例能够正常工作。 @mustimplement
不起作用,因为它定义了更具体的方法签名。
所以这可能需要在编译器中更深入地实现。 在抽象类型定义上,必须跟踪接口名称/签名。 并且在当前抛出“...未定义”错误的那一点上,必须生成适当的错误消息。
当我们有语法和 API 来表达和访问信息时,很容易改变MethodError
打印方式。
这可以让我们得到的另一件事是base.Test
一个函数,用于验证类型(所有类型?)完全实现父类型的接口。 那将是一个非常简洁的单元测试。
谢谢@ivarne。 因此,实现可能如下所示:
has
声明时,解析器需要调整以填充 dict。MethodError
需要查找当前函数是否是全局字典的一部分。大多数逻辑将在MethodError
。
我一直在对此进行一些试验,并使用以下要点https://gist.github.com/tknopp/ed53dc22b61062a2b283我可以做到:
julia> abstract A
julia> addInterface(A,length)
julia> type B <: A end
julia> checkInterface(B)
ERROR: Interface not implemented! B has to implement length in order to be subtype of A ! in error at error.jl:22
定义length
不会抛出错误:
julia> import Base.length
julia> length(::B) = 10
length (generic function with 34 methods)
julia> checkInterface(B)
true
并不是说这目前没有考虑签名。
我稍微更新了要点中的代码,以便可以考虑函数签名。 它仍然非常hacky,但现在可以使用以下方法:
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
我应该补充一点,gist 中的接口缓存现在对符号而不是函数进行操作,以便可以添加接口并在之后声明函数。 我可能必须对签名做同样的事情。
刚刚看到#2248 已经有一些关于接口的材料了。
在我们推出 0.3 版本之前,我将推迟发布关于接口等更具推测性的功能的想法,但既然你已经开始讨论,这里是我前一段时间写的。
这是接口声明和该接口的实现的语法模型:
interface Iterable{T,S}
start :: Iterable --> S
done :: (Iterable,S) --> Bool
next :: (Iterable,S) --> (T,S)
end
implement UnitRange{T} <: Iterable{T,T}
start(r::UnitRange) = oftype(r.start + 1, r.start)
next(r::UnitRange, state::T) = (oftype(T,state), state + 1)
done(r::UnitRange, state::T) = i == oftype(i,r.stop) + 1
end
让我们把它分解成几部分。 首先,有函数类型语法: A --> B
是将A
类型的对象映射到B
类型的函数的类型。 这种表示法中的元组做了显而易见的事情。 孤立地看,我提议f :: A --> B
将声明f
是一个泛型函数,将类型A
映射到类型B
。 这是一个稍微开放的问题,这意味着什么。 这是否意味着当应用于A
类型的参数时, f
将给出B
类型的结果? 这是否意味着f
只能应用于A
类型的参数? 是否应该在任何地方进行自动转换——在输出上,在输入上? 现在,我们可以假设所有这些都是创建一个新的泛型函数而不添加任何方法,并且类型仅用于文档。
其次,有接口Iterable{T,S}
。 这使得Iterable
有点像模块,有点像抽象类型。 它就像一个模块,因为它绑定到名为Iterable.start
、 Iterable.done
和Iterable.next
泛型函数。 它就像一个类型, Iterable
和Iterable{T}
和Iterable{T,S}
可以在任何抽象类型可以使用的地方使用——特别是在方法调度中。
第三, implement
块定义了UnitRange
实现Iterable
接口。 在implement
块内, Iterable.start
、 Iterable.done
和Iterable.next
函数可用,就好像用户已经完成了import Iterable: start, done, next
,允许为这些函数添加方法。 该块与参数类型声明的方式类似 - 在块内, UnitRange
表示特定的UnitRange
,而不是伞形类型。
implement
块的主要优点是它避免了需要显式地扩展的import
函数——它们是为你隐式导入的,这很好,因为人们通常对import
感到困惑无论如何, Base
中用户想要扩展的大多数通用函数应该属于某个接口,因此这应该消除import
的绝大多数用途。 由于您始终可以完全限定名称,也许我们可以完全取消它。
另一个我反复考虑的想法是将接口函数的“内部”和“外部”版本分开。 我的意思是,“内部”函数是您提供用于实现某些接口的方法的函数,而“外部”函数是您调用以根据某些接口实现通用功能的函数。 当您查看sort!
函数的方法(不包括已弃用的方法)时,请考虑:
julia> methods(sort!)
sort!(r::UnitRange{T<:Real}) at range.jl:498
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering) at sort.jl:242
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering) at sort.jl:259
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering) at sort.jl:289
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering,t) at sort.jl:289
sort!{T<:Union(Float64,Float32)}(v::AbstractArray{T<:Union(Float64,Float32),1},a::Algorithm,o::Union(ReverseOrdering{ForwardOrdering},ForwardOrdering)) at sort.jl:441
sort!{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),T<:Union(Float64,Float32)}(v::Array{Int64,1},a::Algorithm,o::Perm{O<:Union(ReverseOrdering{ForwardOrdering},ForwardOrdering),Array{T<:Union(Float64,Float32),1}}) at sort.jl:442
sort!(v::AbstractArray{T,1},alg::Algorithm,order::Ordering) at sort.jl:329
sort!(v::AbstractArray{T,1}) at sort.jl:330
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int32}) at linalg/cholmod.jl:809
sort!{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32)}(A::CholmodSparse{Tv<:Union(Complex{Float32},Complex{Float64},Float64,Float32),Int64}) at linalg/cholmod.jl:809
其中一些方法旨在供公众使用,但其他方法只是公共排序方法内部实现的一部分。 真的,这应该有的唯一公共方法是:
sort!(v::AbstractArray)
其余的都是噪音,属于“内部”。 特别是,
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,::InsertionSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::QuickSortAlg,o::Ordering)
sort!(v::AbstractArray{T,1},lo::Int64,hi::Int64,a::MergeSortAlg,o::Ordering)
各种方法是排序算法实现的以连接到通用排序机制的方法。 目前Sort.Algorithm
是一个抽象类型, InsertionSortAlg
、 QuickSortAlg
和MergeSortAlg
是它的具体子类型。 对于接口, Sort.Algorithm
可以是一个接口,具体的算法将实现它。 像这样的东西:
# module Sort
interface Algorithm
sort! :: (AbstractVector, Int, Int, Algorithm, Ordering) --> AbstractVector
end
implement InsertionSortAlg <: Algorithm
function sort!(v::AbstractVector, lo::Int, hi::Int, ::InsertionSortAlg, o::Ordering)
<strong i="17">@inbounds</strong> for i = lo+1:hi
j = i
x = v[i]
while j > lo
if lt(o, x, v[j-1])
v[j] = v[j-1]
j -= 1
continue
end
break
end
v[j] = x
end
return v
end
end
我们想要的分离可以通过定义来实现:
# module Sort
sort!(v::AbstractVector, alg::Algorithm, order::Ordering) =
Algorithm.sort!(v,1,length(v),alg,order)
这_非常_接近我们目前正在做的事情,除了我们调用Algorithm.sort!
而不是sort!
– 并且在实现各种排序算法时,“内部”定义是Algorithm.sort!
一个方法sort!
函数。 这具有将sort!
的实现与其外部接口分离的效果。
@StefanKarpinski 非常感谢您的撰写! 这肯定不是 0.3 的东西。 很抱歉我在这个时候提出了这个问题。 我只是不确定 0.3 是否会在不久或半年内发生;-)
乍一看,我真的(!)喜欢实现部分定义了自己的代码块。 这使得可以直接验证类型定义上的接口。
不用担心 - 在我们试图稳定发布时推测未来的功能并没有什么坏处。
您的方法更为基础,并试图解决一些与界面无关的问题。 它还在某种程度上为语言带来了新的结构(即接口),使语言稍微复杂一些(这不一定是件坏事)。
我更多地将“接口”视为抽象类型的注释。 如果将has
放入其中,则可以指定接口,但不必指定。
正如我所说,我真的很希望可以在其声明中直接验证接口。 这里侵入性最小的方法可能是允许在类型声明中定义方法。 所以以你的例子为例
type UnitRange{T} <: Iterable{T,T}
start(r::UnitRange) = oftype(r.start + 1, r.start)
next(r::UnitRange, state::T) = (oftype(T,state), state + 1)
done(r::UnitRange, state::T) = i == oftype(i,r.stop) + 1
end
仍然允许在类型声明之外定义函数。 唯一的区别是内部函数声明是针对接口进行验证的。
但同样,也许我的“最小侵入性方法”太短视了。 真不知道。
将这些定义放在类型块中的一个问题是,为了做到这一点,我们至少确实需要接口的多重继承,并且可以想象不同接口之间可能存在名称冲突。 您可能还想添加一个事实,即类型在定义类型之后的某个点支持接口,尽管我不确定这一点。
@StefanKarpinski很
Graphs 包是最需要接口系统的包。 看看这个系统如何表达这里概述的接口会很有趣: http :
@StefanKarpinski :我没有完全看到多重继承和块内函数声明的问题。 在类型块中,必须检查所有继承的接口。
但我有点理解人们可能想让接口实现“打开”。 类型内函数声明可能会使语言过于复杂。 也许我在 #7025 中实施的方法就足够了。 在函数声明之后(或在单元测试中)放置verify_interface
或将其推迟到MethodError
。
这个问题是不同的接口可能具有相同名称的通用函数,这会导致名称冲突并需要执行显式导入或通过完全限定名称添加方法。 这也使得哪些方法定义属于哪些接口变得不太清楚——这就是为什么首先会发生名称冲突的原因。
顺便说一句,我同意将接口添加为语言中的另一个“事物”感觉有点太不正交了。 毕竟,正如我在提案中提到的,它们有点像模块,有点像类型。 感觉一些概念的统一可能是可能的,但我不清楚如何。
我更喜欢 interface-as-library 模型而不是 interface-as-language-feature 模型有几个原因:它使语言更简单(当然是偏好而不是具体的反对),这意味着该功能仍然是可选的并且可以很容易地在不破坏实际语言的情况下改进或完全替换。
具体来说,我认为该提案(或提案至少形状)从@tknopp比从@StefanKarpinski一个更好-它提供了清晰,时间检查,而无需任何语言中的新。 我看到的主要缺点是缺乏处理类型变量的能力; 我认为这可以通过让接口定义为所需函数的类型提供类型 _predicates_ 来处理。
我提议的主要动机之一是由于必须_导入_泛型函数 - 但不导出它们 - 以便向它们添加方法而引起的大量混乱。 大多数情况下,当有人试图实现一个非官方的接口时,就会发生这种情况,所以这看起来就像正在发生的事情一样。
这似乎是一个需要解决的正交问题,除非您想将方法完全限制为属于接口。
不,这显然不是一个好的限制。
@StefanKarpinski你提到你可以在接口上调度。 同样在implement
语法中的想法是特定类型实现接口。
这似乎与多重分派有点矛盾,因为通常方法不属于特定类型,它们属于类型元组。 因此,如果方法不属于类型,那么接口(基本上是方法集)如何属于类型?
假设我正在使用库 M:
module M
abstract A
abstract B
type A2 <: A end
type A3 <: A end
type B2 <: B end
function f(a::A2, b::B2)
# do stuff
end
function f(a::A3, b::B2)
# do stuff
end
export f, A, B, A2, A3, B2
end # module M
现在我想编写一个带有 A 和 B 的通用函数
using M
function userfunc(a::A, b::B, i::Int)
res = f(a, b)
res + i
end
在这个例子中, f
函数形成了一个特殊的接口,它接受一个A
和一个B
,我希望能够假设我可以调用f
对它们起作用。 在这种情况下,不清楚应该考虑使用其中的哪一个来实现接口。
其他想要提供A
和B
具体子类型的模块应该提供f
。 为了避免所需方法的组合爆炸,我希望库针对抽象类型定义f
:
module N
using M
type SpecialA <: A end
type SpecialB <: B end
function M.f(a::SpecialA, b::SpecialB)
# do stuff
end
function M.f(a::A, b::SpecialB)
# do stuff
end
function M.f(a::SpecialA, b::B)
# do stuff
end
export SpecialA, SpecialB
end # module N
诚然,这个例子感觉很做作,但希望它说明了(至少在我看来)感觉就像多重分派和实现接口的特定类型的概念之间存在根本的不匹配。
不过,我确实明白您对import
混淆的看法。 我在这个例子中尝试了几次才记住,当我放入using M
然后尝试向f
添加方法时,它没有按照我的预期执行,我不得不添加方法到M.f
(或者我可以使用import
)。 我不认为接口是解决这个问题的方法。 是否有单独的问题来集思广益使添加方法更直观的方法?
@abe-egnor 我也认为更开放的方法似乎更可行。 我的原型#7025 基本上缺少两件事:
a) 定义接口的更好的语法
b) 参数类型定义
因为我不是一个参数类型的大师,所以我有点确定 b) 可以由有更深入经验的人解决。
关于 a) 可以使用宏。 我个人认为我们可以花一些语言支持来直接将接口定义为抽象类型定义的一部分。 has
方法可能太短视了。 代码块可能会使这更好。 实际上,这与#4935 高度相关,其中定义了“内部”接口,而这是关于公共接口的。 这些不必捆绑,因为我认为这个问题比 #4935 重要得多。 但仍然是语法明智的人可能希望将这两种用例都考虑在内。
https://gist.github.com/abe-egnor/503661eb4cc0d66b4489第一次尝试我正在考虑的实现方式。 简而言之,接口是一个从类型到字典的函数,它定义了该接口所需函数的名称和参数类型。 @implement
宏只是调用给定类型的函数,然后将类型拼接到给定的函数定义中,检查所有函数是否都已定义。
好点:
坏点:
我想我有一个参数化问题的解决方案——简而言之,接口定义应该是类型表达式上的宏,而不是类型值上的函数。 @implement
宏然后可以将类型参数扩展到函数定义,允许类似:
<strong i="7">@interface</strong> stack(Container, Data) begin
stack_push!(Container, Data)
end
<strong i="8">@implement</strong> stack{T}(Vector{T}, T) begin
stack_push!(vec, x) = push!(vec, x)
end
在这种情况下,类型参数被扩展到接口中定义的方法,所以它扩展到stack_push!{T}(vec::Vector{T}, x::T) = push!(vec, x)
,我认为这是完全正确的。
当我有时间时,我将重新工作我的初始实现来做到这一点; 大概一周左右。
我浏览了一些互联网,看看其他编程语言对接口、继承等做了什么,并提出了一些想法。 (如果有人对此感兴趣,我做了非常粗略的笔记 https://gist.github.com/mauro3/e3e18833daf49cdf8f60)
简而言之,接口可以通过以下方式实现:
这会将抽象类型转换为接口,然后需要具体的子类型来实现该接口。
长长的故事:
我发现一些“现代”语言取消了子类型多态性,即没有直接的类型分组,而是根据它们属于接口/特征/类型类对它们的类型进行分组。 在某些语言中,接口/特征/类型类之间可以有顺序并相互继承。 他们似乎(大部分)也对这个选择感到高兴。 例如:去,
锈,哈斯克尔。
Go 是三者中最不严格的,它允许隐式指定它的接口,即如果一个类型实现了一个接口的特定功能集,那么它就属于那个接口。 对于 Rust,接口(特征)必须在impl
块中显式实现。 Go 和 Rust 都没有多方法。 Haskell 有多种方法,它们实际上直接链接到接口(类型类)。
从某种意义上说,这与 Julia 所做的很相似,抽象类型就像一个(隐式)接口,即它们是关于行为的,而不是关于字段的。 这就是@StefanKarpinski在他上面的一篇帖子中也观察到的,并表示另外有接口“感觉有点太不正交了”。 因此,Julia 具有类型层次结构(即子类型多态性),而 Go/Rust/Haskell 则没有。
如何将 Julia 的抽象类型转变为更多的接口/特征/类型类,同时将所有类型保持在None<: ... <:Any
层次结构中? 这将需要:
1)允许(抽象)类型的多重继承(问题#5)
2) 允许将函数与抽象类型相关联(即定义一个接口)
3) 允许为抽象(即默认实现)和具体类型指定该接口。
我认为这可能会产生比我们现在更细粒度的类型图,并且可以逐步实现。 例如,一个数组类型将被拼凑在一起:
abstract Container <: Iterable, Indexable, ...
end
abstract AbstractArray <: Container, Arithmetic, ...
...
end
abstract Associative{K,V} <: Iterable, Indexable, Eq
haskey :: (Associative, _) --> Bool
end
abstract Iterable{T,S}
start :: Iterable --> S
done :: (Iterable,S) --> Bool
next :: (Iterable,S) --> (T,S)
end
abstract Indexable{A,I}
getindex :: (A,I) --> eltype(A)
setindex! :: (A,I) --> A
get! :: (A, I, eltype(A)) --> eltype(A)
get :: (A, I, eltype(A)) --> eltype(A)
end
abstract Eq{A,B}
== :: (A,B) --> Boolean
end
...
因此,基本上抽象类型可以具有作为字段的通用函数(即成为接口),而具体类型只有普通字段。 例如,这可以解决从 AbstractArray 派生的太多东西的问题,因为人们可以只为他们的容器挑选有用的部分,而不是从 AbstractArray 派生。
如果这是一个好主意,还有很多事情需要解决(特别是如何指定类型和类型参数),但也许值得考虑一下?
@ssfrr上面评论说接口和多分派是不兼容的。 这不应该是这种情况,例如,在 Haskell 中,多方法只能通过使用类型类来实现。
在阅读@StefanKarpinski的文章时,我还发现直接使用abstract
而不是interface
是有意义的。 然而,在这种情况下,重要的是abstract
继承了interface
一个关键属性:类型为implement
和interface
_after_ 被定义的可能性。 然后,我可以通过在我的代码中声明 typA 实现 algoB 所需的接口(我猜这意味着具体类型具有一种开放的多重继承)来使用来自 lib A 的类型 typA 和来自 lib B 的算法 algoB。
@mauro3 ,我真的很喜欢你的建议。 对我来说,感觉非常“朱利安”和自然。 我还认为它是接口、多重继承和抽象类型“字段”的独特而强大的集成(尽管不是真的,因为字段只是方法/函数,而不是值)。 我还认为这与@StefanKarpinski区分“内部”和“外部”接口方法的想法很abstract Algorithm
和Algorithm.sort!
来实现他对sort!
示例的建议Algorithm.sort!
。
对不起大家
------------------ 原始邮件 ------------------
发件人:“Jacob Quinn”通知@ github.com
发送时间: 2014年9月12日(发给) 上午6:23
转发:"JuliaLang/julia" [email protected];
抄送:《实施》 [email protected];
主题:Re:[julia] 抽象类型的接口 (#6975)
@mauro3 ,我真的很喜欢你的建议。 对我来说,感觉非常“朱利安”和自然。 我还认为它是接口、多重继承和抽象类型“字段”的独特而强大的集成(尽管不是真的,因为字段只是方法/函数,而不是值)。 我还认为这与@StefanKarpinski区分“内部”和“外部”接口方法的想法很
—
直接回复此邮件或在 GitHub 上查看。
@implement非常抱歉; 不知道我们是如何给你发消息的。 如果您还不知道,您可以使用屏幕右侧的“取消订阅”按钮将自己从这些通知中删除。
不,我只想说我帮不了你太多说 sarry
------------------ 原始邮件 ------------------
发件人:“pao”通知@ github.com
发送时间:2014年9月13日(周六)晚上9:50
转发:"JuliaLang/julia" [email protected];
抄送:《实施》 [email protected];
主题:Re:[julia] 抽象类型的接口 (#6975)
@implement非常抱歉; 不知道我们是如何给你发消息的。 如果您还不知道,您可以使用屏幕右侧的“取消订阅”按钮将自己从这些通知中删除。
—
直接回复此邮件或在 GitHub 上查看。
我们不指望你! 这是一个意外,因为我们谈论的是一个与您的用户名同名的 Julia 宏。 谢谢!
我刚刚看到在 Rust 中有一些潜在的有趣功能(可能与这个问题相关): http : https ://github.com/rust-lang/rfcs/pull/195
在看到THTT (“Tim Holy Trait Trick”)后,我在过去几周对接口/特征进行了更多思考。 我想出了一些想法和实现: Traits.jl 。 首先,(我认为)traits 应该被视为涉及一种或几种类型的契约。 这意味着,正如我和其他人在上面建议的那样,仅将接口的功能附加到一个抽象类型是行不通的(至少在涉及多种类型的特征的一般情况下不会)。 其次,方法应该能够使用特征进行dispatch ,正如@StefanKarpinski上面建议的那样。
Nuff 说,这里有一个使用我的包 Traits.jl 的例子:
<strong i="12">@traitdef</strong> Eq{X,Y} begin
# note that anything is part of Eq as ==(::Any,::Any) is defined
==(X,Y) -> Bool
end
<strong i="13">@traitdef</strong> Cmp{X,Y} <: Eq{X,Y} begin
isless(X,Y) -> Bool
end
这声明Eq
和Cmp
是X
和Y
类型之间的契约。 Cmp
具有Eq
作为超特征,即Eq
和Cmp
需要满足。 在@traitdef
主体中,函数签名指定需要定义哪些方法。 返回类型目前什么都不做。 类型不需要显式实现特征,只需实现函数即可。 我可以检查一下,比如Cmp{Int,Float64}
是否确实是一个特征:
julia> istrait(Cmp{Int,Float64})
true
julia> istrait(Cmp{Int,String})
false
显式 trait 实现尚未包含在包中,但添加起来应该相当简单。
使用 _trait-dispatch_ 的函数可以这样定义
<strong i="31">@traitfn</strong> ft1{X,Y; Cmp{X,Y}}(x::X,y::Y) = x>y ? 5 : 6
这声明了一个函数ft1
,它带有两个参数,它们的类型需要满足Cmp{X,Y}
的约束。 我可以在另一个特征上添加另一个方法调度:
<strong i="37">@traitdef</strong> MyT{X,Y} begin
foobar(X,Y) -> Bool
end
# and implement it for a type:
type A
a
end
foobar(a::A, b::A) = a.a==b.a
<strong i="38">@traitfn</strong> ft1{X,Y; MyT{X,Y}}(x::X,y::Y) = foobar(x,y) ? -99 : -999
现在可以像普通函数一样调用这些特征函数:
julia> ft1(4,5)
6
julia> ft1(A(5), A(6))
-999
稍后将其他类型添加到特征很容易(使用 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
特征函数的 _Implementation_ 及其调度基于Tim 的技巧和分阶段函数,见下文。 Trait 定义相对简单,请参阅此处以获取全部的手动实现。
简而言之,特征调度转向
<strong i="51">@traitfn</strong> f{X,Y; Trait1{X,Y}}(x::X,y::Y) = x+y
变成这样(有点简化)
f(x,y) = _f(x,y, checkfn(x,y))
_f{X,Y}(x::X,y::Y,::Type{Trait1{X,Y}}) = x+y
# default
checkfn{T,S}(x::T,y::S) = error("Function f not implemented for type ($T,$S)")
# add types-tuples to Trait1 by modifying the checkfn function:
checkfn(::Int, ::Int) = Trait1{Int,Int}
f(1,2) # 3
在包中, checkfn
是由stagedfuncitons 自动生成的。 但请参阅 Traits.jl 的 README 以获取更多详细信息。
_性能_ 对于简单的特征函数,生成的机器代码与其对应的鸭子类型相同,即尽可能好。 对于较长的函数,存在差异,长度可达约 20%。 我不知道为什么,因为我认为这应该全部内联。
(10 月 27 日编辑以反映Traits.jl
细微变化)
Traits.jl 包准备好探索了吗? 自述文件说“使用@traitimpl实现接口(尚未完成...)”——这是一个重要的缺点吗?
它已准备好进行探索(包括错误:-)。 缺少的@traitimpl
只是意味着而不是
<strong i="7">@traitimpl</strong> Cmp{T1, T2} begin
isless(t1::T1, t2::T2) = t1.t < t2.f
end
您只需手动定义函数
Base.isless(t1::T1, t2::T2) = t1.t < t2.f
对于您的两种类型T1
和T2
。
我添加了@traitimpl
宏,所以上面的例子现在可以工作了。 我还更新了自述文件,详细说明了使用情况。 我添加了一个实现@lindahua Graphs.jl 接口的示例:
https://github.com/mauro3/Traits.jl/blob/master/examples/ex_graphs.jl
这真的很酷。 我特别喜欢它认识到接口通常是类型元组的属性,而不是单个类型。
我也觉得这很酷。 这种方法有很多值得喜欢的地方。 干得好。
:+1:
感谢您的良好反馈! 我稍微更新/重构了代码,它应该合理地没有错误并且适合玩。
在这一点上,如果人们可以尝试一下,看看它是否适合他们的用例,那可能会很好。
这是让人们以新的眼光看待他/她自己的代码的那些软件包之一。 很酷。
抱歉,我还没有时间认真研究这个问题,但我知道一旦我这样做了,我就会想重构一些东西......
我也会重构我的包:)
我想知道,在我看来,如果特征可用(并允许多次分派,就像上面的建议一样),那么就不需要抽象类型层次结构机制,或者根本不需要抽象类型。 这可能吗?
在实现了 trait 之后,base 和整个生态系统中的每个功能最终都会公开一个仅基于 trait 的公共 API,抽象类型将消失。 当然,这个过程可以通过弃用抽象类型来催化
再考虑一下,用特征替换抽象类型需要像这样参数化类型:
Array{X; Cmp{X}} # an array of comparables
myvar::Type{X; Cmp{X}} # just a variable which is comparable
我同意上面的 mauro3 点,具有特征(根据他的定义,我认为非常好)相当于抽象类型
我还要补充一点,为了允许在定义后将特征分配给类型,还需要允许“延迟继承”,即告诉编译器一个类型在定义后从某个抽象类型继承。
所以总而言之,在我看来,在抽象类型之外开发一些特征/接口概念会导致一些重复,引入不同的方法来实现同样的事情。 我现在认为引入这些概念的最好方法是慢慢地给抽象类型添加特性
编辑:当然在某些时候从抽象类型继承具体类型将不得不被弃用并最终被禁止。 类型特征将被隐式或显式确定,但永远不会通过继承
抽象类型不只是一个“无聊”的特征例子吗?
如果是这样,是否可以保留当前语法并简单地将其含义更改为特征(如果用户需要,则提供正交自由等)?
_我想知道这是否也可以解决Point{Float64} <: Pointy{Real}
示例(不确定是否有问题编号)?_
是的,我认为你是对的。 Trait 功能可以通过增强当前的 julia 抽象类型来实现。 他们需要
1)多重继承
2) 函数签名
3) “惰性继承”,显式地赋予一个已经定义的类型一个新的特征
看起来工作量很大,但也许这可以慢慢发展,而不会对社区造成太大影响。 所以至少我们明白了;)
我认为无论我们选择什么都将是一个巨大的变化,我们还没有准备好在 0.4 中开始工作。 如果我不得不猜测,我敢打赌,我们更有可能朝着特征的方向发展,而不是朝着添加传统多重继承的方向发展。 但是我的水晶球快坏了,所以如果不尝试一些东西,很难确定会发生什么。
FWIW,我发现 Simon Peyton-Jones 在下面的演讲中对类型类的讨论对如何使用类似特征的东西代替子类型非常有用: http :
是的,一整罐蠕虫!
@johnmyleswhite ,感谢您的链接,非常有趣。 这是它的视频链接,非常值得观看以填补空白。 该演示文稿似乎触及了我们在这里提出的许多问题。 有趣的是,类型类的实现与 Traits.jl 中的非常相似(Tim 的技巧,traits 是数据类型)。 Haskell 的https://www.haskell.org/haskellwiki/Multi-parameter_type_class很像 Traits.jl。 他在演讲中的一个问题是:“一旦我们全心全意地采用了泛型,我们还真的需要子类型吗?” (泛型是参数多态函数,我认为,请参阅)这有点像 @skariel和@hayd在上面思考的内容。
参考@skariel和@hayd ,我认为单参数特征(如在 Traits.jl 中)确实非常接近抽象类型,除了它们可以有另一个层次结构,即多重继承。
但是多参数特征似乎有点不同,至少在我的脑海中是这样。 正如我所看到的,抽象类型的类型参数似乎主要是关于一个类型中包含哪些其他类型,例如, Associative{Int,String}
表示 dict 包含Int
键和String
值。 而Tr{Associative,Int,String}...
表示Associative
、 Int
和Strings
之间存在一些“契约”。 但是,也许Associative{Int,String}
应该这样读,即有像getindex(::Associative, ::Int) -> String
、 setindex!(::Associative, ::Int, ::String)
......
@mauro3重要的是将Associative
类型的对象作为参数传递给函数,这样它就可以自己创建Associative{Int,String}
:
function f(A::Associative)
a = A{Int,String}() # create new associative
a[1] = "one"
return a
end
例如,您可以将其称为f(Dict)
。
@eschnett ,抱歉,我不明白你的意思。
@mauro3我想我的想法太复杂了; 不理我。
我用以下内容更新了 Traits.jl:
@doc
寻求帮助有关详细信息,请参阅https://github.com/mauro3/Traits.jl/blob/master/NEWS.md 。 欢迎反馈!
@Rory-Finnegan 整理了一个接口包https://github.com/Rory-Finnegan/Interfaces.jl
我最近与@mdcfrancis讨论了这个
protocol Iterable
start(::_)
done(::_, state)
next(::_, state)
end
我们有isa(Iterable, Protocol)
和Protocol <: Type
。 当然,你可以派遣这些。 您可以使用T <: Iterable
检查类型是否实现了协议。
以下是子类型规则:
设 P, Q 为协议类型
令 T 为非协议类型
| 输入 | 结果|
| --- | --- |
| P <: 任何 | 真实|
| 底部 <: P | 真实|
| (union,unionall,var) <: P | 使用正常规则; 将 P 视为基本类型 |
| P <: (union,unionall,var) | 使用正常规则 |
| P <: P | 真实|
| P <: Q | 检查方法(Q) <: 方法(P) |
| P <: T | 假|
| T <: P | P 的方法存在用 T 代替 _ |
最后一个是大的:为了测试 T <: P,你在 P 的定义中用 T 替换 _ 并检查每个签名的method_exists
。 当然,这本身意味着抛出“你必须实现这个”错误的回退定义成为一件非常糟糕的事情。 希望这更像是一个美容问题。
另一个问题是,如果定义了例如start(::Iterable)
则此定义是循环的。 这样的定义其实没有意义。 我们可以以某种方式阻止这种情况,或者在子类型检查期间检测到这个循环。 我不是 100% 确定简单的循环检测可以修复它,但这似乎是合理的。
对于类型交集,我们有:
| 输入 | 结果|
| --- | --- |
| P ∩ (union,unionall,tvar) | 使用正常规则 |
| P ∩ Q | P |
| P ∩ T | T |
P ∩ Q 有几个选项:
P ∩ T 很棘手。 T 是一个很好的保守近似值,因为非协议类型比协议类型“小”,因为它们将您限制在类型层次结构的一个区域,而协议类型则不然(因为任何类型都可以实现任何协议)。 做得比这更好似乎需要通用的交叉类型,我宁愿在初始实现中避免这种情况,因为这需要彻底检查子类型算法,并在 worm-can 之后打开 worm-can。
特异性:当 P<:Q 时,P 仅比 Q 更具特异性。 但是由于 P ∩ Q 总是非空的,所以在同一个插槽中具有不同协议的定义通常是不明确的,这似乎是您想要的(例如,您会说“如果 x 是可迭代的,则执行此操作,但如果 x 是可打印的,则执行那”)。
然而,没有方便的方法来表达所需的消歧定义,所以这可能是一个错误。
在#13412 之后,协议可以在元组类型的联合上“编码”为UnionAll _
(其中每个内部元组的第一个元素是相关函数的类型)。 这是我以前没有想到的那种设计的好处。 例如,协议的结构子类型似乎会自动消失。
当然,这些协议都是“单参数”风格。 我喜欢它的简单性,而且我不确定如何像T <: Iterable
一样优雅地处理类型组。
过去有一些关于这个想法的评论,x-ref https://github.com/JuliaLang/julia/issues/5#issuecomment -37995516。
我们会支持,例如
protocol Iterable{T}
start(::_)::T
done(::_, state::T)
next(::_, state::T)
end
哇,我真的很喜欢这个(尤其是@Keno的扩展名)!
+1 这正是我想要的!
@Keno这绝对是此功能的一个不错的升级路径,但有理由推迟它。 任何涉及返回类型的东西当然都是有问题的。 参数本身在概念上很好,会很棒,但有点难以实现。 它需要在检查所有方法是否存在的过程周围维护一个类型环境。
似乎您可以将特征(如类数组类型的 O(1) 线性索引)硬塞到这个方案中。 你会定义一个像hassomeproperty(::T) = true
(但 _not_ hassomeproperty(::Any) = false
)的虚拟方法,然后有
protocol MyProperty
hassomeproperty(::_)
end
_
是否可以在协议定义中的同一方法中多次出现,例如
protocol Comparable
>(::_, ::_)
=(::_, ::_0
end
_ 可以在协议定义中以相同的方法多次出现
是的。 您只需为_
每个实例删除候选类型。
@JeffBezanson真的很期待。 对我来说特别值得注意的是协议的“远程性”。 因为我可以为一个类型实现一个特定的/自定义的协议,而该类型的作者不知道该协议的存在。
方法可以在任何时候动态定义(例如使用@eval
)的事实呢? 那么一个类型是否是给定协议的子类型通常是静态不可知的,这似乎会破坏在很多情况下避免动态调度的优化。
是的,这让 #265 变得更糟:) 添加方法时需要更改分派和生成的代码的问题是相同的,只是具有更多的依赖边缘。
很高兴看到这个进步! 当然,我会争辩说多参数特征是前进的方向。 但无论如何,95% 的特征可能是单一参数。 只是它们非常适合多次调度! 如果需要,这可能会在以后重新审视。 说够了。
几点意见:
@Keno的建议(实际上是杰夫原著中的state
)被称为关联类型。 请注意,它们在没有返回类型的情况下也很有用。 Rust 有一个不错的手动输入。 我认为它们是一个好主意,尽管不像 Rust 那样必要。 我不认为它应该是 trait 的参数:在定义一个在Iterable
上调度的函数时,我不知道T
是什么。
根据我的经验, method_exists
在当前形式下无法用于此 (#8959)。 但据推测,这将在 #8974(或与此)中得到解决。 我发现在执行 Traits.jl 时,将方法签名与 trait-sigantures 匹配是最难的部分,尤其是考虑到参数化和可变参数函数(请参阅参考资料)。
想必继承也有可能吧?
我真的很想看到一种允许定义默认实现的机制。 经典的是,为了进行比较,您只需要定义=
、 <
、 >
、 <=
、 >=
。 也许这就是杰夫提到的循环真正有用的地方。 继续上面的例子,定义start(::Indexable) = 1
和done(i::Indexable,state)=length(i)==state
将使它们成为默认值。 因此,许多类型只需要定义next
。
好点。 我认为关联类型与Iterable{T}
的参数有些不同。 在我的编码中,该参数只会对内部的所有内容进行存在量化——“是否存在一个 T 使得 Foo 类型实现了这个协议?”。
是的,似乎我们可以轻松地允许protocol Foo <: Bar, Baz
,只需将 Bar 和 Baz 的签名复制到 Foo 中。
多参数特征绝对是强大的。 我认为考虑如何将它们与子类型集成非常有趣。 你可以有TypePair{A,B} <: Trait
,但这似乎不太正确。
我认为你的提议(在功能方面)实际上更像Swift 而不是 Clojure。
混合名义(类型)和结构(协议)子类型似乎很奇怪(我认为这是未来混淆的根源)(但我想这是不可避免的)。
我也有点怀疑数学/矩阵运算协议的表达能力。 我认为考虑更复杂的例子(矩阵运算)会比具有明确指定接口的 Iteration 更有启发性。 例如,参见core.matrix 库。
我同意; 在这一点上,我们应该收集协议的例子,看看它们是否符合我们的要求。
你想象的方式,协议是它们的方法所属的命名空间吗? 即当你写
protocol Iterable
start(::_)
done(::_, state)
next(::_, state)
end
定义泛型函数start
、 done
和next
并且它们的完全限定名称为Iterable.start
、 Iterable.done
似乎很自然Iterable.next
。 一个类型会实现Iterable
但会实现Iterable
协议中的所有泛型函数。 前段时间我提出了与此非常相似的内容(现在找不到了),但另一方面,当您要实现协议时,您可以这样做:
implement T <: Iterable
# in here `start`, `done` and `next` are automatically imported
start(x::T) = something
done(x::T, state) = whatever
next(x::T, state) = etcetera, nextstate
end
如果我理解的话,这将抵消@mdcfrancis提到的“远程性”,但implement
块将消除几乎所有使用import
而不是using
,这将是一个巨大的好处。
前段时间我提出了与此非常相似的内容(现在找不到了)
也许https://github.com/JuliaLang/julia/issues/6975#issuecomment -44502467 和更早的https://github.com/quinnj/Datetime.jl/issues/27#issuecomment -31305128? (编辑:还有 https://github.com/JuliaLang/julia/issues/6190#issuecomment-37932021。)
是的,就是这样。
@StefanKarpinski快速评论,
如果允许对协议进行某种继承,MySuperIterabe,
可以扩展 Base.Iterable,以重用现有方法。
问题是,如果您只想在一个
协议,但这似乎表明原始协议应该
从一开始就成为一个复合协议。
@mdcfrancis——第一点很好,虽然我的提议不会破坏任何现有的代码,但这只是意味着人们的代码必须“选择”他们类型的协议,然后他们才能依靠调度在职的。
你能扩展 MyModule.MySuperIterable 点吗? 我没有看到多余的冗长来自哪里。 你可以有这样的东西,例如:
protocol Enumerable <: Iterable
# inherits start, next and done; adds the following:
length(::_) # => Integer
end
这基本上就是@ivarne所说的。
在我上面的具体设计中,协议不是命名空间,只是关于其他类型和函数的声明。 然而,这可能是因为我专注于核心类型系统。 我可以想象语法糖扩展到模块和协议的组合,例如
module Iterable
function start end
function done end
function next end
jeff_protocol the_protocol
start(::_)
done(::_, state)
next(::_, state)
end
end
然后在将 Iterable 视为类型的上下文中,我们使用Iterable.the_protocol
。
我喜欢这个观点,因为 jeff/mdcfrancis 协议感觉与这里的其他一切都非常正交。 不需要说“X 实现协议 Y”的轻量级感觉,除非你想对我感觉“朱利安”。
我不知道为什么我订阅了这个问题以及何时订阅。 但是恰好这个协议提案可以解决我在这里提出的问题。
在技术基础上我没有什么可添加的,但作为在 Julia 中(某种意义上)在野外使用的“协议”的一个例子,JuMP 确定求解器的功能,例如:
https://github.com/JuliaOpt/JuMP.jl/blob/master/src/solvers.jl#L223 -L246
# If we already have an MPB model for the solver...
if m.internalModelLoaded
# ... and if the solver supports updating bounds/objective
if applicable(MathProgBase.setvarLB!, m.internalModel, m.colLower) &&
applicable(MathProgBase.setvarUB!, m.internalModel, m.colUpper) &&
applicable(MathProgBase.setconstrLB!, m.internalModel, rowlb) &&
applicable(MathProgBase.setconstrUB!, m.internalModel, rowub) &&
applicable(MathProgBase.setobj!, m.internalModel, f) &&
applicable(MathProgBase.setsense!, m.internalModel, m.objSense)
MathProgBase.setvarLB!(m.internalModel, copy(m.colLower))
MathProgBase.setvarUB!(m.internalModel, copy(m.colUpper))
MathProgBase.setconstrLB!(m.internalModel, rowlb)
MathProgBase.setconstrUB!(m.internalModel, rowub)
MathProgBase.setobj!(m.internalModel, f)
MathProgBase.setsense!(m.internalModel, m.objSense)
else
# The solver doesn't support changing bounds/objective
# We need to build the model from scratch
if !suppress_warnings
Base.warn_once("Solver does not appear to support hot-starts. Model will be built from scratch.")
end
m.internalModelLoaded = false
end
end
酷,这很有用。 m.internalModel
是实现协议的东西就足够了,还是两个参数都很重要?
是的, m.internalModel
实现协议就足够了。 其他参数大多只是向量。
是的,足以让m.internalModel
实现协议
在野外查找协议示例的一个好方法可能是搜索applicable
和method_exists
调用。
Elixir似乎也实现了协议,但是标准库中的协议数量(不符合定义)似乎非常有限。
协议和抽象类型之间的关系是什么? 最初的问题描述提出了一些类似于将协议附加到抽象类型的内容。 事实上,在我看来,目前大多数(现在是非正式的)协议都是作为抽象类型实现的。 添加对协议的支持后,抽象类型将用于什么? 没有任何方式声明其 API 的类型层次结构听起来不太有用。
很好的问题。 那里有很多选择。 首先,重要的是要指出抽象类型和协议是非常正交的,即使它们都是对对象进行分组的方式。 抽象类型纯粹是名义上的; 他们将对象标记为属于该集合。 协议纯粹是结构性的; 如果对象碰巧具有某些属性,则该对象属于该集合。 所以一些选择是
如果我们有类似 (2) 的东西,我认为重要的是要认识到它不是真正的单一功能,而是名义类型和结构类型的组合。
抽象类型似乎有用的一件事是它们的参数,例如编写convert(AbstractArray{Int}, x)
。 如果AbstractArray
是一个协议,则在协议定义中不一定需要提及元素类型Int
。 这是关于类型的额外信息,_aside_ 来自哪些方法是必需的。 所以AbstractArray{T}
和AbstractArray{S}
仍然是不同的类型,尽管指定了相同的方法,所以我们重新引入了名义类型。 所以这种类型参数的使用似乎需要某种名义上的类型。
那么 2. 会给我们多重抽象继承吗?
那么 2. 会给我们多重抽象继承吗?
不。这将是一种集成或组合功能的方式,但每个功能仍将具有它现在拥有的属性。
我应该补充一点,允许多重抽象继承是另一个近乎正交的设计决策。 在任何情况下,过度使用抽象名义类型的问题是(1)你可能会失去协议的事后实现(人 A 定义类型,人 B 定义协议及其对 A 的实现),(2)您可能会丢失协议的结构子类型。
当前系统中的类型参数不是隐式接口的一部分吗? 例如,这个定义依赖于: ndims{T,n}(::AbstractArray{T,n}) = n
和许多用户定义的函数也是如此。
所以,在一个新的协议 + 抽象继承系统中,我们会有一个AbstractArray{T,N}
和ProtoAbstractArray
。 现在,名义上不是AbstractArray
需要能够指定T
和N
参数是什么,大概是通过硬编码eltype
和ndims
。 然后AbstractArray
上的所有参数化函数都需要重新编写以使用eltype
和ndims
而不是参数。 因此,也许让协议也携带参数更有意义,因此关联类型毕竟可能非常有用。 (请注意,具体类型仍然需要参数。)
此外,使用@malmaud的技巧将类型分组到一个协议中: https :
是的,抽象类型的参数绝对是一种接口,在某种程度上与eltype
和ndims
多余的。 主要区别似乎是您可以直接调度它们,而无需额外的方法调用。 我同意使用关联类型,我们会更接近于用协议/特征替换抽象类型。 语法可能是什么样的? 理想情况下,它会比方法调用弱,因为我宁愿在子类型和方法调用之间没有循环依赖。
剩下的问题是实现一个协议_没有_成为相关抽象类型的一部分是否有用。 一个例子可能是字符串,它是可迭代和可索引的,但通常被视为“标量”数量而不是容器。 我不知道这种情况多久出现一次。
我想我不太明白你的“方法调用”声明。 所以这个语法建议可能不是你所要求的:
protocol PAbstractArray{T,N}
size(_)
getindex(_, i::Int)
...
end
type MyType1
a::Array{Int,1}
...
end
impl MyType for PAbstractArray{Int,1}
size(_) = size(_.a)
getindex(_, i::Int) = getindex(_.a,i)
...
end
# an implicit definition could look like:
associatedT(::Type{PAbstractArray}, :T, ::Type{MyType}) = Int
associatedT(::Type{PAbstractArray}, :N, ::Type{MyType}) = 1
size(mt::MyType) = size(mt.a)
getindex(mt::MyType, i::Int) = getindex(mt.a,i)
# parameterized type
type MyType2{TT, N, T}
a::Array{T, N}
...
end
impl MyType2{TT,N,T} for PAbstractArray{T,N}
size(_) = size(_.a)
getindex(_, i::Int) = getindex(_.a,i)
...
end
这可能会起作用,具体取决于如何定义协议类型的子类型。 例如,给定
protocol PAbstractArray{eltype,ndims}
size(_)
getindex(_, i::Int)
...
end
protocol Indexable{eltype}
getindex(_, i::Int)
end
我们有PAbstractArray{Int,1} <: Indexable{Int}
吗? 如果参数按名称匹配,我认为这可以很好地工作。 我们也许还可以自动定义使eltype(x)
返回x
类型的eltype
参数的定义。
我并不特别喜欢将方法定义放在impl
块中,主要是因为单个方法定义可能属于多个协议。
所以看起来有了这样的机制,我们就不再需要抽象类型了。 AbstractArray{T,N}
可以成为一个协议。 然后我们自动获得多重继承(协议的)。 此外,无法从具体类型继承(这是我们有时从新手那里听到的抱怨)是显而易见的,因为仅支持协议继承。
旁白:能够表达Callable
特性真的很好。 它必须看起来像这样:
protocol Callable
::TupleCons{_, Bottom}
end
其中TupleCons
分别匹配元组的第一个元素和其余元素。 这个想法是,只要_
的方法表是非空的(底部是每个参数元组类型的子类型),它就会匹配。 事实上,我们可能想要为TupleCons{a, TupleCons{b, EmptyTuple}}
制作Tuple{a,b}
语法(另见 #11242)。
我不认为这是真的,所有类型参数都是存在量化的_带有约束_所以抽象类型和协议不能直接替代。
@jakebolewski你能举个例子吗? 显然,它们永远不会完全相同; 我想说的问题更多的是我们是否可以按摩一个,这样我们就可以在没有两者的情况下度过难关。
也许我没有抓住重点,但是协议如何使用约束对中等复杂的抽象类型进行编码,例如:
typealias BigMatrix ∃T, T <: Union{BigInt,BigFloat} AbstractArray{T,2}
不必名义上列举每一种可能性?
与我试图强调的抽象子类型相比,提议的Protocol
提案的表现力明显较低。
我可以想象以下内容(自然地,将设计扩展到其实际极限):
BigMatrix = ∃T, T<:Union{BigInt, BigFloat} protocol { eltype = T, ndims = 2 }
伴随着观察,我们需要像关联类型或命名类型属性这样的东西来匹配现有抽象类型的表现力。 有了这个,我们可能有接近兼容性:
AbstractArray = ∃T ∃N protocol { eltype=T, ndims=N }
对象数据字段的结构子类型化对我来说似乎从未很有用,但应用于 _types_ 的属性似乎很有意义。
我还意识到这可以提供一个摆脱歧义问题的方法:如果两个类型的交集对于某些参数具有冲突的值,则它们的交集是空的。 所以如果我们想要一个明确的Number
类型,我们可以有
protocol Number
super = Number
+(_, _)
...
end
这将super
视为另一种类型属性。
我确实喜欢提议的协议语法,但我有一些注意事项。
但是我可能会误解一切。 我最近才开始真正将 Julia 视为我想要从事的工作,而且我还没有完全掌握类型系统。
(a)我认为@mauro3在上面处理的特征特性会更有趣。 特别是因为如果您不能拥有多个调度协议,那么多重调度有什么好处! 稍后我将写出我对真实世界示例的看法。 但它的一般要点归结为“是否存在允许这两个对象交互的行为”。 我可能错了,所有这些都可以包含在协议中,例如:
protocol Foo{bar}
...
end
protocol Bar{foo<:Foo}
...
end
这也暴露了不允许 Foo 协议在同一定义中引用 Bar 协议的关键问题。
(二)
我们有 PAbstractArray{Int,1} <: Indexable{Int} 吗? 如果参数按名称匹配,我认为这可以很好地工作。
我不确定为什么我们必须通过 _name_ 匹配参数(我认为这是eltype
名称,如果我误解了请忽略此部分)。 为什么不匹配潜在的函数签名。 我使用命名的主要问题是因为它阻止了以下内容:
module SomeBigLibrary
# Assuming required definitions
protocol Baz{el1type}
Base.foo(_, i::el1type) # say `convert`
baz(_)
end
end
module SomeOtherLibrary
# Assuming required definitions
protocol Bar{el2type}
Base.foo(_, i::el2type)
bar(_)
end
end
module My
# Assuming required definitions
protocol Protocol{el_type} # What do I put here to get both subtypes correctly!
Base.foo(_, i::el_type)
SomeBigLibrary.baz(_)
SomeOtherLibrary.bar(_)
end
end
另一方面,它确实确保您的协议也只公开我们想要的特定类型层次结构。 如果我们不匹配Iterable
那么我们将无法获得实现 iterable 的好处(并且也不会在依赖项中绘制边缘)。 但我不确定用户 _gain_s 从中获得了什么,除了能够执行以下操作...
(c)所以,我可能遗漏了一些东西,但命名类型有用的主要目的不是描述超集的不同部分的行为方式吗? 考虑Number
层次结构和抽象类型Signed
和Unsigned
,两者都将实现Integer
协议,但有时会表现得完全不同。 为了区分它们,我们现在是否被迫仅在Signed
类型上定义一个特殊的negate
在没有返回类型的情况下尤其困难,因为我们可能实际上想要否定Unsigned
类型)?
我认为这是您在super = Number
示例中描述的问题。 当我们声明bitstype Int16 <: Signed
(我的另一个问题是如何将Number
或Signed
作为协议及其类型属性应用于具体类型?) Signed
( super = Signed
) 协议将其标记为与Unsigned
协议标记的类型不同? 因为在我看来这是一个奇怪的解决方案,而不仅仅是因为我发现命名类型参数很奇怪。 如果两个协议除了它们放在 super 中的类型之外完全匹配,那么它们有什么不同呢? 如果差异在于更大类型(协议)的子集之间的行为,那么我们真的只是在重新发明抽象类型的目的吗?
(d)问题是我们希望抽象类型区分行为,我们希望协议确保某些能力(通常不考虑其他行为),通过这些能力来表达行为。 但是我们正在尝试完善协议允许我们确保的功能以及行为抽象类型的划分。
我们经常跳到的解决方案是“让类型声明其意图实现抽象类并检查合规性”,这在实现中存在问题(循环引用,导致在类型块或impl
添加函数定义)
但更重要的是,协议不描述行为,它们描述跨多个功能的复杂功能(如迭代),该迭代的行为由抽象类型描述(例如,它是排序的,甚至是有序的)。 另一方面,一旦我们掌握了实际类型,协议 + 抽象类型的组合就很有用,因为它允许我们分派能力(能力实用方法)、行为(高级方法)或两者(实现细节)方法)。
(e)如果我们允许协议继承多个协议(无论如何它们基本上都是结构性的)以及与具体类型一样多的抽象类型(例如,没有多个抽象继承,一个)我们可以允许创建纯协议类型,纯抽象类型,和协议+抽象类型。
我相信这可以解决上面的Signed
与Unsigned
问题:
IntegerProtocol
(继承任何协议结构, NumberAddingProtocol
, IntegerSteppingProtocol
等)一个来自AbstractSignedInteger
,另一个来自AbstractUnsignedInteger
)。Signed
类型的用户在功能(来自协议)和行为(来自抽象层次结构)方面都得到保证。AbstractSignedInteger
具体类型无论如何都无法使用。IntegerSteppingProtocol
(这是微不足道的,基本上只是单个函数的别名)存在于给定具体的AbstractUnsignedInteger
我们可以尝试通过实现其他协议来解决Signed
。 也许甚至像convert
。同时保留所有现有类型,将它们中的大部分转换为协议 + 抽象类型,并保留一些为纯抽象类型。
编辑:(f)语法示例(包括(a)部分)。
编辑 2 :更正了一些错误( :<
而不是<:
),修正了一个糟糕的选择( Foo
而不是::Foo
)
protocol {T<: Number}(Foo <: AbstractFoo; Bar <: AbstractBar) # Abstract inheritance
IterableProtocol(::Foo) # Explicit protocol inheritance.
# Implicit protocol inheritance.
start(::Bar)
next(::Bar, state) # These states should really share an anonymous internal type
done(::Bar, state)
# Custom method for protocol involving both participants, defines Foo / Bar relationship.
set(::Foo, ::Bar, v::T)
# Custom method only on Bar
bar(::Bar)
end
# Protocols both Foo{T} and Bar{T}.
我认为这种语法的问题是:
_抽象类型_定义了实体是什么。 _Protocol_ 定义实体做什么。 在单个包中,这两个概念可以互换:实体_是_它_做什么_。 而“抽象类型”更直接。 但是,两个包之间存在差异:您不需要客户“是什么”,但确实需要客户“做什么”。 在这里,“抽象类型”没有提供有关它的信息。
在我看来,协议是一个单一的分派抽象类型。 它可以帮助包的扩展和合作。 因此,在实体密切相关的单个包中,使用抽象类型来简化开发(通过从多个调度中获利); 在包之间,实体更加独立,使用协议来减少实现暴露。
@mason-bally
我不确定为什么我们必须按名称匹配参数
我的意思是按名称匹配_而不是_按位置匹配。 这些名称的作用类似于结构子类型化的记录。 如果我们有
protocol Collection{T}
eltype = T
end
然后用一个属性叫什么eltype
是一个亚型Collection
。 这些“参数”的顺序和位置无关紧要。
如果两个协议除了它们放在 super 中的类型之外完全匹配,那么它们有什么不同呢? 如果差异在于更大类型(协议)的子集之间的行为,那么我们真的只是在重新发明抽象类型的目的吗?
这是一个公平的观点。 事实上,命名参数确实带回了抽象类型的许多属性。 我一开始的想法是我们可能需要同时拥有协议和抽象类型,然后尝试统一和概括这些功能。 毕竟,当您当前声明type Foo <: Bar
,在某种程度上您真正所做的是设置Foo.super === Bar
。 所以也许我们应该直接支持它,以及您可能想要关联的任何其他键/值对。
“让类型声明其实现抽象类的意图并检查合规性”
是的,我反对将这种方法作为核心功能。
如果我们允许协议继承多个协议......以及尽可能多的抽象类型
这是否意味着说例如“T 是协议 P 的子类型,如果它有方法 x、y、z,并声明自己是 AbstractArray 的子类型”? 我认为这种“协议 + 抽象类型”与我的super = T
财产提案非常相似。 诚然,在我的版本中,我还没有想出如何像我们现在一样将它们链接到一个层次结构中(例如Integer <: Real <: Number
)。
从(名义上的)抽象类型继承协议似乎是对它的一个非常强的约束。 是否有_不_实现协议的抽象类型的子类型? 我的直觉是最好将协议和抽象类型保持为正交的东西。
protocol {T :< Number}(Foo :< AbstractFoo; Bar :< AbstractBar) # Abstract inheritance
IterableProtocol(Foo) # Explicit protocol inheritance.
# Implicit protocol inheritance.
start(Bar)
...
我不明白这个语法。
{ }
和( )
里面的东西到底是什么意思?f(x::ThisProtocol)=...
意味着什么?那么任何具有 eltype 属性的东西都是 Collection 的子类型。 这些“参数”的顺序和位置无关紧要。
啊哈,这是我的误解,这更有意义。 即分配的能力:
el1type = el_type
el2type = el_type
解决我的示例问题。
所以也许我们应该直接支持它,以及您可能想要关联的任何其他键/值对。
这个键/值特性将适用于所有类型,因为我们将用它替换抽象。 这是一个很好的通用解决方案。 你的解决方案现在对我来说更有意义。
诚然,在我的版本中,我还没有想出如何像我们现在一样将它们链接到一个层次结构中(例如 Integer <: Real <: Number)。
我认为你可以使用super
(例如Integer
的 super 为Real
)然后要么使super
特殊并像命名类型一样行事或添加一种添加自定义类型解析代码(ala python)并为super
参数制定默认规则的方法。
从(名义上的)抽象类型继承协议似乎是对它的一个非常强的约束。 是否有没有实现协议的抽象类型的子类型? 我的直觉是最好将协议和抽象类型保持为正交的东西。
哦,是的,抽象约束完全是可选的! 我的全部观点是协议和抽象类型是正交的。 您将使用抽象 + 协议来确保获得某些行为_和_相关功能的组合。 如果您只需要功能(对于实用程序功能)或只需要行为,那么您可以正交使用它们。
这个协议有名字吗?
具有两个名称( Foo
和Bar
)的两个协议来自一个块,但后来我习惯于使用宏来扩展这样的多个定义。 我的这部分语法试图解决(a)部分。 如果您忽略这一点,那么第一行可能只是protocol Foo{T <: Number, Bar <: AbstractBar} <: AbstractFoo
(带有Bar
协议的另一个单独定义)。 此外,所有Number
、 AbstractBar
和AbstractFoo
都是可选的,就像在普通类型定义中一样,
{ } 和 ( ) 里面的东西到底是什么意思?
{}
是标准参数类型定义部分。 允许使用Foo{Float64}
来描述使用Float64
实现Foo
协议的类型。 ()
基本上是协议主体的变量绑定列表(因此可以一次描述多个协议)。 你的困惑很可能是我的错,因为我在原文中打错了:<
而不是<:
。 交换它们以保持<<name>> <<parametric>> <<bindings>>
结构也可能是值得的,其中<<name>>
有时可以是绑定列表。
你如何使用这个协议? 可以派件吗? 如果是这样,鉴于协议涉及多种类型,定义
f(x::ThisProtocol)=...
意味着什么?
在我看来,您的调度示例在语法上似乎是正确的,确实请考虑以下定义:
protocol FooProtocol # Single protocol definition shortcut
foo(::FooProtocol) # I changed my syntax here, protocol names inside the protocol block should referenced as types
end
abstract FooAbstract
# This next line could use better syntax, like a type alias with an Intersection or something.
protocol Foo <: FooAbstract
FooProtocol(::Foo)
end
type Bar <: FooAbstract
a
end
type Baz
b
end
type Bax <: FooAbstract
c
end
f(f::Any) = ... # def (0)
foo(x::Bar) = ... # def (1a)
foo(x::Baz) = ... # def (1b)
f(x::FooProtocol) = ... # def (2); Least specific type (structural)
f(Bar(...)) # Would call def (2)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (0)
f(x::FooAbstract) = ... # def (3); Named type, more specific than structural
f(Bar(...)) # Would call def (3)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)
f(x::Foo) = ... # def (4); Named structural type, more specific than equivalent named type
f(Bar(...)) # Would call def (4)
f(Baz(...)) # Would call def (2)
f(Bax(...)) # Would call def (3)
除非给出更具体的抽象类型来检查结构,否则协议有效地使用命名的 Top 类型(Any)。 事实上,允许像typealias Foo Intersect{FooProtocol, Foo}
(_Edit: Intersect 是错误的名称,也许 Join 而不是Intersect 第一次是正确的_)而不是使用协议语法来做到这一点可能是值得的。
啊,太好了,这对我来说更有意义了! 在同一个块中一起定义多个协议很有趣; 我将不得不考虑更多。
几分钟前,我清理了所有示例。 在线程的早些时候有人提到收集协议语料库来测试想法,我认为这是一个好主意。
当我在加载语言时尝试在定义/编译中使用正确的双方类型注释来描述对象之间的复杂关系时,同一个块中的多个协议有点令人讨厌(例如,像 python;例如 Java)没有问题)。 另一方面,它们中的大多数可能很容易修复,可用性明智,无论如何都可以使用多种方法; 但是性能方面的考虑可能源于在协议中正确键入功能(通过将它们专门用于 vtables 优化协议说)。
您之前提到过,协议可以(虚假地)通过使用::Any
的方法实现,我认为这是一个非常简单的情况,如果遇到它,可以直接忽略它。 如果实现方法是在::Any
上分派的,则具体类型不会归类为协议。 另一方面,我不认为这必然是一个问题。
对于初学者来说,如果::Any
方法是在事后添加的(比如因为有人想出了一个更通用的系统来处理它),它仍然是一个有效的实现,如果我们也使用协议作为优化功能,那么::Any
分派方法的专门版本仍然可以提高性能。 所以最后我实际上反对忽略它们。
但是,让协议定义者在两个选项之间进行选择的语法可能是值得的(无论我们将哪个选项设为默认值,都允许另一个选项)。 对于第一个, ::Any
调度方法的转发语法,比如说 global 关键字(另见下一节)。 对于第二种需要更具体方法的方法,我想不出现有的有用关键字。
编辑:删除了一堆毫无意义的东西。
您的Join
正是协议类型的交集。 这实际上是一次“见面”。 令人高兴的是, Join
类型不是必需的,因为协议类型已经在交集下封闭了:要计算交集,只需返回一个新的协议类型,并将两个方法列表连接起来。
我不太担心协议会被::Any
定义所忽视。 对我来说,“寻找匹配的定义,除了Any
不计算在内”的规则与奥卡姆剃刀相悖。 更不用说通过子类型算法来处理“忽略任何”标志会很烦人。 我什至不确定由此产生的算法是否一致。
我非常喜欢协议的想法(让我想起了一些 CLUsters),我只是很好奇,这如何与 Jeff 在 JuliaCon 讨论的新子类型以及特征相适应? (我仍然非常希望在 Julia 中看到的两件事)。
这将添加一种具有自己的子类型规则的新类型 (https://github.com/JuliaLang/julia/issues/6975#issuecomment-160857877)。 乍一看,它们似乎与系统的其余部分兼容,只需插入即可。
这些协议几乎是@mauro3特征的“单一参数”版本。
您的
Join
正是协议类型的交集。
当我说这是十字路口时,我以某种方式说服自己我错了。 尽管我们仍然需要一种在一行中相交类型的方法(例如Union
)。
编辑:
我仍然喜欢将协议和抽象类型概括为一个系统,并允许自定义规则来解析它们(例如,用super
来描述当前的抽象类型系统)。 我认为如果做得对,这将允许人们添加自定义类型系统并最终为这些类型系统进行自定义优化。 虽然我不确定 protocol 是否是正确的关键字,但至少我们可以将abstract
变成一个宏,那会很酷。
来自麦田:最好通过协议和抽象来提升共性,而不是将其概括为目的地。
什么?
概括协议和抽象类型的意图、能力和潜力的过程不是解决它们在质量上最令人满意的合成的有效方法。 首先收集它们在目的、模式、过程方面的内在共性会更好。 并发展这种理解,允许完善一个人的观点以形成综合。
无论 Julia 的成果如何,它都是建立在综合提供的脚手架上的。 更清晰的综合是建设性力量和归纳力量。
什么?
我认为他是说我们应该首先弄清楚我们想要从协议中得到什么以及它们为什么有用。 然后一旦我们有了那个和抽象类型,就可以更容易地对它们进行一般综合。
纯协议
(一)倡导
协议可以扩展为(更详细的)协议。
协议可以简化为(不太详细的)协议。
一个协议可以实现为一个一致的接口[在软件中]。
可以查询协议以确定接口的一致性。
(2) 建议
协议应该支持协议特定的版本号,默认情况下。
支持某种方式这样做会很好:
当接口符合协议时,响应真; 当一个接口
忠实于协议的一个子集,并且如果增加就会符合,
响应不完整,否则响应 false。 一个函数应该列出所有
对协议不完整的接口进行必要的扩充。
(3) 沉思
协议可以是一种特殊的模块。 它的出口将服务于
作为判断某个接口是否符合时的初始比较。
任何协议指定的[导出]类型和函数都可以使用
@abstract
、 @type
、 @immutable
和@function
支持先天抽象。
[pao:切换到代码引用,但请注意,事后您这样做时,马已经离开了谷仓......]
(您需要引用@mentions
!)
谢谢 - 修复它
在周三,2015年12月16日在3:01,毛罗[email protected]写道:
(您需要引用@提及!)
—
直接回复此邮件或在 GitHub 上查看
https://github.com/JuliaLang/julia/issues/6975#issuecomment -165026727。
对不起,我应该更清楚:使用`而不是“的代码引用
修复了引用修复。
谢谢——请原谅我之前的无知
我试图理解最近关于添加协议类型的讨论。 也许我误解了一些东西,但为什么有必要命名协议而不是只使用协议即将描述的关联抽象类型的名称?
在我看来,通过某种方式来扩展当前的抽象类型系统来描述该类型所期望的行为是很自然的。 很像最初在这个线程中提出的,但可能使用 Jeffs 语法
abstract Iterable
start(::_)
done(::_, state)
next(::_, state)
end
走这条路线时,不需要特别指出子类型实现了接口。 这将通过子类型隐式完成。
显式接口机制的主要目标是恕我直言,以获得更好的错误消息并执行更好的验证测试。
所以类型声明如下:
type Foo <: Iterable
...
end
我们是否在...
部分定义了函数? 如果没有,我们什么时候会因为缺少函数(以及与之相关的复杂性)而出错? 另外,对于实现多个协议的类型会发生什么,我们是否启用多个抽象继承? 我们如何处理超方法解析? 这对多重分派有什么作用(它似乎只是将其删除并在其中粘贴一个 java 式的对象系统)? 在定义了第一个类型之后,我们如何为方法定义新的类型特化? 定义好类型后如何定义协议?
通过创建新类型(或创建新类型公式),这些问题都更容易解决。
每个协议不一定有相关的抽象类型(实际上可能不应该有)。 多个当前接口可以由同一类型实现。 这是无法用当前的抽象类型系统描述的。 因此问题来了。
verify_interface
方法,该方法可以在所有函数定义之后调用,也可以在单元测试中调用function a()
b()
end
function b()
end
因此,我认为这里不需要块内函数定义。
我认为类似“协议”的事物和多重继承之间的区别在于,可以在定义类型后将其添加到协议中。 如果你想让你的包(定义协议)与现有类型一起工作,这很有用。 可以允许在创建后修改类型的超类型,但此时最好将其称为“协议”或类似名称。
嗯,所以它允许定义现有类型的替代/增强接口。 我仍然不清楚在哪里真正需要这样做。 当想要向现有接口添加某些内容时(当我们遵循 OP 中提出的方法时),可以简单地进行子类型化并向子类型添加其他接口方法。 这是这种方法的好处。 它可以很好地扩展。
示例:假设我有一些序列化类型的包。 需要为类型实现方法tobits
,然后该包中的所有函数都将使用该类型。 我们称之为Serializer
协议(即定义了tobits
)。 现在我可以通过实现tobits
向它添加Array
(或任何其他类型)。 使用多重继承,我无法让Array
与Serialzer
一起工作,因为我无法在定义后向Array
添加超类型。 我认为这是一个重要的用例。
好的,明白这一点。 https://github.com/JuliaLang/IterativeSolvers.jl/issues/2是一个类似的问题,解决方案基本上是使用duck-typing。 如果我们能有一些东西可以优雅地解决这个问题,那就太好了。 但这是必须在调度级别支持的事情。 如果我正确理解了上面的协议思想,那么可以将抽象类型或协议作为函数中的类型注释。 在这里,将这两个概念与一个足够强大的工具合并会很好。
我同意:同时拥有抽象类型和协议会非常混乱。 如果我没记错的话,上面有人认为抽象类型有一些不能用协议建模的语义,即抽象类型有一些协议没有的特性。 即使情况确实如此(我不相信),它仍然会令人困惑,因为这两个概念之间存在如此大的重叠。 因此,应该删除抽象类型以支持协议。
至于上面对协议的共识,他们强调指定接口。 抽象类型可能已被用于执行某些缺少的协议。 这并不意味着这是他们最重要的用途。 告诉我什么是协议,什么不是,然后我可以告诉你抽象类型有什么不同以及它们带来的一些东西。 我从来没有认为抽象类型与类型学一样与接口有关。 放弃一种自然的类型灵活性方法是昂贵的。
@JeffreySarnoff +1
想想 Number 类型的层次结构。 不同的抽象类型,例如 Signed、Unsigned,不是由它们的接口定义的。 没有定义“无符号”的方法集。 这只是一个非常有用的声明。
我看不出有什么问题,真的。 如果Signed
和Unsigned
类型都支持相同的方法集,我们可以创建两个具有相同接口的协议。 尽管如此,将类型声明为Signed
而不是Unsigned
可以用于调度(即相同函数的方法行为不同)。 这里的关键是在考虑一个类型实现一个协议之前需要一个显式声明,而不是基于它实现的方法隐式检测它。
但是具有隐式关联的协议也很重要,如https://github.com/JuliaLang/julia/issues/6975#issuecomment -168499775
协议不仅可以定义可以调用的函数,还可以记录(隐式地,或以机器可测试的方式)需要保存的属性。 如:
abs(x::Unsigned) == x
signbit(x::Unsigned) == false
-abs(x::Signed) <= 0
Signed
和Unsigned
之间这种外部可见的行为差异使这种区别变得有用。
如果类型之间存在如此“抽象”的区别,以至于无法立即验证(至少在理论上无法从外部验证),那么很可能需要了解类型的实现才能做出正确的选择。 这是当前abstract
可能有用的地方。 这可能是代数数据类型的方向。
没有理由不应该使用协议来简单地对类型进行分组,即不需要任何定义的方法(并且“当前”设计可以使用该技巧:https://github.com/JuliaLang/julia/issues/ 6975#issuecomment-161056795)。 (另请注意,这不会干扰隐式定义的协议。)
考虑(Un)signed
示例:如果我有一个Signed
但由于某种原因必须也是另一个抽象类型的子类型,我该怎么办? 这是不可能的。
@eschnett :目前抽象类型与其子类型的实现无关。 虽然已经讨论过:#4935。
代数数据类型是一个很好的例子,其中连续细化具有内在意义。
任何分类法都更自然地给出,作为抽象类型层次结构比作为协议规范的混合物更直接有用。
关于具有作为多个抽象类型层次结构的子类型的类型的注意事项也很重要。 抽象的多重继承带来了很多功利主义的力量。
@mauro3是的,我知道。 我在想一些等同于有区别的联合的东西,但实现与元组一样有效,而不是通过类型系统(因为联合目前已实现)。 这将包含枚举、可为空类型,并且可能能够比目前的抽象类型更有效地处理其他一些情况。
例如,像带有匿名元素的元组:
DiscriminatedUnion{Int16, UInt32, Float64}
或使用命名元素:
discriminated_union MyType
i::Int16
u::UInt32
f::Float64
end
我试图说明的一点是,抽象类型是将这种结构映射到 Julia 的一种好方法。
没有理由不应该使用协议来简单地对类型进行分组,即不需要任何定义的方法(并且使用技巧:#6975(评论)在“当前”设计中是可能的)。 (另请注意,这不会干扰隐式定义的协议。)
我觉得你必须小心这一点才能实现性能,但似乎没有多少人经常考虑这一点。 在这个例子中,似乎人们只想简单地定义非任何版本,这样编译器仍然可以在编译时选择函数(而不是在运行时调用一个函数来选择正确的函数,或者编译器检查函数来确定他们的结果)。 我个人认为使用多个抽象的“继承”作为标签会是一个更好的解决方案。
我觉得我们应该将类型系统所需的技巧和知识保持在最低限度(虽然它可以被包裹在一个宏中,但感觉像是对宏的一个奇怪的黑客攻击;如果我们使用宏来操作类型系统,那么我认为@ JeffBezanson的统一解决方案可以更好地解决这个问题)。
考虑(未)签名的示例:如果我有一个已签名的类型,但由于某种原因必须也是另一个抽象类型的子类型,我该怎么办? 这是不可能的。
多重抽象继承。
我相信之前已经涵盖了所有这些领域,这次谈话似乎在循环(尽管每次都更紧密)。 我相信有人提到应该获得使用协议的语料库或问题。 这将使我们更容易判断解决方案。
虽然我们在重申事情:) 我想提醒大家抽象类型是名义上的,而协议是结构性的,所以我喜欢将它们视为正交的设计,除非我们实际上可以在协议中提出可接受的抽象类型“编码” (也许巧妙地使用关联类型)。 当然,如果它也产生多个抽象继承,那就加分了。 我觉得这是可能的,但我们还没有做到。
@JeffBezanson “关联类型”与“与 [a] 协议关联的具体类型”不同吗?
是的,我相信; 我的意思是在技术意义上的“关联类型”,即协议指定一些键值对,其中“值”是一种类型,与协议指定方法的方式相同。 例如,“如果有eltype
则 Foo 类型遵循 Container 协议”或“如果其ndims
参数为 2,则 Foo 类型遵循 Matrix 协议”。
抽象类型是名义上的,而协议是结构性的和
抽象类型是定性的,而协议是可操作的
抽象类型(具有多重继承)在协议执行时进行编排
即使在另一个中有一个编码,“嘿,嗨 .. 你好吗?让我们开始吧!” Julia 需要清楚地呈现——协议和多继承抽象类型的普遍有目的的概念(通用目的的概念)。 如果有一个巧妙的展开,让 Julia 两个都被单独折叠起来,那么它更有可能做到这一点,而不是一个一个和另一个。
@mason-bially:所以我们也应该添加多重继承? 这仍然会留下在创建类型后无法添加超类型的问题(除非这也被允许)。
@JeffBezanson :没有什么能阻止我们允许纯粹的名义协议。
@mauro3为什么要决定是否允许事后超类型插入与多重继承相关联? 并且有不同种类的超类型创建,有些显然是无害的,前提是能够插入一个新的任何东西:我想在 Real 和 AbstractFloat 之间添加一个抽象类型,比如 ProtoFloat,这样我就可以分派 double- double floats 和 system Floats 一起浮动而不干扰系统 Floats 作为 AbstractFloat 的子类型。 也许不太容易允许,将能够细分 Integer 的当前子类型,从而避免许多“与 ..define f(Bool) before.. 有歧义”的消息; 或者引入一个 Signed 的超类型,它是 Integer 的一个子类型,并打开数字层次结构以透明处理,比如序数。
对不起,如果我发起了另一轮圈子。 该主题非常复杂,我们确实必须确保该解决方案非常易于使用。 所以我们需要覆盖:
由于最初在#6975 中提出的内容与稍后讨论的协议思想完全不同,因此最好有某种 JEP 来描述协议的外观。
一个如何定义正式接口并使用当前 0.4(没有宏)验证它的示例,除非对 gf.c 进行修改,否则当前调度依赖于特征样式调度。 这使用生成的函数进行验证,所有类型计算都在类型空间中执行。
我开始使用它作为我们正在定义的 DSL 中的运行时检查,我必须确保提供的类型是日期迭代器。
它目前支持超类型的多重继承,_super 字段名称不被运行时使用,可以是任何有效符号。 您可以为 _super 元组提供 n 个其他类型。
https://github.com/mdcfrancis/tc.jl/blob/master/test/runtests.jl
只是在这里指出,我对 JuliaCon 的讨论进行了跟进,讨论了https://github.com/JuliaLang/julia/issues/5#issuecomment -230645040 关于特征的可能语法
Guy Steele 对多调度语言 (Fortress) 中的特征有一些很好的见解,请参阅他的 JuliaCon 2016 主题演讲: https ://youtu.be/EZD3Scuv02g。
一些亮点:代数属性的大特征系统,对实现特征的类型的特征属性进行单元测试,以及他们实现的系统可能太复杂了,他现在会做一些更简单的事情。
用于 tensorflow 编译器 AD 协议的新 Swift 用例:
https://gist.github.com/rxwei/30ba75ce092ab3b0dce4bde1fc2c9f1d
@timholy和@Keno可能对此感兴趣。 有全新的内容
我认为在探索这个问题的设计空间时,这个演示值得关注。
对于非特定想法的讨论和相关背景工作的链接,最好启动相应的话语线程并在那里发布和讨论。
请注意,在静态类型语言的泛型编程研究中遇到和讨论的几乎所有问题都与 Julia 无关。 静态语言几乎只关心提供足够的表达能力来编写他们想要的代码的问题,同时仍然能够静态类型检查不存在类型系统违规。 我们在表达能力方面没有问题,也不需要静态类型检查,所以在 Julia 中这些都不重要。
我们关心的是允许人们以结构化的方式记录协议的期望,然后语言可以动态验证(如果可能的话,提前)。 我们还关心允许人们发送诸如特征之类的东西; 是否应该连接这些仍然是开放的。
底线:虽然关于静态语言协议的学术工作可能引起普遍兴趣,但它在 Julia 的上下文中并不是很有帮助。
我们关心的是允许人们以结构化的方式记录协议的期望,然后语言可以动态验证(如果可能的话,提前)。 我们还关心允许人们发送诸如特征之类的东西; 是否应该连接这些仍然是开放的。
除了避免破坏性更改之外,在 julia 中消除抽象类型和引入 golang 风格的隐式接口是否可行?
不,不会。
好吧,这不是协议/特征的全部内容吗? 有一些讨论是协议需要是隐式的还是显式的。
我认为自 0.3(2014 年)以来,经验表明隐式接口(即不是由语言/编译器强制执行的)工作得很好。 此外,在目睹了一些包是如何演变的,我认为最好的界面是有机地开发的,并且只是在稍后的时间点才正式化(= 文档化)。
我不确定是否需要由语言以某种方式强制执行的对接口的正式描述。 但是,虽然已决定,但最好鼓励以下内容(在文档、教程和样式指南中):
“接口”既便宜又轻量,只是一堆具有指定行为的一组类型的函数(是的,类型是正确的粒度级别 - 对于x::T
, T
应该足够了来决定x
实现了接口)。 因此,如果要定义具有可扩展行为的包,那么记录接口确实很有意义。
接口不需要用子类型关系来描述。 没有公共(非平凡)超类型的类型可以实现相同的接口。 一个类型可以实现多个接口。
转发/组合隐含地需要接口。 “如何让包装器继承父级的所有方法”是一个经常出现的问题,但这不是正确的问题。 实际的解决方案是拥有一个核心接口,并为包装器实现它。
特征很便宜,应该大量使用。 Base.IndexStyle
是一个很好的规范示例。
以下将受益于澄清,因为我不确定最佳实践是什么:
接口是否应该有一个查询功能,比如Tables.istable
来决定一个对象是否实现了接口? 我认为这是一个很好的做法,如果调用者可以使用各种替代接口并且需要遵循回退列表。
文档字符串中接口文档的最佳位置是什么? 我会说上面的查询功能。
- 是的,类型是正确的粒度级别
为什么呢? 类型的某些方面可能会被分解为接口(用于调度目的),例如迭代。 否则,您将不得不重写代码或强加不必要的结构。
- 接口不需要用子类型关系来描述。
也许没有必要,但会不会更好? 我可以对可迭代类型进行函数调度。 平铺的可迭代类型不应该隐式地满足吗? 当用户只关心界面时,为什么他们必须围绕标称类型绘制这些?
如果您本质上只是将它们用作抽象接口,那么名义子类型的意义何在? 特征似乎更细粒度和更强大,因此将是更好的概括。 所以看起来类型几乎是特征,但我们必须有特征来解决它们的局限性(反之亦然)。
如果您本质上只是将它们用作抽象接口,那么名义子类型的意义何在?
调度——您可以调度某事物的名义类型。 如果你不需要调度一个类型是否实现了一个接口,那么你可以直接输入它。 这就是人们通常使用 Holy trait 的目的:该 trait 允许您分派调用假设已实现某些接口的实现(例如“具有已知长度”)。 人们似乎想要避免那层间接,但这似乎只是一种方便,而不是必需品。
为什么呢? 类型的某些方面可能会被分解为接口(用于调度目的),例如迭代。 否则,您将不得不重写代码或强加不必要的结构。
我相信@tpapp是说你只需要类型来确定某个东西是否实现了一个接口,而不是所有的接口都可以用类型层次结构来表示。
只是一个想法,同时使用MacroTools
的forward
:
转发很多方法有时候很烦
<strong i="9">@forward</strong> Foo.x a b c d ...
如果我们可以使用Foo.x
的类型和方法列表然后推断要转发哪个呢? 这将是一种inheritance
并且可以使用现有功能(宏+生成的函数)来实现,它看起来也像某种接口,但我们不需要语言中的任何其他内容。
我知道我们永远无法想出一个将要继承的列表(这也是静态class
模型不太灵活的原因),有时您只需要其中的几个,但它只是方便核心功能(例如,有人想在Array
周围定义一个包装器( AbstractArray
子类型),大部分功能只是转发)
@datnamer :正如其他人所阐明的那样,接口不应该比类型更精细(即实现接口不应该依赖于给定类型的值)。 这与编译器的优化模型非常吻合,在实践中不是约束。
也许我不清楚,但我的回答的目的是指出我们已经拥有在 Julia 中有用的接口,并且它们是轻量级的、快速的,并且随着生态系统的成熟而变得普遍。
用于描述接口的正式规范几乎没有增加 IMO 的价值:它只是文档和检查某些方法是否可用。 后者是接口的一部分,但另一部分是这些方法实现的语义(例如,如果A
是一个数组,则axes(A)
给我一个对getindex
有效的坐标范围
然而,我最想看到的是
对于越来越多的接口(在文档字符串)文档,
测试套件捕捉新定义类型的成熟接口的明显错误(例如,很多T <: AbstractArray
实现eltype(::T)
而不是eltype(::Type{T})
。
@tpapp现在对我有意义,谢谢。
@StefanKarpinski我不太明白。 Traits 不是名义类型(对吧?),不过,它们可以用于调度。
我的观点基本上是@tknopp和@mauro3在这里提出的观点: https : //discourse.julialang.org/t/why-does-julia-not-support-multiple-traits/5278/43 ? u = datnamer
由于具有特征和抽象类型,因此具有两个非常相似的概念会带来额外的复杂性和混淆。
人们似乎想要避免那层间接,但这似乎只是一种方便,而不是必需品。
特征层次结构的各个部分是否可以通过类型参数,健壮地按诸如联合和交集之类的东西分组进行分派? 我没有尝试过,但感觉这需要语言支持。 类型域中的 IE 表达式问题。
编辑:我认为问题在于我将接口和特征混为一谈,因为它们在这里使用。
把这个贴在这里是因为它很有趣:看起来 Concepts 肯定已经被接受并且将成为 C++20 的一部分。 有趣的东西!
https://herbsutter.com/2019/02/23/trip-report-winter-iso-c-standards-meeting-kona/
https://en.cppreference.com/w/cpp/language/constraints
我认为特质是解决这个问题的一个很好的方法,而神圣特质肯定已经取得了长足的进步。 但是,我认为 Julia 真正需要的是一种对属于 trait 的函数进行分组的方法。 这对于文档原因很有用,而且对于代码的可读性也很有用。 从我目前看到的情况来看,我认为像 Rust中的
我认为这非常重要,最重要的用例是索引迭代器。 这是您可能希望可行的那种语法的建议。 抱歉,如果它已经被提出(长线程......)。
import Base: Generator
<strong i="6">@require</strong> getindex(AbstractArray, Vararg{Int})
function getindex(container::Generator, index...)
iterator = container.iter
if <strong i="7">@works</strong> getindex(iterator, index...)
container.f(getindex(iterator, index...))
else
<strong i="8">@interfaceerror</strong> getindex(iterator, index...)
end
end
最有用的评论
对于非特定想法的讨论和相关背景工作的链接,最好启动相应的话语线程并在那里发布和讨论。
请注意,在静态类型语言的泛型编程研究中遇到和讨论的几乎所有问题都与 Julia 无关。 静态语言几乎只关心提供足够的表达能力来编写他们想要的代码的问题,同时仍然能够静态类型检查不存在类型系统违规。 我们在表达能力方面没有问题,也不需要静态类型检查,所以在 Julia 中这些都不重要。
我们关心的是允许人们以结构化的方式记录协议的期望,然后语言可以动态验证(如果可能的话,提前)。 我们还关心允许人们发送诸如特征之类的东西; 是否应该连接这些仍然是开放的。
底线:虽然关于静态语言协议的学术工作可能引起普遍兴趣,但它在 Julia 的上下文中并不是很有帮助。