F # compiler inferring concrete types from first use of generic functions when currying
I have a problem with type inference and currying.
I have a helper method:
requestToGroup :
group:'T array ->
operation:('T -> System.Threading.Tasks.Task<'B>) ->
predicate:('B -> bool) -> timeout:int -> Async<int>
Basically, this method allows me to run the same operation on multiple services in parallel, and when they all end or time out, it returns how many of them successfully evaluate them using a predicate.
Given these simple definitions:
type RequestA(name:string) =
member val Name=name with get,set
type ResultA(has:bool)=
member x.Has()=has
type RequestB(id:int) =
member val Id=id with get,set
type ResultB(fits:bool)=
member x.Fits()=fits
type IService =
abstract member Has: RequestA -> Task<ResultA>
abstract member Fits: RequestB -> Task<ResultB>
I can use this helper like this:
type MyClass<'T>(services:IService array) =
member x.AllHas(value:string) =
let positive = Async.RunSynchronously <| requestToGroup services (fun(s)->s.Has(RequestA(value))) (fun(r)->r.Has()) 1000
positive=services.Length
member x.AllFits(value:int, services:IService array) =
let positive = Async.RunSynchronously <| requestToGroup services (fun(s)->s.Fits(RequestB(value))) (fun(r)->r.Fits()) 1000
positive=services.Length
And everything's good. Then I decide that I want to archive the function requestToGroup
by doing:
type MyClass<'T>(services:IService array) =
let groupOp = requestToGroup services
member x.AllHas(value:string) =
let positive = Async.RunSynchronously <| groupOp (fun(s)->s.Has(RequestA(value))) (fun(r)->r.Has()) 1000
positive=services.Length
member x.AllFits(value:int, services:IService array) =
let positive = Async.RunSynchronously <| groupOp (fun(s)->s.Fits(RequestB(value))) (fun(r)->r.Fits()) 1000
positive=services.Length
But now it groupOp
infers specific types rather than generic, and compilation doesn't work in s.Fits(RequestB(value))
saying what I expected Task<RequestA>
, but I provide Task<RequestB>
.
How can I prevent the compiler from inferring specific types?
BONUS: How can I make this code in a method MyClass
more convenient and understandable?
source to share
Automatic generalization does not apply to value bindings in the way it does to function bindings. This means that it can cause problems if the binding is not syntactically a function, that is, it has no arguments. Try the following:
let groupOp a = requestToGroup services a
(In cases with static resolution it inline
may be required additionally, but it doesn't look like it here.)
Edit: code suggestions (which may not be fully flagged)
In the refactoring process, I'll just post some wild thoughts and leave it to you if they are helpful. Don't mind this if it doesn't apply. There will be one proposal with a little and one with a big change.
As a light refactor, I would suggest making the query and result types immutable. They can be defined as follows:
type RequestA = RequestA of name : string
type ResultA = ResultA of has : bool
type RequestB = RequestB of ident : int
type ResultB = ResultB of fits : bool
This is a use of the named DU fields introduced in F # 3.1. Older versions should omit names. Using them, I can write a generic function checkAll
in MyClass
. If you want to stick with the old types, this does not change the shape of the function.
type MyClass<'T>(services:IService array) =
let checkAll operation predicate =
let op = requestToGroup services operation predicate 1000
Async.RunSynchronously op = Array.length services
member x.AllHas value =
checkAll (fun s -> s.Has(RequestA(value))) (fun (ResultA a) -> a)
member x.AllFits value =
checkAll (fun s -> s.Fits(RequestB(value))) (fun (ResultB b) -> b)
(I am assuming that the services
method parameter AllFits
in the question is a refactoring artifact, it is the shadow of the private value of the same name.)
Stronger refactor . I have no idea if this goes overboard, but it is possible to change the signatures and drop the request / response types entirely.
requestToGroup
does two things that are not necessarily related: executing tasks at the same time while listening to a timeout, and counting the results using a predicate. How to make the first part a separate function:
let tryRunAll (group : 'T array) (getJob : 'T -> Async<'B>) (timeout : int) =
// ... result typed Async<'B option []>
It will return an array of results, where None
timeouts means. This function can be useful on its own if you want to work directly with the results.
Anyway, wildly changing things IService
can work with asynchronous ones. They can be turned into tasks using Async.StartAsTask
.
type IService =
abstract member HasName: string -> Async<bool>
abstract member FitsId: int -> Async<bool>
Then the implementation MyClass
will look like this:
type MyClass<'T> (services : IService array) =
let checkAll getter =
Async.RunSynchronously (tryRunAll services getter 1000)
|> Array.forall ((=) (Some true))
member x.AllHas value = checkAll (fun s -> s.HasName value)
member x.AllFits value = checkAll (fun s -> s.FitsId value)
If you still need it requestToGroup
, it can be implemented like this:
let requestToGroup group operation predicate timeout = async {
let! res = tryRunAll group operation timeout
return res |> Array.choose id |> Array.filter predicate |> Array.length }
And that completes my imaginary attempts at coding. There is no guarantee that any of this is normal, will work, or applicable. But hopefully this helps in getting ideas.
source to share