Asik
Asik

Reputation: 22133

Type inference of generic function parameter

Let's say I want to extend xUnit's Assert.Throws to support F# async in this way:

Assert.AsyncThrows<InvalidOperationException>(fun () -> async { return "" })

The implentation is as such:

module Xunit.Assert

let AsyncThrows<'TException when 'TException :> exn> asyncFunc = async {

    let mutable actualException = None
    try
        let! r = asyncFunc()
        return ()
    with 
    | :? 'TException as e -> actualException <- Some e
    | _ -> ()      

    return Assert.Throws(
            (fun () -> 
                match actualException with
                | Some ex -> raise ex
                | None -> ()))        
}

The type of asyncFunc is inferred to be unit -> Async<obj>. This is needlessly restrictive on callers; it should be unit -> Async<'a>. I have tried the following:

let AsyncThrows<'TException when 'TException :> exn> (asyncTask:unit->Async<'a>)

This doesn't work and still compiles as Async<obj> with a cryptic warning ("...causes the code to be less generic than indicated...").

let AsyncThrows<'TException, 'a when 'TException :> exn> (asyncTask:unit->Async<'a>)

This works, but forces callers to explicitely supply the return type of the async function, e.g.

Assert.AsyncThrows<InvalidOperationException, string>(fun () -> async { return "" } )

Is there a way to only have to supply the type of the exception but not of the async function?

(Note: my actual use case doesn't use async but a another similar computation expression; I used async for illustration purposes).

Upvotes: 2

Views: 97

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80734

The least complicated option is to supply the "please dear compiler figure this out for me" sign (aka the underscore) for the second generic argument:

AsyncThrows<InvalidOperationException, _>( fun() -> async { return "" } )

Another option would be to supply type parameters in stages by returning an interface. This way, the second argument may be inferred:

type IAsyncAssert<'e when 'e :> exn> =
  abstract member When<'a> : (unit -> Async<'a>) -> unit


let AsyncThrows<'e when 'e :> exn> () = 
    { new IAsyncAssert<'e> with
        override x.When<'a> (fn: unit -> Async<'a>) = 
          // Implementation goes here
    }

// Usage:
AsyncThrows<NotImplementedException>().When( fun() -> async { return "" } )

Yet another (more functional) option would be to provide a "dummy object" of the right type just to infer the generic argument:

type ExnType<'e when 'e :> exn> = | ExnType

let exnType<'e when 'e :> exn> : ExnType<'e> = ExnType 

let AsyncThrows<'e, 'a when 'e :> exn> (_ :ExnType<'e>) (fn: unit -> Async<'a>) = 
   // Implementation here

// Usage:
AsyncThrows exnType<NotImplementedException> ( fun() -> async { return "" } )

Also, here's a hint: F# async values, unlike C# Tasks, do not get evaluated immediately, but only when used as part of another async or directly with Async.Start et al. Therefore, you can do without a lambda expression:

...
let AsyncThrows<'e, 'a when 'e :> exn> (_ :ExnType<'e>) (a: Async<'a>) = 
   // Implementation here
   let! r = a
   ...

// Usage:
AsyncThrows exnType<NotImplementedException> (async { return "" })

Upvotes: 4

Related Questions