Расширенные выражения вычислений без for..in..do
Под расширенными выражениями вычислений я подразумеваю выражения вычислений с пользовательскими ключевыми словами, определенными с помощью атрибута CustomOperation.
Читая о расширенных выражениях вычислений, я сталкиваюсь с очень классным IL DSL от @kvb:
let il = ILBuilder()
// will return 42 when called
// val fortyTwoFn : (unit -> int)
let fortyTwoFn =
il {
ldc_i4 6
ldc_i4_0
ldc_i4 7
add
mul
ret
}
Интересно, как операции сочиняются без использования for..in..do
построить. Я чувствую, что это начинается с x.Zero
член, но я не нашел никаких ссылок, чтобы проверить это.
Если приведенный выше пример слишком технический, вот аналогичный DSL, в котором компоненты слайда перечислены без for..in..do
:
page {
title "Happy New Year F# community"
item "May F# continue to shine as it did in 2012"
code @"…"
button (…)
} |> SlideShow.show
У меня есть несколько тесно связанных вопросов:
- Как определить или использовать расширенные выражения вычислений без
For
член (т.е. предоставить небольшой полный пример)? Я не сильно переживаю, если они больше не монады, я заинтересован в их разработке DSL. - Можем ли мы использовать расширенные выражения вычислений с
let!
а такжеreturn!
? Если да, есть ли причина не делать этого? Я задаю эти вопросы, потому что я не встречал ни одного примера использованияlet!
а такжеreturn!
,
2 ответа
Я рад, что вам понравился пример IL. Лучший способ понять, как выражения обессилены, - это, вероятно, взглянуть на спецификацию (хотя она немного плотная...).
Там мы можем видеть, что-то вроде
C {
op1
op2
}
обессилено следующим образом:
T([<CustomOperator>]op1; [<CustomOperator>]op2, [], fun v -> v, true) ⇒
CL([<CustomOperator>]op1; [<CustomOperator>]op2, [], C.Yield(), false) ⇒
CL([<CustomOperator>]op2, [], 〚 [<CustomOperator>]op1, C.Yield() |][], false) ⇒
CL([<CustomOperator>]op2, [], C.Op1(C.Yield()), false) ⇒
〚 [<CustomOperator>]op2, C.Op1(C.Yield()) 〛[] ⇒
C.Op2(C.Op1(C.Yield()))
Что касается почему Yield()
используется, а не Zero
, это потому, что если бы в области были переменные (например, потому что вы использовали некоторые lets
, или были в цикле и т. д.), то вы получите Yield (v1,v2,...)
но Zero
явно не может быть использован таким образом. Обратите внимание, что это означает добавление лишнего let x = 1
в Томас lr
пример не удастся скомпилировать, потому что Yield
будет вызываться с аргументом типа int
скорее, чем unit
,
Есть еще одна хитрость, которая может помочь понять скомпилированную форму выражений вычислений, которая заключается в том, чтобы (ab) использовать поддержку авто-цитат для выражений вычислений в F# 3. Просто определите, что ничего не делать Quote
член и сделать Run
просто верните аргумент:
member __.Quote() = ()
member __.Run(q) = q
Теперь ваше вычислительное выражение будет оцениваться в кавычки его десагаратной формы. Это может быть очень удобно при отладке.
Я должен признать, что не до конца понимаю, как работают выражения для вычислений, когда вы используете такие функции, как CustomOperation
приписывать. Но вот некоторые замечания из моих экспериментов, которые могут помочь...
Во-первых, я думаю, что невозможно свободно комбинировать стандартные функции выражений вычислений (return!
и т. д.) с пользовательскими операциями. Некоторые комбинации, по-видимому, разрешены, но не все. Например, если я определю пользовательскую операцию left
а также return!
тогда я могу использовать только пользовательские операции, прежде чем return!
:
// Does not compile // Compiles and works
moves { return! lr moves { left
left } return! lr }
Что касается вычислений, которые используют только пользовательские операции, наиболее распространенные операции cusotom (orderBy
, reverse
и этот вид) есть тип M<'T> -> M<'T>
где M<'T>
это некоторый (возможно, универсальный) тип, представляющий то, что мы создаем (например, список).
Например, если мы хотим построить значение, представляющее последовательность движений влево / вправо, мы можем использовать следующее Commands
тип:
type Command = Left | Right
type Commands = Commands of Command list
Пользовательские операции, такие как left
а также right
может затем преобразовать Commands
в Commands
и добавьте новый шаг в конец списка. Что-то вроде:
type MovesBuilder() =
[<CustomOperation("left")>]
member x.Left(Commands c) = Commands(c @ [Left])
[<CustomOperation("right")>]
member x.Right(Commands c) = Commands(c @ [Right])
Обратите внимание, что это отличается от yield
который возвращает только одну операцию - или команду - и так yield
потребности Combine
объединить несколько отдельных шагов, если вы используете пользовательские операции, то вам никогда не нужно ничего комбинировать, потому что пользовательские операции постепенно создают Commands
ценность в целом. Нужно только немного начального пустого Commands
значение, которое используется в начале...
Теперь я ожидаю увидеть Zero
там, но это на самом деле вызывает Yield
с единицей в качестве аргумента, так что вам нужно:
member x.Yield( () ) =
Commands[]
Я не уверен, почему это так, но Zero
довольно часто определяется как Yield ()
так что, возможно, цель состоит в том, чтобы использовать определение по умолчанию (но, как я уже сказал, я бы также ожидал использовать Zero
Вот...)
Я думаю, что объединение пользовательских операций с выражениями вычислений имеет смысл. Хотя у меня есть твердое мнение о том, как следует использовать стандартные выражения для вычислений, у меня нет особой интуиции в отношении вычислений с пользовательскими операциями - я думаю, что сообщество все еще должно это выяснить:-). Но, например, вы можете расширить приведенные выше вычисления следующим образом:
member x.Bind(Commands c1, f) =
let (Commands c2) = f () in Commands(c1 @ c2)
member x.For(c, f) = x.Bind(c, f)
member x.Return(a) = x.Yield(a)
(В какой-то момент перевод начнет требовать For
а также Return
, но здесь они могут быть определены так же, как Bind
а также Yield
- и я не до конца понимаю, когда какая альтернатива используется).
Тогда вы можете написать что-то вроде:
let moves = MovesBuilder()
let lr =
moves { left
right }
let res =
moves { left
do! lr
left
do! lr }