Выражение вычисления FSharp: невозможно ссылаться на значение привязки в пользовательской операции

Я пытаюсь создать построитель с помощью FSharp Computation Expression, но получаю ошибку FS0039:

type UpdatebBuilder() =
    member this.Yield (x) = x
    member this.Return (x) = x
    member this.Bind (x, cont) = cont(x)
    member this.Quote (x) = x
    member this.For (x, a) = x

    [<CustomOperation("set", MaintainsVariableSpace =true,AllowIntoPattern=true)>] 
    member this.Set (x, a, b) = x

let update = UpdatebBuilder()

let testUpdate () =
    update {
        for x in [| 1; 2 ; 3|] do
        set x 123  // Compile Error FS0039: The value or constructor 'x' is not defined.
    }

Я хочу реализовать что-то вроде выражения запроса:

query {
    for x in collection do
    where x = 2 // Why no FS0039 error here?
    select x
}

Также попробовал MaintainsVariableSpaceUsingBind=true и получил ту же ошибку. Что мне сделать, чтобы он скомпилировался?


person Chen    schedule 28.12.2019    source источник


Ответы (1)


Мне кажется, что вы пытаетесь определить монаду State и реализовать операцию Set как пользовательскую операцию.

Признаюсь, я никогда полностью не разбирался в пользовательских операциях в F# (и я много использовал F#). ИМХО, похоже, что пользовательские операции имели одну цель; включить синтаксис, подобный LINQ, в F#. Со временем кажется, что лишь немногие разработчики C# используют синтаксис, подобный LINQ (т.е. from x where y select z), и лишь немногие разработчики F# используют вычислительное выражение query. У меня здесь нет данных, но я просто исходил из примера кода, который я вижу.

Это может объяснить, почему документация по пользовательским операциям часто бывает краткой и трудной для понимания. Что это вообще значит? MaintainsVariableSpaceUsingBind: Indicates if the custom operation maintains the variable space of the query or computation expression through the use of a bind operation.

В любом случае, чтобы узнать немного больше о пользовательских операциях, я попытался реализовать монаду состояния с пользовательской операцией для set, и я продвинулся немного дальше, но столкнулся с проблемой, которая, я думаю, является преднамеренным ограничением компилятора. Все еще думал, что делюсь им с надеждой, что это поможет OP продвинуться немного дальше.

Я выбрал это определение для State<_>:

type [<Struct>] State<'T> = S of (Map<string, obj> -> 'T*Map<string, obj>)

State<_> — это функция, которая при заданном глобальном состоянии (карте) создает значение (которое может быть получено из глобального состояния, но не обязательно) и потенциально обновляемое глобальное состояние.

return или value, как я обычно его называю, поскольку return — это ключевое слово F#, которое легко определить, поскольку мы просто возвращаем v и необновленное глобальное состояние:

let value v         = S <| fun m -> v, m

bind полезен для связывания нескольких вычислений состояния вместе. Сначала запустите t для глобального состояния и из возвращенного значения создайте второе вычисление и запустите через него обновленное глобальное состояние:

let bind  uf (S t)  = S <| fun m -> 
  let tv, tm  = t m
  let (S u)   = uf tv
  u tm

get и set используются для взаимодействия с глобальным состоянием:

