31

I repeatedly find myself requiring Haskell style Maybe (especially Maybe chaining) in my project at work. E.g. withdrawal request from customer and we are given the customer ID... lookup customer in cache... if customer is found... lookup her savings account... if there is an account... withdraw... At any point in this chain, if there is a lookup failure, do nothing and return a failure.

My chains are large... sometimes as long as 6... so here is my swipe at Haskell.Data.Maybe in C++0x... (note... this should work in C++ if I stop using variadic templates). I have worked out chaining for free-functions taking one argument or member functions taking no arguments and I am happy with the interface. However, for functions taking multiple parameters... I have to write a lambda function to simulate partial application. Is there a way to avoid it? See the last line of main(). Even if it is uncommented it won't compile, but for const/non-const mixing. But the question still stands.

Sorry about the large chunk of code... I hope this wouldn't turn away people who might otherwise be interested in this...

#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 >;
}
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
zrb
  • 721
  • 1
  • 6
  • 15
  • C++177 now has http://en.cppreference.com/w/cpp/utility/optional (and `variant`.), but no built-in `>>=` (though there is a `fromMaybe` called `value_or`). According to https://blog.tartanllama.xyz/optional-expected/ you can build your own `and_then` which works like `>>=` for `optional`, though I don't see the code there. – unhammer Dec 05 '17 at 08:42

5 Answers5

15

Good start, but I think you're over-engineering in your zeal to make your class foolproof. Personally I'd recommend 'worse is better'. First, let's reuse 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;
}

Some various explanations on things that aren't really important:

  • nothing doesn't have to be an object, it can still be a function (returning nothing_type) like you're doing. That's not important.

  • I made sure to preserve the reference semantics of just to match your version. As a bonus though, it can still deal with values. As such, with int i = 0; auto maybe = just(i); then the type of maybe will be boost::optional<int&>, whereas with auto maybe = just(42); it is boost::optional<int>.

  • the *std::forward<Option>(option) can actually simply be *option as Boost.Optional is not move-aware and not many compilers support lvalue/rvalue *this (which would be needed for it to matter). I just like future-proofing perfect-forwarding templates.

  • you can still name maybe_do operator| instead. I would however recommend putting it in a namespace and use using ns::operator| (or using namespace ns;) to put it into scope. You can additionally (or instead) add an SFINAE check (or write several overloads) to make sure it only participates in overload resolution at appropriate times. I'm advising this to avoid namespace pollution and annoying errors.

The important stuff:

It may look like maybe_do is severely underpowered compared to your overloads that can deal with member pointers. But I'd recommend keeping it simple and instead putting the burden on client-code to adapt member pointers:

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

Similarly client code can use std::bind to do the poor man's partial evaluation:

maybe_do(maybe, std::bind(some_functor, _1, "foo", _2, bar));
Luc Danton
  • 34,649
  • 6
  • 70
  • 114
  • @LucDanton... +1 for highlighting some of the subtleties. I deliberately avoided `bind` to make the client code easy. More importantly... On the use of `Boost.Optional` as the building block... At work I have to handle both `SharedPtr`, `Optional` (no raw pointers) and a wrapper over `Optional`. I think, the `Maybe` wrapper reduces the overloads. – zrb Oct 08 '11 at 00:28
  • Thinking more about it... there maybe a way to reduce overloads by using template template parameters. I'll give it a try. – zrb Oct 08 '11 at 00:48
6

I was the OP (lost my account when SO migrated). Here is the latest I came up with using std::invoke. Life becomes much simpler

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()));
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
zrb
  • 851
  • 7
  • 16
2

As a recovering template-aholic, I feel it my duty to point out the simple non-template exception-based solution for the given example.

Adjust the code to throw an exception instead of returning Maybe/Optional, and the code becomes...

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

That's not to say Maybe/Optional monads are never useful in C++, but for many cases exceptions will get things done in a much more idiomatic and easily understood manner.

goertzenator
  • 1,960
  • 18
  • 28
  • 1
    An excellent point, except that if getCustomer gets called and results in an exception, then nothing in getCheckingAccount will be executed. I imagine that in this situation it'd be fine, but some people might want those functions to be executed regardless in another scenario, eg, suppose getCheckingAccount could make use of a default account instead. – Arafangion May 15 '13 at 02:57
  • 4
    Exceptions are drastically overused. Exceptions should be for situations that are exceptional, and in many cases returning an error is not exceptional. It is entirely likely that the result may fail. There are many other problems with exceptions. – Christopher Sep 01 '16 at 12:29
2

My 5 cts.

Sample usage:

Maybe<string> m1 ("longlonglong");

auto res1 = m1 | lengthy  | length;

lengthy and length are "monadic lambdas", i.e.

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

Complete code:

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


} // ()
cibercitizen1
  • 20,944
  • 16
  • 72
  • 95
0

It's been implemented in C++03 for a long time. You can find it in Boost as boost::optional. boost::optional offers a simple if (value) interface.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • 3
    I have to repeat the boiler plate of checking if `optional::is_initialized` or whatever. Boost.Optional or Std.SharedPtr is probably what I would use as the underlying holder in the Maybe type. However, the question is about avoiding the boiler plate of `check if null, if null do nothing, if not null do something` and repeat this forever. Ugly??? And, I am definitely lazy. – zrb Oct 07 '11 at 17:53
  • @zrb: It's way more effort to make the functional approach, even with lambdas. – Puppy Oct 07 '11 at 17:58
  • Actually what I have is good enough for the cases I encounter at work (single argument functions mostly)... I just want to know if the general case of multiple arguments is possible too. Maybe some clever use of `tuple`... or `initializer_list` – zrb Oct 07 '11 at 18:02