Подтвердить что ты не робот

Стиль Haskell "Возможно" тип & * цепочка * в С++ 11

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

Мои цепочки большие... иногда до тех пор, пока 6... так что вот мой промах в Haskell.Data.Maybe в С++ 0x... (обратите внимание... это должно работать на С++, если я перестаю использовать переменные шаблоны). Я разработал цепочку для бесплатных функций, используя один аргумент или функции-члены, не принимающие аргументов, и я доволен интерфейсом. Однако для функций, принимающих несколько параметров... Я должен написать лямбда-функцию для имитации частичного приложения. Есть ли способ избежать этого? См. Последнюю строку 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 >;
}
4b9b3361

Ответ 1

Хорошее начало, но я думаю, что вы усердствовали в своем рвении, чтобы сделать ваш класс надежным. Лично я бы рекомендовал "хуже, лучше". Во-первых, повторите использование 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));

Ответ 2

Я был 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()));
}

Ответ 3

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

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

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

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

Ответ 4

Мои 5 cts.

Использование образца:

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);


} // ()

Ответ 5

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