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