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

Add a ToString function to create human-readable debug descriptions #2384

Merged
merged 15 commits into from
Feb 15, 2019

Conversation

var-const
Copy link
Contributor

@var-const var-const commented Feb 13, 2019

The motivation is to avoid writing lots of boilerplate for containers.

using value_type = bool;

enum { value = sizeof(Choose(Pointer())) == sizeof(yes_type) };
struct is_objective_c_pointer : std::is_convertible<T, id> {};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is equivalent to the previous implementation (and the tests pass), but I'll run it by Gil before merging.

Copy link
Contributor

Choose a reason for hiding this comment

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

If TEST(TypeTraitsTest, IsObjectiveCPointer) passes I'm all for having less code.

Could this be a using statement? Something like using is_objective_c_pointer = std::is_convertible<T, id>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, good idea.

@var-const
Copy link
Contributor Author

Also removed the conversion from DocumentKey to FSTDocumentKey. I noticed that ToString takes performing the conversion and calling description: on that over calling ToString member function. This could be changed in the implementation, but I found the conversion surprising and unexpected, so I preferred to simply eliminate it.

@rsgowman
Copy link
Member

Also removed the conversion from DocumentKey to FSTDocumentKey.

Change seems reasonable, but mostly unrelated to this PR. I'd prefer if it was separate.

I noticed that ToString takes performing the conversion and calling description: on that over calling ToString member function. This could be changed in the implementation,

See my comment. tl;dr: I kinda think ToString member function should take priority regardless of this particular issue. (But it's your call.)

but I found the conversion surprising and unexpected, so I preferred to simply eliminate it.

Well, we, explicitly added that at some point for ease of porting. The last commit in this PR otherwise adds a bunch of boilerplate back in (though makes things more obvious). I'm neutral as to where we end up on that trade-off, but if we do that with this type, we should probably do that will all of them. Either way, that discussion's probably better served via a separate PR. (... unless this is the only type where we do that?)

#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_

#if !defined(__OBJC__)
#error "This header only supports Objective-C++"
Copy link
Member

Choose a reason for hiding this comment

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

You could split this into to_string.h and to_string_apple.h (or do some ifdef magic around the objc specificities). There's no real urgent need to do so, but it'd make this useful from the pure c++ world too.

Optional. If you do decide to do this, doing this as a separate PR would be reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't bother splitting. Just add #if defined(__OBJC__) around the relevant parts.

Using a mechanism like the one using FormatChoice in string_format.h makes this easier to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done with ifdefs.

{1, "foo"},
{2, "bar"}
* };
* assert(ToString(m) == "{1: foo, 2: bar}");
Copy link
Member

Choose a reason for hiding this comment

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

