2

I'm trying to write a function to return a dynamically typed array. Here is an example of one of the ways I've tried:

let rng = System.Random()

type ConvertType =
    | AsInts
    | AsFloat32s
    | AsFloats
    | AsInt64s

type InputType =
    | Ints of int[]
    | Float32s of float32[]
    | Floats of float[]
    | Int64s of int64[]

let genData : int -> int -> ConvertType -> InputType * int[] =
    fun (sCount:int) (rCount:int) (ct:ConvertType) ->
        let source = 
            match ct with
            | AsInts -> Array.init sCount (fun _ -> rng.Next()) |> Array.map (fun e -> int e) |> Ints
            | AsFloat32s -> Array.init sCount (fun _ -> rng.Next()) |> Array.map (fun e -> float32 e) |> Float32s
            | AsFloats -> Array.init sCount (fun _ -> rng.Next()) |> Array.map (fun e -> float e) |> Floats
            | AsInt64s -> Array.init sCount (fun _ -> rng.Next()) |> Array.map (fun e -> int64 e) |> Int64s
        let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
        source, indices

The problem I have is that on down when I use the function, I need the array to be the primitive type e.g. float32[] and not "InputType."

I have also tried doing it via an interface created by an inline function and using generics. I couldn't get that to work how I wanted either but I could have just been doing it wrong.

Edit: Thanks for the great reply(s), I'll have to try that out today. I'm adding the edit because I solved my problem, although I didn't solve it how I wanted (i.e. like the answer). So an FYI for those who may look at this, I did the following:

let counts = [100; 1000; 10000]
let itCounts = [ 1000; 500; 200]

let helperFunct =
    fun (count:int) (numIt:int) (genData : int -> int -> ('T[] * int[] )) ->
        let c2 = int( count / 2 )
        let source, indices = genData count c2
        ....

[<Test>]
let ``int test case`` () =
    let genData sCount rCount =
        let source = Array.init sCount (fun _ -> rng.Next())
        let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
        source, indices

    (counts, itCounts) ||> List.Iter2 (fun s i -> helperFunct s i genData)
    .....

Then each proceeding test case would be something like:

[<Test>]
let ``float test case`` () =
    let genData sCount rCount =
        let source = Array.init sCount (fun _ -> rng.Next()) |> Array.map (fun e -> float e)
        let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
        source, indices
    .....

But, the whole reason I asked the question was that I was trying to avoid rewriting that genData function for every test case. In my real code, this temporary solution kept me from having to break up some stuff in the "helperFunct."

1 Answers1

3

I think that your current design is actually quite good. By listing the supported types explicitly, you can be sure that people will not try to call the function to generate data of type that does not make sense (say byte).

You can write a generic function using System.Convert (which lets you convert value to an arbitrary type, but may fail if this does not make sense). It is also (very likely) going to be less efficient, but I have not measured that:

let genDataGeneric<'T> sCount rCount : 'T[] * int[] =
  let genValue() = System.Convert.ChangeType(rng.Next(), typeof<'T>) |> unbox<'T>
  let source = Array.init sCount (fun _ -> genValue())
  let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
  source, indices

You can then write

genDataGeneric<float> 10 10

but people can also write genDataGeneric<bool> and the code will either crash or produce nonsense, so that's why I think that your original approach had its benefits too.

Alternatively, you could parameterize the function to take a converter that turns int (which is what you get from rng.Next) to the type you want:

let inline genDataGeneric convertor sCount rCount : 'T[] * int[] =
  let genValue() = rng.Next() |> convertor
  let source = Array.init sCount (fun _ -> genValue())
  let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
  source, indices

Then you can write just getDataGeneric float 10 10 which is still quite elegant and it will be efficient too (I added inline because I think it can help here)

EDIT: Based on the Leaf's comment, I tried using the overloaded operator trick and it turns out you can also do this (this blog has the best explanation of what the hell is going on in the following weird snippet!):

// Specifies conversions that we want to allow
type Overloads = Overloads with
    static member ($) (Overloads, fake:float) = fun (n:int) -> float n
    static member ($) (Overloads, fake:int64) = fun (n:int) -> int64 n

let inline genDataGeneric sCount rCount : 'T[] * int[] =
  let convert = (Overloads $ Unchecked.defaultof<'T>)
  let genValue() = rng.Next() |> convert
  let source = Array.init sCount (fun _ -> genValue())
  let indices = Array.init rCount (fun _ -> rng.Next sCount) |> Array.sort
  source, indices

let (r : float[] * _) = genDataGeneric 10 10  // This compiles & works
let (r : byte[] * _) = genDataGeneric 10 10   // This does not compile

This is interesting, but note that you still have to specify the type somewhere (either by usage later or using a type annotation). If you're using type annotation, then the earlier code is probably simpler and gives less confusing error messages. But the trick used here is certainly interesting.

Community
  • 1
  • 1
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • I had been playing with generic constraints around `op_Explicit` to see if this was possible (I dont' think it is) and hadn't considered passing in the converter. Nice. – Leaf Garland Jun 08 '13 at 20:41
  • @LeafGarland The idea of using `op_Explicit` sounds interesting. Perhaps it would be possible using this trick: http://stackoverflow.com/questions/12971965/overloaded-inline-operators-in-f – Tomas Petricek Jun 08 '13 at 21:03
  • @LeafGarland Wow, thanks for the suggestion. It can actually be done in one more interesting way :-) – Tomas Petricek Jun 08 '13 at 21:11
  • Fascinating. Thanks for the link. – Leaf Garland Jun 08 '13 at 21:25
  • @LeafGarland Yes, it is quite fascinating trick :-) but I personally feel that F# `inline` has not quite been designed for this kind of use (it is certainly not a standard practice), so it might be safer to stay in more comfortable area where things work. – Tomas Petricek Jun 08 '13 at 21:49
  • 1
    @TomasPetricek In my case, the "indices" are being used to remove elements from source, so I changed that line to `code` |> Seq.distinct |> Seq.toArray |> Array.sort - pretty useful little function.. thanks guys! – Aaron Brewbaker Jun 09 '13 at 14:52