`std::flip`

(morwenn.github.io)

70 points | by ashvardanian 3 days ago

9 comments

  • fooker 3 hours ago
    C++ is surprisingly close to being a usable functional language.

    The two missing pieces are -

    * structural pattern matching

    * uniform function call syntax that is : a.foo(b) vs foo(a, b) being interchangeable.

    With the kitchen sink approach of design I’d not be surprised if these get into the language eventually. These ideas have been proposed a few times but haven’t been seriously considered as far as I know.

    • jcranmer 2 hours ago
      > * uniform function call syntax that is : a.foo(b) vs foo(a, b) being interchangeable.

      Herb Sutter has proposed this: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p30... (twice, even, there was an older version of the paper several years ago that didn't pass).

      I'm resolutely opposed to such a thing because, having had to actually wade through C++'s name lookup and overload resolution rules in the past, they're a dense, confusing morass of logic that you can only stand to stare at for a half-hour or so before your brain turns to mush and you need to do something else, and anything that adds to that complexity--especially in "this makes things two things nearly equivalent"--is just a bad idea.

      (For an example of C++ overload resolution insanity, consider this:)

          // Given these overloads...
          void f(std::float32_t);
          void f(double);
          // Which one does this line call? Assume float/double are standard IEEE-754 types.
          void f((float)1);
      • billforsternz 1 hour ago
        I'm going to guess f(double) because floats have always been promoted to doubles for function calls since K&R. But I'm not sure by any means. I'd be ready to get more explicit if I needed some specific behaviour.
        • tredre3 1 hour ago
          By mentioning K&R you seem to imply that C also promotes floats to doubles in function calls? But that is not the case, floats are passed as floats, as you'd expect.

          You can try it yourself on godbolt all the way back to GCC 3, test(float x) has always emitted movss and test(double x) will result in movsd/movlpd.

          • jcranmer 1 hour ago
            Unless you're calling a variable-argument function--floats are promoted to doubles for variable-argument functions:

            > The arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter, if present. The integer promotions are performed on each trailing argument, and trailing arguments that have type float are promoted to double. These are called the default argument promotions. No other conversions are performed implicitly

          • DSMan195276 21 minutes ago
            It's a difference of whether the function arguments are declared or not. If you declare a `void foo()`, and then call `foo((float)f)`, the `foo()` function is actually passed a `double` as the first argument rather than a `float`. If you instead change the declaration to `void foo(float)` then it gets passed as a `float`.

            Ex: https://godbolt.org/z/TKjz3Tqqr

      • Sharlin 1 hour ago
        I think there have been proposals to add universal call syntax since before C++98, some of them by Stroustrup himself.
    • xigoi 2 hours ago
      > With the kitchen sink approach of design I’d not be surprised if these get into the language eventually.

      Based on the history of C++, they will, but with extremely bizarre syntax. Instead of a.foo(b), it will be something like a@<foo>::&{b}.

      • kelseyfrog 2 hours ago
        The justification will, of course, be that the other options break digraph support on some ancient platform that has no public spec and will never sunset - likely one IBM AS/400 in a basement with a 45 year uptime.

        Out of "respect for existing deployments," the syntax must accommodate the relic even though no one has seen it outside of the folklore of one 1993 Usenet post.

        Every tool chain will begrudgingly implement it and then years later when someone proposes removing it the counter argument will be, "we can't remove it now someone is using it!" The someone, of course, is a hobbyist on the mailing list whose project depends critically in the feature.

        • bongodongobob 2 hours ago
          Well, they're still in use. I've encountered 3 in the last 5 years of consulting. Just because your startup doesn't have one doesn't mean they don't exist in the manufacturing world. Are they ubiquitous? No. Are they rare? Depends on the industry.
          • kelseyfrog 1 hour ago
            Perfect! The standards committee will be able to reference this reply the next time they justify eye-bleeding syntax instead of having to go mine Usenet. Thank you.
          • wat10000 24 minutes ago
            When you say they're still in use, do you mean AS/400s or digraphs?
      • p0w3n3d 2 hours ago
        So true. For me C++ syntax is unreadable, but the ideas behind it are familiar
      • vjvjvjvjghv 2 hours ago
        “ a@<foo>::&{b}”

        This made me smile. So true

        • pjmlp 1 hour ago
          See new reflection syntax.
      • im3w1l 2 hours ago
        Isn't structural pattern matching basically the same as creating an anonymous struct whose fields are references, with a default assignment operator? Syntax ideas should spring to mind.
    • codeflo 2 hours ago
      Achieving uniform call syntax is easy, compilers just need to implement a new form of symbol resolution called "Kaiser lookup". It follows 14 easy to understand rules. It first looks up methods, then searches for template definitions in /tmp, then for a matching phase of any Saturn moon at time of compilation, then looks of definitions std::compat, and then in the namespaces of the types of any variables in scope anywhere on the call stack. If none of those work, it tries to interpret the call syntax as a custom float literal, and if even that fails, as a Perl 4 compatible regex. It's really intuitive if you think about it.
      • jcranmer 2 hours ago
        I wish C++ name lookup and overload resolution were that simple.
        • pjmlp 1 hour ago
          Yep, and there are new ways with modules, and reflection, we can't have enough. :)
    • andyjohnson0 1 hour ago
      > uniform function call syntax that is : a.foo(b) vs foo(a, b) being interchangeable.

      Ive written a lot of c++ in the past but I'm not particularly knowledgeable about fp, so I'm wondering why this is important. Is it syntactic sugar or something more significant?

      • fredrikholm 1 hour ago
        It allows existing method-heavy code to be used in a functional style without bending the knee to more OOP inspired patterns.

        Think of a fluid API, but instead of chaining method calls you'd pass data to several functions "chained" together similar to how UNIX pipes work.

        With this type of API, you can pass one argument into the function and pipe the other such that:

          data |> foo(bar) |> baz
        
        Is a more FP friendly version of:

          return baz(foo.bar(data))
        • warkdarrior 15 minutes ago
          Shouldn't that be?

              data |> bar(foo) |> baz
          
          Or maybe:

              data |> bar(&foo) |> baz
        • andyjohnson0 1 hour ago
          Makes sense. Thank you!
    • Sharlin 1 hour ago
      > * structural pattern matching

      Yes, but that has to come with proper sum types. std::variant doesn’t quite cut it.

      I’d also absolutely require a simpler lambda syntax. The current one is terrible for one-liner lambdas.

      • fooker 55 minutes ago
        C++ has a philosophy of not doing anything with the language if it can be done in library.

        Now the question is : what can we improve in the language so it can allow you to define a sum type better and more usable compared to std::variant?

        This is a surprisingly difficult question to answer, hence we haven’t had progress there.

        • MattPalmer1086 40 minutes ago
          > C++ has a philosophy of not doing anything with the language if it can be done in library.

          Then why is the language so ridiculously complicated? I had the most fun working with it back in around 2000, and even then it was quite insane. A lot has been added since then.

    • ashvardanian 3 hours ago
      The second piece (uniform call syntax) looks convenient, though I don’t see a realistic way to integrate it into modern C++. The first (structural pattern matching) is, for me, more of a dividing line between low- and high-level languages. I tend to avoid it in my C++, just as I avoid inheritance, virtual functions, and exceptions… or `<functional>` header contents.

      Still, it’s always fun to stumble on corners of the STL I’d never paid attention to, even if I won’t end up using them. Thought it was worth sharing :)

      • fooker 44 minutes ago
        There was an experimental implementation for uniform function call syntax in a clang fork, so it’s clearly doable.
    • delta_p_delta_x 2 hours ago
    • nextos 2 hours ago
      There are actually a few functional programming in C++ books out there. The language has changed a lot since C++98, when this would be unthinkable. Alexander Granin maintains a curated list of functional programming C++ resources [1].

      [1] https://github.com/graninas/cpp_functional_programming

    • zahlman 2 hours ago
      I don't find it surprising. My impression is that people like Herb Sutter and Alexander Stepanov actively pushed in that direction in the early days. `<functional>` was, AFAIK, part of the STL before it got incorporated into the C++ standard library.
    • constantcrying 2 hours ago
      Another missing piece is a good syntax. C++ has, as you said, most of the capabilities already, but actually using them will quickly turn the code into symbol vomit.
  • arjvik 2 hours ago

      * Some standards such as ISO 6709 (Standard representation of geographic point location by coordinates) describe points as (latitude, longitude).
      * Others such as RFC 7946 (GeoJSON) describes points as (longitude, latitude).
    
    Using (hypothetical) std::flip to reify these APIs seems like a loaded footgun - someone is bound to accidentally use it {zero, two} times to convert between orders when it needs to be used once and wreak havoc.
    • wk_end 2 hours ago
      No more of a loaded footgun than trying to do it manually, I don't think.

      Geographic points should probably be represented as a labeled structure to prevent confusion and passed into functions as such. Using two separate libraries with mutually incompatible error-prone APIs as described is the real loaded footgun IMO. If you can't find better libraries, write wrappers; if you don't have time to write/maintain wrappers, pray. Anything else is just a bandaid.

    • jandrewrogers 2 hours ago
      Probably no better or worse than the alternative. That aside, the example doesn't understand the standards in question.

      The governing standard for geospatial data representation is ISO 19125, which defines (longitude, latitude) order. GeoJSON naturally conforms to ISO 19125 since it is a format for processing data on computers.

      ISO 6709 is essentially a print formatting standard and orthogonal to storing geospatial data on computers. That some data file formats happen to be human readable does not make ISO 6709 apply.

      If you are processing geospatial data on computers the correct order is always (longitude, latitude).

    • usefulcat 2 hours ago
      > someone is bound to accidentally use it {zero, two} times

      That risk is inherent to the problem at hand, and has nothing to do with std::flip.

  • cocoto 2 hours ago
    I love Haskell but when writing C++ I always avoid functional style gibberish. I feel like this style of programming only works in languages properly designed for that.
  • Matheus28 25 minutes ago
    Should be using empty base optimization or [[no_unique_address]] for that implementation
  • zahlman 2 hours ago
    > Interestingly enough, most of these implementations only flip the first two parameters of whichever function they are passed, though it seems to be because most of them are based on the Haskell prelude, and handling arbitrary arity can be tricky in that language.

    Probably because the use case for it with higher arity is hard to imagine. (Indeed, TFA gives only examples with binary operations.)

    > Fortunately it is not just useless knowledge either: flip can be reified at will by copying the following C++17 implementation.

    > [snip 114 lines of code]

    Meanwhile, in Python:

      def flip(f):
          return lambda *args, **kwargs: f(*args[::-1], **kwargs)
    
    (I leave keyword arguments alone because there's no clearer semantic for "flipping" them.)

    The `toolz.functoolz.flip` implementation (being restricted to binary functions) is an even simpler one-liner (https://toolz.readthedocs.io/en/latest/_modules/toolz/functo...), though accompanied by a massive docstring and admittedly simplified through a heavyweight currying decorator (which accomplishes much more than simply getting a function that does the right thing).

    • Matheus28 21 minutes ago
      Your python code allocates an array and inverts it every function call.

      The C++ code has no overhead and is equivalent to a compile time transformation.

      • zahlman 2 minutes ago
        Of course. But if I had to care about things on that level, and I was willing to sit through the C++ compilation process (and everything else that goes along with that), I wouldn't be using Python in the first place.
    • Jaxan 2 hours ago
      For higher arity there is a combinatorial explosion of all the possible permutations.

      But if you want to flip the 2nd and 3rd argument in Haskell it can be done by flip itself:

      flip23 foo = (\x -> flip (foo x))

      • marvinborner 1 hour ago
        Or just (flip .), which also allows ((flip .) .) etc. for further flips.

        In Smullyan's "To Mock a Mockingbird", these combinators are described as "cardinal combinator once/twice/etc. removed", where the cardinal combinator itself defines flip.

  • abalaji 3 hours ago
    this blog post will be a great barometer of commenters who read the post vs those who don't
    • jeffbee 3 hours ago
      THUITFHNGL

      Fortunately almost all the functional features in the article, like range folds and negation wrappers, do exist.

    • forrestthewoods 2 hours ago
      I skimmed the post. I have absolutely no idea what std::flip is supposed to do. All the sample code looks awful and undesirable. And that’s coming from someone who writes C++ every day. Yes I read the plot twist at the end, made me lol.
      • OskarS 2 hours ago
        You write C++ every day, and you didn’t understand the is_descendant_of/is_ancestor_of example? Or how you can use it to reverse a relation like std::less?
        • forrestthewoods 1 hour ago
          I don’t understand why I should care about this. It doesn’t appear to solve real problems. The examples are all dumb toys and simply writing a wrapper by hand is perfectly fine and easier to read.
      • loeg 2 hours ago
        > I have absolutely no idea what std::flip is supposed to do.

        Just reverse parameter order. It seems very silly.

          void f(int, double);
        
          void main() {
            flip(f)(3.14, 1);
          }
        • gblargg 2 hours ago
          You do realize it's not meant for silly situations like that, right?
  • loeg 2 hours ago
    > Fortunately it is not just useless knowledge either: flip can be reified at will by copying the following C++17 implementation.

    I hope not.

  • abrudz 2 hours ago