如何以比较小的代价封装内建类型?

目前我有如下代码:

const allowedaxes = (:first, :second, :third, :fourth, :fifth, :sixth)

function CartesianCoordinates(t::Vararg{T, N}) where {N, T <: Real}
    NamedTuple{tuple(allowedaxes[1:N]...), NTuple{N, T}}((t))
end

应该如何用 NamedTuple 为基础实现一个自己的类型呢?
如果像上面实现的,写一个 wrapper 函数,就直接把 NamedTuple 这个实现暴露给用户了,而且写函数参数类型的时候就要写 a(x::NamedTuple, b::NamedTuple) 会导致不必要的匹配;但是如果自己写一个类型 CartesianCoordinates 就要多写好多方法,把 NamedTuple 的方法们嫁接过来;有没有两全其美的办法呢?

类似的问题我也遇到过,之前搜了下,似乎没找到特别好的解决办法。

如果我没理解错的话,你的CartesianCoordinates其实是一类带约束的NamedTuple。可以这么做:

  1. 构造一个新的类型CartesianCoordinates,将NamedTuple作为其成员,在构造函数里做检查。
    struct CartesianCoordinates{T<:NamedTuple}
        np::T
        CartesianCoordinates(t::Vararg{T, N}) where {N, T <: Real} = new{NamedTuple{tuple(allowedaxes[1:N]...), NTuple{N, T}}((t))
    end
    
  2. 实现NamedTuple的所有方法。这个我没有实际写过(我之前的case只需要支持有限的几个方法,类似这样实现的),不过感觉应该没有你说的那么复杂。
    2.1 通过methodswith函数获取所有NamedTuple的方法
    2.2 通过f.sig获取各个方法的signature,将其中的NamedTuple 替换成CartesianCoordinates
    2.3 执行@eval,将所有x::CartesianCoordinates映射到x.np

如果你写出来了,不妨分享交流下~

1 个赞

如果是笛卡尔坐标,我倾向于这样写

struct CartCoord{N, T}
     positions::NTuple{N, T}
end

不过,如果是别的需要用 NamedTuple ,你可以:

struct Foo{NT <: NamedTuple}
    x::NT
end

using MacroTools: @forward

@forward Foo.x Base.length # blablabla

其实这件事情比较适合通过传递一个interface完成,不过目前还没啥成熟的interface解决方案,很多package就直接用那个forward宏,比起自己重载能少写一写代码

2 个赞

Thanks! 话说是不是 @forward Foo.x Base.length # blablabla? 多打了一个 l.

对于Base.length这种来说@forward是足够用的,另一种情况是需要重新定义一下运算规则,手写一个for循环即可

for op in [:(==), :≈, :<]
  @eval Base.$op(x::TrackedReal, y::Real) = Base.$op(data(x), y)
  @eval Base.$op(x::Real, y::TrackedReal) = Base.$op(x, data(y))
  @eval Base.$op(x::TrackedReal, y::TrackedReal) = Base.$op(data(x), data(y))
end

参考Flux

1 个赞