Archive for September, 2022

Monads are part of C++, and are in your code

Monday, September 19th, 2022

For several reasons, I didn’t get a CppCon ticket this year, but since I live in the Denver metro area I did go on the Friday, which is free to attend. Herb’s closing keynote (Cpp2) was thought provoking. At one point though, he said,

Think about the words and the ideas that we have been using for the last hour and a half. I have not said the word ‘monad’ once!

Herb Sutter, Simplifying C++#9 of N, CppCon 2022

The general audience reaction: laughter.
My reaction: inward sigh.

Herb was trying to make the point, throughout the talk, that he’s sticking with C++. That he’s for evolution, not revolution. That C++, if we can make it simpler, safer and more toolable, is right for the future. And that’s great. It was unfortunate that he picked on monads though, because it came off to my ears as a cheap shot, and because I think it doesn’t make his point, or at least is very likely to be misconstrued.

Because monads are part of C++.

They’re not some alien thing. They’re just as much a part of C++ as RAII; templates; any pattern you care to name; even functions and classes. When I write some code, often I don’t think about writing a particular pattern. But knowing about patterns helps me realise what I’ve done when I step back and take a look. It can be the same story with monads. They’re in the code, waiting to be recognised and to help us understand what we’ve done.

Monads are part of C++, because monads are part of programming.

We have many tools in the C++ toolkit. C++ is famously large; it contains multitudes. By design. In fact, we can take a cue from Herb’s talk and ask, “What would Bjarne say?” Well, he already said it, several times, in The Design and Evolution of C++:

People don’t just write classes that fit a narrowly defined abstract data type or object-oriented style; they also — often for perfectly good reasons — write classes that take on aspects of both. They also write programs in which different parts use different styles to match needs and taste.

The language should support a range of reasonable design and programming styles rather than try to force people into adopting a single notion.

There is always a design choice but in most languages the language designer has made the choice for you. For C++ I did not: the choice is yours. This flexibility is naturally distasteful to people who believe that there is exactly one right way of doing things. It can also scare beginners and teachers who feel that a good language is one that you can completely understand in a week. C++ is not such a language. It was designed to provide a toolset for professionals, and complaining that there are too many features is like the “layman” looking into an upholsterer’s tool chest and exclaiming that there couldn’t possibly be a need for all those little hammers.

Bjarne Stroustrup, The Design and Evolution of C++

Bjarne was very deliberate in designing C++ for working programmers, and it’s clear that making C++ 10x more teachable and learnable — to paraphrase something Herb brought up — is not a goal for C++, at least not when it conflicts with features and choice. Many techniques and tools are available to the C++ programmer, and monads are certainly one of them.

Herb knows this, of course. His throwaway line was just that: a bit of clickbait. But there is a better message here about monads. Let’s not make them a strawman scapegoat for things that we think are weird and don’t belong in C++, because that’s not really a tenable position.

The more nuanced, more important message to convey about monads and other functional patterns is this: write clear APIs that fit the domain.

When we write classes, functions, variables, templates, APIs, we don’t tend to give them — unless the point is to be very generic — generic names. We give them names that fit their domain. So-called functional patterns are currently suffering from abstract names because they are new and they come with these abstract names, and because we programmers — and I include myself very much in this — are neophiles and pedants. There is hope that this is slowly changing, and that as we gain experience with these patterns they are becoming a more normal tool; not particularly more special than any other, but nonetheless very useful. The important thing is not the hammer, but what we build with it.

I’ve given talks where I described techniques that any functional programmer would immediately recognise as applications of monads or monoids. I was completely aware of this; nevertheless I tried to keep the talks focused on familiar domains so as to be accessible and grounded in examples. Understanding of abstraction comes after seeing concrete instances, not the other way around.

David Sankel has also given talks covering this theme: if you’re writing an API for some domain, use that domain’s language. Don’t use “bind” or “fmap” as function names if you have terms that fit your use better.

P2300 is a major proposal presenting a model for asynchronous execution in C++. The word monad does not appear in that paper. This is definitely not because the authors don’t know what monads are! I am quite sure that every author of that paper is well-practised with monads as a general tool and the continuation monad in particular, and as such the design they came up with was very deliberately monadically-informed. And this is incredibly useful, because it means we can have confidence about the power of the P2300 design. We can lean on what we know about the expressive and compositional capabilities of monads.

