Тип Haskell "Maybe", тип & * chaining * в C ++ 11 - PullRequest
30 голосов
/ 07 октября 2011

Я постоянно обнаруживаю, что мне требуется стиль Haskell Maybe (особенно, возможно, цепочка) в моем проекте на работе. Например. запрос на снятие денег от клиента, и нам выдается идентификатор клиента ... поиск клиента в кэше ... если клиент найден ... поиск его сберегательного счета ... если есть аккаунт ... снять ... в любой момент в этой цепочке, если произошла ошибка поиска, ничего не делайте и верните ошибку.

Мои цепочки большие ... иногда до 6 ... так вот мой удар по Haskell.Data.Maybe в C ++ 0x ... (заметьте ... это должно работать в C ++, если я перестану использовать variadic шаблоны). Я разработал цепочку для свободных функций, принимающих один аргумент, или функций-членов, не принимающих аргументов, и я доволен интерфейсом. Тем не менее, для функций, принимающих несколько параметров ... Я должен написать лямбда-функцию для имитации частичного применения. Есть ли способ избежать этого? Смотрите последнюю строку main(). Даже если он не закомментирован, он не будет компилироваться, но для константного / неконстантного микширования. Но вопрос все еще стоит.

Извините за большой кусок кода ... Я надеюсь, что это не отвергнет людей, которые иначе могли бы заинтересоваться этим ...

#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 ]

14 голосов
/ 07 октября 2011

Хорошее начало, но я думаю, что вы слишком усердны в своем рвении, чтобы сделать свой класс надежным. Лично я бы порекомендовал «хуже, тем лучше». Во-первых, давайте снова используем 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 не учитывает перемещения, и не многие компиляторы поддерживают lvalue / rvalue *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));
6 голосов
/ 27 июня 2016

Я был OP (потерял свой аккаунт, когда 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 голосов
/ 22 января 2015

Мои 5 кар.

Пример использования:

Maybe<string> m1 ("longlonglong");

auto res1 = m1 | lengthy  | length;

lengthy и length являются "монадическими лямбдами", т.е.

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 голосов
/ 09 января 2013

Как выздоравливающий шаблонолик, я считаю своим долгом указать на простое нешаблонное решение на основе исключений для данного примера.

Настройте код, чтобы выдавать исключение вместо возврата Maybe / Optional, и код становится ...

try
{
  print(getBalance(getCheckingAccount(getCustomer(customers, "12346"))));
}
catch(my_error_t) 
{}

Это не означает, что Maybe / Необязательные монады никогда не используются в C ++, но во многих случаях исключения приводят к гораздо более идиоматичному и понятному пониманию.

0 голосов
/ 07 октября 2011

Это давно реализовано в C ++ 03.Вы можете найти его в Boost как boost::optional.boost::optional предлагает простой if (value) интерфейс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...