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?
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|]