MGLStyleValue should be reimplemented as a subclass of or wrapper around NSExpression, and MGLStyleLayer layout and paint properties should accept NSExpressions instead of MGLStyleValues.
Especially once we introduce support for arbitrary arithmetic and conditional expressions in style values (mapbox/mapbox-gl-js#4077 and possibly mapbox/mapbox-gl-js#4079), maintaining a custom vocabulary and type system around these values will become a major burden. Though itâs nice that the current MGLStyleValue system is strongly typed, taking better advantage of type inference in Swift, our API surface area and the learning curve will increase quite a bit as we essentially try to reinvent a statically-typed NSExpression.
NSExpression performs runtime type checking for initialization and static type checking for inspection. For maximum developer comfort, it features a format string mini-language and a series of strongly-typed getters that are applicable to certain kinds of expressions. MGLVectorStyleLayer takes advantage of all these features by representing filters as NSPredicates, which consist of NSExpressions. As with NSPredicate, developers are already familiar with NSExpression and its format string syntax, so there wonât be much of a learning curve at all.
The one thing NSExpression doesnât provide is the concept of a style function. It does provide a rather awkward extensibility mechanism; we could extend NSExpression to understand functions and provide a more ergonomic, strongly typed API for working with functions. For constant values and expressions, the developer would use NSExpression directly. https://github.com/mapbox/mapbox-gl-native/issues/5970#issuecomment-253087889 has a sketch of the proposed API.
My understanding is that mapbox/mapbox-gl-js#4077 would require a style specification version bump, which would precipitate an SDK major version bump, so we probably donât need to worry so much about backwards compatibility for this refactoring.
/cc @boundsj @incanus @lucaswoj @jfirebaugh
The one thing NSExpression doesnât provide is the concept of a style function.
Are you referring to categorical/interval/exponential style functions or the general idea of a function?
In this ticket, Iâm referring to category/interval/exponential style functions, of the sort already supported by the style JSON format. We would need to extend the NSExpression system to support that concept. All the other ideas that have been floated, like conditionals and arithmetic, are already supported out of the box. NSExpression could also be extended with arbitrary selectors (aka programmatic callbacks), but thatâs outside the scope of this feature proposal.
https://github.com/mapbox/mapbox-gl-js/pull/4715#pullrequestreview-38868643 outlines how an expression syntax proposed for the style JSON file format would translate to NSExpression.
The expression feature is coming along in mapbox/mapbox-gl-js#4777 and mapbox/mapbox-gl-js#4841. This is a good time to start designing the iOS/macOS equivalent so we know where the impedance mismatches will be.
To restate the points made above, the most natural representation for style JSON expressions would be NSExpression. NSExpression has the benefit of being widely known among iOS developers: as the core of the NSPredicate syntax, itâs used for tasks ranging from filtering an array to searching Core Data or Spotlight. It also takes a pragmatic approach, eschewing compile-time type safety in favor of a compact, memorable format string syntax (with a more structured syntax as a little-used alternative).
The downside is that the iOS and macOS SDKs will diverge syntactically from GL JS, even moreso than the Android SDK. However, the alternatives donât lend themselves to concision or expressiveness in a library written in Objective-C.
To illustrate why we should use NSExpression instead of hewing to the style JSON syntax, consider the following constraints:
@s. In Swift, all the expressions in mapbox/mapbox-gl-js#4836 would have such complex inferred types that theyâd fail to compile or take inordinately long to compile.By committing to NSExpression, weâre signing up to do a lot of translation work, and we may need to perform gymnastics at times to reconcile two very different syntaxes with different feature sets. To the extent possible, I think we should prioritize the following goals:
I wouldnât say the following are non-goals, but theyâre definitely lower priorities:
Here are some areas that, at this early stage, already look like theyâll be a bit messy:
CAST() function will probably be news to most iOS developers who are otherwise familiar with expressions.FUNCTION(####, "stringByAppendingString:", ####) syntax. We can provide a convenience method that wraps this function, but we canât simplify the format string syntax at all./cc @anandthakker
The question came up the other day whether we would consider implementing our own expression system, instead of using NSExpression, if the style JSON expression format ends up dominated by concepts that are difficult to express in NSExpressionâs format string syntax. I think the answer is no for the following reasons:
-description methods return, which would be incompatible with our expression language.Here are some areas that, at this early stage, already look like theyâll be a bit messy
From a cursory glance at #9439, another impedance mismatch will come from the fact that mbgl::style::expression::type::Array expects an explicit element type. This requirement appears to have been introduced to facilitate stronger type checking at the C++ level, whereas thereâs less desire for strong type checking at the Objective-C level.
Objective-C supports annotating an NSArrayâs element type via lightweight generics, but itâs lightweight in the sense that only the compiler ever sees the generic parameter; itâs inaccessible to us at runtime. Our options would be:
FUNCTION() syntax.Iâd prefer the first approach. Hopefully the arrays passed into an expression will tend to be small, so determining the element type would incur only a negligible performance hit.
The first approach jives well with how literal arrays are currently being handled. In the JSON syntax, a literal array is created with ["literal", [x, y, z]], where _x, y, z_ are treated as literal (not expression) values.
This is represented in #9439 by mbgl::style::expression::LiteralExpression. Note that LiteralExpression(..., vector<Value>) does, indeed, iterate over array elements to decide an array literal's type (by way of typeOf(value)), so converting an NSArray node to LiteralExpression should be no problem, as long as we can construct a vector<mbgl::style::expression::Value> convertedValue from the NSArray.
Ah, I missed the distinction between the array and literal expression types. đ
In the JSON syntax, a literal array is created with
["literal", [x, y, z]], where _x, y, z_ are treated as literal (not expression) values.
In the NSExpression system, an NSArray technically normally holds NSExpressions â this is what youâd get with the inline { x, y, z } format string syntax. However, itâs certainly feasible to pull constant values out of NSExpressions and raise an exception for anything that canât be converted into a constant value.
This article mentions a couple flaws in NSExpressionâs design. Iâve already referred to the klunky function call syntax above, but this article also points out that the ** operator (for exponentiation) is left-binding in NSExpression. This is consistent with Excel and Matlab but inconsistent with normal written convention. So a developer might try to write 232 as 2 ** 3 ** 2, whereas they need to write it as 2 ** (3 ** 2). I donât think itâs a huge problem, but itâs something to document clearly.
The
CAST()function will probably be news to most iOS developers who are otherwise familiar with expressions.
Apparently CAST() can only be used on constant value expressions (aka literals), not key path expressions. So weâll need to implement type casting via a custom function.
One of the things Iâd like to improve ahead of the final release is simplifying the syntax for common operations like string concatenation and interpolation. The current syntax is rather arcane:
NSExpression(format: "FUNCTION('Old', 'stringByAppendingString:', FUNCTION(' ', 'stringByAppendingString:', 'MacDonald'))")
https://github.com/mapbox/mapbox-gl-native/pull/10726#issuecomment-353646221 proposes some alternatives:
Extend the private _NSPredicateUtilities class using the Objective-C runtime:
NSExpression(format: "mgl_join({'Old', ' ', 'MacDonald'})")
This would result in the most natural usage, but itâs also a bit Cleverâ˘. Weâll need to tiptoe around a bit to avoid directly using any private APIs, and weâll need to include safeguards in case the underlying NSExpression implementation changes at any point.
Overload operators in unconventional ways:
NSExpression(format: "'Old' & ' ' & 'MacDonald'")
This would only work for a limited number of operations. -expressionValueWithObject:context: will raise an exception on expressions that use these shortcuts. String concatenation is common enough that it would be worth overloading the bitwise and operator & as a shortcut for it. Itâs consistent with both Numbers and AppleScript. Studio is also likely to use & as a concatenation operator in its user-friendly expression syntax. On the other hand, weâll have a problem on our hands if the style specification ever defines a bitwise and operator.
Extend NSExpression with convenience methods and initializers:
NSExpression(forConstantValue: "Old")
.mgl_appending(NSExpression(forConstantValue: " "))
.mgl_appending(NSExpression(forConstantValue: "MacDonald"))
This is the most conventional approach, but itâs also very verbose â even moreso in Objective-C â and difficult to use in conjunction with format strings.
Ideally, weâd implement a combination of all three approaches. However, the added API surface area would be difficult to test comprehensively. We may also need to prioritize one approach over the others due to time constraints. Perhaps we can land #10726 first then work on two or three of these approaches in parallel.
/cc @mapbox/maps-ios @davidtheclark
I'm a fan of using a combination of # 1 (from https://github.com/mapbox/mapbox-gl-native/pull/10726#issuecomment-353646221) and # 3
For # 1, I agree that we'll want some kind of enemy tests or at least integrated tests which have the potential to alert us to changes in the classes we'd be relying on.
In https://github.com/mapbox/mapbox-gl-native/issues/8074#issuecomment-355383677, I used string concatenation as an example, but what should the custom function syntax for interpolation, steps, and coalescing look like? Normally, the function name needs to conform to Objective-C method naming conventions, because NSExpression looks up the function name as a method name on _NSPredicateUtilities when parsing (and calls that method when evaluating the expression). If the method takes multiple parameters, then it starts to depart from the familiar spreadsheet-like function syntax, as with the built-in modulus:by:(1, 2).
mgl_interpolateWithCurveType:parameters:stops: is currently the most verbose function name. Perhaps we can handwave a bit and rename it to INTERPOLATE, with the rationale that TERNARY, also in all-caps, doesnât name all its arguments.
Would we be able to offer something closer to the structure of MGLStyleFunction for categorical
and interval interpolation modes? Those functions have had the steepest learning curve for me, and may be challenging for users.
Currently, categorical interpolation mode looks something like this:
let colors: [UIColor] = [.orange, .red, .yellow, .blue]
layer.circleColor = NSExpression(format:
"TERNARY(FUNCTION(type, 'stringValue') = 'earthquake', %@, " +
"TERNARY(FUNCTION(type, 'stringValue') = 'explosion', %@, " +
"TERNARY(FUNCTION(type, 'stringValue') = 'quarry blast', %@, " +
"%@)))",
argumentArray: colors)
That's pretty intimidating when you are less familiar with NSExpression, and took me a while to suss out.
The convenience method could take a stops dictionary for the style and attribute values, then take the default value and attribute value type. Another possibility is to take a dictionary of options containing the default value and attribute value type. Within the method, we can then format and return the NSExpression.
let stops = [ "earthquake" : UIColor.orange, "explosion" : UIColor.red, "quarry blast" : UIColor.yellow]
layer.fillOpacity = MGLExpression(withCategories: [name : 1], forAttribute: "name", attributeValueType: "stringValue", withdefaultValue: 0)
Not sure how this would work with more complex expressions, but I implemented something along these lines in one of the examples.
Yes, what I currently have here is just a stopgap to get the tests to build. đ
I had hoped to do something like this as an intermediate step:
NSExpression(format: "%@[name]", stops)
Unfortunately, the subscripting syntax is only available for indexing into arrays. (NSExpression lacks a syntax for dictionaries as well.) Itâs a bit uglier, but for now you might be able to use this syntax instead:
NSExpression(format: "%@.%@", stops, NSExpression(forKeyPath: "name"))
Ultimately, the correct replacement for categorical functions is a case expression. I was held up for awhile by the lack of a good way to express a vararg with the custom function syntax, but the idea at the end of https://github.com/mapbox/mapbox-gl-native/issues/8074#issuecomment-355652845 unblocks that by removing the user expectation that the function would name all the arguments.
One issue that I ran into was with [NSExpression expressionForKeyPath:]. [NSObject valueForKeyPath:] is also an option (thanks to autocomplete). Using it in an example led to this exception:
Terminating app due to uncaught exception âNSUnknownKeyExceptionâ, reason: â[
valueForUndefinedKey:]: this class is not key value coding-compliant for the key height.â
Oof, yes, I think this could be a bit of a trap for people who:
I donât think thereâs anything we can do about this confusion at a technical level, since NSExpression is a Foundation class and the parameter to -expressionForKeyPath: is necessarily freeform. But we can sprinkle ânot to be confused withâ in guides and examples to head off this potential problem.
Extend the private _NSPredicateUtilities class using the Objective-C runtime
Tracking in #11015.
Extend NSExpression with convenience methods and initializers
Tracking in #11016.
Ultimately, the correct replacement for categorical functions is a
caseexpression.
Tracking case in #11007 and the even more streamlined match in #11009.
Overload operators in unconventional ways
Tracking in #11053.
I have problem to customise polyline , width and color . Please see the question in the Stackoveflow.
Most helpful comment
I'm a fan of using a combination of # 1 (from https://github.com/mapbox/mapbox-gl-native/pull/10726#issuecomment-353646221) and # 3
For # 1, I agree that we'll want some kind of enemy tests or at least integrated tests which have the potential to alert us to changes in the classes we'd be relying on.