简体   繁体   中英

Issue with a generic dictionary of operations

I have a dictionary of operations:

type INumerics<'T> =
  abstract Zer : 'T
  abstract Add : 'T * 'T -> 'T
  abstract Sub : 'T * 'T -> 'T
  abstract Mul : 'T * 'T -> 'T
  abstract Div : 'T * 'T -> 'T
  abstract Neq : 'T * 'T -> bool

With helper functions:

let inline add (x : 'T) (y : 'T) : 'T   = (+)  x y
let inline sub (x : 'T) (y : 'T) : 'T   = (-)  x y
let inline mul (x : 'T) (y : 'T) : 'T   = (*)  x y
let inline div (x : 'T) (y : 'T) : 'T   = (/)  x y
let inline neq (x : 'T) (y : 'T) : bool = (<>) x y 

Then we have a simple calculator using a MailboxProcessor agent:

type Agent<'T> = MailboxProcessor<'T>

type CalculatorMsg<'T> =
    | Add of 'T * 'T * AsyncReplyChannel<'T>
    | Sub of 'T * 'T * AsyncReplyChannel<'T> 
    | Mul of 'T * 'T * AsyncReplyChannel<'T>  
    | Div of 'T * 'T * AsyncReplyChannel<'T>

type CalculatorAgent< ^T when ^T : (static member get_Zero : unit -> ^T) 
                         and  ^T : (static member Zero : ^T) 
                         and  ^T : (static member (+)  : ^T * ^T -> ^T)
                         and  ^T : (static member (-)  : ^T * ^T -> ^T)
                         and  ^T : (static member (*)  : ^T * ^T -> ^T)
                         and  ^T : (static member (/)  : ^T * ^T -> ^T)
                         and  ^T : equality >() =
    let agent =
        let ops = 
            { new INumerics<'T> with 
                member ops.Zer       = LanguagePrimitives.GenericZero<'T> 
                member ops.Add(x, y) = (x, y) ||> add  
                member ops.Sub(x, y) = (x, y) ||> sub
                member ops.Mul(x, y) = (x, y) ||> mul   
                member ops.Div(x, y) = (x, y) ||> div   
                member ops.Neq(x, y) = (x, y) ||> neq }

        Agent<CalculatorMsg<'T>>.Start(fun inbox ->
            let rec loop () =
                async {
                    let! msg = inbox.TryReceive()
                    if msg.IsSome then
                        match msg.Value with 
                        | Add (x, y, rep) ->
                            printfn "Adding %A and %A ..." x y
                            let res = ops.Add(x, y)
                            res |> rep.Reply  
                            return! loop()
                        | Sub (x, y, rep) -> 
                            printfn "Subtracting %A from %A ..." y x
                            let res = ops.Sub(x, y) 
                            res |> rep.Reply  
                            return! loop()
                        | Mul (x, y, rep) ->
                            printfn "Multiplying %A by %A ... " y x
                            let res = ops.Mul(x, y)
                            res |> rep.Reply  
                            return! loop()
                        | Div (x, y, rep) ->
                            printfn "Dividing %A by %A ..." x y
                            if ops.Neq(y, ops.Zer) then 
                                let res = ops.Div(x, y)
                                res |> rep.Reply  
                            else
                                printfn "#DIV/0" 
                            return! loop()
                    else 
                        return! loop()
                }
            loop()
        )

    // timeout = infinit => t = -1
    let t = 1000

    member inline this.Add(x, y) =
        agent.PostAndTryAsyncReply((fun rep -> Add (x, y, rep)), t)
        |> Async.RunSynchronously
    member inline this.Subtract(x, y) =
        agent.PostAndTryAsyncReply((fun rep -> Sub (x, y, rep)), t)
        |> Async.RunSynchronously
    member inline this.Multiply(x, y) =
        agent.PostAndTryAsyncReply((fun rep -> Mul (x, y, rep)), t)
        |> Async.RunSynchronously
    member inline this.Divide(x, y) =
        agent.PostAndTryAsyncReply((fun rep -> Div (x, y, rep)), t)
        |> Async.RunSynchronously

As a use example, we have:

let calculatorAgentI = new CalculatorAgent<int>()

(2, 1) |> calculatorAgentI.Add 
(2, 1) |> calculatorAgentI.Subtract
(2, 1) |> calculatorAgentI.Multiply
(2, 1) |> calculatorAgentI.Divide
(2, 0) |> calculatorAgentI.Divide

