Haskell风格的"Maybe"类型与C++11中的*chaining*

31
我在工作项目中经常需要使用Haskell风格的Maybe(特别是Maybe链)。例如,从客户那里收到提款请求并获得客户ID...在缓存中查找客户...如果找到客户...查找她的储蓄账户...如果有账户...提款...在此链中的任何时刻,如果查找失败,则不执行任何操作并返回失败。
我的链很长...有时长达6个...因此这是我在C++0x中尝试实现Haskell.Data.Maybe的方式...(请注意...如果我停止使用可变参数模板,则应该可以在C++中工作)。我已经为接受一个参数的自由函数和不接受参数的成员函数解决了链接问题,并且我对接口感到满意。但是,对于接受多个参数的函数...我必须编写lambda函数来模拟部分应用程序。是否有避免它的方法?请参见main()的最后一行。即使取消注释,由于const/non-const混合,它也无法编译。但问题仍然存在。
抱歉代码很长...我希望这不会让本来可能对此感兴趣的人望而却步...
#include <iostream>
#include <map>
#include <deque>
#include <algorithm>
#include <type_traits>

typedef long long int int64;

namespace monad { namespace maybe {

  struct Nothing {};

  template < typename T >
  struct Maybe {
    template < typename U, typename Enable = void >
    struct ValueType {
      typedef U * const type;
    };

    template < typename U >
    struct ValueType < U, typename std::enable_if < std::is_reference < U >::value >::type > {
      typedef typename std::remove_reference < T >::type * const type;
    };

    typedef typename ValueType < T >::type value_type;

    value_type m_v;

    Maybe(Nothing const &) : m_v(0) {}

    struct Just {
      value_type m_v;
      Just() = delete;
      explicit Just(T &v) : m_v(&v) {
      }
    };

    Maybe(Just const &just) : m_v(just.m_v) {
    }
  };

  Nothing nothing() {
    return Nothing();
  }

  template < typename T >
  Maybe < T > just(T &v) {
    return typename Maybe < T >::Just(v);
  }

  template < typename T >
  Maybe < T const > just(T const &v) {
    return typename Maybe < T const >::Just(v);
  }

  template < typename T, typename R, typename A >
  Maybe < R > operator | (Maybe < T > const &t, R (*f)(A const &)) {
    if (t.m_v)
      return just < R >(f(*t.m_v));
    else
      return nothing();
  }

  template < typename T, typename R, typename A >
  Maybe < R > operator | (Maybe < T > const &t, Maybe < R > (*f)(A const &)) {
    if (t.m_v)
      return f(*t.m_v);
    else
      return nothing();
  }

  template < typename T, typename R, typename A >
  Maybe < R > operator | (Maybe < T > const &t, R (*f)(A &)) {
    if (t.m_v)
      return just < R >(f(*t.m_v));
    else
      return nothing();
  }

  template < typename T, typename R, typename A >
  Maybe < R > operator | (Maybe < T > const &t, Maybe < R > (*f)(A &)) {
    if (t.m_v)
      return f(*t.m_v);
    else
      return nothing();
  }

  template < typename T, typename R, typename... A >
  Maybe < R > operator | (Maybe < T const > const &t, R (T::*f)(A const &...) const) {
    if (t.m_v)
      return just < R >(((*t.m_v).*f)());
    else
      return nothing();
  }

  template < typename T, typename R, typename... A >
  Maybe < R > operator | (Maybe < T const > const &t, Maybe < R > (T::*f)(A const &...) const) {
    if (t.m_v)
      return just < R >((t.m_v->*f)());
    else
      return nothing();
  }

  template < typename T, typename R, typename... A >
  Maybe < R > operator | (Maybe < T const > const &t, R (T::*f)(A const &...)) {
    if (t.m_v)
      return just < R >(((*t.m_v).*f)());
    else
      return nothing();
  }

  template < typename T, typename R, typename... A >
  Maybe < R > operator | (Maybe < T const > const &t, Maybe < R > (T::*f)(A const &...)) {
    if (t.m_v)
      return just < R >((t.m_v->*f)());
    else
      return nothing();
  }

  template < typename T, typename A >
  void operator | (Maybe < T > const &t, void (*f)(A const &)) {
    if (t.m_v)
      f(*t.m_v);
  }

}}

struct Account {
  std::string const m_id;
  enum Type { CHECKING, SAVINGS } m_type;
  int64 m_balance;
  int64 withdraw(int64 const amt) {
    if (m_balance < amt)
      m_balance -= amt;
    return m_balance;
  }

  std::string const &getId() const {
    return m_id;
  }
};

std::ostream &operator << (std::ostream &os, Account const &acct) {
  os << "{" << acct.m_id << ", "
 << (acct.m_type == Account::CHECKING ? "Checking" : "Savings")
 << ", " << acct.m_balance << "}";
}

struct Customer {
  std::string const m_id;
  std::deque < Account > const m_accounts;
};

typedef std::map < std::string, Customer > Customers;

using namespace monad::maybe;

