1

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?

Janthelme
  • 989
  • 10
  • 23

2 Answers2

1

Curiously, this is a non-issue in languages like C# and VB, while you may have to do some extra work for F#. You can't do this in general, because F# doesn't have co-variant types.

Vote for that feature here!.

But we do have flexible types which give us rather limited contra-variance, which we can work with.

let anyLength (arr : #obj[]) = 
    arr |> Array.length

And,

let iarr = [| 1; 2; 3|]
let sarr = [|"a"; "b" |]

anyLength iarr // 3
anyLength sarr // 2
Asti
  • 12,447
  • 29
  • 38
  • Thanks Asti. What I am after is an `anyLength` function which takes objects such as `o1 = box [| 1; 2; 3 |]` or `o2 = box [| "a"; "b"; "c" |]` as inputs and return the length of the underlying array. In the case of your `anyLength` function `anyLength (unbox o1)` or `anyLength (unbox o2)` do not work (with an error msg such as `Unable to cast object of type 'System.Int32[]' to type 'System.Object[]'.`). – Janthelme Jun 01 '20 at 08:44
1

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|]
Abel
  • 56,041
  • 24
  • 146
  • 247
  • Thanks. The performance comments are helpful. Would you know how recursion compare against your two approaches, performance wise? – Janthelme Jun 01 '20 at 11:44
  • @Janthelme, recursion in what sense? The suggested solutions do not use recursion. If your worries are with repeatedly applying such casts on an entire array, best thing to do is to cast once and then operate on the result, as opposed to casting many times in a loop or something. if performance is a worry, and you're stuck with untyped data, optimize for the common case by first trying to cast to `array`, `array`, `array` (if these are common). Capture that in a DU for ease of further processing. – Abel Jun 01 '20 at 12:15
  • Apologies, it was a typo that I could not edit when I found out as it was too late. I meant *reflection* (not recursion). Since the solution I originally had was using reflection, I was wondering how reflection would compare, performance wise, with your 2 approaches. – Janthelme Jun 01 '20 at 12:18
  • 1
    @Janthelme, the first approach has negligible performance impact, as it is a normal type-test + up-cast, nothing more. The 2nd approach, it depends. On very large arrays, it is possible that reflection is faster, but you should time it to be sure (use BenchMarkDotNet). The third approach (cover the type-unsafety with partial type-safety in a DU, convert with type-test + up-cast) will be native performance for the common case. If performance is of any concern, don't use object-to-untyped-array conversions, but change the data-type, i.e. by adding a tag for the contained type, plus dynamic cast. – Abel Jun 01 '20 at 12:48