Search code examples
arraysf#downcast

F# Downcasting arrays without reflection


I am working on a program where the user can send me all sort of objects at runtime, and I do not know their type in advance (at compile time). When the object can be down-cast to an (F#) array, of any element type, I would like to perform some usual operations on the underlying array. E.g. Array.Length, Array.sub...

The objects I can get from the users will be of things like box [| 1; 2; 3 |] or box [| "a"; "b"; "c" |], or any 'a[], but I do not know 'a at compile time.

The following does not work :

let arrCount (oarr: obj) : int =
    match oarr with
    | :? array<_> as a -> a.Length
    | :? (obj[]) as a -> a.Length
 // | :? (int[]) as a -> a.Length     // not an option for me here
 // | :? (string[]) as a -> a.Length  // not an option for me here
 // | ... 
    | _ -> failwith "Cannot recognize an array"

E.g both arrCount (box [| 1; 2; 3 |]) and arrCount (box [| "a"; "b"; "c" |]) fail here.

The only solution I found so far is to use reflection, e.g. :

type ArrayOps =
     static member count<'a> (arr: 'a[]) : int = arr.Length
     static member sub<'a> (arr: 'a[]) start len : 'a[] = Array.sub arr start len
     // ...

let tryCount (oarr: obj) =
    let ty = oarr.GetType()
    if ty.HasElementType  && ty.BaseType = typeof<System.Array> then
        let ety = ty.GetElementType()
        let meth = typeof<ArrayOps>.GetMethod("count").MakeGenericMethod([| ety |])
        let count = meth.Invoke(null, [| oarr |]) :?> int
        Some count
    else 
        None

My question: is there a way to use functions such as Array.count, Array.sub, etc... on arguments of the form box [| some elements of some unknown type |] without using reflection?


Solution

  • Since F# is statically type-safe it tries to prevent you from doing this, which is why it isn't trivial. Casting it to an array<_> will not work because from an F# standpoint, array<obj> is not equal to array<int> etc, meaning you would have to check for every covariant type.

    However, you can exploit the fact that an array is also a System.Array, and use the BCL methods on it. This doesn't give you Array.length etc, because they need type-safety, but you can essentially do any operation with a little bit of work.

    If you do have a limited set of known types that the obj can be when it is an array, I suggest you create a DU with these known types and create a simple converter that matches on array<int>, array<string> etc, so that you get your type-safety back.

    Without any type safety you can so something like this:

    let arrCount (x: obj) =
        match x with
        | null -> nullArg "x cannot be null"
        | :? System.Array as arr -> arr.GetLength(0)  // must give the rank
        | _ -> -1  // or failwith
    

    Usage:

    > arrCount (box [|1;2;3|]);;
    val it : int = 3
    
    > arrCount (box [|"one"|]);;
    val it : int = 1
    

    This other answer on SO has a good way of explaining why allowing such casts makes the .NET type system unsound, and why it isn't allowed in F#: https://stackoverflow.com/a/7917466/111575


    EDIT: 2nd alternative

    If you don't mind boxing your entire array, you can expand on the above solution by converting the whole array, once you know it is an array. However, the first approach (with System.Array) has O(1) performance, while this approach is, necessarily, O(n):

    open System.Collections
    
    let makeBoxedArray (x: obj) =
        match x with
        | null -> nullArg "x cannot be null"
        | :? System.Array as arr -> 
            arr :> IEnumerable 
            |> Seq.cast<obj>
            |> Seq.toArray
    
        | _ -> failwith "Not an array"
    

    Usage:

    > makeBoxedArray (box [|1;2;3|]);;  // it accepts untyped arrays
    val it : obj [] = [|1; 2; 3|]
    
    > makeBoxedArray [|"one"|];;  // or typed arrays
    val it : obj [] = [|"one"|]
    
    > makeBoxedArray [|"one"|] |> Array.length;;  // and you can do array-ish operations
    val it : int = 1
    
    > makeBoxedArray (box [|1;2;3;4;5;6|]) |> (fun a -> Array.sub a 3 2);;
    val it : obj [] = [|4; 5|]