7

很快我和我的兄弟 Joel 将发布Wing Beats的 0.9 版。它是用 F# 编写的内部 DSL。有了它,您可以生成 XHTML。灵感来源之一是 Ocsigen 框架的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 只静态接受元音,而不是像上面那样,在运行时检查它。

我们怎样才能做到这一点?有人有什么主意吗?必须有一个聪明的方法来做到这一点。如果没有联合案例,也许有接口?我一直在为此苦苦挣扎,但被困在盒子里,无法在外面思考。

4

6 回答 6

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
于 2010-05-11T21:02:03.570 回答
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类型是一个简单的可区分联合,可以存储第一种类型的值或第二种类型的值-您可以定义自己的可区分联合,但是为联合情况想出合理的名称有点困难(由于嵌套)。

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

对于数字类型,您还可以使用度量单位,它允许您向类型添加附加信息(例如,数字具有类型float<kilometer>),但这不适用于string. 如果是这样,您可以定义度量单位vowelandconsonant并编写string<vowel>and string<consonant>,但度量单位主要关注数值应用。

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

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

创建 valueVowel "a"时,它​​可以用作采用任何一种类型的函数的参数,但 valueConsonant "a"只能用于采用第二种类型的函数。

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

可以使用F# 中的静态成员约束来完成某种形式的结构子类型化,但由于从 F# 的角度来看,区分联合情况不是类型,我认为它在这里不可用。

于 2010-05-11T19:55:08.973 回答
2

我谦虚的建议是:如果类型系统不容易支持静态检查“X”,那么就不要通过荒谬的扭曲尝试静态检查“X”。只需在运行时抛出异常。天不会塌,世界不会灭。

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

于 2010-05-12T01:12:58.810 回答
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 时使编译器或开发人员崩溃。

于 2010-05-14T02:42:48.923 回答
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
于 2010-05-12T18:15:12.003 回答
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" }; 

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

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

于 2010-05-13T11:12:09.713 回答