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

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

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

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

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

Сегодня я смотрел на Контракты Кода, но это, кажется, является несовместимым с Интерактивным F#. Когда я совершил нападки, высокий звук + входят, замораживается.

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

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

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

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

7
задан Joel Mueller 11 May 2010 в 22:18
поделиться

6 ответов

Похоже, эта библиотека использует полиморфные варианты 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
4
ответ дан 6 December 2019 в 21:11
поделиться

Мое скромное предложение: если система типов с трудом поддерживает статическую проверку «X», то не делайте нелепых искажений, пытаясь статически проверить » ИКС'. Просто создайте исключение во время выполнения. Небо не упадет, мир не погибнет.

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

2
ответ дан 6 December 2019 в 21:11
поделиться

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

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

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

1
ответ дан 6 December 2019 в 21:11
поделиться

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

Поэтому, возможно, лучшим вариантом будет полагаться на проверку времени выполнения в некоторых случаях.

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

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

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

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

2
ответ дан 6 December 2019 в 21:11
поделиться

Классы?

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
1
ответ дан 6 December 2019 в 21:11
поделиться

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

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.

2
ответ дан 6 December 2019 в 21:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: