Почему для вычислительных выражений F# требуется объект builder (а не класс)?
выражения вычисления F# имеют синтаксис:
ident { cexpr }
здесь ident
является объектом builder (этот синтаксис взят из запись в блоге Дона Сайма 2007).
во всех примерах, которые я видел, объекты builder являются одноэлементными экземплярами и без состояния для загрузки. Дон приводит пример определения объекта builder с именем attempt
:
let attempt = new AttemptBuilder()
мой вопрос: почему F# просто не использует AttemptBuilder
класс непосредственно в расчетах выражения? Конечно, нотация может быть де-подслащена к вызовам статического метода так же легко, как вызовы метода экземпляра.
использование значения экземпляра означает, что теоретически можно создать несколько объектов builder одного и того же класса, предположительно параметризованных каким-либо образом или даже (не дай бог) с изменяемым внутренним состоянием. Но я не представляю, как это может быть полезно.
обновление: синтаксис, который я процитировал выше, предполагает, что строитель должен появиться как единый идентификатор, который вводит в заблуждение и, вероятно, отражает более раннюю версию языка. Самые последние Спецификация Языка F# 2.0 определяет синтаксис:
expr { comp-or-range-expr }
что дает понять, что любое выражение (которое оценивает объект builder) может использоваться в качестве первого элемента конструкции.
3 ответов
ваше предположение верно; экземпляр builder может быть параметризован, и параметры могут быть впоследствии использованы во время вычисления.
Я использую этот шаблон для построения дерева математического доказательства для определенного вычисления. Каждый вывод это тройкаимя, a результат вычисления, и N-дерево базовые выводы (лемм).
позвольте мне привести небольшой пример, удаление доказательство дерево, но сохраняя имя. Назовем это аннотации как это кажется более подходящим.
type AnnotationBuilder(name: string) =
// Just ignore an original annotation upon binding
member this.Bind<'T> (x, f) = x |> snd |> f
member this.Return(a) = name, a
let annotated name = new AnnotationBuilder(name)
// Use
let ultimateAnswer = annotated "Ultimate Question of Life, the Universe, and Everything" {
return 42
}
let result = annotated "My Favorite number" {
// a long computation goes here
// and you don't need to carry the annotation throughout the entire computation
let! x = ultimateAnswer
return x*10
}
Это просто вопрос гибкости. Да, было бы проще, если бы классы Builder должны были быть статическими, но это отнимает некоторую гибкость у разработчиков, не получая многого в процессе.
например, предположим, вы хотите создать рабочий процесс для связи с сервером. Где-то в коде вам нужно будет указать адрес этого сервера (Uri, IPAddress и т. д.). В каких случаях вам нужно / хотите общаться с несколькими серверами в пределах единый рабочий процесс? Если ответ "нет", то для вас имеет смысл создать объект builder с помощью конструктора, который позволяет передавать Uri/IPAddress сервера вместо того, чтобы передавать это значение непрерывно через различные функции. Внутренне ваш объект builder может применить значение (адрес сервера) к каждому методу в рабочем процессе, создав что-то вроде (но не совсем) монады чтения.
с объектами построителя на основе экземпляра вы также можете используйте наследование для создания иерархий типов построителей с некоторыми унаследованными функциями. Я еще не видел, чтобы кто-то делал это на практике, но опять же-гибкость есть в случае, если людям это нужно, чего не было бы со статически типизированными объектами builder.
другой альтернативой является использование одиночных случаев дискриминируемых союзов, как в:
type WorkFlow = WorkFlow with
member __.Bind (m,f) = Option.bind f m
member __.Return x = Some x
затем вы можете напрямую использовать его как
let x = WorkFlow{ ... }