Почему выражения вычисления 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{ ... }