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