F# 和静态检查联合案例

发布于 2024-08-31 19:16:56 字数 953 浏览 3 评论 0原文

很快我和我的战友 Joel 将发布 Wing Beats 的 0.9 版本。它是用 F# 编写的内部 DSL。使用它您可以生成 XHTML。灵感来源之一是 XHTML.M 模块奥西根框架。我不习惯 OCaml 语法,但我确实了解 XHTML.M 以某种方式静态检查元素的属性和子元素是否为有效类型。

我们无法在 F# 中静态检查同样的事情,现在我想知道是否有人知道如何做到这一点?

我的第一个天真的方法是将 XHTML 中的每个元素类型表示为联合案例。但不幸的是,您无法静态限制哪些情况作为参数值有效,如 XHTML.M 中那样。

然后,我尝试使用接口(每个元素类型为每个有效父级实现一个接口)和类型约束,但如果不使用显式转换,我就无法使其工作,这种方式使解决方案使用起来很麻烦。无论如何,这都不是一个优雅的解决方案。

今天我一直在研究Code Contracts,但它似乎与F# Interactive 不兼容。当我按下 Alt + Enter 时,它冻结了。

只是为了让我的问题更清楚。这是同一问题的一个超级简单的人工示例:

type Letter = 
    | Vowel of string
    | Consonant of string
let writeVowel = 
    function | Vowel str -> sprintf "%s is a vowel" str

我希望 writeVowel 仅静态接受元音,而不是像上面那样在运行时检查它。

我们怎样才能做到这一点?有人知道吗?必须有一种巧妙的方法来做到这一点。如果不使用联合案例,也许使用接口?我一直在努力解决这个问题,但我被困在盒子里,无法跳出盒子思考。

Soon me and my brother-in-arms Joel will release version 0.9 of Wing Beats. It's an internal DSL written in F#. With it you can generate XHTML. One of the sources of inspiration have been the XHTML.M module of the Ocsigen framework. I'm not used to the OCaml syntax, but I do understand XHTML.M somehow statically check if attributes and children of an element are of valid types.

We have not been able to statically check the same thing in F#, and now I wonder if someone have any idea of how to do it?

My first naive approach was to represent each element type in XHTML as a union case. But unfortunately you cannot statically restrict which cases are valid as parameter values, as in XHTML.M.

Then I tried to use interfaces (each element type implements an interface for each valid parent) and type constraints, but I didn't manage to make it work without the use of explicit casting in a way that made the solution cumbersome to use. And it didn't feel like an elegant solution anyway.

Today I've been looking at Code Contracts, but it seems to be incompatible with F# Interactive. When I hit alt + enter it freezes.

Just to make my question clearer. Here is a super simple artificial example of the same problem:

type Letter = 
    | Vowel of string
    | Consonant of string
let writeVowel = 
    function | Vowel str -> sprintf "%s is a vowel" str

I want writeVowel to only accept Vowels statically, and not as above, check it at runtime.

How can we accomplish this? Does anyone have any idea? There must be a clever way of doing it. If not with union cases, maybe with interfaces? I've struggled with this, but am trapped in the box and can't think outside of it.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

错爱 2024-09-07 19:16:56

看起来该库使用了 O'Caml 的多态变体,这些变体在 F# 中不可用。不幸的是,我也不知道在 F# 中对它们进行编码的可靠方法。

一种可能性可能是使用“幻像类型”,尽管我怀疑考虑到您正在处理的不同类别内容的数量,这可能会变得笨拙。以下是处理元音示例的方法:

module Letters = begin
  (* type level versions of true and false *)
  type ok = class end
  type nok = class end

  type letter<'isVowel,'isConsonant> = private Letter of char

  let vowel v : letter<ok,nok> = Letter v
  let consonant c : letter<nok,ok> = Letter c
  let y : letter<ok,ok> = Letter 'y'

  let writeVowel (Letter(l):letter<ok,_>) = sprintf "%c is a vowel" l
  let writeConsonant (Letter(l):letter<_,ok>) = sprintf "%c is a consonant" l
end

open Letters
let a = vowel 'a'
let b = consonant 'b'
let y = y

writeVowel a
//writeVowel b
writeVowel y

It looks like that library uses O'Caml's polymorphic variants, which aren't available in F#. Unfortunately, I don't know of a faithful way to encode them in F#, either.

One possibility might be to use "phantom types", although I suspect that this could become unwieldy given the number of different categories of content you're dealing with. Here's how you could handle your vowel example:

