Remove EmptySequence exception in favor of optional returns
authorTom Jackson <tjackson@fb.com>
Mon, 20 Jul 2015 21:05:59 +0000 (14:05 -0700)
committerfacebook-github-bot-4 <folly-bot@fb.com>
Tue, 21 Jul 2015 23:52:35 +0000 (16:52 -0700)
Summary: Forces consumers to handle the empty sequence case instead of cumbersome exceptions.

Reviewed By: @​jrichey, @yfeldblum

Differential Revision: D2219505

folly/gen/Base-inl.h
folly/gen/Base.h
folly/gen/test/BaseTest.cpp

index fe3a78f45c479075cedf8e2638f5cfe2ab4b4b8a..0c4e92f4556959aff2f6a17d994eb595e86c9ae3 100644 (file)
@@ -1678,7 +1678,7 @@ class Cycle : public Operator<Cycle<forever>> {
   explicit Cycle(off_t limit) : limit_(limit) {
     static_assert(
         !forever,
-        "Cycle limit consturctor should not be used when forever == true.");
+        "Cycle limit constructor should not be used when forever == true.");
   }
 
   template <class Value, class Source>
@@ -1790,16 +1790,13 @@ class First : public Operator<First> {
   template <class Source,
             class Value,
             class StorageType = typename std::decay<Value>::type>
-  StorageType compose(const GenImpl<Value, Source>& source) const {
+  Optional<StorageType> compose(const GenImpl<Value, Source>& source) const {
     Optional<StorageType> accum;
     source | [&](Value v) -> bool {
       accum = std::forward<Value>(v);
       return false;
     };
-    if (!accum.hasValue()) {
-      throw EmptySequence();
-    }
-    return std::move(accum.value());
+    return accum;
   }
 };
 
@@ -1862,7 +1859,7 @@ class Reduce : public Operator<Reduce<Reducer>> {
   template <class Source,
             class Value,
             class StorageType = typename std::decay<Value>::type>
-  StorageType compose(const GenImpl<Value, Source>& source) const {
+  Optional<StorageType> compose(const GenImpl<Value, Source>& source) const {
     static_assert(!Source::infinite, "Cannot reduce infinite source");
     Optional<StorageType> accum;
     source | [&](Value v) {
@@ -1872,10 +1869,7 @@ class Reduce : public Operator<Reduce<Reducer>> {
         accum = std::forward<Value>(v);
       }
     };
-    if (!accum.hasValue()) {
-      throw EmptySequence();
-    }
-    return accum.value();
+    return accum;
   }
 };
 
@@ -1981,7 +1975,7 @@ class Min : public Operator<Min<Selector, Comparer>> {
             class StorageType = typename std::decay<Value>::type,
             class Key = typename std::decay<
                 typename std::result_of<Selector(Value)>::type>::type>
-  StorageType compose(const GenImpl<Value, Source>& source) const {
+  Optional<StorageType> compose(const GenImpl<Value, Source>& source) const {
     static_assert(!Source::infinite,
                   "Calling min or max on an infinite source will cause "
                   "an infinite loop.");
@@ -1994,10 +1988,7 @@ class Min : public Operator<Min<Selector, Comparer>> {
         min = std::forward<Value>(v);
       }
     };
-    if (!min.hasValue()) {
-      throw EmptySequence();
-    }
-    return min.value();
+    return min;
   }
 };
 
@@ -2066,7 +2057,7 @@ class Collect : public Operator<Collect<Collection>> {
  * The allocator defaults to std::allocator, so this may be used for the STL
  * containers by simply using operators like 'as<set>', 'as<deque>',
  * 'as<vector>'. 'as', here is the helper method which is the usual means of
- * consturcting this operator.
+ * constructing this operator.
  *
  * Example:
  *
@@ -2093,6 +2084,126 @@ class CollectTemplate : public Operator<CollectTemplate<Container, Allocator>> {
   }
 };
 
+/**
+ * UnwrapOr - For unwrapping folly::Optional values, or providing the given
+ * fallback value. Usually used through the 'unwrapOr' helper like so:
+ *
+ *   auto best = from(scores) | max | unwrapOr(-1);
+ *
+ * Note that the fallback value needn't match the value in the Optional it is
+ * unwrapping. If mis-matched types are supported, the common type of the two is
+ * returned by value. If the types match, a reference (T&& > T& > const T&) is
+ * returned.
+ */
+template <class T>
+class UnwrapOr {
+ public:
+  explicit UnwrapOr(T&& value) : value_(std::move(value)) {}
+  explicit UnwrapOr(const T& value) : value_(value) {}
+
+  T& value() { return value_; }
+  const T& value() const { return value_; }
+
+ private:
+  T value_;
+};
+
+template <class T>
+T&& operator|(Optional<T>&& opt, UnwrapOr<T>&& fallback) {
+  if (T* p = opt.get_pointer()) {
+    return std::move(*p);
+  }
+  return std::move(fallback.value());
+}
+
+template <class T>
+T& operator|(Optional<T>& opt, UnwrapOr<T>& fallback) {
+  if (T* p = opt.get_pointer()) {
+    return *p;
+  }
+  return fallback.value();
+}
+
+template <class T>
+const T& operator|(const Optional<T>& opt, const UnwrapOr<T>& fallback) {
+  if (const T* p = opt.get_pointer()) {
+    return *p;
+  }
+  return fallback.value();
+}
+
+// Mixed type unwrapping always returns values, moving where possible
+template <class T,
+          class U,
+          class R = typename std::enable_if<
+              !std::is_same<T, U>::value,
+              typename std::common_type<T, U>::type>::type>
+R operator|(Optional<T>&& opt, UnwrapOr<U>&& fallback) {
+  if (T* p = opt.get_pointer()) {
+    return std::move(*p);
+  }
+  return std::move(fallback.value());
+}
+
+template <class T,
+          class U,
+          class R = typename std::enable_if<
+              !std::is_same<T, U>::value,
+              typename std::common_type<T, U>::type>::type>
+R operator|(const Optional<T>& opt, UnwrapOr<U>&& fallback) {
+  if (const T* p = opt.get_pointer()) {
+    return *p;
+  }
+  return std::move(fallback.value());
+}
+
+template <class T,
+          class U,
+          class R = typename std::enable_if<
+              !std::is_same<T, U>::value,
+              typename std::common_type<T, U>::type>::type>
+R operator|(Optional<T>&& opt, const UnwrapOr<U>& fallback) {
+  if (T* p = opt.get_pointer()) {
+    return std::move(*p);
+  }
+  return fallback.value();
+}
+
+template <class T,
+          class U,
+          class R = typename std::enable_if<
+              !std::is_same<T, U>::value,
+              typename std::common_type<T, U>::type>::type>
+R operator|(const Optional<T>& opt, const UnwrapOr<U>& fallback) {
+  if (const T* p = opt.get_pointer()) {
+    return *p;
+  }
+  return fallback.value();
+}
+
+/**
+ * Unwrap - For unwrapping folly::Optional values in a folly::gen style. Usually
+ * used through the 'unwrap' instace like so:
+ *
+ *   auto best = from(scores) | max | unwrap; // may throw
+ */
+class Unwrap {};
+
+template <class T>
+T&& operator|(Optional<T>&& opt, const Unwrap&) {
+  return std::move(opt.value());
+}
+
+template <class T>
+T& operator|(Optional<T>& opt, const Unwrap&) {
+  return opt.value();
+}
+
+template <class T>
+const T& operator|(const Optional<T>& opt, const Unwrap&) {
+  return opt.value();
+}
+
 } //::detail
 
 /**
@@ -2193,6 +2304,8 @@ constexpr detail::Dereference dereference{};
 
 constexpr detail::Indirect indirect{};
 
+constexpr detail::Unwrap unwrap{};
+
 inline detail::Take take(size_t count) { return detail::Take(count); }
 
 inline detail::Stride stride(size_t s) { return detail::Stride(s); }
index 08a901a99a21aa9224170c2b8a7fb1508eb0461e..6e661f16183d4019b38d6056b4dbb54c055859ae 100644 (file)
 
 namespace folly { namespace gen {
 
-class EmptySequence : public std::exception {
-public:
-  virtual const char* what() const noexcept {
-    return "This operation cannot be called on an empty sequence";
-  }
-};
-
 class Less {
 public:
   template<class First,
@@ -420,6 +413,11 @@ template<class Exception,
          class ErrorHandler>
 class GuardImpl;
 
+template <class T>
+class UnwrapOr;
+
+class Unwrap;
+
 }
 
 /**
@@ -823,6 +821,12 @@ GuardImpl guard(ErrorHandler&& handler) {
   return GuardImpl(std::forward<ErrorHandler>(handler));
 }
 
+template<class Fallback,
+         class UnwrapOr = detail::UnwrapOr<typename std::decay<Fallback>::type>>
+UnwrapOr unwrapOr(Fallback&& fallback) {
+  return UnwrapOr(std::forward<Fallback>(fallback));
+}
+
 }} // folly::gen
 
 #include <folly/gen/Base-inl.h>
index 06acd00dc37e82fe958ae4f0e668d088c856c304..7741b053fb52695f72ccdae8d67e1269eac7122d 100644 (file)
@@ -165,16 +165,16 @@ TEST(Gen, Field) {
   std::vector<X> xs(1);
   EXPECT_EQ(2, from(xs)
              | field(&X::a)
-             | first);
+             | sum);
   EXPECT_EQ(3, from(xs)
              | field(&X::b)
-             | first);
+             | sum);
   EXPECT_EQ(4, from(xs)
              | field(&X::c)
-             | first);
+             | sum);
   EXPECT_EQ(2, seq(&xs[0], &xs[0])
              | field(&X::a)
-             | first);
+             | sum);
   // type-verification
   empty<X&>() | field(&X::a) | assert_type<const int&>();
   empty<X*>() | field(&X::a) | assert_type<const int&>();
@@ -613,13 +613,14 @@ TEST(Gen, MinBy) {
              | minBy([](int i) -> double {
                  double d = i - 6.8;
                  return d * d;
-               }));
+               })
+             | unwrap);
 }
 
 TEST(Gen, MaxBy) {
   auto gen = from({"three", "eleven", "four"});
 
-  EXPECT_EQ("eleven", gen | maxBy(&strlen));
+  EXPECT_EQ("eleven", gen | maxBy(&strlen) | unwrap);
 }
 
 TEST(Gen, Min) {
@@ -708,17 +709,13 @@ TEST(Gen, Foldl) {
 TEST(Gen, Reduce) {
   int expected = 2 + 3 + 4 + 5;
   auto actual = seq(2, 5) | reduce(add);
-  EXPECT_EQ(expected, actual);
+  EXPECT_EQ(expected, actual | unwrap);
 }
 
 TEST(Gen, ReduceBad) {
   auto gen = seq(1) | take(0);
-  try {
-    EXPECT_TRUE(true);
-    gen | reduce(add);
-    EXPECT_TRUE(false);
-  } catch (...) {
-  }
+  auto actual = gen | reduce(add);
+  EXPECT_FALSE(actual); // Empty sequences are okay, they just yeild 'none'
 }
 
 TEST(Gen, Moves) {
@@ -731,10 +728,8 @@ TEST(Gen, Moves) {
 }
 
 TEST(Gen, First) {
-  auto gen =
-      seq(0)
-    | filter([](int x) { return x > 3; });
-  EXPECT_EQ(4, gen | first);
+  auto gen = seq(0) | filter([](int x) { return x > 3; });
+  EXPECT_EQ(4, gen | first | unwrap);
 }
 
 TEST(Gen, FromCopy) {
@@ -1106,7 +1101,7 @@ TEST(Gen, Dereference) {
 
 TEST(Gen, Indirect) {
   vector<int> vs{1};
-  EXPECT_EQ(&vs[0], from(vs) | indirect | first);
+  EXPECT_EQ(&vs[0], from(vs) | indirect | first | unwrap);
 }
 
 TEST(Gen, Guard) {
@@ -1171,24 +1166,24 @@ TEST(Gen, Just) {
   {
     int x = 3;
     auto j = just(x);
-    EXPECT_EQ(&x, j | indirect | first);
+    EXPECT_EQ(&x, j | indirect | first | unwrap);
     x = 4;
-    EXPECT_EQ(4, j | first);
+    EXPECT_EQ(4, j | sum);
   }
   {
     int x = 3;
     const int& cx = x;
     auto j = just(cx);
-    EXPECT_EQ(&x, j | indirect | first);
+    EXPECT_EQ(&x, j | indirect | first | unwrap);
     x = 5;
-    EXPECT_EQ(5, j | first);
+    EXPECT_EQ(5, j | sum);
   }
   {
     int x = 3;
     auto j = just(std::move(x));
-    EXPECT_NE(&x, j | indirect | first);
+    EXPECT_NE(&x, j | indirect | first | unwrap);
     x = 5;
-    EXPECT_EQ(3, j | first);
+    EXPECT_EQ(3, j | sum);
   }
 }
 
@@ -1202,16 +1197,115 @@ TEST(Gen, GroupBy) {
   EXPECT_EQ(3, gb | count);
 
   vector<string> mode{"zero", "four", "five", "nine"};
-  EXPECT_EQ(
-      mode,
-      gb | maxBy([](const Group<size_t, string>& g) { return g.size(); })
-         | as<vector>());
+  EXPECT_EQ(mode,
+            gb | maxBy([](const Group<size_t, string>& g) { return g.size(); })
+               | unwrap
+               | as<vector>());
 
   vector<string> largest{"three", "seven", "eight"};
-  EXPECT_EQ(
-      largest,
-      gb | maxBy([](const Group<size_t, string>& g) { return g.key(); })
-         | as<vector>());
+  EXPECT_EQ(largest,
+            gb | maxBy([](const Group<size_t, string>& g) { return g.key(); })
+               | unwrap
+               | as<vector>());
+}
+
+TEST(Gen, Unwrap) {
+  Optional<int> o(4);
+  Optional<int> e;
+  EXPECT_EQ(4, o | unwrap);
+  EXPECT_THROW(e | unwrap, OptionalEmptyException);
+
+  auto oup = folly::make_optional(folly::make_unique<int>(5));
+  // optional has a value, and that value is non-null
+  EXPECT_TRUE(oup | unwrap);
+  EXPECT_EQ(5, *(oup | unwrap));
+  EXPECT_TRUE(oup.hasValue()); // still has a pointer (null or not)
+  EXPECT_TRUE(oup.value()); // that value isn't null
+
+  auto moved1 = std::move(oup) | unwrapOr(folly::make_unique<int>(6));
+  // oup still has a value, but now it's now nullptr since the pointer was moved
+  // into moved1
+  EXPECT_TRUE(oup.hasValue());
+  EXPECT_FALSE(oup.value());
+  EXPECT_TRUE(moved1);
+  EXPECT_EQ(5, *moved1);
+
+  auto moved2 = std::move(oup) | unwrapOr(folly::make_unique<int>(7));
+  // oup's still-valid nullptr value wins here, the pointer to 7 doesn't apply
+  EXPECT_FALSE(moved2);
+
+  oup.clear();
+  auto moved3 = std::move(oup) | unwrapOr(folly::make_unique<int>(8));
+  // oup is empty now, so the unwrapOr comes into play.
+  EXPECT_TRUE(moved3);
+  EXPECT_EQ(8, *moved3);
+
+  {
+  // mixed types, with common type matching optional
+    Optional<double> full(3.3);
+    decltype(full) empty;
+    auto fallback = unwrapOr(4);
+    EXPECT_EQ(3.3, full | fallback);
+    EXPECT_EQ(3.3, std::move(full) | fallback);
+    EXPECT_EQ(3.3, full | std::move(fallback));
+    EXPECT_EQ(3.3, std::move(full) | std::move(fallback));
+    EXPECT_EQ(4.0, empty | fallback);
+    EXPECT_EQ(4.0, std::move(empty) | fallback);
+    EXPECT_EQ(4.0, empty | std::move(fallback));
+    EXPECT_EQ(4.0, std::move(empty) | std::move(fallback));
+  }
+
+  {
+  // mixed types, with common type matching fallback
+    Optional<int> full(3);
+    decltype(full) empty;
+    auto fallback = unwrapOr(5.0); // type: double
+    // if we chose 'int' as the common type, we'd see truncation here
+    EXPECT_EQ(1.5, (full | fallback) / 2);
+    EXPECT_EQ(1.5, (std::move(full) | fallback) / 2);
+    EXPECT_EQ(1.5, (full | std::move(fallback)) / 2);
+    EXPECT_EQ(1.5, (std::move(full) | std::move(fallback)) / 2);
+    EXPECT_EQ(2.5, (empty | fallback) / 2);
+    EXPECT_EQ(2.5, (std::move(empty) | fallback) / 2);
+    EXPECT_EQ(2.5, (empty | std::move(fallback)) / 2);
+    EXPECT_EQ(2.5, (std::move(empty) | std::move(fallback)) / 2);
+  }
+
+  {
+    auto opt = folly::make_optional(std::make_shared<int>(8));
+    auto fallback = unwrapOr(folly::make_unique<int>(9));
+    // fallback must be std::move'd to be used
+    EXPECT_EQ(8, *(opt | std::move(fallback)));
+    EXPECT_TRUE(opt.value()); // shared_ptr copied out, not moved
+    EXPECT_TRUE(opt); // value still present
+    EXPECT_TRUE(fallback.value()); // fallback value not needed
+
+    EXPECT_EQ(8, *(std::move(opt) | std::move(fallback)));
+    EXPECT_FALSE(opt.value()); // shared_ptr moved out
+    EXPECT_TRUE(opt); // gutted value still present
+    EXPECT_TRUE(fallback.value()); // fallback value not needed
+
+    opt.clear();
+
+    EXPECT_FALSE(opt); // opt is empty now
+    EXPECT_EQ(9, *(std::move(opt) | std::move(fallback)));
+    EXPECT_FALSE(fallback.value()); // fallback moved out!
+  }
+
+  {
+    // test with nullptr
+    vector<int> v{1, 2};
+    EXPECT_EQ(&v[1], from(v) | indirect | max | unwrap);
+    v.clear();
+    EXPECT_FALSE(from(v) | indirect | max | unwrapOr(nullptr));
+  }
+
+  {
+    // mixed type determined by fallback
+    Optional<std::nullptr_t> empty;
+    int x = 3;
+    EXPECT_EQ(&x, empty | unwrapOr(&x));
+  }
 }
 
 int main(int argc, char *argv[]) {