Есть ли способ создать другую реализацию do! и let! в вычислительном выражении?

#f# #computation-expression

#f# #вычисление-выражение

Вопрос:

Мне нужно другое поведение для do! и let! в моем пользовательском вычислительном выражении.

Я пытаюсь достичь этого следующим образом:

 type FooBuilder() = class
    member b.Bind<'T, 'U>(x:'T, f:unit->'U):'U = failwith "not implemented" //do! implementation
    member b.Bind<'T, 'U>(x:'T, f:'T->'U):'U = failwith "not implemented" //let! implementation
    member b.Return<'T>(x:'T):'T = failwith "not implemented" //return implementation
end

let foo = FooBuilder()
let x = foo {
    do! ()
    return 2
}
  

Но компилятор выдает мне ошибку:

Не удалось определить уникальную перегрузку для метода ‘Bind’ на основе информации о типе до этого программного пункта. Доступные перегрузки показаны ниже (или в окне списка ошибок). Может потребоваться аннотация типа.

Есть ли способ создать другую реализацию do! и let!?

Ответ №1:

Если вы хотите сохранить Bind операцию, используемую в let! generic, то нет способа сказать, что F # должен использовать другую реализацию при переводе do! (перегрузки обязательно должны будут перекрываться).

В общем, если вы хотите получить другое поведение для let! и для do! , то это наводит на мысль, что ваше вычислительное выражение, вероятно, определено неправильно. Концепция довольно гибкая, и ее можно использовать не только для объявления монад, но вы, возможно, слишком сильно ее растягиваете. Если вы можете написать больше информации о том, чего вы хотите достичь, это было бы полезно. В любом случае, вот несколько возможных обходных путей…

Вы можете добавить некоторое дополнительное обертывание и написать что-то вроде do! wrap <| expr .

 type Wrapped<'T> = W of 'T
type WrappedDo<'T> = WD of 'T

type FooBuilder() = 
  member b.Bind<'T, 'U>(x:Wrapped<'T>, f:'T->'U):'U = failwith "let!" 
  member b.Bind<'T, 'U>(x:WrappedDo<unit>, f:unit->'U):'U = failwith "do!"
  member b.Return<'T>(x:'T):Wrapped<'T> = failwith "return"

let wrap (W a) = WD a
let bar arg = W arg

let foo = FooBuilder()

// Thanks to the added `wrap` call, this will use the second overload
foo { do! wrap <| bar()
      return 1 }

// But if you forget to add `wrap` then you still get the usual `let!` implementation
foo { do! wrap <| bar()
      return 1 }
  

Другой альтернативой было бы использовать тесты динамического типа. Это немного неэффективно (и немного неэлегантно), но может сработать, в зависимости от вашего сценария:

 member b.Bind<'T, 'U>(x:Wrapped<'T>, f:'T->'U):'U = 
  if typeof<'T> = typeof<unit> then 
    failwith "do!" 
  else 
    failwith "let!" 
  

Однако при этом все равно будет использоваться do! перегрузка при записи let! () = bar .

Ответ №2:

Вы могли бы попробовать что-то другое, немного уродливое, но должно сработать:

 let bindU (x, f) = f x // you must use x, or it'll make the Bind method less generic.
let bindG (x, f) = f x
member b.Bind(x : 'a, f : 'a -> 'b) =
    match box x with
    | :? unit -> bindU (x, f)
    | _ -> bindG (x, f)
  

Он выделяет a (преобразует его в obj ) и проверяет, имеет ли он тип unit , затем перенаправляет на правильную перегрузку.

Комментарии:

1. Возможно, вы также захотите взглянуть на ramon.org.il/wp/2011/04 /… . Это дает вам немного больше свободы при использовании ключевых слов привязки.