-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Install aftermarket expression functions #11472
Conversation
f66b1ee converts |
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.
Yes, they are complementary, but I don’t think they have to be coupled. We’re still going to need conventional functions, just in case the mechanism in this PR breaks for any reason. I’ve added items to the to-do list above for those PRs, in case I get to them first. |
e82177e
to
15bf3d0
Compare
Thinking about this for a moment:
|
c989d11
to
8df1e98
Compare
0203d2d819f3c335c84e7db932953b30ad5f773f implements a catch-all |
|
||
// Install functions that resemble control structures, taking arbitrary | ||
// numbers of arguments. Vararg aftermarket functions need to be declared | ||
// with an explicit and implicit first argument. | ||
INSTALL_CONTROL_STRUCTURE(MGL_LET); | ||
INSTALL_CONTROL_STRUCTURE(MGL_MATCH); | ||
INSTALL_CONTROL_STRUCTURE(MGL_SWITCH); |
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 suggested MGL_MATCH
and MGL_SWITCH
as counterparts to mgl_match:
and mgl_case:
, respectively, but I think I got mixed up. MGL_SWITCH
implies the same semantics as MGL_MATCH
. I do think “case” would be problematic, as it makes it impossible to refer to the cases inside the expression. “Ternary” is also problematic because there can be multiple cases. Do you think MGL_CONDITION
or MGL_IF
could work?
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 think MGL_IF
will do the work. It's more descriptive to what the function actually does.
@@ -792,19 +820,19 @@ - (id)mgl_jsonExpressionObject { | |||
} | |||
|
|||
return expressionObject; | |||
} else if ([function isEqualToString:@"mgl_match:"]) { | |||
NSMutableArray *expressionObject = [NSMutableArray arrayWithObjects:@"match", self.operand.mgl_jsonExpressionObject, nil]; | |||
} else if ([function isEqualToString:@"MGL_MATCH"]) { |
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.
This removes support for the conventional custom function mgl_match:
. In this PR, I’ve left in support for those functions, just in case aftermarket expressions break with a new iOS SDK version. (I’ve equivocated about whether to remove public documentation about the conventional functions, but the implementations remain.) See the methods at the bottom of this file for an example of how to support both versions of the method simultaneously.
@@ -352,11 +357,11 @@ In style specification | Method, function, or predicate type | Format string syn | |||
`case` | `+[NSExpression expressionForConditional:trueExpression:falseExpression:]` | `TERNARY(condition, trueExpression, falseExpression)` | |||
`coalesce` | | | |||
`match` | | |
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.
These rows need to be updated.
`mgl_interpolate:withCurveType:parameters:stops:` | `mgl_interpolate:withCurveType:parameters:stops:(x, 'linear', nil, %@)` | Produces continuous, smooth results by interpolating between pairs of input and output values (“stops”). Compared to the `mgl_interpolateWithCurveType:parameters:stops:` custom function, the input expression (that function’s target) is instead passed in as the first argument to this function. | ||
`mgl_step:from:stops:` | `mgl_step:from:stops:(x, 11, %@)` |Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values ("stops"). Compared to the `mgl_stepWithMinimum:stops:` custom function, the input expression (that function’s target) is instead passed in as the first argument to this function. | ||
`mgl_join:` | `mgl_join({'Old', 'MacDonald'})` | Returns the result of concatenating together all the elements of an array in order. Compared to the `stringByAppendingString:` custom function, this function takes only one argument, which is an aggregate expression containing the strings to concatenate. | ||
`MGL_LET` | `MGL_LET('age', uppercase('old'), 'name', uppercase('MacDonald'), mgl_join({$age, $name}))` | Any number of variable names interspersed with their assigned `NSExpression` values, followed by an `NSExpression` that may contain references to those variables. Compared to the `mgl_expressionWithContext:` custom function, this function takes the variable names and values inline before the expression that contains references to those variables. |
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.
Rows need to be added for MGL_MATCH
, MGL_SWITCH
, and mgl_coalesce:
.
@@ -111,7 +111,7 @@ let stops: [Float: UIColor] = [ | |||
10: .white, | |||
] | |||
|
|||
layer.circleColor = NSExpression(format: "FUNCTION(mag, 'mgl_stepWithMinimum:stops:', %@, %@)", | |||
layer.circleColor = NSExpression(format: "mgl_step:from:stops:(mag, %@, %@)", | |||
UIColor.green, stops) | |||
``` | |||
|
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.
Right below here is an example that could potentially use MGL_MATCH
. On the other hand, I do find the use of a dictionary with valueForKeyPath:
to be somewhat elegant, so perhaps all it needs is a call to mgl_coalesce:
to avoid the redundant valueForKeyPath:
call.
Per chat with @fabian-guerra, we’re going to consider this defensive approach for a followup release, possibly on hold until we know there’s going to be a problem with an upcoming version of iOS or macOS. The existing code is already resilient to minor changes like the private class being renamed, just not to more substantial, public-facing changes to NSExpression. |
Only works for literal strings.
8874184
to
a51ae90
Compare
Renamed mgl_hasProperty:properties: to mgl_does:have: for better readability and consistency with the conventional mgl_has: function. Documented both forms of mgl_has:.
This is the preferred syntax for simple conditionals on iOS 9 and above, because you can inline the predicate instead of wrapping it in a constant value expression, which means you can write a conditional in a single format string.
@@ -343,7 +343,7 @@ In style specification | Method, function, or predicate type | Format string syn | |||
`properties` | |`$mgl_featureProperties` | |||
`at` | `objectFrom:withIndex:` | `array[n]` | |||
`get` | `+[NSExpression expressionForKeyPath:]` | Key path | |||
`has` | `mgl_hasProperty:properties:` | | |||
`has` | `mgl_does:have:` | `mgl_does:have:(self, 'key')` |
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 renamed and reordered this function. Instead of calling mgl_hasProperty:properties:('key', self)
, you call mgl_does:have:(self, 'key')
. I think this reads better: “Does self have ‘key’?” It’s also consistent with the order when you call the conventional function: FUNCTION(self, 'mgl_has:', 'key')
.
@@ -354,7 +354,7 @@ In style specification | Method, function, or predicate type | Format string syn | |||
`>=` | `NSGreaterThanOrEqualToPredicateOperatorType` | `key >= value` | |||
`all` | `NSAndPredicateType` | `p0 AND … AND pn` | |||
`any` | `NSOrPredicateType` | `p0 OR … OR pn` | |||
`case` | `MGL_IF` | `MGL_IF(1 = 2, YES, 2 = 2, YES, NO)` | |||
`case` | `+[NSExpression expressionForConditional:trueExpression:falseExpression:]` or `MGL_IF` | `TERNARY(1 = 2, YES, NO)` or `MGL_IF(1 = 2, YES, 2 = 2, YES, NO)` |
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 restored full support for +[NSExpression expressionForConditional:trueExpression:falseExpression:]
where available, along with the tests for it. On iOS 9.0 and above, we convert case
to TERNARY()
when it has only one conditional. This is the preferred syntax, because you can inline the predicate instead of wrapping it in a constant value expression, which means you can write a conditional in a single format string.
/cc @friedbunny
The code ticks in the table at |
Install Mapbox-specific functions as first-class NSExpression functions, prefixed to avoid collisions with any additional functions that may be built into NSExpression in the future. Various functions need to be renamed because the aftermarket variant no longer takes an explicit operand.
The result of this PR is that nontrivial expression usage will be a lot more legible and easier to write by hand without frequently consulting a reference. For example, concatenating strings goes from:
to:
boolValue
mgl_expressionWithContext:
→MGL_LET()
mgl_interpolateWithCurveType:parameters:stops:
→mgl_interpolate:withCurveType:parameters:stops:
mgl_numberWithFallbackValues:
,doubleValue
,floatValue
,decimalValue
→CAST()
mgl_stepWithMinimum:stops:
→mgl_step:from:stops:
stringByAppendingString:
→mgl_join:
stringValue
→CAST()
mgl_matchWithOptions:default:
→MGL_MATCH
(followup to [ios, macos] Add match expressions support. #11464)mgl_coalesce
→mgl_coalesce:
(followup to [ios, macos] Add match expressions support. #11464)mgl_case
→MGL_IF()
(followup to [ios, macos] Change the format for case expressions to a flat structure. #11450)mgl_has:
→mgl_does:have:
(followup to [ios, macos] Add lookup and feature operators. #11528)MGL_FUNCTION()
to unblock Eviscerate mbgl expression to Foundation JSON object conversion #11389Safeguard against changes to NSExpression that render this approach ineffectiveRemove redundantFUNCTION()
-based functions?Fixes #11015.
/cc @fabian-guerra @jmkiley @akitchen