Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic functions in F#

Tags:

f#

I'm trying to explore the dynamic capabilities of F# for situations where I can't express some function with the static type system. As such, I'm trying to create a mapN function for (say) Option types, but I'm having trouble creating a function with a dynamic number of arguments. I've tried:

let mapN<'output> (f : obj) args =
  let rec mapN' (state:obj) (args' : (obj option) list) = 
    match args' with
    | Some x :: xs -> mapN' ((state :?> obj -> obj) x) xs
    | None _ :: _ -> None
    | [] -> state :?> 'output option

  mapN' f args

let toObjOption (x : #obj option) = 
  Option.map (fun x -> x :> obj) x

let a = Some 5
let b = Some "hi"
let c = Some true

let ans = mapN<string> (fun x y z -> sprintf "%i %s %A" x y z) [a |> toObjOption; b |> toObjOption; c |> toObjOption]

(which takes the function passed in and applies one argument at a time) which compiles, but then at runtime I get the following:

System.InvalidCastException: Unable to cast object of type 'ans@47' to type 
'Microsoft.FSharp.Core.FSharpFunc`2[System.Object,System.Object]'.

I realize that it would be more idiomatic to either create a computation expression for options, or to define map2 through map5 or so, but I specifically want to explore the dynamic capabilities of F# to see whether something like this would be possible.

Is this just a concept that can't be done in F#, or is there an approach that I'm missing?

like image 827
Nathan Wilson Avatar asked Jan 29 '23 13:01

Nathan Wilson


2 Answers

I think you would only be able to take that approach with reflection.

However, there are other ways to solve the overall problem without having to go dynamic or use the other static options you mentioned. You can get a lot of the same convenience using Option.apply, which you need to define yourself (or take from a library). This code is stolen and adapted from F# for fun and profit:

module Option =
    let apply fOpt xOpt = 
        match fOpt,xOpt with
        | Some f, Some x -> Some (f x)
        | _ -> None

let resultOption =  
    let (<*>) = Option.apply

    Some (fun x y z -> sprintf "%i %s %A" x y z)
    <*> Some 5
    <*> Some "hi"
    <*> Some true
like image 153
TheQuickBrownFox Avatar answered Feb 07 '23 09:02

TheQuickBrownFox


To explain why your approach does not work, the problem is that you cannot cast a function of type int -> int (represented as FSharpFunc<int, int>) to a value of type obj -> obj (represented as FSharpFunc<obj, obj>). The types are the same generic types, but the cast fails because the generic parameters are different.

If you insert a lot of boxing and unboxing, then your function actually works, but this is probably not something you want to write:

let ans = mapN<string> (fun (x:obj) -> box (fun (y:obj) -> box (fun (z:obj) -> 
  box (Some(sprintf "%i %s %A" (unbox x) (unbox y) (unbox z)))))) 
    [a |> toObjOption; b |> toObjOption; c |> toObjOption]

If you wanted to explore more options possible thanks to dynamic hacks - then you can probably do more using F# reflection. I would not typically use this in production (simple is better - I'd just define multiple map functions by hand or something like that), but the following runs:

let rec mapN<'R> f args = 
  match args with 
  | [] -> unbox<'R> f
  | x::xs ->
      let m = f.GetType().GetMethods() |> Seq.find (fun m -> 
        m.Name = "Invoke" && m.GetParameters().Length = 1)
      mapN<'R> (m.Invoke(f, [| x |])) xs

mapN<obj> (fun a b c -> sprintf "%d %s %A" a b c) [box 1; box "hi"; box true]  
like image 44
Tomas Petricek Avatar answered Feb 07 '23 09:02

Tomas Petricek