سؤال

(Having failed to 'grok' FParsec, I followed the advice I read somewhere and started trying to write a little parser myself. Somehow I spotted what looked like a chance to try and monadify it, and now I have N problems...)

This is my 'Result' type (simplified)

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

Here's the computation expression builder

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

In this first example, everything works (compiles) as expected:

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) }

My problem is here: I'd like to be able to catch any failure in the 'combining' function and return a failure, but it says that I should define a 'Zero'.

    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) }

Having no idea what I should return in a Zero, I just threw in member m.Zero() = Failure("hello world"), and it now says I need TryWith.

So:

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

And now it wants Delay, so member m.Delay f = (fun () -> f()).

At which point it says (on the ex -> Failure), This expression should have type 'unit', but has type 'Result<'a>', and I throw up my arms and turn to you guys...

Link for playing: http://dotnetfiddle.net/Ho1sGS

هل كانت مفيدة؟

المحلول

The with block should also return a result from the computation expression. Since you want to return Result.Failure you need to define the member m.ReturnFrom a = a and use it to return the Failure from the with block. In the try block you should also specify that fn returns Success if it doesn't throw.

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)
                         }

Update:

The original implementation was showing a warning, not an error. The expression in the with block was not used since you returned from the try block, so you could simply add |> ignore. In that case if fn throws then the return value is m.Zero() and the only difference is that you would get "hello world" instead of ex.Message. Illustrated with an example below. Full script here: http://dotnetfiddle.net/mFbeZg

Original implementation with |> ignore to mute the warning:

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
                         }

Run it:

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

Result:

val test2 : Result<int> = Failure "Attempted to divide by zero."
val test3 : Result<int> = Failure "hello world"

نصائح أخرى

If you want to support try ... with inside a computation builder, you need to add TryWith (as you tried) as well as a few other members including Delay and Run (depending on how you want to implement Delay). In order to be able to return failure, you also need to support return! by adding 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

Now you can do the following:

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) }

The trick is that the normal branch uses just return (representing success), but the exception handler uses return! to return an explicitly created result using Failure.

That said, if you are interested in parsers, then you need to use a different type - what you are describing here is more like the option (or Maybe) monad. To implement parser combinators you need a type that represents a parser rather than result of a parser. See for example this article.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top