Hacker News new | ask | show | jobs
by gkmoyn 4299 days ago
Somehow I knew the top comment was gonna be "C++ is insane!!"

How would you implement pipe functions in Javascript? And I mean literally using the pipe operator, and not just chained. You can't, so you don't.

In Java? You can't, do you don't.

In C++ you can. You probably won't. You probably shouldn't so others can maintain your code in the future. But still, you can do very advanced things.

Thanks, pfultz2, for flexing C++'s muscles.

7 comments

I've been quite disillusioned with C++ myself as of late. I am currently writing a wrapper class around shared pointers to objects with inheritance (which embed their own private implementations, which also utilize inheritance) so that I can pass them by value and use operator. for method chaining. The SFINAE used for construction from base objects to these shared pointer wrappers is something out of the ninth circle of hell.

    template<typename T> struct Shared : std::shared_ptr<T> {
      template<typename U> struct is_compatible {
        typedef char yes[1], no[0];

        template<typename V> static yes& test1(typename std::enable_if<std::is_base_of<std::shared_ptr<T>, V>::value>::type*);
        template<typename V> static no& test1(...);

        template<typename V> static yes& test2(typename std::enable_if<std::is_base_of<element_type, typename V::element_type>::value>::type*);
        template<typename V> static no& test2(...);

        static constexpr bool value = sizeof(test1<U>(0)) == sizeof(yes) || sizeof(test2<U>(0)) == sizeof(yes);
      };
And from the function binder:

      //value = true if R L::operator()(P...) exists
      template<typename L> struct compatible {
        template<typename T> static constexpr typename std::is_same<R, decltype(std::declval<T>().operator()(std::declval<P>()...))>::type exists(T*);
        template<typename T> static constexpr std::false_type exists(...);
        static constexpr bool value = decltype(exists<L>(0))::value;
      };
      template<typename L> function(const L& object, typename std::enable_if<compatible<L>::value>::type* = nullptr) { callback = new lambda<L>(object); }
And yet ... when actually using the library, it is a thing of beauty.

    struct TextEditor : Window {
      MenuBar menuBar = {this};
        Menu menuFile = {&menuBar, "File"};
          MenuItem menuQuit = {&menuFile, "Quit"};
    
      VerticalLayout layout = {this};
        TextEdit editor = {&layout, Size{~0, ~0}};
    
      TextEditor() {
        StatusBar statusBar{this};
        statusBar.setFont(Font::sans(8, "Bold")).setText("Line 1, Column 1");

        HorizontalLayout findBar(&layout);
        findBar.append(Label(&findBar, "Find:"));
        findBar.append(LineEdit(&findBar).setBackgroundColor(Color::Yellow)
        .onChange([&] { searchFor(text()); }));
        findBar.append(Button(&findBar, "Clear")
        .onActivate([&] { findBar.widget(1).setText(""); }));
    
        menuQuit.onActivate(&Application::quit);
        edit.onChange([&] { updateStatusBar(); });
      }
    };
There is no need for any memory management, or any usage of pointers. We build UIs, and everything gets automatically released safely when nothing is referring to it anymore. We can declare named objects that we can use later, or we can create dynamic objects and pop them right inside of other objects. We can destroy and unparent things whenever we want. And it's deterministic, reference-counted GC. No pauses for a tracer. No dynamic typing anywhere, all errors are at compile-time.

I highly suspect that C++ is unreasonably complicated and that all of this rvalue-reference, variadic template, meta-programming, dynamic-casting polymorphism, is all just voodoo that isn't applicable to general programming. And yet, being able to do it gives me amazing expressive power to write awesome libraries that I could never hope to accomplish in another language.

Until I find a language that's even in the same ballpark as C++ in terms of performance, and offers similar expressiveness, it really doesn't even matter how bad C++ can be for library authors. There's no other viable option right now. D is the closest we have, but its complexity already rivals, if not exceeds, that of C++.

A side note: I think you may be able to simplify `is_compatible` by getting rid of hand-rolled `yes` and `no` constants (and subsequent manual size-testing for `value` computation) and using `std::true_type` and `std::false_type`, respectively: http://en.cppreference.com/w/cpp/types/integral_constant

Edit: noticed you're using these in another place (`compatible` implementation), so perhaps there's a reason for a different approach?

Thanks, the is_compatible test certainly gave me a good bit of trouble.

Ideally you'd want to do enable_if< conditionA || conditionB >, but of course if one of the conditions fails to evaluate, the overload is ignored. So you have to split out the conditions and them merge them back together later on.

We could use true_type / false_type, but they have equivalent sizes. So unlike the function version that only needs one test and can just take the return type directly, the test at the end would then have to become std::is_same<decltype(test1<U>(0)), std::true_type>::value | std::is_same<decltype(test2<U>(0)), std::true_type>::value.

I still think we can do better than even this, so I'll have to keep working at it.

Just because you can use some ambiguous syntax construct does not make it good or even clever.

This sort of thing can be done a lot clear with functional constructs like compose and curry.

Common Lisp example (easily done with any language that supports higher order functions): (funcall (compose 'add-one 'add-one) 1)

And you can roll that a number of ways; adding in a curry of the identity of 1 if you just want a nullary function.

(funcall (compose 'add-one 'add-one (curry 'identity 1))

And if you are interested in typing less, maybe you are a dreadful typist...try Haskell.

There are three reasons why a program would look like this:

  1. It is something simple implemented in a complicated way.
  2. It is something complicated implemented in a simple way.
  3. It is something complicated implemented in a complicated way.
I believe this is an example of case 3, meaning even though it is something advanced, it should not take code that looks insane to express it.
I disagree. It makes it very simple to make a pipable function, you just wrap the function object in pipable. There's nothing complicated about using it.

Futhermore, doing it by hand is not as simple, but still not very complicated. This just helps alleviate the boilerplate in defining pipable functions.

There's nothing complicated about using it.

I'm not talking about using the pipe operator, I'm talking about implementing it. The C++ version is ridiculously complicated.

Let's compare your code with an analogous implementation in Common Lisp.

  (defun pipe (val &rest fns)
    (reduce (lambda (acc f)
              (funcall f acc))
            fns :initial-value val))
Just this allows for some pretty similar code:

  (pipe 99 #'1+ #'sqrt #'1-) which evaluates to 9.0
It is possible to implement it in C++, but the implementation winds up being ridiculously complicated. I just implemented something very similar in Common Lisp and it wound up being incredibly simple. Why doesn't C++ allow for a definition nearly as nice as the Common Lisp one?
> It is possible to implement it in C++, but the implementation winds up being ridiculously complicated.

It may be more verbose in C++, but its not more complicated, and its definitely not "ridiculously" complicated.

> I just implemented something very similar in Common Lisp and it wound up being incredibly simple.

Maybe for you as a lisp programmer, but for me I find the lisp code incomprehensible.

>How would you implement pipe functions in Javascript? And I mean literally using the pipe operator, and not just chained.

>But still, you can do very advanced things.

Why do you think "advanced" means the ability to overload operators? I'd personally define "advanced" to mean something to do with semantics, not syntax.

Just because a language allows you to morph its syntax doesn't make it any more powerful. I'm not sure how this is considered an "advanced" thing. It's certainly a convoluted thing, but that's not at all the same.
For JavaScript there is this: https://github.com/hij1nx/pipe
Interestingly, you can in Powershell, where piping objects works very naturally.