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?

+3


source to share


1 answer


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.

+6


source







All Articles