Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax typing for equality and inequality expressions #6961

Merged
merged 7 commits into from
Jul 24, 2018

Conversation

anandthakker
Copy link
Contributor

@anandthakker anandthakker commented Jul 13, 2018

Closes #6459

  • Relax ==/!= typing by allowing ["==", value, value] (e.g., ["==", ["get", "x"], ["get", "y"])

  • Relax inequality typing by also allowing arguments of type value (e.g. ["get", "x"] without a wrapping type assertion). At runtime, if the feature property doesn't have the same type as the other argument, or if the argument type is not string or number, then the expression will evaluate to an error.

  • briefly describe the changes in this PR

  • write tests for all new functionality

  • document any changes to public APIs

  • post benchmark scores

  • manually test the debug page

  • tagged @mapbox/studio and/or @mapbox/maps-design if this PR includes style spec changes

@anandthakker anandthakker changed the title Closes #6459 Relax typing for equality and inequality expressions Jul 13, 2018
@jfirebaugh
Copy link
Contributor

Relax inequality typing by allowing one of the two arguments to be value (e.g. ["get", "x"] without a wrapping type assertion).

What do you think about relaxing this to the same level as ==/!=, i.e. allowing both arguments to be value (but still erroring if they're not the same runtime type).

@anandthakker
Copy link
Contributor Author

anandthakker commented Jul 13, 2018

@jfirebaugh I lean against it, but I don't feel super strongly about it. My thinking is that requiring one argument to be concretely typed lets us know--and requires the user to know--at compile time what kind of comparison we're doing. And that [">", ["get", "pop_2010"], ["get", "pop_2000"]] could silently produce surprising results for a messy dataset in which, say, some features had strings for both pop_2010 and pop_2000.

@jfirebaugh
Copy link
Contributor

jfirebaugh commented Jul 13, 2018

The advantage is it eliminates another case where explicit assertions are required -- I think the last major one besides #6190. Given that feedback indicates that assertions and coercions are one of the most confusing and unergonomic features of expressions, I think that's a win that outweighs the drawbacks in the relatively less common "messy data" case.

parameters: ['value', 'value']
}];

const inequalitySignatures = [{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name here tripped me up for a bit because inequalitySignatures sounds like it applies to !=. I guess "comparison" already includes both types... maybe something like "orderingComparisonSignatures"? or "sortComparison"?

}],
'==': [].concat(equalitySignatures),
'!=': [].concat(equalitySignatures),
'<': [].concat(inequalitySignatures),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do the arrays have to be copied here instead of shared? Not that I see a problem with it, I think I'm just missing some hidden requirement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh they don't -- I don't even know why I did it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like explicitly listing out the possible combinations might be more confusing than collapsing the "formal" signature down to just (value, value), and explaining the constraints in prose. Something like...

For ==, !=:

The comparison is strictly typed; arguments of different runtime types are considered unequal. Cases where the types are known to be different at parse time produce a parse error.

For others:

The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time produce a parse error.

lhs: Expression;
rhs: Expression;
collator: ?Expression;
compare: (EvaluationContext, Expression, Expression, ?Expression) => any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to explicitly omit this from the serialization registration, right? I think this is what's causing the test failure in regressions/mapbox-gl-js#5947.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good catch -- fixed

@anandthakker
Copy link
Contributor Author

Given that feedback indicates that assertions and coercions are one of the most confusing and unergonomic features of expressions, I think that's a win that outweighs the drawbacks in the relatively less common "messy data" case.

Sold. Added the (value, value) case.

let collator = null;
if (args.length === 4) {
if (lhs.type.kind !== 'string' && rhs.type.kind !== 'string') {
return context.error(`Cannot use collator to compare non-string types.`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be deferred to runtime for (value, value) comparisons, given that one of the key uses for collator ["==", ["get", "name"], ["get", "name_en"], ["collator", {...}]]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 if there's a collator argument, what should ["==", ["get", "name"], ["get", "name_en"], ["collator", {...}]] do on a feature where one or both of name or name_en aren't strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two possibilities:

  • When there's a collator, infer ["string", ...] annotations, so that it's a runtime error if either one is not a string. (So: no relaxed type semantics when there's a collator)
  • Use collator.compare() if both arguments evaluate to strings, and === otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A third possibility:

  • Effectively do the first option, but special-case null, returning false if one is null and the other is string, and true both are null. Other types would produce a runtime type error just as with a ["string", ...] wrapper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nickidlugash ^ any gut feeling on this issue?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anandthakker For the use case of ["==", ["get", "name"], ["get", "name_en"], ["collator", {...}]], it will be common for there to be null values in either/both string. Given that, I think the desired behavior would be for this expression to return false if there is a null value, rather than producing an error. I think either the second or third option would give the desired result?

Are we also special-casing null values for other comparisons? E.g. does ["==", ["get", "name"], ["get", "name_en"]] return false if one of the values is null? What about if the comparison was between a number and a null value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we also special-casing null values for other comparisons? E.g. does ["==", ["get", "name"], ["get", "name_en"]] return false if one of the values is null? What about if the comparison was between a number and a null value?

Yep, these will both now return false

evaluate(ctx: EvaluationContext) {
const lhs = this.lhs.evaluate(ctx);
const rhs = this.rhs.evaluate(ctx);
if (this.needsRuntimeTypeCheck) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do the benchmark scores look like? One potential optimization is to specialize evaluate in subclasses to eliminate needsRuntimeTypeCheck and collator branching.

return context.concat(2).error(`"${op}" comparisons are not supported for type '${toString(rhs.type)}'.`);
}

if (!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it more difficult to understand this condition than if the ! was distributed.

}],
'==': [].concat(equalitySignatures),
'!=': [].concat(equalitySignatures),
'<': [].concat(inequalitySignatures),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like explicitly listing out the possible combinations might be more confusing than collapsing the "formal" signature down to just (value, value), and explaining the constraints in prose. Something like...

For ==, !=:

The comparison is strictly typed; arguments of different runtime types are considered unequal. Cases where the types are known to be different at parse time produce a parse error.

For others:

The arguments are required to be either both strings or both numbers; if during evaluation they are not, expression evaluation produces an error. Cases where this constraint is known not to hold at parse time produce a parse error.

@anandthakker
Copy link
Contributor Author

Benchmarks show no significant changes.

@jfirebaugh the new == semantics almost match filter-==: the only difference is that checking for equality between an undefined/missing property and null, legacy filters treat it as unequal, and expressions treat it as equal

anandthakker pushed a commit to mapbox/mapbox-gl-native that referenced this pull request Aug 2, 2018
anandthakker added a commit to mapbox/mapbox-gl-native that referenced this pull request Aug 2, 2018
* Relax typing for comparison operators

Ports mapbox/mapbox-gl-js#6961

* Review comments

* Lint fixes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants