Stroniax
Stroniax

Reputation: 860

F# Async<_> to Async<obj>

I am working with F# to develop PowerShell tooling. I am currently running into a block because Async<_>is a generic type that is not derived from a non-generic type, so I can't request an Async<_> or Async as a parameter value - I have to specify the exact generic type parameter.

(For those unfamiliar with the interaction between these two languages, I can write a class in a .NET language such as F#, derive it from a class in the PowerShell library, and give it a specific attribute and when I run PowerShell and import my library, my class is exposed as a command. The command type can't be generic. Properties of the type are exposed as PowerShell parameters.)

As far as I'm aware I can't avoid this by having a generic member on a non-generic type, so ideally I'd have a transformation attribute (for non-PS users, transformation attributes effectively perform type conversion during runtime parameter binding) to turn Async<_> into Async<obj>. For the most part, this would work great for me. However, I can't figure out a way to check if a value is Async<_>, because the check computation :? Async<_> at compile time ends up as computation :? Async<obj>, which is not, unfortunately, the same, and returns false when passed Async<int>.

I ran into a similar issue in C# and was able to leverage the dynamic keyword after running a reflection test, and making the parameter be of the derived base type System.Threading.Tasks.Task, e.g.

const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHeirarchy;
var isTaskOf = task.GetType()
    .GetProperty("GetAwaiter", flags)
    .PropertyType
    .GetMethod("GetResult", flags)
    .ReturnType != typeof(void);
if (isTaskOf) {
  var result = await (dynamic)task;
}

I am willing to do something like this in F# if possible, but:

The solutions I have tried are:

/// Transform from Async<_> to Async<obj>
override _.Transform(_, item : obj) : obj =
  match item with
  // only matches Async<obj>. I get a compiler warning that _ is constrained to obj
  | :? Async<_> as computation ->
    let boxedComputation : Async<obj> = async { return! computation }
    boxedComputation
  // if the value is not an async computation, let it pass through. This will allow other transformation or type converters to try to convert the value
  | _ -> item

override _.Transform(_, item) =
  // no compiler warning about the type being constrained to obj, but the if test does not pass unless item is Async<obj>
  if (item :? Async<_>) then async { return! item :?> Async<_> }
  else item

The other thing I can think of is to use reflection entirely - get the async type, call all of the AsyncBuilder methods reflectively to create a computation expression, and then cast it to Async. As I'm fairly new to F# I'm not sure how well I'd be able to piece together a computation expression like that, and either way it seems a lot more complicated than it ought to be. I'm hoping there is some better way to identify the return type of an async computation and/or just box the result without caring what type it actually is.


EDIT After trying something ridiculously complicated using reflection with the AsyncBuilder type I realized I could leverage it a little more simply. Here is my current working solution, but I'm still looking out for any better options.


    static let boxAsyncReturnValue v = async { return v :> obj }
    static let bindFunctionReflected = typeof<FSharpAsyncObjTransformationAttribute>.GetMethod(
        nameof boxAsyncReturnValue,
        BindingFlags.NonPublic ||| BindingFlags.Static
        )

    override _.Transform(engineIntrinsics, item) =
        // I need to identify the current return type of the computation, and quit if "item" is not Async<_>
        if item = null then item else
        let itemType = item.GetType()
        if not itemType.IsGenericType then item else
        let genericItemType = itemType.GetGenericTypeDefinition()
        if genericItemType <> typedefof<Async<_>> then item else
        let returnType = itemType.GetGenericArguments()[0]
        if returnType = typeof<obj> then item else
        bindFunctionReflected.MakeGenericMethod(itemType).Invoke(null, [|item|])

Upvotes: 2

Views: 115

Answers (1)

Phillip Carter
Phillip Carter

Reputation: 5005

This is how I would do it:

let convert (a: Async<_>) =
    async {
        let! x = a
        return box x
    }

And at compile time it behaves as you'd expect:

let a = async { return "hello" }

let o: Async<obj> = convert a

let res = Async.RunSynchronously o

printfn "%s" res // Error: expected type 'string' but is type 'obj'
printfn "%s" (unbox<string> res) // compiles, prints the string

Upvotes: 2

Related Questions