Skip to content
This repository was archived by the owner on Aug 8, 2023. It is now read-only.

Refactor: use mbgl::style::conversion system to convert MGLStyleFunction to mbgl core types#8026

Merged
anandthakker merged 25 commits into
masterfrom
mgl-function-converstion
Feb 15, 2017
Merged

Refactor: use mbgl::style::conversion system to convert MGLStyleFunction to mbgl core types#8026
anandthakker merged 25 commits into
masterfrom
mgl-function-converstion

Conversation

@anandthakker
Copy link
Copy Markdown
Contributor

Quoting from #7944 (comment) (see the thread there for a bit more context):

Followup on #7944 (comment): As I started digging into resolving the compilation issues in the MGLCompositeStyleFunction => mbgl::style::CompositeFunction conversion today, it felt like I was solving the same problems that are being solved by mbgl::style::conversion::*.

I'm now thinking that we should accomplish the MGL => mbgl conversion — at least for property values — by having the darwin platform bindings implement the Value conversions required to leverage the existing mbgl::style::conversion system. This will hopefully mean less code/logic duplication and better maintainability.


The approach I'm going for here is for MGLStyleValueTransformer::toDataDrivenPropertyValue() to:

  1. Use the new NSObject * MGLStyleValueTransformer::toRawStyleSpecValue(MGLStyleFunction<T> *value) method to convert an MGLStyleFunction to an plain NSObject-based hierarchy isomorphic to the style spec JSON for the given style function value.
  2. Stuff that into a MGLConversionValue, whose job is simply to be a convenience wrapper for the mbgl::style::conversion overloads provided in darwin_conversion.hpp.
  3. Having implemented these base overloads, we can now just call convert<mbgl::style::DataDrivenPropertyValue<MBGLType>>(value) and it "just works". (More on the conversion system here)

cc @1ec5 @boundsj - (note this is still WIP; happy to have early course corrections if you want to peek, but feel free to just ignore for now and I'll ping when it's ready)

@mention-bot
Copy link
Copy Markdown

@anandthakker, thanks for your PR! By analyzing this pull request, we identified @boundsj, @1ec5 and @jfirebaugh to be potential reviewers.

@anandthakker
Copy link
Copy Markdown
Contributor Author

anandthakker commented Feb 11, 2017

Oh, another note: my intent was to keep this PR/refactor scoped to handling StyleFunction types (because those are the ones currently presenting difficulties / blocking my symbol DDS work). But once it's working, propagating this approach to the other MGL => mbgl conversions should be pretty much as simple as implementing toRawStyleSpecValue for each MGL type.

