Преодоление TryWith в вычислительных выражениях

(Не сумев «нащупать» FParsec, я последовал совету, который где-то читал, и сам начал пытаться написать небольшой синтаксический анализатор. Каким-то образом я заметил то, что выглядело как шанс попытаться монадифицировать его, и теперь у меня N проблем. ..)

Это мой тип "Результат" (упрощенный)

type Result<'a> = 
    | Success of 'a
    | Failure of string

Вот построитель вычислительных выражений

type ResultBuilder() =
    member m.Return a = Success(a)
    member m.Bind(r,fn) =
        match r with
        | Success(a) -> fn a
        | Failure(m) -> Failure(m)

В этом первом примере все работает (компилируется), как ожидалось:

module Parser = 
    let res = ResultBuilder()

    let Combine p1 p2 fn = 
        fun a -> res { let! x = p1 a
                       let! y = p2 a
                       return fn(x,y) }

Моя проблема здесь: я хотел бы иметь возможность уловить любой сбой в функции «комбинирования» и вернуть сбой, но в нем говорится, что я должен определить «ноль».

    let Combine2 p1 p2 fn =
        fun a -> res { let! x = p1 a
                       let! y = p2 a
                       try
                          return fn(x,y) 
                       with
                         | ex -> Failure(ex.Message) }

Не имея представления, что мне вернуть в Zero, я просто бросил member m.Zero() = Failure("hello world"), и теперь он говорит, что мне нужно TryWith.

So:

member m.TryWith(r,fn) =
    try 
        r()
    with
     | ex -> fn ex

А теперь ему нужна задержка, так что member m.Delay f = (fun () -> f()).

Тут написано (на ex -> Failure) This expression should have type 'unit', but has type 'Result<'a>', и я вскидываю руки и поворачиваюсь к вам, ребята ...

Ссылка для игры: http://dotnetfiddle.net/Ho1sGS


person Benjol    schedule 15.02.2014    source источник


Ответы (2)


Блок with также должен возвращать результат из выражения вычисления. Поскольку вы хотите вернуть Result.Failure, вам необходимо определить член m.ReturnFrom a = a и использовать его для возврата Failure из блока with. В блоке try вы также должны указать, что fn возвращает Success, если он не срабатывает.

let Combine2 p1 p2 fn =
            fun a -> res { let! x = p1 a
                           let! y = p2 a
                           return! 
                                try
                                    Success(fn(x,y))
                                with
                                    | ex -> Failure(ex.Message)
                         }

Обновление:

Исходная реализация показывала предупреждение, а не ошибку. Выражение в блоке with не использовалось, так как вы вернулись из блока try, поэтому вы можете просто добавить |> ignore. В этом случае, если fn выдает, то возвращаемое значение будет m.Zero(), и единственная разница в том, что вы получите "hello world" вместо ex.Message. Проиллюстрировано на примере ниже. Полный сценарий здесь: http://dotnetfiddle.net/mFbeZg

Исходная реализация с |> ignore для отключения предупреждения:

let Combine3 p1 p2 fn =
            fun a -> res { let! x = p1 a
                           let! y = p2 a

                           try
                                return fn(x,y)
                           with
                                | ex -> Failure(ex.Message) |> ignore // no warning
                         }

Запустить его:

let comb2 a  =
    let p1' x = Success(x)
    let p2' y = Success(y)
    let fn' (x,y) = 1/0 // div by zero
    let func = Parser.Combine2 p1' p2' fn' a
    func()

let comb3 a  =
    let p1' x = Success(x)
    let p2' y = Success(y)
    let fn' (x,y) = 1/0 // div by zero
    let func = Parser.Combine3 p1' p2' fn' a
    func()

let test2 = comb2 1
let test3 = comb3 1

Результат:

val test2 : Result<int> = Failure "Attempted to divide by zero."
val test3 : Result<int> = Failure "hello world"
person V.B.    schedule 15.02.2014
comment
Спасибо, мне не хватало именно ReturnFrom! (Думаю, тогда мне нужно решить, возвращает ли fn 'a или _2 _...) - person Benjol; 16.02.2014
comment
Спасибо, что приняли это! Пожалуйста, посмотрите последнюю правку, где return! перед try/with, а не два раза внутри. Если fn возвращает Result<'a>, тогда вы можете сделать всю логику исключения внутри него и не переносить ее в Success(), но вам все равно понадобится метод ReturnFrom для возврата результата измененного fn - person V.B.; 16.02.2014

Если вы хотите поддерживать try ... with внутри построителя вычислений, вам нужно добавить TryWith (как вы пытались), а также несколько других членов, включая Delay и Run (в зависимости от того, как вы хотите реализовать Delay). Чтобы иметь возможность возвращать ошибку, вам также необходимо поддержать return!, добавив ReturnFrom:

type ResultBuilder() =
    member m.Return a = Success(a)
    member m.Bind(r,fn) =
        match r with
        | Success(a) -> fn a
        | Failure(m) -> Failure(m)
    member m.TryWith(r,fn) =
      try r() with ex -> fn ex
    member m.Delay(f) = f
    member m.Run(f) = f()
    member m.ReturnFrom(r) = r

Теперь вы можете сделать следующее:

let Combine2 p1 p2 fn = fun a -> res {  
  let! x = p1 a
  let! y = p2 a
  try
    return fn(x,y) 
  with ex ->
    return! Failure(ex.Message) }

Уловка заключается в том, что обычная ветвь использует только return (представляя успех), а обработчик исключений использует return! для возврата явно созданного результата с использованием Failure.

Тем не менее, если вас интересуют парсеры, вам нужно использовать другой тип - то, что вы здесь описываете, больше похоже на монаду option (или Maybe). Для реализации комбинаторов синтаксического анализатора вам понадобится тип, представляющий синтаксический анализатор, а не результат синтаксического анализатора. См., Например, эту статью.

person Tomas Petricek    schedule 15.02.2014
comment
Спасибо за это. Я думаю, что мой мозг еще не совсем приспособлен к монадическим синтаксическим анализаторам. И монадический результат у меня сейчас работает нормально. Может быть позже. - person Benjol; 16.02.2014