gpt4 book ai didi

generics - 在 F# 中强制内部 DSL 的现有类型

转载 作者:行者123 更新时间:2023-12-03 20:02:22 25 4
gpt4 key购买 nike

给定一个 AST 的 DU(一个非常简单的表达式树)

type Expression =
| Add of list<Expression>
| Var of string
| Val of float
我想写一个运算符 +这样我就可以写
let x = "s" + 2.0
并且有
x = Add [(Var "s"); (Val 2.0)]
此外,我希望它可以从其他程序集中使用(通过打开一些东西)。
我的实际应用程序是一个类似但更大的 DU,用于真正的 AST。我在 F# 4.8 上。
到目前为止有效的是
let (+) a b =
Add [a; b]
x = (Var "s") + (Val 2.0)
但在这里我还是要包装 "s""2.0"用手。我想避免这种包装。
我尝试了其他几件事:
声明类型扩展和接口(interface)并同时使用静态类型参数和接口(interface)约束:
首先是接口(interface)
type IToExpression =
abstract member ToExpression : Expression

type Expression with
member this.ToExpression = this

type System.String with
member this.ToExpression = Var this

type System.Double with
member this.ToExpression = Val this

let (+++)
(a : 'S when 'S :> IToExpression)
(b : 'T when 'T :> IToExpression) =
Add [a.ToExpression; b.ToExpression]
然后与静态解析的类型参数相同。
let (++) a b =
let a = (^t : (member ToExpression: Expression) a)
let b = (^t : (member ToExpression: Expression) b)
Add [a; b]
编辑:但正如本文中所指出的 answer (并且由于我的草率复制)这种方法需要更多的工作才能解决真正的问题。
但两者都是 +++++未能在想要的表达式中进行类型检查,即行
let x = "s" ++ 2.0
let x = "s" +++ 2.0
我通读了
  • statically resolved type parameters | SO
  • F# generic type constraints and duck typing | SO
  • fsharp extension methods
  • type extensions | MS docs/类型扩展
  • statically resolved type parameters | MS docs

  • 我的理解是,使用一些额外的代码可以转换所有内容,但是将接口(interface)重新安装到现有类型并将其与静态解析的类型参数混合以应用成员约束是具有挑战性的。
    有没有办法解决这个问题?

    最佳答案

    这里有几个微妙的问题。
    一、 SRTP 仅适用于 inline职能。这是因为它们不能编译成 IL(因为 IL 不支持这种约束),所以必须在编译时解决它们。静态的。这就是为什么它们是“静态解决的”。 inline关键字使编译器能够做到这一点。

    let inline (+) a b = ...
    二、这是什么泛型类型 ^t你在引用?它是什么类型的?我认为你的意思是它是 a 的类型和 b ,但你还没有这样声明它,所以它只是一些随机泛型类型。它需要绑定(bind)到参数:
    let inline (+) (a: ^t) (b: ^t) = ...
    三、从您的示例来看,您实际上的意思是 ab是不同的类型,不是吗?
    let inline (+) (a: ^a) (b: ^b) = ...
    四、扩展方法不计入静态解析类型,因此您不能定义 ToExpressionStringfloat并期望它起作用。通常的技巧是在一个特殊的类上声明所有方法,该类仅用于保存这些方法:
    type ToExpressionStub() =
    static member ToExpression s = Var s
    static member ToExpression f = Val f
    然后人们可能会期望这会起作用:
    let inline (+) (a: ^a) (b: ^b) =
    let a = (ToExpression : (static member ToExpression: ^a -> Expression) a)
    let b = (ToExpression : (static member ToExpression: ^b -> Expression) b)
    Add [a; b]
    但不,它不会。这是因为静态解析的约束不能应用于具体类型,只能应用于类型变量,如 ^a。或 ^b .那么该怎么办?
    好吧,我们可以给我们的函数一个额外的参数,如下所示:
    let inline (+) (dummy: ^c) (a: ^a) (b: ^b) =
    let a = (^c : (static member ToExpression: ^a -> Expression) a)
    let b = (^c : (static member ToExpression: ^b -> Expression) b)
    Add [a; b]
    然后我们必须传递一个值 ToExpressionStub()每次通话:
    let x = (+) (ToExpressionStub()) "foo" 2.0
    当然,这非常不方便,因此我们将添加另一个带有三个参数的中间函数,并从运算符 (+) 调用它。 :
    let inline doAdd (dummy: ^c) (a: ^a) (b: ^b) =
    let a = (^c : (static member ToExpression: ^a -> Expression) a)
    let b = (^c : (static member ToExpression: ^b -> Expression) b)
    Add [a; b]

    let inline (+) (a: ^a) (b: ^b) = doAdd (ToExpressionStub()) a b
    差不多好了!但这也不太奏效:在线 let b = ...我们得到一个警告“这个结构使代码不那么通用......”,然后在使用站点我们得到一个错误,我们不能使用 stringfloat ,取决于哪个先出现。
    发生这种情况是出于一些非常晦涩的原因。编译器看到两个约束具有相同的形状并应用于相同的类型,并决定它们必须是相同的约束,因此, ^a = ^b .为了打破这种僵局,我们可以简单地改变约束的形状,使它们变得不同:
    let inline doAdd (dummy: ^c) (a: ^a) (b: ^b) =
    let a = ((^a or ^c) : (static member ToExpression: ^a -> Expression) a)
    let b = ((^b or ^c) : (static member ToExpression: ^b -> Expression) b)
    Add [a; b]
    请注意,它们现在应用于 ^a or ^c^b or ^c分别,不仅仅是 ^c独自的。这还有一个额外的效果:我们不再局限于我们在 ToExpressionStub 中列举的类型。 .我们可以使用任何有自己的非扩展方法的类型 ToExpression定义。例如, Expression本身:
    type Expression =
    | Add of list<Expression>
    | Var of string
    | Val of float
    with
    static member ToExpression (e: Expression) = e
    就是这样!这现在有效:
    > let x = 2.0 + "foo"
    Add [Val 2.0; Var "foo"]

    > let y = x + "bar"
    Add [Add [Val 2.0; Var "foo"]; Var "bar"]
    最后,为了减少运行时分配成本,我通常有一个 ToExpressionStub 的单例实例。而不是在每次调用时创建一个新的:
    type ToExpressionStub() = 
    static member val Value = ToExpressionStub()
    ...

    let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b

    最重要的是,是的,你 可以做,但请仔细想想你是否 应该 .
    在实际实践中,这种诡计实际上是在阻碍而不是帮助它。当然,一开始它可能看起来非常简洁和聪明,但几个月后你会看到你自己的代码并且无法理解发生了什么。请相信我的经验:必须将值包装在 Val 中和 Var是一个功能,而不是一个错误。程序代码的阅读量远大于其编写量。不要朝自己的脚开枪。

    关于generics - 在 F# 中强制内部 DSL 的现有类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65395343/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com