简体   繁体   中英

F# compiler inferring concrete types from first use of generic functions when currying

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?

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, ie 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.)


Edit: code suggestions (that may be totally off the mark)

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM