Forcing F# type inference on generics and interfaces to stay loose

后端 未结 2 1434
[愿得一人]
[愿得一人] 2020-12-15 09:57

We\'re gettin\' hairy here. I\'ve tested a bunch of tree-synchronizing code on concrete representations of data, and now I need to abstract it so that it can run with any so

相关标签:
2条回答
  • 2020-12-15 10:13

    This is an old post, but it was the #1 result for my search. I have something to add that may help anyone else struggling with type inference as I (and the OP) have.

    I've found it helps to think of inference as some exponential function of the structure of your function calls, what signatures those calls may have, and what signatures they may not have. It's very important to take all three into consideration.

    Just for kicks, consider this function with three variables: sqrt(2*2*3)

    Right away, it's obvious that it will simplify to 2 times some irrational number, which must be rounded (thus acquiring an infinite level of imprecision) to make it useful in everyday life.

    The F# version feeds back into itself, compounding the error until the "rounding" culminates into undesirable type inferences. Because what a type may or may -not- be is a factor into this equation, it isn't always possible/easy to solve the problem directly with type annotations.

    Now imagine that adding an additional perfectly generic (i.e. neutral) functional between the two problem functions, changing our equation to this: sqrt(2*2*4)

    Suddenly, the result is perfectly rational, producing a perfectly accurate value of 4. By contrast, modifying the inversely related first and second values by 1 would have done absolutely nothing to help us.

    Don't be afraid to modify structure if it has the potential to make or break your entire program. One extra function versus all the hoops you'd have to jump through to (continually) bend F# to your will is a very small price to pay, and chances are you can find a way to make the extra structure useful. In some situations, doing the above can turn a very, very, very argumentative program into a perfect little angel, for many functions to come.

    0 讨论(0)
  • 2020-12-15 10:19

    I have not analyzed the code enough to figure out why, but adding

      member internal tree.SyncStep() : unit =
                                 //   ^^^^^^
    

    seems to fix it.

    EDIT

    See also

    Why does F# infer this type?

    Understanding F# Value Restriction Errors

    Unknown need for type annotation or cast

    It takes experience to get a very deep understanding of the F# type inference algorithm's capabilities and limitations. But this example seems to be in a class of issues people run into when they do very advanced things. For members of a class, the F# inference algorithm does something like

    1. Look at all the member explicit signatures to set up an initial type environment for all the members
    2. For any members that have fully explicit signatures, fix their types to the explicit signature
    3. Start reading the method bodies top to bottom, left to right (you'll encounter some 'forward references' that may involved unsolved type variables when doing this, and that can cause trouble, because...)
    4. Solve all the member bodies concurrently (... but we have not done any 'generalization' yet, the part that would 'infer type parameters' rather than 'fix' what in theory could be a function of 'a to be whatever concrete type its first call site used)
    5. Generalize (any remaining unsolved type variables become actual inferred type variables of generic methods)

    That may not be exactly right; I don't know it well enough to describe the algorithm, I just have a sense of it. You can always go read the language spec.

    What often happens is you get as far as bullet 3 and forcing the inferencer to start trying to concurrently solve/constrain all the method bodies when in fact it's unnecessary because, e.g. maybe some function has an easy concrete fixed type. Like SyncStep is unit->unit, but F# doesn't know it yet in step 3, since the signature was not explicit, it just says ok SyncStep has type "unit -> 'a" for some yet-unsolved type 'a and then now SyncStep is now unnecessarily complicating all the solving by introducing an unnecessary variable.

    The way I found this was, the first warning (This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'V') was on the last line of the body of UpdateRenditions at the call to docTree.Compare(). Now I know that Compare() should be unit -> unit. So how could I possibly be getting a warning about generic-ness there? Ah, ok, the compiler doesn't know the return type is unit at that point, so it must thing that something is generic that's not. In fact, I could have added the return type annotation to Compare instead of SyncStep - either one works.

    Anyway, I'm being very long-winded. To sum up

    • if you have a well-type program, it should 'work'
    • sometimes the details of the inference algorithm will require some 'extra' annotations... in the worst case you can 'add them all' and then 'subtract away the unnecessary ones'
    • by using the compiler warnings and some mental model of the inference algorithm, you can quickly steer towards the missing annotation with experience
    • very often the 'fix' is just to add one full type signature (including return type) to some key method that is 'declared late' but 'called early' (introducing a forward reference among the set of members)

    Hope that helps!

    0 讨论(0)
提交回复
热议问题