F#和静态检查的联合案例

时间:2010-05-11 19:15:39

标签: html f# code-contracts

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

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

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

然后我尝试使用接口(每个元素类型为每个有效的父实现一个接口)和类型约束,但是我没有设法使它工作而不使用显式转换使解决方案变得麻烦使用。无论如何,它并不像一个优雅的解决方案。

今天我一直在寻找Code Contracts,但它似乎与F#Interactive不兼容。当我按下alt +输入它冻结。

只是为了让我的问题更清晰。这是一个同样问题的超简单人工例子:

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

我希望writeVowel只能静态接受元音,而不是如上所述,在运行时检查它。

我们怎样才能做到这一点?有谁有想法吗?必须有一个聪明的方法来做到这一点。如果没有工会案例,可能还有接口?我一直在努力解决这个问题,但我被困在盒子里,无法想到它。

6 个答案:

答案 0 :(得分:4)

看起来该库使用的是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

答案 1 :(得分:2)

严格地说,如果你想在编译时区分某些东西,你需要给它不同的类型。在您的示例中,您可以定义两种类型的字母,然后类型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类型是一个简单的区分联合,它可以存储第一种类型的值或第二种类型的值 - 您可以定义自己的区分联合,但是它有点难以出现具有合理的工会案件名称(由于嵌套)。

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

对于数字类型,您还可以使用units of measure,它允许您向该类型添加其他信息(例如,数字的类型为float<kilometer>),但这不适用于{ {1}}。如果是,您可以定义度量单位stringvowel并撰写consonantstring<vowel>,但衡量单位主要关注数字应用。

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

[编辑] 要添加有关OCaml实现的一些细节 - 我认为在OCaml中实现这一点的技巧是它使用结构子类型,这意味着(转换为F#术语)你可以用一些标准来定义歧视联盟(例如只有string<consonant>),然后用另一个有更多成员(VowelVowel)定义。

当您创建值Consonant时,它可以用作接受任一类型的函数的参数,但值Vowel "a"只能用于采用第二种类型的函数。

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

可以使用F#中的static member constraints来完成某种形式的结构子类型,但由于有区别的联合案例不是F#视角下的类型,我不认为它在这里可用。

答案 2 :(得分:2)

我的谦虚建议是:如果类型系统不容易支持静态检查'X',那么不要经历荒谬的扭曲试图静态检查'X'。只是在运行时抛出异常。天空不会下降,世界也不会结束。

进行静态检查的荒谬扭曲通常以使API复杂化为代价,使错误信息难以辨认,并导致接缝处的其他退化。

答案 3 :(得分:2)

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

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时它会使编译器或开发人员崩溃。

答案 4 :(得分:1)

类?

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

答案 5 :(得分:1)

感谢所有的建议!以防万一它能激发任何人提出解决问题的方法:下面是一个用我们的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" }; 

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

好吧,如果有人想出任何事情,请告诉我们!