Function chaining in CQ?

101 views
Skip to first unread message

trayracing

unread,
Jan 3, 2022, 8:26:21 AM1/3/22
to CadQuery
Anyone tinkered with a function chaining operator in cq to chain at a higher level? So to create this:
plate.PNG
one could define functions for individual features and then write this:

      result = cq.Workplane("XY") >> Body >> CenterHole >> MountHoles >> (FilletZ, 2.0)

Cq uses method chaining to great effect, with workplane serving as coin of the realm. However, as in OpenSCAD , it's all too easy to create a long, hard to decipher chain, essentially creating write-only code. In OpenSCAD, modules came to the rescue, creating a module for each feature and the modules naturally chain in with existing functions. As a nice side effect, features become easily testable. For cq, it's not quite so natural to mix cq and user level code, unless you add your code as methods to the global workspace class. That doesn't scale well. But you can add a single chaining operator to Workplane, so code looks something like:

  import cadquery as cq

  (height, width, thickness) = (60.0, 80.0, 10.0)
  (diameter, padding) = (22.0, 12)

  #add function chaining operator to Workplane
  cq.Workplane.__rshift__ = lambda self, a: a[0](self, *a[1:]) if type(a) == tuple else a(self)

  def Body(wp): return wp.box(height, width, thickness)
  def CenterHole(wp): return wp.faces(">Z").workplane().hole(diameter)
  def MountHoles(wp):
      return (wp.faces(">Z").workplane()
              .rect(height - padding, width - padding, forConstruction=True)
              .vertices().cboreHole(2.4, 4.4, 2.1))
  def FilletZ(wp, dia): return wp.edges("|Z").fillet(dia)

  result = cq.Workplane("XY") >> Body >> CenterHole >> MountHoles >> (FilletZ, 2.0)

Really that's too simple of a model to bother decomposing. but it gives the gist of the technique. Note I didn't implement kwargs. That should be doable, although a bit clunky.
Anyhow, I'm a python noob, so there are probably better ways to do this. (pypy's python--chain, for example, which isn't a great fit for this). I could also imagine a different syntax that isn't specific to workplane:
  chain((cq.Workplane, "XY"), Body, CenterHole, MountHoles, (FilletZ, 2.0))

Anyhow, does this look generally useful? Are there ways to do this better?

Dave Cowden

unread,
Jan 3, 2022, 9:18:09 AM1/3/22
to trayracing, CadQuery
Hi, trayracing:

Thanks for thinking about/raising this question. I have not thought enough about this to directly propose what'd be best, but I do want to throw a couple of ideas into the mix. 

First, from version 1.x of CQ, it's been apparent to me that the fluent layer, while good for many things, is a hindrance when models are very complex.  I think you're seeking to solve that problem to some extent or another. 

I think the most straightforward method is to fix the root cause, which is making the core CQ functions code accessible in a non-fluent way.  This is discussed in this issue.   This is a flexible solution because then it would be possible to use any number of coding techniques/interfaces on top of that api.  In an ideal world, the current fluent api would be a separate layer on top of a more procedural, operation-oriented api. A function-chaining api that provides some of the benefits you describe could be implemented on top of the core lower-level api.

My gut reaction is that function chaining on top of the fluent api would be complexity on top of complexity-- it could get very hard to understand and debug.  Beginning users already struggle to understand the current fluent api. I do see the benefits of a function chaining approach.  There are a number of ways to accomplish it in python, including closures, generators, or operation objects,  but I think most of those ways would be more straightforward if implemented on top of a more procedural low level api than on top of the current fluent api. 

One question regarding what you are wanting to accomplish: are you primarily seeking to provide a way to organize user code (ie, your code), to increase readability and maintainability, or are you seeking to organize the code in a way that it's more easily shared with other users for their own projects ? I think the 'right' approach depends on which of these two you're primarily interested in. 

If it's the former, most users will bring certain coding styles with them, and an opinionated style isn't ideal. Best to provide a clean, simple low level api, and let users code it how they like it best.  On the other hand, if the latter is your interest, then we're talking more about a module system for general use.  In that case, the most important conversation isn't how the code is structured per se, but instead what the interfaces are.  As an example, a key element of the 'direct design api' is that objects represent operations, so that the operation can be created and managed before it is executed, and so that each operation can track metrics like its execution time, its affects on the modeling context, etc.  It's a lot like the 'Command pattern' in a sense.  

I don't think that answers your question at all-- but all the above to say there's definitely been a lot of thought on how to make the api more flexible!




--
cadquery home: https://github.com/CadQuery/cadquery
post issues at https://github.com/CadQuery/cadquery/issues
run it at home at : https://github.com/CadQuery/CQ-editor
---
You received this message because you are subscribed to the Google Groups "CadQuery" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cadquery+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cadquery/4cd10230-fcec-44a2-b244-e4a764c64d8bn%40googlegroups.com.

trayracing

unread,
Jan 3, 2022, 11:08:25 AM1/3/22
to CadQuery
Thanks for the ideas on fluent layers, TheBlueDirt. Here, I'm focusing on finding an effective coding style well suited to the problem space and the current APIs (or nearly so) to tackle the designs I have today, rather than proposing a different library API design. (a valid but separate discussion). Clarity in code is a big thing for me: understandable across time for me, as well as for sharing code & ideas.  Debuggability and potential for reuse are up there too.
Certainly other people have experimented with various styles like client level chaining, and their experiences and/or regrets would be valuable. Many people will just adopt the style of the tutorials, which seems to be glomming together one long statement. That's optimal in the sense of introducing the fewest concepts, but doesn't scale well. Breaking it into shorter, well named intermediates would improve clarity.

