Hacker News new | ask | show | jobs
by p4wnc6 3742 days ago
This is nothing new at all, it is called the "Fluent Interface" design. Many Python libraries implement this design, for instance the Pandas library is a popular example.

I hate this design approach deep in my soul. It makes for very brittle code that creates lots of backward compatibility issues. If you're working on some legacy code that has some nonsense like

    foo.get_status().dispatch_handler().log_error().close()
it is maddening! You have to untangle just what exactly gets returned by every step of the chain, so that you can ensure you're in the right context to know exactly what the next call of the chain is doing.

In that example, say someone changes `foo.get_status()` to return some new kind of "status" object, and it alters the `dispatch_handler` and so on. Of course one can implement this in a way where the chain of downstream calls doesn't break, but the point isn't so much that, through huge engineering effort it is possible, but rather that it is extremely brittle and adds a layer of complexity that's not needed.

It's just so much better to write something like:

    dispatch_result = run_dispatcher(foo.get_status())
    log_error(dispatch_result)
When the intermediate points of the chain are just functions, instead of member functions of a class, it means you can easily experiment with them and figure out what's going on without needing to recreate the entire set of context along the whole chain.

`run_dispatcher` in my example would be a hell of a lot easier to unit test and throw some mocked example class into for debugging or refactoring than if it is `some_class.run_dispatcher` ... and then if `some_class` has child classes that specialize the behavior, you're just hosed.

The problem is composability. People think that the fluent interface makes things composable because from some arbitrary point in the middle of the chain of calls, they have easy attribute-like access to the next operation they want to do. This artificially feels easy and convenient.

But contrast this to a functional language like Haskell, where none of these things need to be member functions of an object, and hence the context of the object doesn't have to be created at any point in the fluent chain. Then you can write something even better:

    (close . logError . dispatchHandler . getStatus) foo
We can even easily refer to this whole chain of events with a single function name:

    let statusDispatchLog = (close . logError . dispatchHandler . getStatus)
(And, of course, we get lots of nice type checking in statically typed languages to ensure that the composition actually makes sense -- which not only protects you at run time, but is also a huge help to clue you in to your design flaws. If you're trying to shoehorn some stuff into a fluent interface and it's not working, it probably means you have thought clearly about how the methods should "flow" in the call chain.)

To do the same thing in a fluent interface, we need a horrible lambda or a whole new function definition, exactly because the fluent interface is only sweeping the composability issues under the rug.

    statusDispatchLog = lambda x: x.get_status().dispatch_handler().log_error().close()
The difference is subtle, but important. Instead of making a new function that is explicitly the composition of other functions, you are making a function that just happens to access other functions as attributes, and if you set it up correctly then it acts as a sequence of composition.

In Python this is particularly a shame because functions are first class objects. Of course, you can write helper functions / decorators that sort of do function composition (if you're willing to throw away useful argument signatures), or you can use flaky hacks like the common Infix pattern in Python, and then live with ugly "<< . >>" or "|.|" misleading syntax.

It always makes me sad that Python lacks an extremely short function composition infix operator that provides some information about the function signatures of the functions being composed.

Because not even a comprehension can help you when you need to do the fluent interface stuff in Python.

    [x.h().f().g() for x in some_iterator]
This is so much worse than

    map(g.f.h, someIterator)
or

    [(g.f.h) x | x <- someIterator]
or even

    [g(f(h(x))) for x in some_iterator]