Skip to content

Latest commit

 

History

History
114 lines (79 loc) · 5 KB

File metadata and controls

114 lines (79 loc) · 5 KB

二十五、也许是单子

在 C++ 中,像在许多其他语言中一样,我们有不同的方式来表达一个值的存在或不存在。特别是在 C++ 中,我们可以使用以下任何一种:

  • 使用nullptr对缺勤进行编码。
  • 使用智能指针(例如,shared_ptr),同样可以测试其是否存在。
  • std::optional<T>是库解决方案;如果缺少值,它可以存储类型为Tstd::nullopt的值。

假设我们决定采用nullptr方法。在这种情况下,让我们假设我们的域模型定义了一个Person,它可能有也可能没有一个Address,反过来,它可以有一个可选的house_name 1 :

1   struct Address {
2     string* house_name = nullptr;
3   };
4
5   struct Person {
6     Address* address = nullptr;
7   };

我们感兴趣的是写一个函数,给定一个人,安全地打印这个人的房屋名称,当然如果它存在的话。在“传统的”C++ 中,我们会这样实现它:

1   void print_house_name(Person* p)
2   {
3     if (p != nullptr &&
4       p->address != nullptr &&
5       p->address->house_name != nullptr) // ugh!
6     cout << *p->address->house_name << endl;
7   }

前面的代码代表了深入对象结构的过程,注意不要访问nullptr值。相反,这种向下钻取的过程可以通过使用可能单子以函数的方式来表示。

为了构造单子,我们将定义一个新的类型Maybe<T>。此类型将用作参与下钻过程的临时对象:

1   template <typename T> struct Maybe {
2     T* context;
3     Maybe(T *context) : context(context) { }
4   };

到目前为止,Maybe看起来像一个指针容器,没什么令人兴奋的。它也不是很有用,因为给定一个Person* p,我们不能产生一个Maybe(p),因为我们不能从构造器中传递的参数推导出类模板参数。在这种情况下,我们还创建了一个 helper 全局函数,因为函数实际上可以推导出模板参数:

1   template <typename T> Maybe<T> maybe(T* context)
2   {
3     return Maybe<T>(context);
4   }

现在,我们想要做的是给Maybe一个成员函数

  • 如果context != nullptr,则更深地钻入对象;或者
  • 如果上下文实际上是nullptr,则什么也不做

“向下钻取”的过程被封装到模板参数Func中,如下所示:

1   template <typename Func>
2   auto With(Func evaluator)
3   {
4     return context != nullptr ? maybe(evaluator(context)) : nullptr;
5   }

前面是高阶函数的一个例子,也就是取函数的函数。 2 我们创建的这个函数采用了另一个名为evaluator的函数,假设当前上下文为非空,可以在上下文中调用这个函数,并返回一个可以包装在另一个Maybe中的指针。这个技巧允许链接With()呼叫。

现在,以类似的方式,我们可以创建另一个成员函数,这次只需调用context上的给定函数,而不改变上下文本身:

1   template <typename TFunc>
2   auto Do(TFunc action)
3   {
4     if (context != nullptr) action(context);
5     return *this;
6   }

我们完事了。我们现在可以做的是重新定义我们的print_house_name()函数如下:

1   void print_house_name(Person* p)
2   {
3     auto z = maybe(p)
4       .With([](auto x) { return x->address; })
5       .With([](auto x) { return x->house_name; })
6       .Do([](auto x) { cout << *x << endl; });
7   }

这里有几点需要注意。首先,我们设法创建了一个流畅的接口,即一个可以将函数调用一个接一个链接起来的设置。这种说法是有道理的,因为每个操作符(WithDo等)。)返回*this或者一个新的Maybe<T>。同样值得注意的是,下钻过程是如何在每一个转折点被 lambda 函数封装的。

正如您可能猜到的,前面的方法确实有性能成本,尽管这些成本很难预测,并且取决于编译器优化代码的能力。它也远非完美,因为我很乐意省略[](auto x)部分,以支持一些速记符号。理想情况下,类似于maybe(p).With{it->address}的东西会很好。T33

Footnotes 1

房子的名字是真实存在的(至少在英国是这样的):当你买了一座城堡,它的地址不是“伦敦路 123 号”,而只是“蒙特菲奥里城堡”,这就是它的地址。你可以猜到,并不是所有的房子都有名字,这就解释了为什么这个字段是可选的。

  2

严格地说,高阶函数要么接受一个函数作为一个或多个参数,要么返回一个函数(或两者都有)。

  3

例如,Kotlin 和 Swift 编程语言支持这种方法。如果没有必要,这两种语言都允许程序员避免额外的 lambda 函数仪式。这包括省略参数、捕获列表和返回值,以及使用花括号,而不是圆括号,这让您可以简单地打开一个事实上的作用域,并放置所有要由 lambda 执行的语句。