Dave Cowden

unread,
Jan 3, 2022, 1:52:47 PM1/3/22
to trayracing, CadQuery
i see ok that helps me understand what you're after.

One area I think might be of some interest -- and hear me out on this -- is pandas and data science.

While pandas is completely different from cq, it shares the concerns you raise.  The api is very complex, very powerful, and fluent. So, you can easily write code you can't understand later. 

The reason i think this is relevant is because pandas has a much huger following, and thus has a LOT of people fighting the same battle. As an example, you might find this article interesting, which  talks through materially the same problem, but with dataframes as the subject rather than solid operations. 

My  main intent isn't to suggest a particular direction-- rather, to point out that looking at articles about pandas ( or other very popular apis that have complex underpinnings but a fluent-capable api ) might give you a lot of interesting points of view, that would translate rather readily to the cq problem space.

Adam Urbanczyk

unread,
Jan 3, 2022, 2:48:28 PM1/3/22
to CadQuery
CQ is a python module so you can use whatever is available (e.g. https://funcy.readthedocs.io ) for chaining. I personally don't find adding new operators useful - just split your code into functions and don't make it more complex than needed.

trayracing

unread,
Jan 3, 2022, 3:06:01 PM1/3/22
to CadQuery
Yes, " The Unreasonable Effectiveness of Method Chaining in Pandas" was a good read - I'd actually read that while searching before writing the above example. Panda's pipe method lacks that addictive that addictive syntactic sugar. :) but kwargs handling is cleaner.  Actually, I was surprised how little there is on function chaining (vs. method chaining).

trayracing

unread,
Jan 4, 2022, 9:49:39 AM1/4/22
to CadQuery
Thinking about it more, adding a chain operator doesn't fit the smaller or larger ecosystems - it doesn't chain allow client functions to use a syntax identical to CQ chaining, nor does it use a syntax useful for general python chaining. If I were to embrace chaining in my general python code, then I would want CQ to play nice with it so I can use a single chaining syntax.
That brings me back around to thebluedirt's position that that a non-fluent CQ API is a desirable addition.

A slight expansion of the dot operator definition in Python would make this discussion moot, but I don't see any function chaining PEPs in discussion.

With python's current definition, I can chain functions using:
          result = chain(cq.Workplane("XY"))(Body)(CenterHole)(MountHoles)(FilletZ, 2.0)()
with the proper implementation of "chain". That  works, but leaves me with two syntaxes for chaining. That is a Python language issue, not CQ.

Dave Cowden

unread,
Jan 4, 2022, 12:16:17 PM1/4/22
to trayracing, CadQuery
yeah, and language issues quickly become things we mere mortals can't influence.  personally, i'm not sure that this:

  result = chain(cq.Workplane("XY"))(Body)(CenterHole)(MountHoles)(FilletZ, 2.0)()

is actually more readable than this:

 result = cq.Workplane("XY").body().centerHole().mountHoles().Fillet(">Z")

I'm sure they would be equal to someone very familiar with the chain() method, but the second is readable to others (me) too. Plus, the second form still allows you to clearly and easily accept function parameters

Which can be accomplished today by simply patching your functions into CQ -- supported and totally ok in CQ land even if not ok to the function and OOP gods.

trayracing

unread,
Jan 4, 2022, 7:03:42 PM1/4/22
to CadQuery
>   i'm not sure that this... is actually more readable than this:

Dot chaining, at least at the point of invocation, is a lot cleaner looking. But once you count in gunk of patching the functions into workplane class, maybe it evens out. Also, patching single use local functions into the global class is not great for scalability. And, as a reader of the code, you might end up looking at CQ docs vainly trying to find these added methods.

trayracing

unread,
Jan 4, 2022, 8:00:31 PM1/4/22
to CadQuery
Before I wander off this topic: I can't imagine that chaining function is unique, graceful, or clever but I renamed it foldFuns and made a gist for it.

Dave Cowden

unread,
Jan 4, 2022, 9:13:30 PM1/4/22
to trayracing, CadQuery
Your points are valid.  There are a lot of considerations, and probably as you point out, how nice it looks is tiny in comparison with many other things. For each use case, different advantages may win out over others.

... Which means ultimately the 'right' answer is probably for CQ not to be opinionated on the subject.  It's an api. You can use foldFuns() and others can patch, and still others can make their own old-school OOP wrappers.  The ability to let coders be coders and use a variety of techniques is probably _the_ most important benefit of using python ( a real language)-- everyone can do whatever!



Adam Urbanczyk

unread,
Jan 5, 2022, 1:55:38 AM1/5/22
to CadQuery
I'm afraid it is not unique: https://funcy.readthedocs.io/en/stable/funcs.html#compose. IMHO it is better to not reinvent wheels.

trayracing

unread,
Jan 5, 2022, 8:54:26 AM1/5/22
to CadQuery
Thanks for the link Adam. `compose`  (or really `rcompose`)  is undoubtedly more powerful than foldFuns and can get to the same answer.  But look at readability for this use:
from operator import sub, mul

z0 = rcompose(rpartial(sub, 3), rpartial(mul, 5))(6)

z1 = foldFuns(6)(sub, 3)(mul, 5)()

The latter is less noisy and puts the incoming argument next to the function that uses it, which is more in the flavor of chaining.
Reply all
Reply to author
Forward
0 new messages