3

I have a discriminated-union type of the form

type ParameterName = string
type ParameterValues =
    | String of string[]
    | Float of float[]
    | Int of int[]
type Parameter = Parameter of ParameterName * ParameterValues

I want to pass the ParameterValues part to a function taking generic arguments returning unit, such as

let func1 (name:string) (data:'a) = printfn "%s" name

To deconstruct Parameter I could wrap func1 like this

let func2 (Parameter (name, values)) =
    match values with
        | String s -> func1 name s
        | Float s -> func1 name s
        | Int s -> func1 name s

however this is inconvenient if I have to do this for multiple functions. Instead, I would like to define a more flexible wrapper like this:

let func3 (fn: ('a -> 'b -> unit)) (Parameter (name, values)) =
    match values with
        | String s -> fn name s
        | Float s -> fn name s
        | Int s -> fn name s

This however fails, as the type of b gets restricted to string[] in the first option of the match expression; consequently the match expression fails with the error Type string does not match type float.

Is this expected? How can I work around this problem?

sebhofer
  • 611
  • 8
  • 19
  • 1
    Yes it is expected, F# does not support higher-ranked polymorphism. You could change the type of `data` to `obj`. – Lee Oct 23 '17 at 14:26
  • In addtion to @Tomas answer you can also receive three times the same function: ``let func3 fn1 fn2 fn3 (Parameter (name, values))`` otherwise there is [another compile-time solution](https://stackoverflow.com/questions/7213599/generic-higher-order-function/7224269#7224269) – Gus Oct 23 '17 at 14:51

2 Answers2

4

This is an expected behaviour. The problem is that you cannot directly pass a generic function as an argument to another function in F#. When you define a function as follows:

let func3 (fn: ('a -> 'b -> unit)) (Parameter (name, values)) = (...)

... you are defining a generic function func3 that has two generic parameters and, when those are specified, can be called with a given function and a parameter. This can be written as:

\forall 'a, 'b . (('a -> 'b -> unit) -> Parameter -> unit)

What you would need to do is to make those type parameters not top-level, but make the first parameter itself a generic function. You could write this as:

(\forall 'a, 'b . ('a -> 'b -> unit)) -> Parameter -> unit

This can be clumsily written in F# using interfaces:

type IFunction<'a> =
  abstract Invoke<'b> : 'a -> 'b -> unit

let func1 = 
  { new IFunction<string> with
    member x.Invoke<'b> name (data:'b) = printfn "%s" name }

let func3 (fn: IFunction<string>) (Parameter (name, values)) =
    match values with
    | String s -> fn.Invoke name s
    | Float s -> fn.Invoke name s
    | Int s -> fn.Invoke name s

In practice, your function does not really need to be generic, because you are never using the second argument - but you could probably achieve pretty much anything that you can achieve with this interfaces trick just by passing the data as obj and your code would be significantly simpler than this monstrosity!

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • Thanks for the quick answer. I'm very new to F# so it's a bit above my head, but I think I understood the `obj` idea. Could you take a look at my answer and tell me if that's what you had in mind? – sebhofer Oct 23 '17 at 14:55
  • @sebhofer Yep, your `obj` implementation is what I had in mind! +1 to your answer from me! (it's always tricky to answer these kinds of questions - people coming from Haskell would complain that my answer was too non-technical and others would find it pretty challenging; glad you find a solution that works for you though!) – Tomas Petricek Oct 23 '17 at 23:37
  • My comment was certainly not meant as a criticism, I like that you explained some of the background. It's just that to understand your example code I would have to read up on interfaces now, and I have the feeling there are other aspects of F# I should learn first. – sebhofer Oct 24 '17 at 07:36
  • One more quick question: The way I understand the solution using `obj` right now is that it effectively disables type checking as everything is an `obj` (I'm not sure if this is a technically correct way to say this, but I hope you get the gist). Is this correct? If so this is probably considered a "hack", right? – sebhofer Oct 24 '17 at 07:48
  • @sebhofer Yes, that's correct - I would not say it's a hack if you are not doing anything unsafe with the `obj` value afterwards - if you just want to print it, that's perfectly fine, because you can safely print any objects. If you wanted to do more, then it depends on what exactly... – Tomas Petricek Oct 24 '17 at 22:50
  • Makes sense! Thanks for all your help! – sebhofer Oct 25 '17 at 07:38
2

Following the suggestions by Lee and Tomas, I came up with the following solution:

type Parameter = Parameter of string * obj

let func0 (name:string) (data:obj) = printfn "%s %A" name data

let func1 (fn: string->obj->unit) (Parameter (name, value)) =
    fn name value

let p1 = Parameter ("p1", [|"a"; "b"|])
let p2 = Parameter ("p1", [|1.; 2.|])

func1 func0 p1
func1 func0 p2
sebhofer
  • 611
  • 8
  • 19