我如何在涉及两种类型的特质上进行分派,其中同时满足特质的第二种类型由第一种类型唯一确定?

90
假设我有一个Julia特质,它涉及两种类型:一种类型是某种“基本”类型,可能满足某种部分特质,另一种类型是由基本类型唯一确定的“关联”类型。(也就是说,从BaseType到AssociatedType的关系是一个函数。)这些类型共同满足了我感兴趣的复合特质。
例如:
using Traits

@traitdef IsProduct{X} begin
    isnew(X) -> Bool
    coolness(X) -> Float64
end

@traitdef IsProductWithMeasurement{X,M} begin
    @constraints begin
        istrait(IsProduct{X})
    end
    measurements(X) -> M
    #Maybe some other stuff that dispatches on (X,M), e.g.
    #fits_in(X,M) -> Bool
    #how_many_fit_in(X,M) -> Int64
    #But I don't want to implement these now
end

现在这里有几个示例类型。请忽略示例的具体细节;它们只是作为最小工作示例(MWE)而存在,并且细节中没有任何相关内容。
type Rope
    color::ASCIIString
    age_in_years::Float64
    strength::Float64
    length::Float64
end

type Paper
    color::ASCIIString
    age_in_years::Int64
    content::ASCIIString
    width::Float64
    height::Float64
end

function isnew(x::Rope) 
    (x.age_in_years < 10.0)::Bool 
end
function coolness(x::Rope) 
    if x.color=="Orange" 
        return 2.0::Float64
    elseif x.color!="Taupe" 
        return 1.0::Float64
    else 
        return 0.0::Float64
    end
end
function isnew(x::Paper) 
    (x.age_in_years < 1.0)::Bool 
end
function coolness(x::Paper) 
    (x.content=="StackOverflow Answers" ? 1000.0 : 0.0)::Float64 
end

自从我定义了这些函数,我就可以做到

@assert istrait(IsProduct{Rope})
@assert istrait(IsProduct{Paper})

现在如果我定义

function measurements(x::Rope)
    (x.length)::Float64
end

function measurements(x::Paper)
    (x.height,x.width)::Tuple{Float64,Float64}
end

那么我就可以做

@assert istrait(IsProductWithMeasurement{Rope,Float64})
@assert istrait(IsProductWithMeasurement{Paper,Tuple{Float64,Float64}})

到目前为止一切都很顺利;这些代码没有错误。现在,我想编写一个像下面这样的函数:
@traitfn function get_measurements{X,M;IsProductWithMeasurement{X,M}}(similar_items::Array{X,1})
    all_measurements = Array{M,1}(length(similar_items))
    for i in eachindex(similar_items)
        all_measurements[i] = measurements(similar_items[i])::M
    end
    all_measurements::Array{M,1}
end

通俗来说,这个函数是一个例子,用于展示“我想利用我作为程序员所知道的BaseType总是与AssociatedType相关联的事实,来帮助编译器进行类型推断。我知道无论何时我执行某个任务[在本例中,get_measurements,但一般情况下这可能适用于许多情况],我都希望编译器以一致的模式推断该函数的输出类型。”

也就是说,例如

do_something_that_makes_arrays_of_assoc_type(x::BaseType)

将始终输出Array{AssociatedType},并且

do_something_that_makes_tuples(x::BaseType)

将始终返回Tuple{Int64, BaseType, AssociatedType}

而且,对于所有的<BaseType, AssociatedType>对都成立;例如,如果BatmanType是与RobinType关联的基类型,而SupermanType是与LexLutherType始终关联的基类型,则

do_something_that_makes_tuple(x::BatManType)

将始终输出Tuple {Int64,BatmanType,RobinType},并且

do_something_that_makes_tuple(x::SuperManType)

将始终输出Tuple {Int64,SupermanType,LexLutherType}

所以,我理解这种关系,并希望编译器为了速度而理解它。

现在,回到函数示例。如果这有意义,您会意识到,虽然我给出的函数定义在满足此关系并且确实编译方面是“正确”的,但由于编译器不理解XM之间的关系,因此无法调用该函数,即使我理解。特别地,由于M未出现在方法签名中,因此Julia无法对该函数进行分派。

到目前为止,我想到的唯一解决此问题的方法是创建一种解决方法,其中我可以“计算”相关类型,并且仍然可以使用方法分派来执行此计算。考虑:

function get_measurement_type_of_product(x::Rope)
    Float64
end
function get_measurement_type_of_product(x::Paper)
    Tuple{Float64,Float64}
