Подтвердить что ты не робот

Расширенные выражения вычислений без...

То, что я подразумеваю под расширенными выражениями вычислений, - это выражения вычислений с пользовательскими ключевыми словами, определяемыми атрибутом 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!.
4b9b3361

Ответ 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 }