2010-05-11 22 views
7

不久我和我的兄弟Joel將發佈0.9版Wing Beats。這是一個用F#編寫的內部DSL。有了它,你可以生成XHTML。靈感來源之一是Ocsigen框架的XHTML.M模塊。我不習慣OCaml語法,但我確實理解XHTML.M以某種方式靜態檢查元素的屬性和子元素是否爲有效類型。F#和靜態檢查工會案件

我們還沒有能夠靜態檢查F#中的同一個東西,現在我想知道如果有人有任何想法如何做到這一點?

我的第一個幼稚的方法是在XHTML中將每個元素類型表示爲一個聯合案例。但不幸的是,你不能靜態地限制哪些情況作爲參數值有效,就像在XHTML.M中一樣。

然後我嘗試使用接口(每個元素類型實現每個有效父級的接口)和類型約束,但是我沒有設法使它在沒有使用顯式強制轉換的情況下工作,這使得解決方案變得非常繁瑣使用。無論如何,它並不是一個優雅的解決方案。

今天我一直在看代碼合同,但它似乎與F#Interactive不兼容。當我擊中ALT +輸入它凍結。

只是爲了讓我的問題更清楚。這裏是同一個問題的一個超級簡單的人工例如:

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

我想writeVowel只接受元音靜態的,而不是如上述,在運行時檢查。

我們該如何做到這一點?有人有什麼主意嗎?必須有一個聰明的方式來做到這一點。如果不是工會案件,也許與接口?我一直在努力,但我被困在盒子裏,無法想到它。

+0

請問您能否將代碼合同示例凍結FSI到fsbugs(at)microsoft dot com? – Brian 2010-05-12 01:08:18

回答

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 
+0

我剛剛意識到 - 我正在考慮使用靜態成員約束(例如'^ T'),它們有點類似,但我不確定這是否會導致任何地方......(可能不會,但也許你會有一些聰明的想法:-))。 – 2010-05-11 21:07:29

+0

+1這是一個不錯的技巧:-)如果F#支持協變,它可能會更好。你可以有一個幻像類型的層次結構,並且可以將字母「」作爲參數傳遞給期望「字母」(「vovel」將是any的子類型)的函數! – 2010-05-11 21:44:49

+0

@Tomas - 實際上,沒有協變,這個解決方案有一個限制,就是你不能輕易創建一個包含'a'和'y'或'b'和'y'的列表,這是一個恥辱。 – kvb 2010-05-11 22:24:37

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#集成的經驗。

對於數值類型,也可以使用units of measure,這樣就可以將附加信息添加到該類型的(例如,一些具有式float<kilometer>),但這不適用於string。如果是,您可以定義度量單位vowelconsonant並編寫string<vowel>string<consonant>,但度量單位主要關注數值應用。

因此,也許最好的選擇是在某些情況下依靠運行時檢查。

[編輯]要添加關於OCaml中實現的一些細節 - 我認爲這OCaml中使這成爲可能,關鍵是它使用結構子類型,這意味着(轉換爲F#計算),你可以定義歧視與一些mebers(例如只有Vowel)聯合,然後再與更多成員(VowelConsonant)聯合。

當您創建值Vowel "a"時,它可以用作採用任一類型的函數的參數,但值Consonant "a"只能用於採用第二種類型的函數。

這非常不幸地無法輕易地添加到F#中,因爲.NET本身不支持結構化子類型(儘管在.NET 4.0中可能使用一些技巧,但這必須由編譯器完成)。所以,我知道了解你的問題,但我沒有任何好的想法如何解決它。

某些形式的結構子類型可以在F#中使用static member constraints來完成,但由於歧視的聯合事例不是F#觀點中的類型,我認爲它在這裏不可用。

+0

我明白這一點,但坦率地說我有點失望,我不能添加某種聯盟案例約束。就我可以看到的,當我調查XHTML.M模塊時,它可能在OCaml中。 有誰知道爲什麼在F#中這是不可能的? .NET有限制嗎? 順便說一下,我喜歡你的書「真實世界的功能編程」! – 2010-05-11 20:15:28

+0

@Johan:謝謝!我想我現在明白你的動機 - 我添加了一些關於OCaml的細節(雖然我不是專家)。不幸的是,F#(AFAIK)不能輕鬆支持這個關鍵特性。 – 2010-05-11 21:04:50

2

我卑微的建議是:如果類型系統不容易支持靜態檢查「X」,然後不經過荒謬扭曲試圖靜態檢查「X」 。只需在運行時拋出異常。天空不會倒塌,世界將不會結束。

爲獲得靜態檢查而產生的荒謬扭曲通常會以使API複雜化爲代價,並使錯誤消息難以辨認,並導致接口處出現其他降級。

+0

嗯,我想你是對的。我甚至聽說有人在使用語言,他們幾乎沒有進行靜態類型檢查。 =) 在我們的案例中,我們將靜態檢查事情是否會使我們的DSL更易於使用,否則不會。如果我們無法弄清楚靜態檢查的方法,那麼W3C的驗證器將在運行時執行這個技巧! – 2010-05-12 07:15:47

+1

我不一定不同意,但我認爲這取決於API。有時你必須通過一些額外的工具來作爲圖書館作者,但客戶端會獲得一個靜態阻止非法使用的API。對我來說,糟糕的錯誤消息的代價通常超過了靜態正確性的好處(並且它不像F#以其清晰的錯誤消息而聞名)。 – kvb 2010-05-12 18:56:28

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 
+0

雖然這對於問題中給出的簡化示例有效,但它不適用於XHTML庫中更常見的問題。由於XHTML實體的內容類型和類型之間存在多對多的關係,我認爲缺乏多重繼承會造成難以克服的問題。基於接口的解決方案可能會解決這個問題,但最初的問題似乎要求採取其他方法。 – kvb 2010-05-12 19:40:24

+0

我其實嘗試了一個基於接口的解決方案。我做了一個概念的證明,它的工作。我也設法隱藏它的用戶。它並不優雅,並且有很多接口。但它的工作。 但是......好吧,我不得不以一種使得解決方案繁瑣的方式來幫助類型推斷。 – 2010-05-13 10:25:34

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" }; 

它有其優點和缺點,但它應該工作。

好吧,如果有人想出任何東西,請告訴我們!

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。

然後,您可以定義一組類型來表示合法內容。每個人都會爲其接受的內容公開「製作」成員。

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可能是類似如果你在沒有增加類型安全性的情況下實現這個功能,你會手工編寫代碼。

您可以使用相同的方法來處理屬性。

我想知道它是否會在接縫處降解,正如Brian指出柔術解決方案可能。 (這是否算作柔術?或者,如果編譯器或開發人員在實現所有XHTML的時候就會融化它們。

+0

啊,鴨打字=)這是一個值得調查的想法!我會盡快測試你的想法。 – 2010-05-14 06:50:37