-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Conversation
What do you think about relaxing this to the same level as |
@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 |
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 = [{ |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, good catch -- fixed
cfbc92f
to
0ccf28f
Compare
Sold. Added the (value, value) case. |
0ccf28f
to
212844d
Compare
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.`); |
There was a problem hiding this comment.
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", {...}]]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good point
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
, returningfalse
if one isnull
and the other isstring
, andtrue
both arenull
. Other types would produce a runtime type error just as with a["string", ...]
wrapper.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 (!( |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
Use collator.compare() if both arguments evaluate to strings, and === otherwise
ccb6e1d
to
a964dcc
Compare
Benchmarks show no significant changes. @jfirebaugh the new |
* Relax typing for comparison operators Ports mapbox/mapbox-gl-js#6961 * Review comments * Lint fixes
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 notstring
ornumber
, 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