ta0kira / zeolite

Zeolite is a statically-typed, general-purpose programming language.
Apache License 2.0
18 stars 0 forks source link

Validate the logic for merging "any" vs. "all" guesses in param inference. #100

Closed ta0kira closed 3 years ago

ta0kira commented 3 years ago

Specifically:

  1. "All" merges attempt to target the most-general guess. Each "all" guess for a param must be satisfied because all of the conversions they came from are required to happen.
  2. "Any" merges attempt to target the least-general guess. In theory, we could take any one of them as "the" guess, since all guesses came from successful conversions. But, the existence of multiple guesses causes ambiguity, so it makes sense to try to merge them.

Is that's actually what happens? I think that those semantics originally had some relationship to variance, so I also need to look into how "any" and "all" conversions relate to covariant and contravariant conversions. (mergeInferredTypes might need more patterns in anyCheck and allCheck to ensure that variance is handled consistently.)

Another consideration is that mergeAllM Nothing is the default return when conversion succeeds and there are no guesses. What happens when we process something like mergeAny [mergeAll Nothing,someGuess]? The result should always be someGuess, even though mergeAll Nothing isn't an identity for mergeAny.

It's possible that "any" and "all" guess merges don't need to be treated differently, due to the way conversion checks deal with covariance and contravariance.

ta0kira commented 3 years ago

Looking just at variance for now:

Merging covariant and contravariant too early could cause other merges to fail; therefore, it's probably better to keep both and then have a "final" merge stage that prefers the contravariant, i.e., the more constrained end of the range.

This still leaves a possible scenario where we have covariant B and C and contravariant D. This can only happen if B and C cannot be converted in either direction. Rather than trying to replace both with D, it would be better to keep both, thus giving the user an error due to the ambiguity.

The above makes sense for "all" aggregation, but it's unclear how to deal with "any".


It would make sense to keep "all" as a merge operation and treat "any" as an operation on alternatives, e.g., All{ a, Any{ b, c } } becomes Try{ Merge{ a, b }, Merge{ a, c } }, where Try selects any alternative that has a single result. The rationale here is that if #x can be b or c but doesn't need to be both, then the failure to merge one should eliminate it as a possibility.

On the other hand, another way to look at it is using duality: "all" (intersection) and "any" (union) are duals, just as covariant and contravariant are duals. So, if "all" preferred covariant guesses over contravariant and "any" preferred the reverse, then the merge operation would (probably?) be self-dual.

The main issue with that approach is "all" loses upper bounds and "any" loses lower bounds before moving onto the next level. This will only be problematic if "all" and "any" are nested with each other, e.g., there are multiple function args and one of them is a type union with both a covariant guess and a contravariant guess. In such cases, a type error after inference would be possible.


So, for now I'm going to accept that type-inference could result in a bad type guess, i.e., it's not going to be a dichotomy between "valid inference" vs. "failed inference".

ta0kira commented 3 years ago

It's worth noting that a union with covariant guess A and contravariant guess B has a "range" of valid guesses (all,B] ∪ [A,any), i.e., anything not strictly in (B,A). If AB then literally any type on a chain passing through A will do. So, I don't think there's any point preserving A in this situation.

Similar logic can be applied to the intersection case.

Therefore, it should be fine to drop covariant guesses for "any" and contravariant guesses for "all" where possible, as mentioned in the previous comment.

In the case of "all", invariant guesses must be preserved. On the other hand, they don't need to be preserved in "any"; even if they can't be merged. For consistency, however, it's appropriate to only eliminate invariant guesses in "any" if they can be merged.

So, I think the actual end result of all of this is that the original behavior was appropriate.

ta0kira commented 3 years ago

If this approach becomes problematic, it would be reasonable to infer a meta type when things can't be merged. For example, if A and B can't be merged then go with [A&B] or [A|B], for "all" and "any", respectively.

ta0kira commented 3 years ago

It turns out it isn't quite that simple, but unions and intersections can still be helpful. (Note that, since unions and intersections are defined as limits and colimits, they are the "best" way to combine two types.)

"All" Semantics

"Any" Semantics

Invariance

ta0kira commented 3 years ago

Actually, the reasoning for "any" is invalid above because the merged result needs to be between the original guesses and #x.

ta0kira commented 3 years ago

Another thing I hadn't thought about before: Conversion checks for Invariant (in TypeInstance.hs) perform a mergeAllM on the results of separate Covariant and Contravariant checks. because of this, there will generally not be invariant guesses; instead, it will likely be a pair of covariant and contravariant guesses with the same type.

So, the merging logic needs to treat invariant guesses as if they were both lower and upper bounds.

I think these issues can be summarized as a failure to properly distribute an "or" operation over "and" operations, i.e., (a&b)|(c&d)(a|c)&(b|d). Put another way, "any" fails to turn an "or" operation into an "and" operation to pass on to the next merging stage. Or, multiple guesses from "any" should be treated as an "or".


A more set-theoretic approach using ranges:


It's probably better to transform the guesses from a MergeTree into something that treats "all" and "any" differently.

For example:

data GuessRange a =
  GuessRange {
    grLower :: a,
    grUpper :: a
  }

data GuessUnion a =
  GuessUnion {
    guGuesses :: [GuessRange a]
  }

The operations for reduceMergeTree would then be:

The final result will be a set of possible ranges for the inferred param; the final choice is arbitrary.