Расширенные выражения вычислений без 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 }
Другие вопросы по тегам