C++有多种方式来表示某个”对象”是否有值:

  • 使用nullptr来表示没有值

  • 使用智能指针(例如, shared_ptr). 它提供了检测手段

  • std::optional<T>是一种库解决方案. 它可以存储T值或std::nullopt.

如果我们打算使用nullptr的方法, 假定我们的领域对象定义了一个Person对象, 它可能有一个Address成员属性, 而后者有一个可选的house_name属性.

至少在英国, 有些房子是有名字的. 例如, 如果你买一座城堡, 它的地址可能不是”123 Londo Road”, 而是”Montefiore Castle”, 而这也是它的地址.
当然, 不是所有的house都有name.

1
2
3
4
5
6
7
8
9
10
struct Address
{
string* house_name = nullptr;
}

struct Person
{
Address* address = nullptr;
}

我们关心的是, 比如, 如何安全地打印出某个人的house name, 如果它存在的话.

使用”传统”的C++, 我们可能需要这样写:

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

我们称这种现象为drill down into an object’s struct.

使用一种称为Maybe Monad的模式

首先定义类型Maybe<T>. 它作为一个临时对象参与到”drill down”的过程中去:

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

到目前, Maybe似乎没有什么大用, 我们也无法从Person* p来构造出Maybe(p), 因为无法从传递给构造函数的参数中推导出类模板参数. 为此, 还需要定义一个全局辅助函数, 因为函数是可以推到模板参数的:

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

接下来要做的就是为Maybe定义成员函数:

  • 如果context!=nullptr, 就继续深入范根对象, 或者
  • 如果context==nullptr, 就什么也不做
1
2
3
4
5
template <typename Func>
auto with(Func evaluator)
{
return (context!=nullptr) ? maybe(evaluator(context)) : nullptr;
}

这是一个高阶函数的例子. 它接受一个参数作为参数, 并返回一个指针, 它可以被封装到另一个Maybe里面. 这样, 我们可以链式调用With().

同样可以定义另一个成员函数, 它仅仅是激活给定义的作用在context上的函数, 而不会改变context.

1
2
3
4
5
6
7

template <typenae TFunc>
auto Do(TFunc action)
{
if( context != nullptr) action(context);
return *this;
}

现在, 就可以使用它:

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