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
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With