module Letters = begin
  (* type level versions of true and false *)
  type ok = class end
  type nok = class end

  type letter<'isVowel,'isConsonant> = private Letter of char

  let vowel v : letter<ok,nok> = Letter v
  let consonant c : letter<nok,ok> = Letter c
  let y : letter<ok,ok> = Letter 'y'

  let writeVowel (Letter(l):letter<ok,_>) = sprintf "%c is a vowel" l
  let writeConsonant (Letter(l):letter<_,ok>) = sprintf "%c is a consonant" l
end

open Letters
let a = vowel 'a'
let b = consonant 'b'
let y = y

writeVowel a
//writeVowel b
writeVowel y
魂牵梦绕锁你心扉 2024-09-07 19:16:56

严格来说,如果你想在编译时区分某些东西,你需要给它不同的类型。在您的示例中,您可以定义两种类型的字母,然后 Letter 类型将是第一个或第二个。

这有点麻烦,但它可能是实现您想要的效果的唯一直接方法:

type Vowel = Vowel of string
type Consonant = Consonant of string
type Letter = Choice<Vowel, Consonant>

let writeVowel (Vowel str) = sprintf "%s is a vowel" str
writeVowel (Vowel "a") // ok
writeVowel (Consonant "a") // doesn't compile

let writeLetter = function
  | Choice1Of2(Vowel str) -> sprintf "%s is a vowel" str
  | Choice2Of2(Consonant str) -> sprintf "%s is a consonant" str

Choice 类型是一个简单的可区分联合,它可以存储第一种类型的值或第二种类型的值类型 - 您可以定义自己的可区分联合,但为联合案例提供合理的名称有点困难(由于嵌套)。

代码契约允许您根据值指定属性,这在这种情况下更合适。我认为它们应该与 F# 一起使用(在创建 F# 应用程序时),但我没有任何将它们与 F# 集成的经验。

对于数字类型,您还可以使用度量单位,它允许您可以向该类型添加附加信息(例如,数字的类型为float),但这不适用于string。如果是,您可以定义测量单位元音辅音并编写字符串<元音>字符串<辅音> code>,但计量单位主要侧重于数值应用。

因此,也许最好的选择是在某些情况下依赖运行时检查。