template <class Fn>
optional<Error> eachMember(const MGLConversionValue&, Fn&&) {
// TODO
// mbgl::Log::Warning(mbgl::Event::Android, "eachMember not implemented");
Copy link
Copy Markdown
Contributor Author

@anandthakker anandthakker Feb 11, 2017

Choose a reason for hiding this comment

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

(note to self) Here, and in some other places, need to raise/log an error in whatever way is "right" for this SDK/project.

Copy link
Copy Markdown
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

This is a good idea for avoiding repetition and modularizing the code. The multistep process is a clever way to work around the fact that C++ method overloading is unable to account for Objective-C class inheritance.

Most of the feedback below pertains to Objective-C concepts. I encourage you to read the linked articles at some point, but a general introduction to the language would help to explain some of the quirkiness you see in any Objective-C++ codebase.

//
// Created by Anand Thakker on 2/10/17.
// Copyright © 2017 Mapbox. All rights reserved.
//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In this repository, we typically remove the comment block that Xcode adds by default. Authorship can be determined from commit history and copyright information is explained in excruciating detail in LICENSE.md.

//

#ifndef MGLConversionValue_h
#define MGLConversionValue_h
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Neither guard defines nor #pragma once is needed in Objective-C headers, because #import is smart enough to avoid including twice.


class MGLConversionValue {
public:
Value(NSObject * _value) : value(_value) {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the predominant Objective-C style leaves no space between * and the identifier.

};

private:
NSObject * value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use id rather than NSObject * when you don’t want to constrain the variable’s class at all. The compiler won’t bother performing type checking on id. Moreover, unlike void *, id must be an Objective-C object. As a result, as long as you use [bracket syntax] instead of dot.syntax, you can pass any message you like to the id without casting.

(Technically, id also allows the object to be an NSProxy; however, proxies are quite rare in practice.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems like we do want to constrain the object's class here, though, since we're making assumptions about value being an instance of the NSObject class hierarchy -- no?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For all practical purposes, NSObject and NSProxy are the only two root classes in Objective-C. If you don't care about the difference between NSObject and NSProxy – you may never encounter any reason to care about this distinction – then id is tantamount to NSObject * except with the convenience of not having to cast to a more specific type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see 👍

Value(NSObject * _value) : value(_value) {}

bool isNull() const {
return value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to be returning true if the value is non-null. Moreover, in some existing code that converts from Foundation types to C++ types, we also treat NSNull as a null value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🙈

/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
17C4564F1E4E19BB0029C9B1 /* darwin_conversion.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = darwin_conversion.hpp; sourceTree = "<group>"; };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remember to add this file to macos.xcodeproj as well, since all the runtime styling code is shared between both platforms.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

return value.getLength();;
}

inline mbgl::android::Value arrayMember(const MGLConversionValue& value, std::size_t i) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you mean mbgl::Value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Heh, actually it should have been MGLConversionValue.

};

Value get(const char* key) const {
NSDictionary *dict = (NSDictionary *)value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it guaranteed that this method will only be called if isObject() returns true? Even if so, it might be worth asserting that we’re dealing with an NSDictionary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's guaranteed in the spec, but agreed on asserting anyway.

return { value.toString() };
} else if (value.isNumber()) {
// Need to cast to a double here as the float is otherwise considered a bool...
return { (double) value.toNumber() };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In C++ and Objective-C++, the static_cast syntax allows the compiler to warn you when you try to cast to an unrelated type. A C-style cast, by contrast, allows you to cast to any type with abandon.

return { value.toString() };
} else if (value.isNumber()) {
// Need to cast to a double here as the float is otherwise considered a bool...
return { (double) value.toNumber() };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could MGLConversionValue::toNumber() return a double in the first place, avoiding the need for this cast? Converting float to double often leaves unwanted artifacts at the smallest decimal places.

@1ec5 1ec5 added iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS refactor runtime styling labels Feb 11, 2017
@1ec5 1ec5 added this to the ios-v3.5.0 milestone Feb 11, 2017
@anandthakker
Copy link
Copy Markdown
Contributor Author

@1ec5 Thank you for this extremely helpful review-slash-Objective-C-lesson!

@anandthakker anandthakker force-pushed the mgl-function-converstion branch from f679f55 to 204ebb7 Compare February 13, 2017 18:13
@@ -0,0 +1,68 @@
#include <string>

class MGLConversionValue {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the purpose of MGLConversionValue in this PR, as opposed to inlining each of its methods in the corresponding method in darwin_conversion.hpp? E.g.:

inline bool isUndefined(NSObject *value) {
    return !value || value == [NSNull null];
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jfirebaugh hah -- yeah, this is exactly the question I started asking myself this morning: #8026 (comment). I started out with this wrapper because I didn't know at the time that a fully/cleanly NSObject-based hierarchy would be possible. If you agree that the wrapper's unnecessary, I'd love to remove it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good point – we aren't actually using method overloading here, so the fact that both bool and float are encapsulated in NSNumber just means the implementation of isBool() and isNumber() would involve an ||.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So this file should be removed now, right?

@anandthakker
Copy link
Copy Markdown
Contributor Author

@1ec5 @jfirebaugh okay, I think this should now be ready for a (re-)review. Two outstanding issues:

  1. One failing unit test--MGLStyleValueTests::testFunctionsWithDataDrivenProperties--due to precision loss in color values -- this happens because, in order to use the conversion subsystem, we end up roundtripping the color value through an "rgb(...)" string -- so [..., 0.5, ...] becomes "rgb(..., 127.5, ...)" becomes [..., 0.498039,...].
  2. Per Refactor: use mbgl::style::conversion system to convert MGLStyleFunction to mbgl core types #8026 (review), remove MGLConversionValue, as it is an unnecessary level of indirection. (I just tried this locally and am getting some mysterious error '__val__' is unavailable in ARC. Storing that on this temporary branch till tomorrow AM when I'll take another look.)

@jfirebaugh
Copy link
Copy Markdown
Contributor

"rgb(...)" strings don't limit the precision of their components, do they? Seems like we should be able to fix the precision loss.

@jfirebaugh
Copy link
Copy Markdown
Contributor

Oh, I see, the issue isn't the stringification, it's that the conversion back goes through CSSColorParser::Color which uses unsigned char components. We don't guarantee more precision than that in other parts of the rendering, so I think this is something that the test will have to adjust for.

@1ec5
Copy link
Copy Markdown
Contributor

1ec5 commented Feb 14, 2017

some mysterious error 'val' is unavailable in ARC

ARC is unable to reason about references like NSObject *&. With an Objective-C object, you should only ever need to use * or more rarely **, never & or *&.

@anandthakker
Copy link
Copy Markdown
Contributor Author

ARC is unable to reason about references like NSObject *&. With an Objective-C object, you should only ever need to use * or more rarely **, never & or *&.

Ah. Yeah, the *& did seem weird and unnecessary even when I was typing it. But, alas, changing the types of the function parameters to the more sensical NSObject * doesn't solve the problem -- I still get the ARC error, and I think it's because we need an optional<NSObject *> -- the error log references a line from std::experimental::optional that looks like:

    _LIBCPP_INLINE_VISIBILITY constexpr optional(const value_type& __v)

So it seems like trying to make an optional<NSObject *>, we're still stuck with NSObject *& under the hood.

@jfirebaugh @1ec5 does this mean we basically need to stick with having a wrapper class? Or is there another way out?

@anandthakker
Copy link
Copy Markdown
Contributor Author

Added an accuracy threshold to the color equality checks per #8026 (comment). @1ec5 as those were my first lines of Swift, I would imagine that I've violated any number of conventions/idioms, heh.

@1ec5
Copy link
Copy Markdown
Contributor

1ec5 commented Feb 14, 2017

So it seems like trying to make an optional<NSObject *>, we're still stuck with NSObject *& under the hood.

It’s already a pointer, so normally you’d the absence of a value with nil. If the conversion system is insisting on using optional, then yes, we probably need the wrapper class.

@anandthakker
Copy link
Copy Markdown
Contributor Author

anandthakker commented Feb 14, 2017

It’s already a pointer, so you can indicate the absence of a value with nil. If the conversion system is insisting on using optional, then yes, we probably need the wrapper class.

Yep, it expects optional<>s from some of the conversion functions: https://github.com/mapbox/mapbox-gl-native/blob/master/include/mbgl/style/conversion.hpp#L47-L52



#if os(iOS) || os(watchOS) || os(tvOS)
typealias ColorType = UIColor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In Objective-C code, we’ve been using MGLColor as the name of the platform-agnostic color type.

#endif

extension MGLStyleValueTests {
func assertColorsEqualWithAccuracy(_ actual: ColorType, _ expected: ColorType, accuracy: Float = 0.1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just an observation, not a request for change: Swift lets you overload methods like in C++. So you could overload XCTAssertEqualWithAccuracy(_:_:accuracy:) with two MGLColor arguments. Copying the existing XCTAssertEqualWithAccuracy(_:_:accuracy:) signature, you’d wind up with:

func XCTAssertEqualWithAccuracy<T : FloatingPoint>(_ color1: MGLColor, _ color2: MGLColor, accuracy: T, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line)

With this fancy signature, you could pass file and line into the calls to the normal XCTAssertEqualWithAccuracy(_:_:accuracy:) and get a failure message that refers to the original line number.



#if os(iOS) || os(watchOS) || os(tvOS)
typealias ColorType = UIColor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With this typealias in place, it might be possible to unconditionalize the giant #if block in testFunctionsWithDataDrivenProperties().

#endif

extension MGLStyleValueTests {
func assertColorsEqualWithAccuracy(_ actual: ColorType, _ expected: ColorType, accuracy: Float = 0.1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is 0.1 all the accuracy we can guarantee?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh, whoops, I meant for that to be 0.01, although I guess 0.005 is maybe a better, as a roundish number close to 1/255.

Copy link
Copy Markdown
Contributor

@1ec5 1ec5 Feb 14, 2017

Choose a reason for hiding this comment

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

= 1/255 also compiles.

if let actualConstant = actual as? MGLStyleConstantValue<ColorType> {
XCTAssertTrue(expected is MGLStyleConstantValue<ColorType>)
assertColorsEqualWithAccuracy(actualConstant.rawValue, (expected as! MGLStyleConstantValue<ColorType>).rawValue)
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It looks like no code would get executed beyond this if statement, so no return statement is needed.

// TODO: assert default values are equal
if actualFunction.stops == nil {
XCTAssertNil(expectedFunction.stops)
return
Copy link
Copy Markdown
Contributor

@1ec5 1ec5 Feb 14, 2017

Choose a reason for hiding this comment

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

Use a guard … else statement for an early return. (Accentuate the positive!) Better yet, use a guard let … else statement for an early return conditioned on the existence of a variable you want to make use of below.

Copy link
Copy Markdown
Contributor Author

@anandthakker anandthakker Feb 14, 2017

Choose a reason for hiding this comment

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

Ooh, guard let is so nice!

@@ -0,0 +1,68 @@
#include <string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also import any Objective-C headers you need, namely the Foundation/Foundation.h umbrella header. You’re getting away with omitting them right now because you happen to be importing this header after other headers in other files.

return [array count];
};

MGLConversionValue get(const std::size_t index ) const {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

size_t isn’t explicitly defined to be the same size as NSUInteger (the type used to subscript NSArray below), even though I think that happens to be the case. To avoid even the potential of an out-of-bounds array access, you could assert that index is at most NSUIntegerMax.

#import "NSValue+MGLStyleAttributeAdditions.h"
#import "MGLTypes.h"

#include "darwin_conversion.hpp"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The convention for our predominantly Objective-C targets is to use .h for Objective-C++ headers. We also use MGLCamelCase for these headers.

@@ -0,0 +1,132 @@
#pragma once
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As long as this header is #imported, no #pragma once guard is needed.

#include <mbgl/util/logging.hpp>
#include <mbgl/style/conversion.hpp>
#include <mbgl/util/feature.hpp>
#include <mbgl/util/optional.hpp>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also import the Foundation/Foundation.h umbrella header.

}

NSObject* operator*() {
assert(this);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use NSCAssert() instead of assert() for files that are part of the Objective-C targets. NSCAssert() provides better diagnostics via NSException and works with Xcode’s built-in Objective-C exception breakpoint.

// due to things like `optional(const value_type& __v)`
class OptionalNSObjectValue {
public:
OptionalNSObjectValue(NSObject *_value) : value(_value) {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Annotate value’s type as _Nullable and surround the header’s contents in NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END to enable better compiler/analyzer diagnostics.

return value;
}

NSObject* operator*() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: move the space to before the *.


inline std::size_t arrayLength(const NSObject* value) {
NSCAssert([value isKindOfClass:[NSArray class]], @"Value must be an NSArray for getLength().");
NSArray * array = (NSArray *)value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’d still encourage you to explore the use of id instead of NSObject *. Saying “NSObject” here tells the compiler (and the code reviewer) that the function wants an NSObject specifically, not some subclass, which is why you end up having to cast all the time.

return { *toString(value) };
} else if (_isNumber(value)) {
// Need to cast to a double here as the float is otherwise considered a bool...
return { (double) *toNumber(value) };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use static_cast instead of a C-style cast.

class OptionalNSObjectValue {
public:
OptionalNSObjectValue(NSObject *_value) : value(_value) {}
OptionalNSObjectValue() {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Omit this constructor...

if (value && value != [NSNull null]) {
return { member };
} else {
return {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

...and return { nullptr }; here.

}

template <class Fn>
optional<Error> eachMember(const NSObject*, Fn&&) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't define this method at all. Then you'll get a compiler error if something tries to use it.

Copy link
Copy Markdown
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

This is looking pretty good, especially on the C++ side. assertColorValuesEqual(_:_:) could use some more massaging, but you’re almost over the finish line.

Comment thread platform/darwin/src/MGLConversion.h Outdated

// A minimal wrapper class conforming to the requirements for `objectMember(v, name)` (see mbgl/style/conversion.hpp)
// This is necessary because using `NSObject*` as the value type in `optional<NSObject*>` causes problems for the ARC,
// due to things like `optional(const value_type& __v)`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like a documentation comment. You can use the /** … */ syntax for a multiline documentation comment or the /// … syntax for a single-line documentation comment.

Comment thread platform/darwin/src/MGLConversion.h Outdated
NSCAssert([value isKindOfClass:[NSArray class]], @"Value must be an NSArray for getLength().");
NSArray *array = value;
auto length = [array count];
NSCAssert(length < std::numeric_limits<size_t>::max(), @"Array length out of bounds.");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be pedantic, shouldn’t a length equal to the maximum representable length also be allowable? 😉

Comment thread platform/darwin/src/MGLConversion.h Outdated
inline OptionalNSObjectValue objectMember(const id value, const char *key) {
NSCAssert([value isKindOfClass:[NSDictionary class]], @"Value must be an NSDictionary for get(string).");
NSDictionary *dict = value;
NSObject *member = dict[@(key)];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: subscript notation in this case is shorthand for a call to -[NSDictionary objectForKey:]. You can avoid creating a separate dict variable by calling -objectForKey: on the id-typed value.

Comment thread platform/darwin/src/MGLConversion.h Outdated
NSCAssert([value isKindOfClass:[NSArray class]], @"Value must be an NSArray for get(int).");
NSCAssert(i < NSUIntegerMax, @"Index must be less than NSUIntegerMax");
NSArray *array = value;
return array[i];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: subscript notation in this case is shorthand for a call to -[NSArray objectAtIndex:]. You can avoid creating a separate array variable by calling -objectAtIndex: on the id-typed value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 @1ec5 I take it that the purpose of doing this would be for code conciseness, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes. It matters more for more complex functions where you’d otherwise potentially have to keep both variables in sync.

Comment thread platform/darwin/src/MGLConversion.h Outdated

inline optional<std::string> toString(const id value) {
if (_isString(value)) {
return std::string([((NSString *)value) UTF8String]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: extra parentheses around value make the line harder to read. Incidentally, you can also write:

std::string((const char *)[value UTF8String])

Copy link
Copy Markdown
Contributor Author

@anandthakker anandthakker Feb 15, 2017

Choose a reason for hiding this comment

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

In the form you suggested, would static_cast be preferable? If not, why not?

}

func assertColorValuesEqual(_ actual: MGLStyleValue<MGLColor>, _ expected: MGLStyleValue<MGLColor>) {
if let actualConstant = actual as? MGLStyleConstantValue<MGLColor> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is fine, but FYI you can avoid having to come up with a unique name for the coerced value by shadowing the original variable’s name:

if let actual = actual as? MGLStyleConstantValue<MGLColor> 

let expectedStops = expectedFunction.stops!
XCTAssertEqual(actualStops.keys.count, expectedStops.keys.count)
let actualKeys = actualStops.keys.sorted(by: { String(describing: $0) < String(describing: $1) })
let expectedKeys = expectedStops.keys.sorted(by: { String(describing: $0) < String(describing: $1) })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can assert that actualStops.keys == expectedStops.keys to check both the count and the existence of identical pairs at the same time.

}
let expectedStops = expectedFunction.stops!
XCTAssertEqual(actualStops.keys.count, expectedStops.keys.count)
let actualKeys = actualStops.keys.sorted(by: { String(describing: $0) < String(describing: $1) })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Normally, you can call the operator directly instead of writing a new closure:

let actualKeys = actualStops.keys.sorted(by: <)

This syntax relies on type inference to choose the right operator. I’m not sure if it’s a problem that the compiler thinks the keys are of type Any rather than Comparable.

The original keys are Strings in some cases and NSNumbers (or perhaps Floats?) in other cases. With NSNumber, you’d probably need .floatValue or at least as Float. With the current code, you’re going to wind up with some weird sorting in the case of NSNumber. For example, "100" < "2". Fortunately, there shouldn’t be a problem as long as both arrays are sorted consistently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah -- I originally tried just using < as the sort function, but it seemed to think that the keys were of type Any. I landed at the same conclusion as you: that the weird alphanumeric sort wouldn't matter here... but, alas, I think we should probably figure this key type thing out in more detail -- if the conversion stuff is turning numeric stop values into string values, that's a bug, and comparing stringified values of the keys like we're doing here would occlude it.

Copy link
Copy Markdown
Contributor

@1ec5 1ec5 Feb 15, 2017

Choose a reason for hiding this comment

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

What if?

var actualKeys: [Any]
if let keys = actualStops.keys as? [String] {
    actualKeys = keys.sorted(by: <)
} else if let keys = actualStops.keys as? [Float] {
    actualKeys = keys.sorted(by: <)
} else {
    XCTFail("Stop keys must be either Strings or Floats.")
}

let actualKeys = actualStops.keys.sorted(by: { String(describing: $0) < String(describing: $1) })
let expectedKeys = expectedStops.keys.sorted(by: { String(describing: $0) < String(describing: $1) })

for (ak, ek) in zip(actualKeys, expectedKeys) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The documentation for Array.values says:

When iterated over, values appear in this collection in the same order as they occur in the dictionary’s key-value pairs.

So it might be possible to say zip(actual.values, expected.values) to avoid the relative inefficiency of subscripting both dictionaries for each key.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's not clear to me from that documentation that two different dictionaries' .values would be in a consistent order... indeed, why would they?

let expectedFunction = expected as! MGLStyleFunction<MGLColor>
XCTAssertEqual(actualFunction.interpolationBase, expectedFunction.interpolationBase)
XCTAssertEqual(actualFunction.interpolationMode, expectedFunction.interpolationMode)
// TODO: assert default values are equal
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this still need to happen?

template <class T, class V>
Result<optional<T>> convertDefaultValue(const V& value) {
auto defaultValueValue = objectMember(value, "defaultValue");
auto defaultValueValue = objectMember(value, "default");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you investigate why this mistake didn't trigger test failures? Contrary to my earlier statement, we do have tests which I would have expected to fail due to this, added in mapbox/mapbox-gl-js@e328b80.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 yep, will do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jfirebaugh The reason it didn't trigger failures is that the tests are marked ignored on native. I'll submit a PR to gl-js to un-ignore them.

@anandthakker
Copy link
Copy Markdown
Contributor Author

@1ec5 updated to address the issues you raised, including an attempt to clean up the assertColorValuesEqual. Note that in the end, I found using .keys to require much more weird acrobatics than just switching on the type of the dict itself -- but lemme know what you think.

@anandthakker anandthakker force-pushed the mgl-function-converstion branch from 5fd7cf5 to 9a1094b Compare February 15, 2017 21:22
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS refactor runtime styling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants