Reputation: 35895
I am having an issue with type inference and currying.
I have a helper method like this:
requestToGroup :
group:'T array ->
operation:('T -> System.Threading.Tasks.Task<'B>) ->
predicate:('B -> bool) -> timeout:int -> Async<int>
Basically, this method allows me to launch the same operation in several services in parallel, and when all of them finish or the timeout expires, returns how many of them are successful evaluating them with the predicate.
Considering 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 it is all good. Then I decides I want to curry the requestToGroup
function, 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, groupOp
infers concrete types rather than the generic, and the compilation fails at s.Fits(RequestB(value))
saying that was expecting Task<RequestA>
but I am providing Task<RequestB>
.
How can I prevent the compiler from inferring concrete types?
BONUS: How can I make that code in MyClass
method to look better and more legible?
Upvotes: 3
Views: 110
Reputation: 6223
Automatic generalization doesn't apply to value bindings in the way it applies to function bindings. This means that it can cause problems if the binding isn't syntactically a function, i.e. doesn't have arguments. Try this:
let groupOp a = requestToGroup services a
(In cases with static resolution, inline
can be required additionally, but this doesn't seem relevant here.)
On refactoring, I'll just post some wild thoughts and leave it to you whether they are useful. Don't mind this if it doesn't apply. There will be one proposal with small and one with big changes.
As a light refactor, I'd suggest to make the request and result types immutable. They could be defined like this:
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 using the named DU fields introduced in F# 3.1. Older versions have to omit the names. Using these, I can write a general checkAll
function in MyClass
. If you want to stick with the old types, that doesn't 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'm assuming the parameter services
of the method AllFits
in the question is a refactoring artifact; it shadows the private value of the same name.)
Heavier refactor I have no idea whether this is going overboard, but one could change the signatures and drop the request/response types completely.
requestToGroup
is doing two things that are not necessarily related: performing tasks in parallel while heeding a timeout and counting results using the predicate. How about making the first part a separate function:
let tryRunAll (group : 'T array) (getJob : 'T -> Async<'B>) (timeout : int) =
// ... result typed Async<'B option []>
It would return an array of the results, where None
signifies timeouts. This function may be useful on its own, if you want to work on the results directly.
Wildly changing things anyway, IService
could work with asyncs. 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 of MyClass
would 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 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 concludes my imaginary coding attempts. No warranty that any of this is sane, would work, or is applicable. But hopefully it helps to get ideas.
Upvotes: 6