[编辑] 添加一些有关 OCaml 实现的细节 - 我认为在 OCaml 中实现这一点的技巧是它使用结构子类型,这意味着(翻译为 F# 术语)您可以定义与一些成员(例如只有元音)的可区分联合,然后另一个具有更多成员(元音辅音)的联合。

当您创建值元音“a”时,它可以用作采用任一类型的函数的参数,但值辅音“a”只能使用函数采用第二种类型。

不幸的是,这不能轻易地添加到 F# 中,因为 .NET 本身并不支持结构子类型(尽管可以使用 .NET 4.0 中的一些技巧,但这必须由编译器完成)。所以,我知道理解你的问题,但我不知道如何解决它。

某种形式的结构子类型可以使用 F# 中的静态成员约束来完成,但由于从 F# 的角度来看,受歧视的联合案例不是类型,因此我认为它在这里不可用。

Strictly speaking, if you want to distinguish between something at compile-time, you need to give it different types. In your example, you could define two types of letters and then the type Letter would be either the first one or the second one.

This is a bit cumbersome, but it's probably the only direct way to achieve what you want:

type Vowel = Vowel of string
type Consonant = Consonant of string
type Letter = Choice<Vowel, Consonant>

let writeVowel (Vowel str) = sprintf "%s is a vowel" str
writeVowel (Vowel "a") // ok
writeVowel (Consonant "a") // doesn't compile

let writeLetter = function
  | Choice1Of2(Vowel str) -> sprintf "%s is a vowel" str
  | Choice2Of2(Consonant str) -> sprintf "%s is a consonant" str

The Choice type is a simple discriminated union which can store either a value of the first type or a value of the second type - you could define your own discriminated union, but it is a bit difficult to come up with reasonable names for the union cases (due to the nesting).

Code Contracts allow you to specify properties based on values, which would be more appropriate in this case. I think they should work with F# (when creating F# application), but I don't have any experience with integrating them with F#.

For numeric types, you can also use units of measure, which allow you to add additional information to the type (e.g. that a number has a type float<kilometer>), but this isn't available for string. If it was, you could define units of measure vowel and consonant and write string<vowel> and string<consonant>, but units of measure focus mainly on numerical applications.

So, perhaps the best option is to rely on runtime-checks in some cases.

[EDIT] To add some details regarding the OCaml implementation - I think that the trick that makes this possible in OCaml is that it uses structural subtyping, which means (translated to the F# terms) that you can define discriminated union with some mebers (e.g. only Vowel) and then another with more members (Vowel and Consonant).

When you create a value Vowel "a", it can be used as an argument to functions taking either of the types, but a value Consonant "a" can be used only with functions taking the second type.

This unfrotunately cannot be easily added to F#, because .NET doesn't natively support structural subtyping (although it may be possible using some tricks in .NET 4.0, but that would have to be done by the compiler). So, I know understand your problem, but I don't have any good idea how to solve it.

Some form of structural subtyping can be done using static member constraints in F#, but since discriminated union cases aren't types from the F# point of view, I don't think it is usable here.

空心↖ 2024-09-07 19:16:56

我的拙见是:如果类型系统不容易支持静态检查“X”,那么就不要尝试静态检查“X”。只需在运行时抛出异常即可。天不会塌下来,世界不会末日。

为了获得静态检查而进行的荒谬扭曲通常会以 API 复杂化为代价,并使错误消息难以解读,并导致其他性能下降。

My humble suggestion is: if the type system does not easily support statically checking 'X', then don't go through ridiculous contortions trying to statically check 'X'. Just throw an exception at runtime. The sky will not fall, the world will not end.

Ridiculous contortions to gain static checking often come at the expense of complicating an API, and make error messages indecipherable, and cause other degradations at the seams.

如梦初醒的夏天 2024-09-07 19:16:56

您可以使用带有静态解析类型参数的内联函数来根据上下文生成不同的类型。

let inline pcdata (pcdata : string) : ^U = (^U : (static member MakePCData : string -> ^U) pcdata)
let inline a (content : ^T) : ^U = (^U : (static member MakeA : ^T -> ^U) content)        
let inline br () : ^U = (^U : (static member MakeBr : unit -> ^U) ())
let inline img () : ^U = (^U : (static member MakeImg : unit -> ^U) ())
let inline span (content : ^T) : ^U = (^U : (static member MakeSpan : ^T -> ^U) content)

以 br 函数为例。它将产生一个 ^U 类型的值,该值在编译时静态解析。仅当 ^U 有静态成员 MakeBr 时才会编译。给出下面的示例,这可能会生成 A_Content.Br 或 Span_Content.Br。

然后,您定义一组类型来表示合法内容。每个都公开其接受的内容的“Make”成员。

type A_Content =
| PCData of string
| Br
| Span of Span_Content list
        static member inline MakePCData (pcdata : string) = PCData pcdata
        static member inline MakeA (pcdata : string) = PCData pcdata
        static member inline MakeBr () = Br
        static member inline MakeSpan (pcdata : string) = Span [Span_Content.PCData pcdata]
        static member inline MakeSpan content = Span content

and Span_Content =
| PCData of string
| A of A_Content list
| Br
| Img
| Span of Span_Content list
    with
        static member inline MakePCData (pcdata : string) = PCData pcdata
        static member inline MakeA (pcdata : string) = A_Content.PCData pcdata
        static member inline MakeA content = A content
        static member inline MakeBr () = Br
        static member inline MakeImg () = Img
        static member inline MakeSpan (pcdata : string) = Span [PCData pcdata]
        static member inline MakeSpan content = Span content

and Span =
| Span of Span_Content list
        static member inline MakeSpan (pcdata : string) = Span [Span_Content.PCData pcdata]
        static member inline MakeSpan content = Span content

然后您可以创建值...

let _ =
    test ( span "hello" )
    test ( span [pcdata "hello"] )
    test (
        span [
            br ();
            span [
                br ();
                a [span "Click me"];
                pcdata "huh?";
                img () ] ] )

此处使用的测试函数打印 XML...此代码表明这些值可以合理使用。

let rec stringOfAContent (aContent : A_Content) =
    match aContent with
    | A_Content.PCData pcdata -> pcdata
    | A_Content.Br -> "<br />"
    | A_Content.Span spanContent -> stringOfSpan (Span.Span spanContent)

and stringOfSpanContent (spanContent : Span_Content) =
    match spanContent with
    | Span_Content.PCData pcdata -> pcdata
    | Span_Content.A aContent ->
        let content = String.concat "" (List.map stringOfAContent aContent)
        sprintf "<a>%s</a>" content
    | Span_Content.Br -> "<br />"
    | Span_Content.Img -> "<img />"
    | Span_Content.Span spanContent -> stringOfSpan (Span.Span spanContent)

and stringOfSpan (span : Span) =
    match span with
    | Span.Span spanContent ->
        let content = String.concat "" (List.map stringOfSpanContent spanContent)
        sprintf "<span>%s</span>" content

let test span = printfn "span: %s\n" (stringOfSpan span)

输出如下:

span: <span>hello</span>

span: <span><br /><span><br /><a><span>Click me</span></a>huh?<img /></span></span>

错误消息看起来很合理...

test ( div "hello" )

Error: The type 'Span' does not support any operators named 'MakeDiv'

因为 Make 函数和其他函数是内联的,所以如果您在没有添加类型安全的情况下实现此功能,则生成的 IL 可能与您手动编写的代码类似。

您可以使用相同的方法来处理属性。

我确实想知道它是否会在接缝处退化,正如布莱恩指出的柔术解决方案可能会那样。 (这算不算柔术?)或者,当它实现所有 XHTML 时,编译器或开发人员是否会崩溃。

You can use inline functions with statically-resolved type parameters to yield different types depending on context.

let inline pcdata (pcdata : string) : ^U = (^U : (static member MakePCData : string -> ^U) pcdata)
let inline a (content : ^T) : ^U = (^U : (static member MakeA : ^T -> ^U) content)        
let inline br () : ^U = (^U : (static member MakeBr : unit -> ^U) ())
let inline img () : ^U = (^U : (static member MakeImg : unit -> ^U) ())
let inline span (content : ^T) : ^U = (^U : (static member MakeSpan : ^T -> ^U) content)

Take the br function, for example. It will produce a value of type ^U, which is statically resolved at compilation. This will only compile if ^U has a static member MakeBr. Given the example below, that could produce either a A_Content.Br or a Span_Content.Br.

You then define a set of types to represent legal content. Each exposes "Make" members for the content that it accepts.

type A_Content =
| PCData of string
| Br
| Span of Span_Content list
        static member inline MakePCData (pcdata : string) = PCData pcdata
        static member inline MakeA (pcdata : string) = PCData pcdata
        static member inline MakeBr () = Br
        static member inline MakeSpan (pcdata : string) = Span [Span_Content.PCData pcdata]
        static member inline MakeSpan content = Span content

and Span_Content =
| PCData of string
| A of A_Content list
| Br
| Img
| Span of Span_Content list
    with
        static member inline MakePCData (pcdata : string) = PCData pcdata
        static member inline MakeA (pcdata : string) = A_Content.PCData pcdata
        static member inline MakeA content = A content
        static member inline MakeBr () = Br
        static member inline MakeImg () = Img
        static member inline MakeSpan (pcdata : string) = Span [PCData pcdata]
        static member inline MakeSpan content = Span content

and Span =
| Span of Span_Content list
        static member inline MakeSpan (pcdata : string) = Span [Span_Content.PCData pcdata]
        static member inline MakeSpan content = Span content

You can then create values...

let _ =
    test ( span "hello" )
    test ( span [pcdata "hello"] )
    test (
        span [
            br ();
            span [
                br ();
                a [span "Click me"];
                pcdata "huh?";
                img () ] ] )

The test function used there prints XML... This code shows that the values are reasonable to work with.

let rec stringOfAContent (aContent : A_Content) =
    match aContent with
    | A_Content.PCData pcdata -> pcdata
    | A_Content.Br -> "<br />"
    | A_Content.Span spanContent -> stringOfSpan (Span.Span spanContent)

and stringOfSpanContent (spanContent : Span_Content) =
    match spanContent with
    | Span_Content.PCData pcdata -> pcdata
    | Span_Content.A aContent ->
        let content = String.concat "" (List.map stringOfAContent aContent)
        sprintf "<a>%s</a>" content
    | Span_Content.Br -> "<br />"
    | Span_Content.Img -> "<img />"
    | Span_Content.Span spanContent -> stringOfSpan (Span.Span spanContent)

and stringOfSpan (span : Span) =
    match span with
    | Span.Span spanContent ->
        let content = String.concat "" (List.map stringOfSpanContent spanContent)
        sprintf "<span>%s</span>" content

let test span = printfn "span: %s\n" (stringOfSpan span)

Here's the output:

span: <span>hello</span>

span: <span><br /><span><br /><a><span>Click me</span></a>huh?<img /></span></span>

Error messages seem reasonable...

test ( div "hello" )

Error: The type 'Span' does not support any operators named 'MakeDiv'

Because the Make functions and the other functions are inline, the generated IL is probably similar to what you would code by hand if you were implementing this without the added type safety.

You could use the same approach to handle attributes.

I do wonder if it will degrade at the seams, as Brian pointed out contortionist solutions might. (Does this count as contortionist or not?) Or if it will melt the compiler or the developer down by the time it implements all of XHTML.

情独悲 2024-09-07 19:16:56

上课?

type Letter (c) =
    member this.Character = c
    override this.ToString () = sprintf "letter '%c'" c

type Vowel (c) = inherit Letter (c)

type Consonant (c) = inherit Letter (c)

let printLetter (letter : Letter) =
    printfn "The letter is %c" letter.Character

let printVowel (vowel : Vowel) =
    printfn "The vowel is %c" vowel.Character

let _ =
    let a = Vowel('a')
    let b = Consonant('b')
    let x = Letter('x')

    printLetter a
    printLetter b
    printLetter x

    printVowel a
//    printVowel b  // Won't compile

    let l : Letter list = [a; b; x]
    printfn "The list is %A" l

Classes?

type Letter (c) =
    member this.Character = c
    override this.ToString () = sprintf "letter '%c'" c

type Vowel (c) = inherit Letter (c)

type Consonant (c) = inherit Letter (c)

let printLetter (letter : Letter) =
    printfn "The letter is %c" letter.Character

let printVowel (vowel : Vowel) =
    printfn "The vowel is %c" vowel.Character

let _ =
    let a = Vowel('a')
    let b = Consonant('b')
    let x = Letter('x')

    printLetter a
    printLetter b
    printLetter x

    printVowel a
//    printVowel b  // Won't compile

    let l : Letter list = [a; b; x]
    printfn "The list is %A" l
面犯桃花 2024-09-07 19:16:56

感谢您的所有建议!以防万一它会激励任何人提出问题的解决方案:下面是一个用我们的 DSL Wing Beats 编写的简单 HTML 页面。跨度是身体的一个孩子。这不是有效的 HTML。如果不编译就好了。

let page =
    e.Html [
        e.Head [ e.Title & "A little page" ]
        e.Body [
            e.Span & "I'm not allowed here! Because I'm not a block element."
        ]
    ]

或者还有其他我们没有想到的检查方法吗?我们务实!每一种可能的方式都值得研究。 Wing Beats 的主要目标之一是使其像 (X)Html 专家系统一样,指导程序员。我们希望确保程序员只会在他选择的情况下生成无效的 (X)Html,而不是因为缺乏知识或粗心的错误。

我们认为我们有一个静态检查属性的解决方案。它看起来像这样:

module a = 
    type ImgAttributes = { Src : string; Alt : string; (* and so forth *) }
    let Img = { Src = ""; Alt = ""; (* and so forth *) }
let link = e.Img { a.Img with Src = "image.jpg"; Alt = "An example image" }; 

它有其优点和缺点,但它应该有效。

好吧,如果有人提出任何建议,请告诉我们!

Thanks for all the suggestions! Just in case it will inspire anyone to come up with a solution to the problem: below is a simple HTML page written in our DSL Wing Beats. The span is a child of the body. This is not valid HTML. It would be nice if it didn't compile.

let page =
    e.Html [
        e.Head [ e.Title & "A little page" ]
        e.Body [
            e.Span & "I'm not allowed here! Because I'm not a block element."
        ]
    ]

Or are there other ways to check it, that we have not thought about? We're pragmatic! Every possible way is worth investigating. One of the major goals with Wing Beats is to make it act like an (X)Html expert system, that guides the programmer. We want to be sure a programmer only produces invalid (X)Html if he chooses to, not because of lacking knowledge or careless mistakes.

We think we have a solution for statically checking the attributes. It looks like this:

module a = 
    type ImgAttributes = { Src : string; Alt : string; (* and so forth *) }
    let Img = { Src = ""; Alt = ""; (* and so forth *) }
let link = e.Img { a.Img with Src = "image.jpg"; Alt = "An example image" }; 

It has its pros and cons, but it should work.

Well, if anyone comes up with anything, let us know!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文