let get k : State<'T option> = S <| fun m ->
  match m |> Map.tryFind k with
  | Some (:? 'T as v) -> Some v, m
  | _                 -> None, m

let set k v = S <| fun m ->
  let m = m |> Map.add k (box v)
  (), m

Я также создал некоторые другие методы, но в итоге конструктор был создан следующим образом:

type Builder() =
  class
    member x.Bind       (t, uf) = bind    uf t
    member x.Combine    (t, u)  = combine u  t
    member x.Delay      tf      = delay   tf
    member x.For        (s, tf) = forEach s  tf
    member x.Return     v       = value   v
    member x.ReturnFrom t       = t             : State<'T>
    member x.Yield      v       = value   v
    member x.Zero ()            = value   ()

    [<CustomOperation("set", MaintainsVariableSpaceUsingBind = true)>] 
    member x.Set (s, k, v)      = s |> combine (set k v)
  end

Я использовал MaintainsVariableSpaceUsingBind, потому что иначе он не видит v. MaintainsVariableSpace выдает странные ошибки, запрашивающие типы seq, которые, как я смутно подозреваю, являются оптимизацией для вычислений, основанных на seq. Проверка сгенерированного кода, по-видимому, делает правильную вещь, поскольку она связывает пользовательские операции вместе, используя мою функцию связывания в правильном порядке.

Теперь я готов определить вычисление state

state {
  // Works fine
  set "key" -1
  for v in 0..2 do
    // Won't work because: FS3086: A custom operation may not be used in conjunction with 'use', 'try/with', 'try/finally', 'if/then/else' or 'match' operators within this computation expression
    set "hello" v
  return! State.get "key"
}

К сожалению, компилятор не позволяет мне использовать пользовательские операции в условных операциях, таких как if, try, а также for (хотя его нет в списке, в каком-то смысле он условный). Похоже, это намеренное ограничение. Это можно обойти, но это не так

state {
  set "key" -1
  for v in 0..2 do
    // Meh
    do! state { set "key" v }
  return! State.get "key"
}

ИМХО, я предпочитаю использовать обычные do!/let! вместо пользовательских операций:

state {
  for v in 0..2 do
    do! State.set "key" v
  return! State.get "key"
}

Так что это не совсем правильный ответ на вопрос от OP, но, возможно, это поможет вам продвинуться немного дальше?

Полный исходный код:

type [<Struct>] State<'T> = S of (Map<string, obj> -> 'T*Map<string, obj>)

module State =
  let value v         = S <| fun m -> v, m

  let bind  uf (S t)  = S <| fun m -> 
    let tv, tm  = t m
    let (S u)   = uf tv
    u tm

  let combine u (S t) = S <| fun m -> 
    let _, tm   = t m
    let (S u)   = u
    u tm

  let delay tf  = S <| fun m -> 
    let (S t) = tf ()
    t m

  let forEach s tf  = S <| fun m -> 
    let mutable a = m
    for v in s do
      let (S t)   = tf v
      let (), tm  = t m
      a <- tm
    (), a

  let get k : State<'T option> = S <| fun m ->
    match m |> Map.tryFind k with
    | Some (:? 'T as v) -> Some v, m
    | _                 -> None, m

  let set k v = S <| fun m ->
    let m = m |> Map.add k (box v)
    (), m

  let run (S t) m = t m

  type Builder() =
    class
      member x.Bind       (t, uf) = bind    uf t
      member x.Combine    (t, u)  = combine u  t
      member x.Delay      tf      = delay   tf
      member x.For        (s, tf) = forEach s  tf
      member x.Return     v       = value   v
      member x.ReturnFrom t       = t             : State<'T>
      member x.Yield      v       = value   v
      member x.Zero ()            = value   ()

      [<CustomOperation("set", MaintainsVariableSpaceUsingBind = true)>] 
      member x.Set (s, k, v)      = s |> combine (set k v)
    end
let state = State.Builder ()

let testUpdate () =
  state {
    // Works fine
    set "key" -1
    for v in 0..2 do
      // Won't work because: FS3086: A custom operation may not be used in conjunction with 'use', 'try/with', 'try/finally', 'if/then/else' or 'match' operators within this computation expression
      // set "hello" v
      // Workaround but kind of meh
      // do! state { set "key" v }
      // Better IMHO
      do! State.set "key" v
    return! State.get "key"
  }

[<EntryPoint>]
let main argv =
  let tv, tm = State.run (testUpdate ()) Map.empty
  printfn "v:%A" tv
  printfn "m:%A" tm
  0
person Just another metaprogrammer    schedule 28.12.2019
comment
Хорошо, ключевым моментом является атрибут [<ProjectionParameter>]. Я должен добавить этот атрибут ко второму параметру метода set, чтобы он скомпилировался. Ваш ответ тоже очень помогает, спасибо! - person Chen; 28.12.2019