Generics 强制F中内部DSL的现有类型#
为AST(一个非常简单的表达式树)提供DU 而且Generics 强制F中内部DSL的现有类型#,generics,f#,Generics,F#,为AST(一个非常简单的表达式树)提供DU 而且 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”。我想避免这种包装 我尝试了其他
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”
。我想避免这种包装
我尝试了其他几种方法:
声明类型扩展和接口,并同时使用静态类型参数和接口约束:
首先是界面
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]
编辑:但正如本文所指出的(由于我草率的复制),这种方法需要更多的工作才能解决真正的问题
但是++
和++++
都无法在想要的表达式(即行)中进行类型检查
let x = "s" ++ 2.0
let x = "s" +++ 2.0
我通读了一遍
- /类型扩展
有办法解决这个问题吗?这里有几个微妙的问题 首先,SRTPs仅适用于
内联功能。这是因为它们不能被编译成IL(因为IL不支持这种约束),所以它们必须在编译期间被解析。静态地。这就是为什么它们是“静态解析的”。inline
关键字使编译器能够这样做
let inline (+) a b = ...
其次,您所引用的通用类型是什么?是什么类型的?我想你的意思是它是a
和b
的类型,但你没有这样声明,所以它只是一些随机的泛型类型。它需要绑定到以下参数:
let inline (+) (a: ^t) (b: ^t) = ...
第三,从你的例子来看,你实际上是想让a
和b
属于不同的类型,不是吗
let inline (+) (a: ^a) (b: ^b) = ...
第四,扩展方法不计算静态解析的类型,因此您不能在String
和float
上定义ToExpression
,并期望它工作。通常的技巧是在一个特殊类上声明所有方法,该类的存在只是为了保存这些方法:
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]
然后我们必须在每次调用时传递一个值到expressionStub()
:
let x = (+) (ToExpressionStub()) "foo" 2.0
type ToExpressionStub() =
static member val Value = ToExpressionStub()
...
let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b
当然,这非常不方便,因此我们将添加另一个带有三个参数的中间函数,并从操作符(+)
调用它:
快到了!但这也不太管用:在let b=…
行上,我们得到一个警告,“这个构造使代码不那么通用…”,然后在使用站点上,我们得到一个错误,我们不能使用string
或float
,这取决于哪个先到
发生这种情况的原因非常模糊。编译器看到两个具有相同形状且应用于相同类型的约束,并确定它们必须是相同的约束,因此,^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或^c
和^b或^c
,而不仅仅是^c
。这还有一个额外的效果:我们不再局限于在ToExpressionStub
中枚举的类型。我们可以使用任何定义了非扩展方法ToExpression
的类型。例如,表达式本身:
type Expression =
| Add of list<Expression>
| Var of string
| Val of float
with
static member ToExpression (e: Expression) = e
最后,为了降低运行时分配成本,我通常使用ToExpressionStub
的单例实例,而不是每次调用都创建一个新实例:
let x = (+) (ToExpressionStub()) "foo" 2.0
type ToExpressionStub() =
static member val Value = ToExpressionStub()
...
let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b
底线是,是的,你可以做到,但请仔细考虑你是否应该做到
在实际操作中,这种欺骗实际上阻碍而不是帮助发展。当然,一开始它可能看起来非常整洁和聪明,但几个月后,您将看到自己的代码,无法理解发生了什么。请相信我的经验:必须在Val
和Var
中包装值是一种特性,而不是bug。程序代码读的比写的多得多。不要打自己的脚。读到这篇文章,我想起了我试图修改其中一个任务库,使其与选项和结果包装器集成时的情景……感谢您的详细回答。我认为一些错误(内联、静态成员)我可以自己解决,但是使用伪值来快速处理编译器泛化类型可能会结束这个实验。在封闭世界的场景中,使用接口很容易,这里的问题是集成现有类型。TypeClass和模块函子可能会有所帮助。但这两种方法在F#中都不可用。我在我的完整示例中尝试了这种方法,并同意:它不值得付出代价。这个想法是用我可以用来在Jupyther笔记本中快速编写内容的东西来包装一个很好的功能框架(只使用核心ML构造),以生成一个更稳定的表示。因此,我认为阅读维护不太重要,但对实现的关注使我不再关注。然而,这是值得探索的。
> let x = 2.0 + "foo"
Add [Val 2.0; Var "foo"]
> let y = x + "bar"
Add [Add [Val 2.0; Var "foo"]; Var "bar"]
type ToExpressionStub() =
static member val Value = ToExpressionStub()
...
let inline (+) (a: ^a) (b: ^b) = doAdd ToExpressionStub.Value a b