Расширенные выражения вычислений без...
То, что я подразумеваю под расширенными выражениями вычислений, - это выражения вычислений с пользовательскими ключевыми словами, определяемыми атрибутом 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!
.
Ответы
Ответ 1
Я рад, что вам понравился пример IL. Лучшим способом понять, как выражаются выражения, вероятно, является просмотр spec (хотя он немного плотный...).
Там мы видим, что что-то вроде
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
или были в цикле for и т.д.), тогда вы получите Yield (v1,v2,...)
, но Zero
явно нельзя использовать таким образом. Обратите внимание, что это означает, что добавление лишнего let x = 1
в Tomas lr
пример не скомпилируется, потому что Yield
будет вызываться с аргументом типа int
, а не unit
.
Там есть еще один трюк, который может помочь понять скомпилированную форму вычислений, которая должна (ab) использовать поддержку автококтации для вычислений в F # 3. Просто определите член do-nothing Quote
и сделайте Run
> просто верните свой аргумент:
member __.Quote() = ()
member __.Run(q) = q
Теперь ваше вычисляющее выражение будет оценивать цитату его десугатной формы. Это может быть очень удобно при отладке.
Ответ 2
Я должен признать, что я не совсем понимаю, как работают выражения вычислений, когда вы используете функции выражения запроса, такие как атрибут 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 }