Скоро меня и моего собрата по оружию 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 только принял Гласные статически, и не как выше, проверил его во времени выполнения.
Как мы можем выполнить это? У кого-либо есть какая-либо идея? Должен быть умный способ сделать его. Если не со случаями объединения, возможно, с интерфейсами? Я боролся с этим, но захватываюсь в поле и не может думать за пределами него.
Похоже, эта библиотека использует полиморфные варианты 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
Мое скромное предложение: если система типов с трудом поддерживает статическую проверку «X», то не делайте нелепых искажений, пытаясь статически проверить » ИКС'. Просто создайте исключение во время выполнения. Небо не упадет, мир не погибнет.
Нелепые искажения для получения статической проверки часто происходят за счет усложнения API, делают сообщения об ошибках нечитаемыми и вызывают другие ухудшения качества.
Спасибо за все предложения! На всякий случай это вдохновит кого-нибудь придумать решение проблемы: ниже представлена простая 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" };
У него есть свои плюсы и минусы, но он должен работать.
Что ж, если кто-нибудь что-нибудь придумает, дайте нам знать!
Строго говоря, если вы хотите различать что-то во время компиляции, вам нужно дать ему разные типы. В вашем примере можно определить два типа букв, и тогда тип 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#, я не думаю, что это можно использовать здесь.
Классы?
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
Вы можете использовать встроенные функции со статически разрешенными параметрами типа для получения различных типов в зависимости от контекста.
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.