Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Higher order functions with generic argument in F#

RE: What is the best way to pass generic function that resolves to multiple types

Please read the referenced link before going further below

I am trying to extend the concept and pass a generic function that takes 2 parameters and does something with them.

The static approach works, however the interface based one causes a compile error (see the code lines marked with //error):

The declared type parameter '?' cannot be used here since the type parameter cannot be resolved at compile time.

Does anyone know how to fix it?

module MyModule

type T = Content of int
with 
    static member (+) ((Content i1), (Content i2)) = Content (i1 + i2)
    static member (*) ((Content i1), (Content i2)) = Content (i1 * i2)

type W = { Content: int }
with 
    static member (+) ({Content = i1}, {Content = i2}) = { Content = i1 + i2 }
    static member (*) ({Content = i1}, {Content = i2}) = { Content = i1 * i2 }

type Sum = Sum with static member inline ($) (Sum, (x, y)) = x + y
type Mul = Mul with static member inline ($) (Mul, (x, y)) = x * y

let inline f1 (la: 'a list) (lb: 'b list) reducer = 
    let a = la |> List.reduce (fun x y -> reducer $ (x, y))
    let b = lb |> List.reduce (fun x y -> reducer $ (x, y))
    (a, b)

type I = abstract member Reduce<'a> : 'a -> 'a -> 'a

let f2 (la: 'a list) (lb: 'b list) (reducer: I) = 
    let a = la |> List.reduce reducer.Reduce
    let b = lb |> List.reduce reducer.Reduce
    (a, b)

let main ()=
    let lt = [Content 2; Content 4]
    let lw = [{ Content = 2 }; { Content = 4 }]

    let _ = f1 lt lw Sum
    let _ = f1 lt lw Mul

    let _ = f2 lt lw { new I with member __.Reduce x y = x + y} //error
    let _ = f2 lt lw { new I with member __.Reduce x y = x * y} //error
    0
like image 290
Franco Tiveron Avatar asked Mar 01 '23 10:03

Franco Tiveron


1 Answers

The problem with your attempt is that you can't use operators + or * on parameters x and y, because it's not known that their type 'a has those operators defined.

To answer your further question in comments about how to achieve it anyway - if you want to use multiplication and addition on any type 'a that the caller chooses, you have to specify that. For an interface method, the only way to do this is by constraining the type parameter 'a, and the only two kinds of constraints that .NET runtime supports are "has a parameterless constructor" and "implements a given interface or inherits from a given class".

The latter one would be useful in your case: make both types implement the interface and then constrain type parameter 'a to implement that interface:

type IArithmetic<'a> =
    abstract member add : 'a -> 'a
    abstract member mult : 'a -> 'a

type T = Content of int
  with
    interface IArithmetic<T> with
        member this.add (Content y) = let (Content x) = this in Content (x + y)
        member this.mult (Content y) = let (Content x) = this in Content (x * y)

type W = { Content: int }
  with
    interface IArithmetic<W> with
        member this.add y = { Content = this.Content + y.Content }
        member this.mult y = { Content = this.Content * y.Content }

type I = abstract member Reduce<'a when 'a :> IArithmetic<'a>> : 'a -> 'a -> 'a
//                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                  the constraint right here

... 

  let _ = f2 lt lw { new I with member __.Reduce x y = x.add y }
  let _ = f2 lt lw { new I with member __.Reduce x y = x.mult y }

Is this a bit awkward? I guess so, but you're kind of doing the same thing for the SRTP version, so why not?

The core idea is: if you want your Reduce method to work not with just any type, but only with types that can do certain things, you have to specify what those things are. In the SRTP case you're doing that by defining the (+) and (*) operators. In the interface case you're doing that by implementing the interface.

Q: But can I make the interface somehow pick up the (+) and (*) operators?
A: In general, no. The .NET runtime just doesn't support the kind of constraints like "any type that has a method with certain signature". This means that such constraints can't be compiled down to IL, which means they can't be used in an interface implementation.

And this is the price you pay for using SRTPs: all those inline functions - they don't get compiled to IL, they always get expanded (inserted, substituted) at use sites. For small, simple functions, this is no big deal. But if your whole program is like that, you might see some unexpected compiled code bloat, potentially translating to slower startup time etc.


Having said all that, I must note that the code you're showing is toy POC kind of code, not intended to solve any real, practical problem. And as such, most musings on it are in danger of being completely useless.

If you have an actual problem in mind, perhaps try sharing it, and somebody would suggest the best solution for that specific case.

In particular, I have a nagging feeling that you might not actually need higher-rank functions (that's what it's called when a function doesn't lose genericity when passed as parameter).

like image 74
Fyodor Soikin Avatar answered Mar 06 '23 17:03

Fyodor Soikin