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

HTC*_*HTC 4 generics f#

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

type Expression =
  | Add of list<Expression>
  | Var of string
  | Val of float
Run Code Online (Sandbox Code Playgroud)

我想写一个运算符+,以便我可以写

let x = "s" + 2.0
Run Code Online (Sandbox Code Playgroud)

并且有

x = Add [(Var "s"); (Val 2.0)]
Run Code Online (Sandbox Code Playgroud)

此外,我希望它可以从其他程序集中使用(通过打开一些东西)。

我的实际应用程序是一个类似但更大的 DU,用于真正的 AST。我在 F# 4.8 上。

到目前为止有效的是

let (+) a b =
    Add [a; b]
x = (Var "s") + (Val 2.0)
Run Code Online (Sandbox Code Playgroud)

但是在这里我还是要包"s""2.0"手工制作。我想避免这种包装。

我尝试了其他几件事:

声明类型扩展和接口并使用静态类型参数和接口约束:

首先是接口

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]
Run Code Online (Sandbox Code Playgroud)

然后与静态解析的类型参数相同。

let (++) a b =
  let a = (^t : (member ToExpression: Expression) a)
  let b = (^t : (member ToExpression: Expression) b)
  Add [a; b]
Run Code Online (Sandbox Code Playgroud)

编辑:但是正如这个答案中所指出的(并且由于我草率的复制)这种方法需要更多的工作才能解决真正的问题。

但两者+++++不能在想表达类型检查,即行

let x = "s" ++ 2.0
let x = "s" +++ 2.0
Run Code Online (Sandbox Code Playgroud)

我通读了

我的理解是,使用一些额外的代码可以转换所有内容,但是将接口重新调整到现有类型并将其与静态解析的类型参数混合以应用成员约束是具有挑战性的。

有没有办法解决这个问题?

Fyo*_*kin 8

这里有几个微妙的问题。

首先, SRTP 仅适用于inline函数。这是因为它们不能被编译成 IL(因为 IL 不支持这种约束),所以它们必须在编译期间解决。静态。这就是它们“静态解析”的原因。该inline关键字使编译器能够做到这一点。

let inline (+) a b = ...
Run Code Online (Sandbox Code Playgroud)

其次,^t你引用的这个泛型类型是什么?这是什么类型?我认为你的意思是它是aand的类型b,但你没有这样声明,所以它只是一些随机的泛型类型。它需要绑定到参数:

let inline (+) (a: ^t) (b: ^t) = ...
Run Code Online (Sandbox Code Playgroud)

第三,从你的例子,它看起来像你实际上意味着ab到是不同类型的,不是吗?

let inline (+) (a: ^a) (b: ^b) = ...
Run Code Online (Sandbox Code Playgroud)

四,扩展方法不计的静态解析的类型,所以你不能定义ToExpressionStringfloat它期望的工作。通常的技巧是在一个特殊的类上声明所有方法,该类只是为了保存这些方法:

type ToExpressionStub() =
    static member ToExpression s = Var s
    static member ToExpression f = Val f
Run Code Online (Sandbox Code Playgroud)

然后人们可能会期望这会起作用:

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]
Run Code Online (Sandbox Code Playgroud)

但不,它不会。这是因为静态解析的约束不能应用于具体类型,只能应用于类型变量,如^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]
Run Code Online (Sandbox Code Playgroud)

然后我们必须ToExpressionStub()在每次调用时传递一个值:

let x = (+) (ToExpressionStub()) "foo" 2.0
Run Code Online (Sandbox Code Playgroud)

This is, of course, mighty inconvenient, so instead we will add another intermediate function with three parameters and call it from the operator (+):

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
Run Code Online (Sandbox Code Playgroud)

Almost there! But this also doesn't quite work: on the line let b = ... we get a warning that "This construct makes the code less generic...", and then at use sites we get an error that we can't use either string or float, depending on which came first.

This happens for some very-very obscure reasons. The compiler sees the two constraints having the same shape and applied to the same type, and decides that they must be the same constraint, and therefore, ^a = ^b. To break this stalemate, we can simply change the shape of the constraints to make them different:

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]
Run Code Online (Sandbox Code Playgroud)

Note that they are now applied to ^a or ^c and ^b or ^c respectively, not just ^c alone. This also has a bonus effect: we are no longer limited to just the types that we have enumerated in ToExpressionStub. We can use any type that has its own non-extension method ToExpression defined. For example, Expression itself:

type Expression =
  | Add of list<Expression>
  | Var of string
  | Val of float
  with
    static member ToExpression (e: Expression) = e
Run Code Online (Sandbox Code Playgroud)

And that's it! This now works:

> let x = 2.0 + "foo"
Add [Val 2.0; Var "foo"]

> let y = x + "bar"
Add [Add [Val 2.0; Var "foo"]; Var "bar"]
Run Code Online (Sandbox Code Playgroud)

Finally, to reduce runtime allocation costs, I usually have a singleton instance of ToExpressionStub rather than create a new one on every call:

type ToExpressionStub() = 
    static member val Value = ToExpressionStub()
    ...

let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b
Run Code Online (Sandbox Code Playgroud)

The bottom line is that yes, you can do it, but please carefully think about whether you should.

In actual practice this kind of trickery actually hinders development rather than help it. Sure, it may look all super neat and clever at first, but several months from now you will look at your own code and be unable to understand what's going on. Please trust my experience: having to wrap values in Val and Var is a feature, not a bug. Program code is read much more than it's written. Don't shoot yourself in the foot.