A proper question to ask in a critique of any programming interface, that Herb was implicitly and explicitly asking throughout his talk, but which the monad jab jarred with, is not “can we do X?” but “is doing X ergonomic, safe, explainable?” As programmers we get so caught up in asking binary questions that we frequently get the wrong idea that everything should be so categorised. But that line of questions just leads to uninteresting answers and lack of communication. More often than “is it Turing complete?” perhaps we should ask, “is it Pac-man complete?”

Monads are part of C++. They’re also part of Cpp2, even if Herb didn’t say the word. And just like other patterns, if you develop a sense for them, they become part of your toolkit. And they are a very useful tool. But we don’t name the things we build after the tools we use. And choosing not to say “monad” in a talk is a pedagogical device and a recognition of familiar C++ vocabulary — not an implication that monads are weird and other. That is not helpful, and not the C++ way.

C++23’s new function syntax

Monday, September 5th, 2022

We’ve had a couple of ways to spell functions for a long time:

[[nodiscard]] auto say_a_to(
    std::string_view what, std::string_view name) -> std::string {
  return std::string{what} + ", " + std::string{name} + "!";
}

say_a_to("Hi", "Kate"); // -> "Hi, Kate!"

struct {
  [[nodiscard]] auto operator()(
      std::string_view what, std::string_view name) const -> std::string {
    return std::string{what} + ", " + std::string{name} + "!";
  }
} say_b_to;

say_b_to("Hello", "Tony"); // -> "Hello, Tony!"

And we’ve had a shortcut for that second one for a while, too:

auto say_c_to = [] [[nodiscard]] (
    std::string_view what, std::string_view name) -> std::string {
  return std::string{what} + ", " + std::string{name} + "!";
};

say_c_to("Bye", "Kate"); // -> "Bye, Kate!"

(Aside: notice where [[nodiscard]] is positioned, since C++23’s P2173, to apply to the lambda’s call operator.)

All these ways to spell functions look the same at the call site. But “regular functions” and function objects behave a bit differently, so as application and library writers we use them for different things according to our needs – how we need users to customize or overload them; whether we want ADL; etc.

C++23 added a new way to spell functions:

struct {
  [[nodiscard]] auto operator[](
      std::string_view what, std::string_view name) const -> std::string {
    return std::string{what} + ", " + std::string{name} + "!";
  }
} say_d_to;

say_d_to["Goodbye", "Tony"]; // -> "Goodbye, Tony!"

It hasn’t been publicized as such, but that’s exactly what it is. C++20 removed the ability to use the comma operator in subscripts (P1161). As a follow-up to that, C++23 now allows multi-argument subscript operators (P2128). And that effectively gives us an alternative syntax for calling functions. The subscript operator now has the same mechanics as the function call operator. These operators are now the only two that can take arbitrary numbers of arguments of arbitrary types. And they’re also the same precedence.

So we can do things like this too:

struct {
  template <std::integral ...Ts>
  [[nodiscard]] auto operator[](Ts... ts) const noexcept {
    return (0 + ... + ts);
  }
} sum;

And call it accordingly:

const auto s1 = sum[1, 2, 3]; // 6
const auto s2 = sum[];        // 0

(Yes, operator[] can also be written as a nullary function.)

This works, today. Probably because to the compiler this was already the bread-and-butter of how functions work anyway, so I’m guessing (although IANACW) it was pretty easy to implement these papers.

P2128‘s envisaged use cases are all about numeric computing and multi-dimensional arrays with integral subscripting. But that’s not all that operator[] is now. Quite literally, it’s alternative syntax for a function call, with everything that might imply.

What use might this be? Well a few things spring to mind. Using operator[] for function calls has all the same lookup and customization implications as using operator(), but adds the inability to call through type-erased function wrappers — at least at the moment. So that might be useful to someone.

A second convention that springs to mind is perhaps for pure functions. If a function is “pure” then it will always return the same output given the same input, which means mathematically it can be implemented with a lookup table. Using operator[] historically looks something like a map lookup, so perhaps it’s a natural fit for pure function syntax?

It might also be useful to naturally express two different areas of functionality within a library, or operations with different evaluation semantics (compile-time? runtime? lazy?) as characterized by function calls with operator() and with operator[]. This would perhaps provide a nice call-site indication to make the code more readable.

There are sure to be other uses. Should you look for operator[] coming soon to a library near you? I don’t know. This might seem strange to some folks, but it’s not necessarily less readable; just less familiar. And if there’s one thing I know about C++, it’s that it’s a short hop from bizarre newly-discovered quirk to established technique. operator[] is now equivalent to operator(), and when someone finds a use for that, it will get used.