I have a question on making a function generic so that I can reuse the behavior. I have a situation where I want to perform the same set of actions whether the data is a float or a string. I have included a bit of the data model so you can see what I am doing. Near the bottom I have two different functions in the DataSeries module, simpleHigh and preferredHigh. The simpleHigh function does exactly what I want. You can see that in both cases, float or string, I am using the same set of functions.
Ideally I could do what I attempt to do in the preferredHigh function. I define a single function and then use it for both conditions. The compiler complains though saying there is a type mismatch. I tried making the inner high function take a type argument:
let high<'T> v = (Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value))
But because it is contained inside another function definition the compiler says, "Explicit type parameters may only be used on module or member bindings".
Am I fighting a loosing battle here? Should I just settle with the simple version? My instinct tells me I am missing simple subtle and simple here. I would rather reuse the behavior if possible instead of writing the exact some thing twice. Should I take a different approach? All feedback welcome!
open System
type Observation<'T> = {
ObsDateTime : DateTimeOffset
Value : 'T
}
type Result =
| Float of float
| String of string
type DataSeries =
| Float of Observation<float> seq
| String of Observation<string> seq
module DataSeries =
let summarise
(f: float Observation seq -> float)
(s: string Observation seq -> string)
(ds: DataSeries) =
match ds with
| DataSeries.Float v -> f v |> Result.Float
| DataSeries.String v -> s v |> Result.String
// What works
let simpleHigh ds =
summarise
(Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value))
(Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value))
ds
// What I would rather write
let preferredHigh ds =
let high v = (Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value))
summarise
high
high
ds
The way you have preferredHigh written right now, it doesn't compile for an entirely different reason: high is a function of two arguments (one explicitly defined v and another coming from the function composition), but you're passing it to summarise, which expects a function of one argument.
So, the obvious way would be to remove the extra argument v:
let high = (Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value))
However, this wouldn't compile for the reason that you have already noted: the compiler determines the type of high based on the first usage as float Observation seq -> float, and then refuses to accept it in the second usage, because it doesn't match string Observation seq -> string.
This (as you probably already know) is due to the fact that in F# values cannot be generic, unless you write them as such explicitly. Aka "value restriction".
However, functions can be generic. Even nested functions can: even though you can't write the generic parameters explicitly, the compiler would still infer a generic type for a nested function where appropriate.
From this, the solution is obvious: make high a function, not a value. Give it an argument, and don't forget to apply the composed function to that argument, so that the resulting function doesn't end up with two arguments (aka "eta-abstraction"):
let high x = (Seq.sortBy (fun x -> x.Value) >> Seq.last >> (fun x -> x.Value)) x
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