end
@traitfn function get_measurements{X;IsProduct{X}}(similar_items::Array{X,1})
    M = get_measurement_type_of_product(similar_items[1]::X)
    all_measurements = Array{M,1}(length(similar_items))
    for i in eachindex(similar_items)
        all_measurements[i] = measurements(similar_items[i])::M
    end
    all_measurements::Array{M,1}
end

那么,确实可以编译并调用此代码:

julia> get_measurements(Array{Rope,1}([Rope("blue",1.0,1.0,1.0),Rope("red",2.0,2.0,2.0)]))
2-element Array{Float64,1}:
 1.0
 2.0

但这并不理想,因为 (a) 我每次都需要重新定义这个映射表,即使我感觉我已经通过使它们满足特征向编译器描述了 XM 之间的关系,而且 (b) 据我所猜测——也许这是错误的;我没有直接证据——编译器可能不能像我想的那样进行优化,因为 XM 之间的关系被“隐藏”在函数调用的返回值中。
最后一点想法:如果我有能力,我会 理想地 做类似这样的事情:
@traitdef IsProduct{X} begin
    isnew(X) -> Bool
    coolness(X) -> Float64
    ∃ ! M s.t. measurements(X) -> M
end

然后有一些方法可以引用唯一证明存在关系的类型,例如

@traitfn function get_measurements{X;IsProduct{X},IsWitnessType{IsProduct{X},M}}(similar_items::Array{X,1})
    all_measurements = Array{M,1}(length(similar_items))
    for i in eachindex(similar_items)
        all_measurements[i] = measurements(similar_items[i])::M
    end
    all_measurements::Array{M,1}
end

因为这将以某种方式是可分派的。

所以:我的具体问题是什么?假设您现在理解了我的目标:

  1. 使我的代码通用地展现这种结构,以便我可以在许多情况下有效地重复此设计模式, 然后在高级别上以 XM 的抽象方式进行编程,并且
  2. 以这种方式执行 (1),使编译器仍然能够尽其所能地进行优化 / 与类型之间的关系就像我一样,也意识到它们之间的关系。

那么,我该怎么做呢? 我认为答案是:

  1. 使用 Traits.jl
  2. 做与你迄今所做的相似的事情
  3. 还要进行一些巧妙的操作,由回答者指示

但我也接受这个想法:实际上,正确的答案是:

  1. 放弃这种方法,你正在错误地思考问题
  2. 相反,这样考虑:MWE

我也会对以下形式的回答感到完全满意:

  1. 你所要求的是 Julia 的“高级”功能,目前仍在开发中,预计将包含在 v0.x.y 中,所以请耐心等待...

对于以下回答,我不太热衷(但仍然很好奇):

  1. 放弃 Julia;相反,使用专为此类事情设计的语言 ________

我还认为这可能与定义 Julia 函数输出类型的问题有关,尽管我还没有能够解决这个问题的确切表示。


13
哇哇哇,看起来你试图重新实现inference.jl的所有内容。我不建议这样做。你真正想要实现什么?你的示例函数可以用一个推导式简单地写成:get_measurements(similar_items) = [measurements(item) for item in similar_items] - mbauman
顺便说一句,尽管我所针对的示例/用例似乎很复杂,但我也不排除自己可能只是在做一些“错误”或“愚蠢”的事情,并且有一种完全不同且更简单的解决方案。如果确实如此,我希望能看到这种情况的论证,因为我自己还没有找到任何解决办法。 - Philip
问题确实很长,但也许在回答你的另一个问题中使用的技巧也可以在这里帮到你(使用Base.return_types)。请参阅https://dev59.com/opTfa4cB1Zd3GeqPRHmw。 - Dan Getz
2
你在这里的最终目标是一个可静态验证的程序吗?要实现这一点并不容易... 可能会以高级学位而不是几个声望点来奖励。例如,查看Haskell中的[此静态合同检查论文(pdf)](https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-737.pdf)。 - mbauman
根据我对你的链接的快速浏览,那应该是足够的,甚至有些多余(我认为)。我想要的其实更简单:我“知道”某个函数的输出类型与输入类型之间会有一致的关系(这个关系正好可以通过Traits来捕捉,但这只是一个细节)。这种推理并不复杂,所以如果我是一个更好的程序员,为什么不能在代码本身中表示这个事实(就像“让编译器和我一样了解”)? - Philip
显示剩余8条评论
1个回答

1
关于参数化函数呢?不要试图定义类型之间的关系,而是可以使用参数化函数来指定类型之间的关系。你可以定义一个函数,例如get_measurements{T}(similar_items::Array{T}),并为每个T指定预期的输出类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接