Maybe < Customer const > getCustomer(Customers const &customers, std::string const &id) {
  auto customer = customers.find(id);
  if (customer == customers.end())
    return nothing();
  else
    return just(customer->second);
};

Maybe < Account const > getAccountByType(Customer const &customer, Account::Type const type) {
  auto const &accounts = customer.m_accounts;
  auto account = std::find_if(accounts.begin(), accounts.end(), [type](Account const &account) -> bool { return account.m_type == type; });
  if (account == accounts.end())
    return nothing();
  else
    return just(*account);
}

Maybe < Account const > getCheckingAccount(Customer const &customer) {
  return getAccountByType(customer, Account::CHECKING);
};

Maybe < Account const > getSavingsAccount(Customer const &customer) {
  return getAccountByType(customer, Account::SAVINGS);
};

int64 const &getBalance(Account const &acct) {
  return acct.m_balance;
}

template < typename T >
void print(T const &v) {
  std::cout << v << std::endl;
}

int main(int const argc, char const * const argv[]) {
  Customers customers = {
    { "12345", { "12345", { { "12345000", Account::CHECKING, 20000 }, { "12345001", Account::SAVINGS, 117000 } } } }
  , { "12346", { "12346", { { "12346000", Account::SAVINGS, 1000000 } } } }
  };

  getCustomer(customers, "12346") | getCheckingAccount | getBalance | &print < int64 const >;
  getCustomer(customers, "12345") | getCheckingAccount | getBalance | &print < int64 const >;
  getCustomer(customers, "12345") | getSavingsAccount | &Account::getId | &print < std::string const >;
  //  getCustomer(customers, "12345") | getSavingsAccount | [](Account &acct){ return acct.withdraw(100); } | &print < std::string const >;
}

5个回答

15

很好的开始,但我认为你在追求使你的类防错时有些过度工程。个人建议采用“较差即更好”的方式。首先,让我们重用Boost.Optional:

struct nothing_type {
    template<typename T>
    operator boost::optional<T>() const
    { return {}; }
};
constexpr nothing_type nothing;

template<typename T>
boost::optional<T>
just(T&& t)
{
    return std::forward<T>(t);
}

template<typename Option, typename Functor>
auto maybe_do(Option&& option, Functor&& functor)
-> boost::optional<
    decltype( functor(*std::forward<Option>(option)) )
>
{
    // Forwarding 
    if(option)
        return functor(*std::forward<Option>(option));
    else
        return nothing;
}

以下是一些不太重要的解释:

  • nothing 不一定是一个对象,它也可以是一个函数(返回 nothing_type),就像你现在所做的那样。但这并不重要。

  • 我保留了 just 的引用语义以匹配你的版本。但作为额外的奖励,它仍然可以处理值。因此,对于 int i = 0; auto maybe = just(i);maybe 的类型将是 boost::optional<int&>,而对于 auto maybe = just(42);,它是 boost::optional<int>

  • *std::forward<Option>(option) 实际上可以简写为 *option,因为 Boost.Optional 不支持移动语义,也没有多少编译器支持左值/右值 *this(这需要它有所作为)。我只是喜欢未来证明完美转发模板。

  • 你仍然可以将 maybe_do 命名为 operator|。但我建议将其放入一个命名空间中,并使用 using ns::operator|(或 using namespace ns;)将其放入作用域中。你还可以(或者只是)添加一个 SFINAE 检查(或编写多个重载),以确保它仅在适当的时候参与重载分辨率。我建议这样做是为了避免命名空间污染和烦人的错误。

重要的内容:

maybe_do 看起来可能比你可以处理成员指针的重载要弱得多。但我建议保持简单,而是将负担放在客户端代码上,以适应成员指针:

auto maybe = /* fetch an optional<T cv ref> from somewhere */
maybe_do(maybe, std::bind(&T::some_member, _1));

同样,客户端代码可以使用 std::bind 来进行弱类型的部分求值操作:
maybe_do(maybe, std::bind(some_functor, _1, "foo", _2, bar));

@LucDanton... 感谢您强调了一些微妙之处。我故意避免使用 bind,以使客户端代码更容易。更重要的是,在使用 Boost.Optional 作为构建块时......在工作中,我必须处理 SharedPtrOptional(没有原始指针)和一个包装器。我认为,Maybe 包装器可以减少负担。 - zrb
再仔细想想...也许可以通过使用模板模板参数来减少负载。我会尝试一下。 - zrb

6

我是原问题的提问者(在 SO 迁移时失去了我的账号)。以下是我使用 std::invoke 得出的最新内容。使用它后,生活变得更简单了。

template < typename T >
auto operator | (Maybe < T > const & v, auto && f)
{
    using U = std::decay_t < decltype(f(v.get())) >;
    if (v.isNull())
        return Maybe < U >::nothing();
    else
        return Maybe < U >::just(std::invoke(f, v.get()));
}

template < typename T >
auto operator | (Maybe < T > & v, auto && f)
{
    using U = std::decay_t < decltype(f(v.get())) >;
    if (v.isNull())
        return Maybe < U >::nothing();
    else
        return Maybe < U >::just(std::invoke(f, v.get()));
}

