Either와 expected 모나드

2022-12-10, Sat

C++23에 optional/expected에 대한 모나딕 함수들이 생겼다.

12월 28일 기준 gcc trunk에서만 작동하는 중.

and_then은 하스켈의 >>=(bind, flat map) 역할을 하고 transform은 하스켈의 <&>(fmap) 역할을 한다. 같은 내용을 두 언어로 각각 구현했다. 그냥 체이닝만 하면 별 차이가 없어 보이지만 do notation을 적극 활용하면 가독성이 많이 달라진다.

자세한 문법은 cppreference expected에서 볼 수 있는데 아직 작성이 안 되었으니 optional의 문서를 보면 된다.

#include <charconv>
#include <expected>
#include <string>
#include <system_error>

static_assert(__cpp_lib_constexpr_charconv >= 202207L);
static_assert(__cpp_lib_to_chars >= 202306L);
auto constexpr to_string_constexpr = [](unsigned int i) noexcept {
    std::string s(10, '\0');

    if (auto const res = std::to_chars(s.data(), s.data() + s.size(), i))
        return std::string{s.data(), res.ptr};
    else
        return std::make_error_code(res.ec).message();
};

static_assert(__cpp_lib_expected >= 202202L);
using exp_int_str = std::expected<unsigned int, std::string>;
auto constexpr div_by = [](unsigned int const d) noexcept {
    return [d](unsigned int const a) noexcept -> exp_int_str {
        if (d == 0 || a % d != 0)
            return std::unexpected{"Can't div " + to_string_constexpr(a) +
                                   " by " + to_string_constexpr(d)};
        return a / d;
    };
};

auto constexpr div_by_0 = div_by(0);
auto constexpr div_by_2 = div_by(2);
auto constexpr div_by_3 = div_by(3);
auto constexpr div_by_5 = div_by(5);

static_assert(div_by_0(11) == std::unexpected{"Can't div 11 by 0"});
static_assert(div_by_2(8) == exp_int_str{4});
static_assert(div_by_2(1) == std::unexpected{"Can't div 1 by 2"});

static_assert(__cpp_lib_expected >= 202211L);
static_assert(exp_int_str{12}.and_then(div_by_2).and_then(div_by_3) ==
              exp_int_str{2});
static_assert(exp_int_str{10}.and_then(div_by_2).and_then(div_by_3) ==
              std::unexpected{"Can't div 5 by 3"});
auto constexpr div30mul7 = [](unsigned int const a) noexcept {
    exp_int_str const a2 = div_by_2(a);
    exp_int_str const a2_3 = a2.and_then(div_by_3);
    exp_int_str const a2_3_7 = a2_3.transform([](auto a) { return a * 7; });
    exp_int_str const a2_3_7_5 = a2_3_7.and_then(div_by_5);
    return a2_3_7_5;
};
static_assert(div30mul7(60) == exp_int_str{14});
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

divBy :: Word -> (Word -> Either String Word)
divBy d = \a -> if d == 0 || mod a d /= 0
  then Left $ "Can't div " ++ show a ++ " by " ++ show d
  else Right $ div a d

divBy0, divBy2, divBy3, divBy5 :: Word -> Either String Word
divBy0 = divBy 0
divBy2 = divBy 2
divBy3 = divBy 3
divBy5 = divBy 5

div30mul7 :: Word -> Either String Word
div30mul7 a = do
  (div2 :: Word) <- divBy2 a
  (div2_3 :: Word) <- divBy3 div2
  let (div2_3_7 :: Word) = div2_3 * 7
  (div2_3_7_5 :: Word) <- divBy5 div2_3_7
  (return :: Word -> Either String Word) div2_3_7_5

main :: IO ()
main = do
  print $ divBy0 11 == Left "Can't div 11 by 0"
  print $ divBy2 1 == Left "Can't div 1 by 2"
  print $ divBy2 8 == Right 4
  print $ (return 12 >>= divBy2 >>= divBy3) == Right 2
  print $ (return 10 >>= divBy2 >>= divBy3) == Left "Can't div 5 by 3"
  print $ div30mul7 60 == Right 14