The issue is that Add and Multiply and the Last Divide work ok:

> 
Adding 2 and 1 ...
val it : int option = Some 3

> 
Multiplying 1 by 2 ... 
val it : int option = Some 2

> 
Dividing 2 by 0 ...
#DIV/0
val it : int option = None

As soon as we use Subtract and first Divide which would return int option = None , I get into trouble and the following is the only output I would get from any of the operations:

> 
val it : int option = None

As long and hard as I think about it, I cannot figure out if there is a problem in the "subtract"/"divide" part or the "receive"/"reply" operations.

Running this code with the Debugger attached, you'll see that you get a System.NotSupportedException when trying to run the sub function from inside the agent:

System.NotSupportedException occurred
  HResult=0x80131515
  Message=Specified method is not supported.
  Source=FSI-ASSEMBLY
  StackTrace:
  at FSI_0002.ops@60.FSI_0002-INumerics`1-Sub(T X1, T X2) 

The reason you get val it : int option = None is because you have specified a 1-second timeout, and you are hitting that after the exception is thrown.

Calling the sub function directly works fine, but calling it through the CalculatorAgent class does not. This is because the type parameters are defined on the class in this case, and there are limitations on the use of structural type constraints on classes in F#. I would suggest reading up on Statically Resolved Type Parameters and their limitations.

The behaviour seems to be an irregularity or a bug. I was also expecting that this would behave the same for all operations.

In any case, you can workaround this by capturing ops in a static inline member (rather than using statically resolved type parameters on a type). The following works fine for me:

type CalculatorAgent<'T>(ops:INumerics<'T>) = 
  let agent = 
    Agent<CalculatorMsg<'T>>.Start(fun inbox ->
      let rec loop () = async {
        let! msg = inbox.TryReceive()
        match msg with 
        | Some(Add (x, y, rep)) ->
            printfn "Adding %A and %A ..." x y
            let res = ops.Add(x, y)
            res |> rep.Reply  
            return! loop()
        | Some(Sub (x, y, rep)) -> 
            printfn "Subtracting %A from %A ..." y x
            let res = ops.Sub(x, y) 
            res |> rep.Reply  
            return! loop()
        | Some(Mul (x, y, rep)) ->
            printfn "Multiplying %A by %A ... " y x
            let res = ops.Mul(x, y)
            res |> rep.Reply  
            return! loop()
        | Some(Div (x, y, rep)) ->
            printfn "Dividing %A by %A ..." x y
            if ops.Neq(y, ops.Zer) then 
                let res = ops.Div(x, y)
                res |> rep.Reply  
            else
                printfn "#DIV/0" 
            return! loop()
        | _ ->
            return! loop() }
      loop() )

  // timeout = infinit => t = -1
  let t = 1000

  member this.Add(x, y) =
    agent.PostAndTryAsyncReply((fun rep -> Add (x, y, rep)), t)
    |> Async.RunSynchronously
  member this.Subtract(x, y) =
    agent.PostAndTryAsyncReply((fun rep -> Sub (x, y, rep)), t)
    |> Async.RunSynchronously
  member this.Multiply(x, y) =
    agent.PostAndTryAsyncReply((fun rep -> Mul (x, y, rep)), t)
    |> Async.RunSynchronously
  member this.Divide(x, y) =
    agent.PostAndTryAsyncReply((fun rep -> Div (x, y, rep)), t)
    |> Async.RunSynchronously

type CalculatorAgent = 
  static member inline Create() = 
    let ops = 
      { new INumerics<_> with 
          member ops.Zer = LanguagePrimitives.GenericZero<_> 
          member ops.Add(x, y) = x + y
          member ops.Sub(x, y) = x - y
          member ops.Mul(x, y) = x * y
          member ops.Div(x, y) = x / y
          member ops.Neq(x, y) = x <> y }
    CalculatorAgent<_>(ops)

let calculatorAgentI = CalculatorAgent.Create<int>()

(2, 1) |> calculatorAgentI.Add 
(2, 1) |> calculatorAgentI.Subtract
(2, 1) |> calculatorAgentI.Multiply
(2, 1) |> calculatorAgentI.Divide
(2, 0) |> calculatorAgentI.Divide

That said, I think the cases where you really need generic numerical code are pretty rare - so I suspect it might be better to avoid introducing all this complexity altogether and just write the code for a specific numerical type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM