Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to downcast from obj to option<obj>?

Tags:

casting

f#

I have a function that takes a parameter of type object and needs to downcast it to an option<obj>.

member s.Bind(x : obj, rest) =
    let x = x :?> Option<obj>

If I pass (for example) an Option<string> as x, the last line throws the exception: Unable to cast object of type 'Microsoft.FSharp.Core.FSharpOption'1[System.String]' to type 'Microsoft.FSharp.Core.FSharpOption'1[System.Object]'.

Or, if I try a type test:

member s.Bind(x : obj, rest) =
   match x with
    | :? option<obj> as x1 -> ... // Do stuff with x1
    | _ -> failwith "Invalid type"

then x never matches option<obj>.

In order to make this work, I currently have to specify the type the option contains (e.g. if the function is passed an option<string>, and I downcast the parameter to that rather than option<obj>, the function works.

Is there a way I can downcast the parameter to option<obj> without specifying what type the option contains? I've tried option<_>, option<#obj>, and option<'a> with the same results.

By way of background, the parameter needs to be of type obj because I'm writing an interface for a monad, so Bind needs to bind values of different types depending on the monad that implements the interface. This particular monad is a continuation monad, so it just wants to make sure the parameter is Some(x) and not None, then pass x on to rest. (The reason I need the interface is because I'm writing a monad transformer and I need a way to tell it that its parameter monads implement bind and return.)

Update: I managed to get around this by upcasting the contents of the option before it becomes a parameter to this function, but I'm still curious to know if I can type-test or cast an object (or generic parameter) to an option without worrying about what type the option contains (assuming of course the cast is valid, i.e. the object really is an option).

like image 893
FSharpN00b Avatar asked Jun 09 '11 07:06

FSharpN00b


3 Answers

I am not certain why you need to get your input as obj, but if your input is an Option<_>, then it is easy:

member t.Bind (x : 'a option, rest : obj option -> 'b) =
    let x = // val x : obj option
        x
        |> Option.bind (box >> Some)
    rest x
like image 138
Ramon Snir Avatar answered Nov 04 '22 16:11

Ramon Snir


There isn't any nice way to solve this problem currently.

The issue is that you'd need to introduce a new generic type parameter in the pattern matching (when matching against option<'a>), but F# only allows you to define generic type parameters in function declarations. So, your only solution is to use some Reflection tricks. For example, you can define an active pattern that hides this:

let (|SomeObj|_|) =
  let ty = typedefof<option<_>>
  fun (a:obj) ->
    let aty = a.GetType()
    let v = aty.GetProperty("Value")
    if aty.IsGenericType && aty.GetGenericTypeDefinition() = ty then
      if a = null then None
      else Some(v.GetValue(a, [| |]))
    else None

This will give you None or Some containing obj for any option type:

let bind (x : obj) rest =   
    match x with    
    | SomeObj(x1) -> rest x1
    | _ -> failwith "Invalid type"

bind(Some 1) (fun n -> 10 * (n :?> int))
like image 9
Tomas Petricek Avatar answered Nov 04 '22 16:11

Tomas Petricek


To answer your last question: you can use a slight variation of Tomas' code if you need a general-purpose way to check for options without boxing values beforehand:

let (|Option|_|) value = 
  if obj.ReferenceEquals(value, null) then None
  else
    let typ = value.GetType()
    if typ.IsGenericType && typ.GetGenericTypeDefinition() = typedefof<option<_>> then
      let opt : option<_> = (box >> unbox) value
      Some opt.Value
    else None
//val ( |Option|_| ) : 'a -> 'b option    

let getValue = function
  | Option x ->  x
  | _ -> failwith "Not an option"

let a1 : int = getValue (Some 42)
let a2 : string = getValue (Some "foo")
let a3 : string = getValue (Some 42) //InvalidCastException
let a4 : int = getValue 42 //Failure("Not an option")
like image 1
Daniel Avatar answered Nov 04 '22 17:11

Daniel