Почему выражения вычисления F # требуют объекта-строителя (а не класса)?
Вычисления выражения F # имеют синтаксис:
ident { cexpr }
Где ident
- объект-строитель (этот синтаксис взят из записи в блоге Don Syme 2007).
Во всех примерах, которые я видел, объекты-строители являются экземплярами singleton и без атак для загрузки. Дон дает пример определения объекта-строителя с именем attempt
:
let attempt = new AttemptBuilder()
Мой вопрос: Почему F # просто не использует класс AttemptBuilder
непосредственно в выражениях вычислений? Разумеется, нотация может быть отклонена к вызовам статических методов так же легко, как вызовы метода экземпляра.
Использование значения экземпляра означает, что в теории можно было бы создать экземпляр нескольких объектов-компиляторов того же класса, предположительно параметризованных каким-либо образом или даже (запрещающих небо) с изменчивым внутренним состоянием. Но я не могу представить, как это когда-нибудь будет полезно.
Обновление: Синтаксис, приведенный выше, предполагает, что строитель должен отображаться как один идентификатор, который вводит в заблуждение и, вероятно, отражает более раннюю версию языка. В последней F # 2.0 Language Specification определяется синтаксис как:
expr { comp-or-range-expr }
что дает понять, что любое выражение (которое оценивает объект-строитель) может использоваться как первый элемент конструкции.
Ответы
Ответ 1
Ваше предположение верно; экземпляр builder может быть параметризован, а параметры могут быть впоследствии использованы во время вычислений.
Я использую этот шаблон для построения дерева математического доказательства для определенного вычисления. Каждый вывод представляет собой тройку имени проблемы, результат вычисления и 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
}
Ответ 2
Это просто вопрос гибкости. Да, было бы проще, если бы классы Builder были бы статичными, но от разработчиков требуется небольшая гибкость, не получая многого в этом процессе.
Например, предположим, что вы хотите создать рабочий процесс для связи с сервером. Где-то в коде вам нужно указать адрес этого сервера (Uri, IPAddress и т.д.). В каких случаях вам понадобится/хотите общаться с несколькими серверами в рамках одного рабочего процесса? Если ответ "нет", тогда вам будет более полезно создать объект-строитель с помощью конструктора, который позволит вам передать Uri/IPAddress сервера вместо того, чтобы передавать это значение непрерывно через различные функции. Внутренне объект вашего строителя может применять значение (адрес сервера) к каждому методу в рабочем процессе, создавая нечто вроде (но не точно) монады Reader.
С экземплярами объектов на основе экземпляров вы также можете использовать наследование для создания иерархии типов строителей с некоторыми унаследованными функциями. Я еще не видел, чтобы кто-то делал это на практике, но опять же - гибкость существует, если люди нуждаются в ней, чего бы у вас не было со статически типизированными объектами-строителями.
Ответ 3
Еще одна альтернатива заключается в использовании однодисковых дискриминируемых объединений, как в:
type WorkFlow = WorkFlow with
member __.Bind (m,f) = Option.bind f m
member __.Return x = Some x
то вы можете напрямую использовать его, как
let x = WorkFlow{ ... }