template < typename T >
auto operator | (Maybe < T > && v, auto && f)
{
    using U = std::decay_t < decltype(f(v.get())) >;
    if (v.isNull())
        return Maybe < U >::nothing();
    else
        return Maybe < U >::just(std::invoke(f, v.get()));
}

2

我的看法:

使用示例:

Maybe<string> m1 ("longlonglong");

auto res1 = m1 | lengthy  | length;

lengthylength是“单子lambda”,即

auto length = [] (const string & s) -> Maybe<int>{ return Maybe<int> (s.length()); };

完整代码:

// g++ -std=c++1y answer.cpp

#include <iostream>
using namespace std;

// ..................................................
// begin LIBRARY
// ..................................................
template<typename T>
class Maybe {
  // 
  //  note: move semantics
  //  (boxed value is never duplicated)
  // 

private:

  bool is_nothing = false;

public:
  T value;

  using boxed_type = T;

  bool isNothing() const { return is_nothing; }

  explicit Maybe () : is_nothing(true) { } // create nothing

  // 
  //  naked values
  // 
  explicit Maybe (T && a) : value(std::move(a)), is_nothing(false) { }

  explicit Maybe (T & a) : value(std::move(a)), is_nothing(false) { }

  // 
  //  boxed values
  // 
  Maybe (Maybe & b) : value(std::move(b.value)), is_nothing(b.is_nothing) { b.is_nothing = true; }

  Maybe (Maybe && b) : value(std::move(b.value)), is_nothing(b.is_nothing) { b.is_nothing = true; }

  Maybe & operator = (Maybe & b) {
    value = std::move(b.value);
    (*this).is_nothing = b.is_nothing;
    b.is_nothing = true;
    return (*this);
  }
}; // class

// ..................................................
template<typename IT, typename F>
auto operator | (Maybe<IT> mi, F f)  // chaining (better with | to avoid parentheses)
{
  // deduce the type of the monad being returned ...
  IT aux;
  using OutMonadType = decltype( f(aux) );
  using OT = typename OutMonadType::boxed_type;

  // just to declare a nothing to return
  Maybe<OT> nothing;

  if (mi.isNothing()) {
    return nothing;
  }

  return f ( mi.value );
} // ()

// ..................................................
template<typename MO>
void showMonad (MO m) {
  if ( m.isNothing() ) {
    cout << " nothing " << endl;
  } else {
    cout << " something : ";
    cout << m.value << endl;
  }
}

// ..................................................
// end LIBRARY
// ..................................................

// ..................................................
int main () {

  auto lengthy = [] (const string & s) -> Maybe<string> { 
    string copyS = s;
    if  (s.length()>8) {
      return Maybe<string> (copyS);
    }
    return Maybe<string> (); // nothing
  };

  auto length = [] (const string & s) -> Maybe<int>{ return Maybe<int> (s.length()); };

  Maybe<string> m1 ("longlonglong");
  Maybe<string> m2 ("short");

  auto res1 = m1 | lengthy  | length;

  auto res2 = m2 | lengthy  | length;

  showMonad (res1);
  showMonad (res2);


} // ()

2
作为一个戒掉模板瘾的人,我认为我的职责是指出针对给定示例的简单非模板异常解决方案。
将代码调整为抛出异常而不是返回Maybe/Optional,代码就变成了...
try
{
  print(getBalance(getCheckingAccount(getCustomer(customers, "12346"))));
}
catch(my_error_t) 
{}

这并不是说在C++中Maybe/Optional monads从来没有用处,但对于许多情况,异常处理可以以更加惯用且易于理解的方式完成任务。


1
一个很好的观点,但是如果调用getCustomer导致异常,那么getCheckingAccount中的任何内容都不会被执行。我想在这种情况下这是可以的,但有些人可能希望在另一种情况下无论如何都执行这些函数,例如,假设getCheckingAccount可以使用默认账户。 - Arafangion
4
异常情况的使用过于频繁。异常应该仅应用于异常情况,而在许多情况下,返回错误并不属于异常情况。结果可能失败是完全可能的。异常还存在许多其他问题。 - Christopher

0

这个功能在C++03中已经实现了很长时间。你可以在Boost库中找到它,名为boost::optionalboost::optional提供了一个简单的if (value)接口。


3
我需要重复检查 optional::is_initialized 或其他内容的样板代码。也许 Boost.Optional 或 Std.SharedPtr 是我在 Maybe 类型中使用的基础持有者。然而,问题在于避免重复无休止地执行“检查是否为空,如果为空则不执行任何操作,如果不为空则执行某些操作”的样板代码。这很丑陋???而且,我绝对很懒。 - zrb
@zrb:即使使用lambda表达式,采用函数式编程方法需要更多的努力。 - Puppy
实际上,我目前所拥有的已经足够应对工作中遇到的案例了(主要是单参数函数)... 我只是想知道多个参数的一般情况是否也可能。也许可以巧妙地使用tuple或者initializer_list来实现。 - zrb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接