Стиль 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 >;
}
Ответы
Ответ 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)
.