An unordered map might be interesting... (Or at least difficult to test. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (sort of).

Copy link
Member

Choose a reason for hiding this comment

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

Ha. That's a pretty good way to dodge the issue. :)

using MapT = SortedMap<int, std::string>;
MapT sorted_map = MapT{}.insert(1, "foo").insert(2, "bar");
EXPECT_EQ(ToString(sorted_map), "{1: foo, 2: bar}");
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider SortedSet too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{2, DocumentKey({"foo", "baz"})},
};
EXPECT_EQ(ToString(key_map), "{1: foo/bar, 2: foo/baz}");
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider some of the other standard containers. (You could test vector, but it's pretty well covered by the others. list seems somewhat uninteresting. But maybe set?)

As mentioned above, the unordered containers would be challenging to test. You could do some regex magic, but it's probably not worth it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added. I cheated by using unoredred_multimap.

* `std::string`;
*
* - otherwise, if `value` defines a member function called `ToString`, the
* description is created by invoking the function;
Copy link
Member

Choose a reason for hiding this comment

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

If a type is both a (custom) container and defines ToString, then this priority list implies that ToString won't be called. It would seem like ToString() should be given the highest priority rather than the lowest?

Regardless of where you end up on the above question, if you care about this ordering, consider adding a test for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. The ordering is now:

  • ToString
  • description
  • conversion to std::string;
  • assoc. container;
  • container;
  • std::to_string.

What do you think?

Test -- added a simple test. I mostly care about the ordering between associative and simple containers, because it's the only one that will always "conflict", but it's implicitly tested already.

Copy link
Member

Choose a reason for hiding this comment

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

New ordering seems reasonable. And yeah, the ordering between assoc/simple containers is definitely already tested. The one I was referring to, was the ToString + container ordering, which you've now added. ltgm.

@rsgowman rsgowman assigned var-const and unassigned rsgowman Feb 13, 2019
Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

but I found the conversion surprising and unexpected, so I preferred to simply eliminate it.

Well, we, explicitly added that at some point for ease of porting. The last commit in this PR otherwise adds a bunch of boilerplate back in (though makes things more obvious). I'm neutral as to where we end up on that trade-off, but if we do that with this type, we should probably do that will all of them. Either way, that discussion's probably better served via a separate PR. (... unless this is the only type where we do that?)

We added this initially because FSTDocumentKey was so prevalent and the implicit conversions were a crutch that made incremental progress possible. Implicit conversions are otherwise undesirable so if we're at a state where we can reasonably remove them I'm all for it.

@@ -717,6 +719,7 @@
54740A561FC913EB00713A1A /* util */ = {
isa = PBXGroup;
children = (
B68B1E002213A764008977EF /* to_string_apple_test.mm */,
Copy link
Contributor

Choose a reason for hiding this comment

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

If you run scripts/sync_project.rb does this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -17,6 +17,7 @@
#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_
#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_

#include <type_traits>
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean to include util/type_traits.h?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leftover, sorry. I tried adding some static_asserts, but that didn't turn out to be very useful, because compilation attempts proceed after hitting a static_assert, drowning it in a pool of useless noise.

Firestore/core/src/firebase/firestore/util/to_string.h Outdated Show resolved Hide resolved
Firestore/core/src/firebase/firestore/util/to_string.h Outdated Show resolved Hide resolved
// Fallback

template <typename T>
std::string ToStringDefault(const T& value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You're mixing "ToString" as a prefix and as a suffix in these member names. Shouldn't these be DefaultToString and CustomToString to match the others below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor Author

@var-const var-const left a comment

Choose a reason for hiding this comment

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

@rsgowman I mainly did the DocumentKey change in this PR because FSTDocumentKey converts to a string differently, and I didn't want to modify the tests. Let me know if you feel strongly about this, but it's easier to make both changes in a single PR.

using MapT = SortedMap<int, std::string>;
MapT sorted_map = MapT{}.insert(1, "foo").insert(2, "bar");
EXPECT_EQ(ToString(sorted_map), "{1: foo, 2: bar}");
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{2, DocumentKey({"foo", "baz"})},
};
EXPECT_EQ(ToString(key_map), "{1: foo/bar, 2: foo/baz}");
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added. I cheated by using unoredred_multimap.

* `std::string`;
*
* - otherwise, if `value` defines a member function called `ToString`, the
* description is created by invoking the function;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. The ordering is now:

  • ToString
  • description
  • conversion to std::string;
  • assoc. container;
  • container;
  • std::to_string.

What do you think?

Test -- added a simple test. I mostly care about the ordering between associative and simple containers, because it's the only one that will always "conflict", but it's implicitly tested already.

{1, "foo"},
{2, "bar"}
* };
* assert(ToString(m) == "{1: foo, 2: bar}");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done (sort of).

@@ -17,6 +17,7 @@
#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_
#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_

#include <type_traits>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leftover, sorry. I tried adding some static_asserts, but that didn't turn out to be very useful, because compilation attempts proceed after hitting a static_assert, drowning it in a pool of useless noise.

Firestore/core/src/firebase/firestore/util/to_string.h Outdated Show resolved Hide resolved
Firestore/core/src/firebase/firestore/util/to_string.h Outdated Show resolved Hide resolved
// Fallback

template <typename T>
std::string ToStringDefault(const T& 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.

Done.

using value_type = bool;

enum { value = sizeof(Choose(Pointer())) == sizeof(yes_type) };
struct is_objective_c_pointer : std::is_convertible<T, id> {};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, good idea.

@var-const var-const assigned rsgowman and wilhuff and unassigned var-const Feb 13, 2019
@rsgowman
Copy link
Member

I mainly did the DocumentKey change in this PR because FSTDocumentKey converts to a string differently, and I didn't want to modify the tests. Let me know if you feel strongly about this, but it's easier to make both changes in a single PR.

That's seems reasonable. (I was reading it as almost entirely unrelated.)

@rsgowman rsgowman removed their assignment Feb 13, 2019
Firestore/core/src/firebase/firestore/util/to_string.h Outdated Show resolved Hide resolved
using model::DocumentKey;

TEST(ToStringTest, StdToString) {
EXPECT_EQ(ToString(123), "123");
Copy link
Contributor

Choose a reason for hiding this comment

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

std::to_string isn't guaranteed to be specialized for all types, so I would expect this to fail for booleans and any trivial implementation would accidentally capture non-Objective-C pointer types. Please add tests.

StringFormat handles this case specially in order to give reasonable formats for things that are exactly bool but not convertible to bool: https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/core/src/firebase/firestore/util/string_format.h#L72

Ideally we'd have similar behavior here.

This brings me to what is perhaps an unfair criticism of this change: it's unfortunate that this implementation is completely separate from StringFormat. Is there any way to reuse FormatArg here so that we don't need to re-implement all these transformations here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I'm delegating to FormatArg instead of std::to_string now, what do you think? Also, added a test -- let me know if you can think of any additional cases worth testing.

* - if `value` defines a member function called `ToString`, the description is
* created by invoking the function;
*
* - (in Objective-C++ only) otherwise, if `value` is an Objective-C class, the
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment isn't quite right. I think you mean "if value is an Objective-C object" rather than "class" since it's the type of the instance that's interesting rather than just classes themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I think it was supposed to be "if value is of an Objective-C class", but it's probably awkward, so I used your suggestion.

// Objective-C class

template <typename T>
std::string ObjCToString(const T& value, std::false_type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a little hard to follow the precedence in here because CustomToString changes its definition based on __OBJC__. Do you think it would be clearer to always define ObjcToString but if not __OBJC__ always delegate down?

Something like this (untested):

template <typename T, typename Tag>
 std::string ObjCToString(const T& value, Tag tag) {
   return StringToString([value description], tag);
 }

That may not work though because tag dispatch may get defeated in the presence of an actual value.

However, as I'm wrestling with this I end up coming back to the idea that the FormatChoice mechanism used in StringFormat ends up being clearer:

  • The choices read in the natural order
  • The conditions that affect whether or not a choice is viable are right above it (rather than in the caller)
  • Alternatives that don't apply on a platform can be harmlessly #ifed out without affecting readability
  • Only one method signature is required per level
  • There's no std::true_type and std::false_type tags to interpret.

The downside is std::enable_if but I think the price to pay isn't that bad, and we already have the type metamagic figured out--it's just a matter of moving it from the caller to the type declared above the alternative. Compare:

#if __OBJC__

template <typename T>
std::string ObjCToString(const T& value, std::false_type) {
  return StringToString(value, std::is_same<T, std::string>{});
}

template <typename T>
std::string ObjCToString(const T& value, std::true_type) {
  return MakeString([value description]);
}

template <typename T>
std::string CustomToString(const T& value, std::false_type) {
  return ObjCToString(value, is_objective_c_pointer<T>{});
}

#else

template <typename T>
std::string CustomToString(const T& value, std::false_type) {
  return StringToString(value, std::is_same<T, std::string>{});
}

#endif  // __OBJC__

vs

#if __OBJC__
template <
    typename T,
    typename = typename std::enable_if<is_objective_c_pointer<T>{}>::type>
std::string ToString(T object, internal::ToStringChoice<1>) {
  return MakeString([value description]);
}
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think it would be clearer to always define ObjcToString but if not __OBJC__ always delegate down?

Another way would be something like

template <typename T>
std::string ObjCToString(const T& value, std::false_type) {
  return StringToString(value, std::is_same<T, std::string>{});
}

#if __OBJC__
template <typename T>
std::string ObjCToString(const T& value, std::true_type) {
  return MakeString([value description]);
}
#endif

template <typename T>
std::string CustomToString(const T& value, std::false_type) {
  // This would require a different is_objective_c_pointer definition
  // in pure C++, or another ifdef block here
  return ObjCToString(value, is_objective_c_pointer<T>::value);
}

I don't have a strong preference on this mechanism versus FormatChoice. I agree with all your points except the last one (to me, there's no significant difference between FormatChoice<0> and std::true_type). It's certainly good for readability that the order isn't reversed and there's less boilerplate. On the other hand, the way FormatChoice works is more complicated. There's recursive inheritance, but more significantly, every function there is a potential candidate for the overload set, so it's necessary to remember to apply enable_if appropriately (but not always necessary, e.g., the const char* overload doesn't have it). Also, adding a new element to the chain would require modifying several indices as well as their total number (though it's not a huge deal since the number is small).

I tried out the FormatChoice approach, what do you think? Since I don't have a strong preference, I'll go with whichever you prefer.

Copy link
Contributor

@zxu123 zxu123 left a comment

Choose a reason for hiding this comment

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

LGTM

@zxu123 zxu123 assigned var-const and unassigned zxu123 Feb 14, 2019
@wilhuff wilhuff removed their assignment Feb 14, 2019
@var-const var-const assigned wilhuff and unassigned var-const Feb 14, 2019
Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

LGTM.


} // namespace impl

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Documentation comments are usually better off on the declaration than the definition when they're separate.

std::string contents = absl::StrJoin(
value, ", ", [](std::string* out, const typename T::value_type& kv) {
out->append(
StringFormat("%s: %s", ToString(kv.first), ToString(kv.second)));
Copy link
Contributor

Choose a reason for hiding this comment

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

StringFormat is pretty heavyweight for this, and it would be useful to make these implementations not mutually recursive. Consider just using three append calls.

@var-const var-const assigned var-const and unassigned wilhuff Feb 15, 2019
@var-const var-const merged commit 5efbb08 into master Feb 15, 2019
bstpierr added a commit that referenced this pull request Feb 19, 2019
* master:
  E2e tests split (#2401)
  Split auth api tests into multiple cases (#2399)
  Add a ToString function to create human-readable debug descriptions (#2384)
  Split Auth unit tests (#2396)
  Create umbrella header for FIAM (#2392)
  Deprecate setMinimumSessionInterval (#2305)
  Forbid queries endAt an uncommitted server timestamp. (#2382)
  update changelog for release (#2383)
  Fix FIAM Travic CI issues (#2380)
  Remove swift sample for Auth (#2371)
  C++ migration: port `FSTTransaction` (#2362)
  Fix a bug where unlinking emailpassword doesn’t remove provider data (#2370)
  Fix a bug where sign in with Game Center doesn’t return additional user info (#2368)
  Firebase In-app messaging callbacks (#2354)
  Fix a bug where sign in with email link always return isNewUser as false (#2363)
  C++ migration: eliminate `FSTRemoteStore` (#2338)
@paulb777 paulb777 deleted the varconst/to-string branch October 2, 2019 19:20
@firebase firebase locked and limited conversation to collaborators Oct 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants