Pregunta

I'm interested in understanding more about how the following is possible. Given a collection constrained to type obj and the following function:

let myList = new ResizeArray<obj>()

let addToMyListTuple<'a> (item : string * 'a) =
    let boxed = box item
    let unboxed = unbox<string * 'a> boxed
    let item1 = match unboxed with first, _ -> first
    Console.WriteLine(sprintf "%A" item1)
    myList.Add(boxed)

Interacting with these 2 gives the expected results and the string part of the tuple can be consumed regardless of the associated type in the second part.

addToMyListTuple("integer", 3)
addToMyListTuple("float", 3.0)
addToMyListTuple("string", "string")
addToMyListTuple("tuple", (3, "integer"))

However, what I was hoping would be possible was this where I would be able to interact with items in the list at a later time and unbox the obj in such a way that accessing the string part of the tuple would be possible.

myList
|> Seq.iter(fun x ->
    let unboxed = unbox<string * 'a> x
    let item1 = match unboxed with first, _ -> first
    Console.WriteLine(sprintf "%A" item1)
)

Running this gives me the compile time warning

This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'obj'.

and the runtime exception

System.InvalidCastException: Unable to cast object of type 'System.Tuple`2[System.String,System.Int32]' to type 'System.Tuple`2[System.String,System.Object]'.

Is there any other way to accomplish this behavior?

¿Fue útil?

Solución

When you call addToMyTupleList<'a>, the concrete type of 'a is statically known by the compiler (that is, you're calling addToMyTupleList<int>, addToMyTupleList<float>, etc.). By contrast, when you're trying to do the unboxing within Seq.iter, you're hoping that 'a will be determined based on the runtime type of the argument, which isn't how the F# type system works.

As I see it, you have a few options:

  1. Use a type test, as Daniel suggests.
  2. Instead of storing the raw values in the list, store the outputs you want to generate (that is, use a string list where you call sprintf as you're putting things in.
  3. Be a bit more precise with the types you're storing in your list. Encode the type ∃'a.string * 'a (that is, it's a pair consisting of a string and an 'a, for some unknown 'a) and store a list of these. In a language like Haskell this isn't too bad, but faithfully encoding it in F# is ugly/confusing:

    type ExPair<'x> =
        abstract Apply : string * 'a -> 'x
    type ExPair =
        abstract ForAll : ExPair<'x> -> 'x
    
    let pack p = { new ExPair with 
                        member __.ForAll<'x>(e:ExPair<'x>) : 'x = e.Apply p }
    
    let myList = [pack ("integer", 3)
                  pack ("float", 3.0)
                  pack ("string", "string")
                  pack ("tuple", (3, "integer"))]
    
    myList
    |> List.map (fun e ->
        e.ForAll { new ExPair<string> with member __.Apply(s,x) = sprintf "%s,%A" s x })
    

Otros consejos

If you show more of what you're trying to do there may be a better way to make it polymorphic (e.g., unions), but you could do this with type tests:

let tuples = ResizeArray()
let addToTuples (k, v) = tuples.Add(k, box v)
addToTuples ("int", 3)
addToTuples ("float", 3.0)
addToTuples ("string", "foo")
addToTuples ("tuple", (3, "int"))
addToTuples ("option", Some 1)

tuples
|> Seq.iter (fun (s, x) ->
    printf "String: %s, Value: " s
    match x with
    | :? int as i -> printfn "%d" i
    | :? float as d -> printfn "%f" d
    | :? string as s -> printfn "%s" s
    | :? (int * string) as t -> let x, y = t in printfn "(%d, %s)" x y
    | _ -> printfn "{%A}"  x
)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top