Search code examples
dictionarygenericsf#operationsnumerics

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.


Solution

  • 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.