F# и статически проверенные случаи объединения

Вскоре я и мой соратник Джоэл выпустим версию 0.9 Wing Beats. Это внутренний DSL, написанный на F#. С его помощью вы можете генерировать XHTML. Одним из источников вдохновения был модуль XHTML.M платформы Ocsigen. Я не привык к синтаксису OCaml, но я понимаю, что XHTML.M как-то статически проверяет, имеют ли атрибуты и дочерние элементы допустимые типы.

Мы не смогли статически проверить одну и ту же вещь в F#, и теперь мне интересно, есть ли у кого-нибудь идеи, как это сделать?

Мой первый наивный подход состоял в том, чтобы представлять каждый тип элемента в XHTML как объединенный случай. Но, к сожалению, вы не можете статически ограничить, какие случаи допустимы в качестве значений параметров, как в XHTML.M.

Затем я попытался использовать интерфейсы (каждый тип элемента реализует интерфейс для каждого допустимого родителя) и ограничения типа, но мне не удалось заставить его работать без использования явного приведения таким образом, чтобы сделать решение громоздким для использования. И все равно это не было элегантным решением.

Сегодня я смотрел на контракты по коду, но он кажется несовместимым с F# Interactive. Когда я нажимаю Alt + Enter, он зависает.

Просто чтобы прояснить мой вопрос. Вот супер простой искусственный пример той же проблемы:

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

Я хочу, чтобы writeVowel принимал только гласные статически, а не как указано выше, проверяйте их во время выполнения.

Как мы можем сделать это? У кого-нибудь есть идеи? Там должен быть умный способ сделать это. Если не со случаями объединения, может быть, с интерфейсами? Я боролся с этим, но я в ловушке в коробке и не могу думать вне этого.

6 ответов

Решение

Похоже, что библиотека использует полиморфные варианты О'Камла, которые недоступны в 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

Вы можете использовать встроенные функции со статически разрешенными параметрами типов, чтобы получить различные типы в зависимости от контекста.

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, он растопит компилятор или разработчика.

Строго говоря, если вы хотите различать что-то во время компиляции, вам нужно дать ему разные типы. В вашем примере вы можете определить два типа букв, а затем тип 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, Если бы это было, вы могли бы определить единицы измерения vowel а также consonant и писать string<vowel> а также string<consonant>, но единицы измерения сосредоточены в основном на численных приложениях.

Так что, возможно, лучший вариант - в некоторых случаях полагаться на проверки во время выполнения.

[EDIT] Чтобы добавить некоторые детали, касающиеся реализации OCaml - я думаю, что хитрость, которая делает это возможным в OCaml, заключается в том, что он использует структурный подтип, что означает (в переводе на термины F#), что вы можете определить дискриминационное объединение с некоторыми членами (например, только Vowel) а затем еще один с большим количеством участников (Vowel а также Consonant).

Когда вы создаете значение Vowel "a", он может использоваться в качестве аргумента для функций, принимающих любой из типов, но значение Consonant "a" может использоваться только с функциями второго типа.

Это, к сожалению, не может быть легко добавлено в F#, потому что.NET изначально не поддерживает структурные подтипы (хотя это может быть возможно с использованием некоторых приемов в.NET 4.0, но это должно быть сделано компилятором). Итак, я знаю, понимаю вашу проблему, но я не знаю, как ее решить.

Некоторая форма структурного подтипирования может быть выполнена с использованием статических ограничений членов в F#, но поскольку случаи различенного объединения не являются типами с точки зрения F#, я не думаю, что это пригодно для использования здесь.

Мое скромное предложение таково: если система типов не поддерживает статическую проверку "X", то не проходите через нелепые искажения, пытаясь статически проверить "X". Просто выбросить исключение во время выполнения. Небо не упадет, мир не кончится.

Смешные искажения для получения статической проверки часто происходят за счет усложнения API, а также делают сообщения об ошибках неразборчивыми и вызывают другие искажения в швах.

Классы?

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

Спасибо за все предложения! На всякий случай это вдохновит любого найти решение проблемы: ниже приведена простая HTML-страница, написанная на нашем DSL Wing Beats. Пролет - это дитя тела. Это не правильный 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" }; 

У него есть свои плюсы и минусы, но это должно сработать.

Ну, если кто-нибудь что-нибудь придумает, дайте нам знать!

Другие вопросы по тегам