discuss@lists.openscad.org

OpenSCAD general discussion Mailing-list

View all threads

Re: New feature in 2025.07.11: the object() function

CC
Cory Cross
Sat, Aug 16, 2025 11:33 AM

On 8/15/25 11:20 AM, Jordan Brown wrote:

I don't think prototype inheritance is a great fit for a language
with immutable data types. I think either generic functions or class
definitions are a better fit. Just because you /can/ use
/object/+lambdas to keep many things you want doesn't mean you /should/.

Perhaps I just don't know enough OO theory.

I like JS because it has relatively simple building blocks, and yet
you can build very complex and powerful things with it. (Contrast with
C++, which adds so many syntactic and semantic constructs to C that it
makes me dizzy.)  JS has been what I've been primarily looking to in
thinking about adding OO-ish features to OpenSCAD.


BTW, note that I think that what we're talking about is of interest to
less than one in ten OpenSCAD users, and quite possibly less than one
in a hundred.  I expect that people like Adrian and Revar (authors of
BOSL2) are very interested, but I'm a pretty advanced user and I run
out of interest shortly after getting the basic data-only objects, and
almost completely once we have "this".  I don't think my projects
would benefit from "super".  (But, indirectly, if it helps build
BOSL2++, it helps me.)

I don't think OpenSCAD should have complex OO, which is what worries me
about the this discussion and all the complexities of scoping and such
you have brought up. Here's what I've been thinking about:

In the context of the 3 problems I stated (maps/structs, namespacing,
data compartmentalization). Add namespaces. Define
maps/structs/records/whatever-you-call-it as only containing Values
(ints, lists, lambdas, other records, etc) and having one parent. Both
of these things can be implemented simply (name mangling for namespaces,
records can be a hash map + parent lookup on miss). Both of these have
immediate benefits, both are easy to explain to people. They both have
established ways of being optimized for speed.

That just leaves "how do I do OO?" We know from Java's history that long
inheritance chains are bad and people rarely change out implementations
and OO didn't make this any easier.
But then that isn't the real request. The real request is "How do I make
it less likely to make mistakes and spend less time writing code to get
my result?"

Go's philosophy is, in part, to notice people don't spend much time
typing, so don't focus on minimizing key strokes, instead focus on
readability and unit testability, even if that means you write a lot of
redundant things over and over again.
Go has classes ("structs") which are allowed to have up to one parent
and must be a concrete type.
It also has "interfaces" which is a list of method signatures and has
its own methods. If your class has all the method signatures of the
interface, you can cast it to the interface and call the interface's
methods' on it.

The single-inheritance is a good choice. The only inheriting from
concrete types is good. We learned from Java.

However, Go is very imperative. Further lessons are not helpful to a
language with immutable values.

So, what do languages with immutable values usually have in common?
Elixir/Erlang, Haskell, Ocaml, etc: Pattern matching.

And if you combine functional language, objects, and pattern matching,
you get generic functions. The exact syntax details of the following are
mostly not important.

namespace bosl2 { namespace threading { namespace MyClass {
   obj = struct(some_default=2);

   // Constructors can be just normal functions
   function new(something,some_default) =
     assert(!is_undef(something))
     let (
       args = [["something" something]],
       args = concat(args,is_undef(some_default) ? [] : [["some_default" 
some_default]])
     ) struct(obj, args);

   // Methods are functions and modules.
   function sum(o is obj) = o.something+o.some_default;
   module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); }

   // Constructors can also be method functions
   function coerce(l is list) = new(l[0],l[1]);
} // namespace MyClass

// Modules/functions can be generic on anybody's class:
module nut(o is MyClass::obj, normal_arg) {
   if(normal_arg) {
     cylinder(d=MyClass::sum(o));
   } else {
     cylinder(d=o->sum()); // -> walks from object through its ancestors 
for the first matching method in each object's creation namespace
     o->fat_cylinder();
   }
}

module nut(o is list, a_different_arg_is_possible) {
   obj_o = MyClass::coerce(o);
   assert(!is_undef(obj_o));
   nut(o, normal_arg=a_different_arg_is_possible);
}
}}

// backward compatibility
module which_takes_complex_thing(existing, arg, list) {
   assert(on arg);
bosl2::threading::nut([existing, arg*2, list]);
}

Okay, so if you've got the basics of how this works, how does this
actually help?

Let's consider the BOSL2 Threading library (surprise, I bet you wouldn't
have guessed).

You have very long and repeated argument lists
https://github.com/BelfrySCAD/BOSL2/blob/d252f781232a9262583806fe05bbb8529c108899/threading.scad#L384.
And we could say, from Go, that's it's okay to type things repeatedly,
but it's really easy to screw up. So how can we avoid this?

The builder pattern can help. In our hypothetical class hierarchy,
threaded_nut_builder inherits from generic_threaded_nut_builder.

bosl2::threading::nut_builder::new(required, args,
here)->optional_generic_arg(its_value)->reify();

From a user's standpoint, this used to be:

threaded_nut(required, args, here, optional_generic_arc=its_value);

From a user standpoint, it's hardly different. We might even have
syntax sugar to simplify ::new and ->reify().

But from a maintainer's standpoint, you no longer have to edit an
ever-growing list of places for new arguments to generic_threaded_nut.
threaded_nut_builder's reify() module only has the same dozen lines of
computation before chucking it up to generic_threaded_nut. You have a
private namespace for any logic you want to unit test. No semantics of
the language are changed regarding types, handling missing args, etc.
You can maintain 100% compatibility with the existing API while adding
options/features to the new API. You can incrementally update the
codebase a single module or function at a time, so it's easy to adopt.
It's focused around answering the question "How do I make it less likely
to make mistakes and spend less time writing code to get my result?"
from real lessons.

  • Cory Cross
On 8/15/25 11:20 AM, Jordan Brown wrote: >> I don't think prototype inheritance is a great fit for a language >> with immutable data types. I think either generic functions or class >> definitions are a better fit. Just because you /can/ use >> /object/+lambdas to keep many things you want doesn't mean you /should/. >> > Perhaps I just don't know enough OO theory. > > I like JS because it has relatively simple building blocks, and yet > you can build very complex and powerful things with it. (Contrast with > C++, which adds so many syntactic and semantic constructs to C that it > makes me dizzy.)  JS has been what I've been primarily looking to in > thinking about adding OO-ish features to OpenSCAD. > > --- > > BTW, note that I think that what we're talking about is of interest to > less than one in ten OpenSCAD users, and quite possibly less than one > in a hundred.  I expect that people like Adrian and Revar (authors of > BOSL2) are very interested, but I'm a pretty advanced user and I run > out of interest shortly after getting the basic data-only objects, and > almost completely once we have "this".  I don't think my projects > would benefit from "super".  (But, indirectly, if it helps build > BOSL2++, it helps me.) I don't think OpenSCAD should have complex OO, which is what worries me about the `this` discussion and all the complexities of scoping and such you have brought up. Here's what I've been thinking about: In the context of the 3 problems I stated (maps/structs, namespacing, data compartmentalization). Add namespaces. Define maps/structs/records/whatever-you-call-it as only containing Values (ints, lists, lambdas, other records, etc) and having one parent. Both of these things can be implemented simply (name mangling for namespaces, records can be a hash map + parent lookup on miss). Both of these have immediate benefits, both are easy to explain to people. They both have established ways of being optimized for speed. That just leaves "how do I do OO?" We know from Java's history that long inheritance chains are bad and people rarely change out implementations and OO didn't make this any easier. But then that isn't the real request. The real request is "How do I make it less likely to make mistakes and spend less time writing code to get my result?" Go's philosophy is, in part, to notice people don't spend much time typing, so don't focus on minimizing key strokes, instead focus on readability and unit testability, even if that means you write a lot of redundant things over and over again. Go has classes ("structs") which are allowed to have up to one parent and must be a concrete type. It also has "interfaces" which is a list of method signatures and has its own methods. If your class has all the method signatures of the interface, you can cast it to the interface and call the interface's methods' on it. The single-inheritance is a good choice. The only inheriting from concrete types is good. We learned from Java. However, Go is very imperative. Further lessons are not helpful to a language with immutable values. So, what do languages with immutable values usually have in common? Elixir/Erlang, Haskell, Ocaml, etc: Pattern matching. And if you combine functional language, objects, and pattern matching, you get generic functions. The exact syntax details of the following are mostly not important. ``` namespace bosl2 { namespace threading { namespace MyClass {   obj = struct(some_default=2);   // Constructors can be just normal functions   function new(something,some_default) =     assert(!is_undef(something))     let (       args = [["something" something]],       args = concat(args,is_undef(some_default) ? [] : [["some_default" some_default]])     ) struct(obj, args);   // Methods are functions and modules.   function sum(o is obj) = o.something+o.some_default;   module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); }   // Constructors can also be method functions   function coerce(l is list) = new(l[0],l[1]); } // namespace MyClass // Modules/functions can be generic on anybody's class: module nut(o is MyClass::obj, normal_arg) {   if(normal_arg) {     cylinder(d=MyClass::sum(o));   } else {     cylinder(d=o->sum()); // -> walks from object through its ancestors for the first matching method in each object's creation namespace     o->fat_cylinder();   } } module nut(o is list, a_different_arg_is_possible) {   obj_o = MyClass::coerce(o);   assert(!is_undef(obj_o));   nut(o, normal_arg=a_different_arg_is_possible); } }} // backward compatibility module which_takes_complex_thing(existing, arg, list) {   assert(on arg); bosl2::threading::nut([existing, arg*2, list]); } ``` Okay, so if you've got the basics of how this works, how does this actually help? Let's consider the BOSL2 Threading library (surprise, I bet you wouldn't have guessed). You have very long and repeated argument lists <https://github.com/BelfrySCAD/BOSL2/blob/d252f781232a9262583806fe05bbb8529c108899/threading.scad#L384>. And we could say, from Go, that's it's okay to type things repeatedly, but it's really easy to screw up. So how can we avoid this? The builder pattern can help. In our hypothetical class hierarchy, threaded_nut_builder inherits from generic_threaded_nut_builder. bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify(); From a user's standpoint, this used to be: threaded_nut(required, args, here, optional_generic_arc=its_value); From a user standpoint, it's hardly different. We might even have syntax sugar to simplify ::new and ->reify(). But from a maintainer's standpoint, you no longer have to edit an ever-growing list of places for new arguments to generic_threaded_nut. threaded_nut_builder's reify() module only has the same dozen lines of computation before chucking it up to generic_threaded_nut. You have a private namespace for any logic you want to unit test. No semantics of the language are changed regarding types, handling missing args, etc. You can maintain 100% compatibility with the existing API while adding options/features to the new API. You can incrementally update the codebase a single module or function at a time, so it's easy to adopt. It's focused around answering the question "How do I make it less likely to make mistakes and spend less time writing code to get my result?" from real lessons. - Cory Cross
JB
Jordan Brown
Sat, Aug 16, 2025 3:49 PM

[ Changed the subject to more accurately represent the topic. ]

[ I don't have a strong opinion on whether we should have this
discussion on the mailing list or on Github, but it should probably only
be one of the two. ]

On 8/16/2025 1:33 PM, Cory Cross wrote:

I don't think OpenSCAD should have complex OO,

I'm not sure, but I think what you suggested is approximately three
times as complex as anything we've discussed before...

You're introducing type safety, at least sort of, and I have very mixed
feelings about that.  On the one hand I like type safety. On the other
hand, it's entirely new to OpenSCAD.

which is what worries me about the this discussion and all the
complexities of scoping and such you have brought up.

You propose an answer to the "this" questions; you just aren't explicit
about it.  The answer that you propose is that "this" must be explicitly
specified as the first parameter to the method.  Yes, that addresses the
scoping problems.  We didn't like that it adds boilerplate and makes the
argument list for a call be different from the parameter list for a
declaration, and so were trying to avoid it.  (But, combined with the
idea that calling a method automatically includes the object as the
first argument, and calling that same function not as a method does not,
it does tie up a number of loose ends.)

Define maps/structs/records/whatever-you-call-it as only containing
Values (ints, lists, lambdas, other records, etc) and having one parent.

Why do you need this "parent" concept?  I don't think it adds anything
to what we're got today with object(), and it makes iteration behavior
less obvious.  (And seems to get drop the ability to merge objects.)  I
think maybe you expect it to make super more possible, but that would
seem to require mechanisms not yet discussed.

(Note that object() has a feature that looks sort of like your
parentage, but does not establish any sort of parentage relationship;
instead, it copies the object into the object being constructed.)

name mangling for namespaces

Name mangling is for people who have linkers that don't support
namespaces :-)

Go's philosophy is, in part, to notice people don't spend much time
typing, so don't focus on minimizing key strokes, instead focus on
readability and unit testability, even if that means you write a lot
of redundant things over and over again.

I'm not familiar with Go, so I can only respond to your comment. If you
write a lot of redundant things, that reduces readability and increases
the opportunity for error.  Are you suggesting that DRY
https://en.wikipedia.org/wiki/Don%27t_repeat_yourself is not a good
philosophy?

I certainly agree that words are better than punctuation, except in the
most common of cases.  (But:  words can intrude on namespace, where
punctuation doesn't.)

  function sum(o is obj) = o.something+o.some_default;
  module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); }

I notice that because you do not have methods being members, you had to
introduce "->" to distinguish between them, and that sum() sometimes
takes one argument and sometimes takes none. (Yes, I see the pattern,
but it still seems weird.)

module nut(o is MyClass::obj, normal_arg) {

I am confused about whether obj is a variable or a data type, or what it
means if it is both.  (Maybe it means "has obj in its parentage chain".)

module nut(o is list, a_different_arg_is_possible) {
  obj_o = MyClass::coerce(o);
  assert(!is_undef(obj_o));
  nut(o, normal_arg=a_different_arg_is_possible);
}
}}

I think you meant that call to be to MyClass::nut().

  assert(on arg);

Is this a typo?  If not, I have no idea what it means.

bosl2::threading::nut_builder::new(required, args,
here)->optional_generic_arg(its_value)->reify();

I'm very sympathetic to the desire to reduce repetition in argument
handling, but I'm not understanding what that means at all.  Partly
that's presentation; is this intended to be how the library would say
something, or how the caller would invoke the function?

If the former, I don't understand what it means.  If the latter, are you
seriously suggesting this as a replacement for

threaded_nut(required, args, here, optional_generic_arc=its_value);

?

It seems like we were struggling with one or two questions, and you've
implicitly answered that question but then added several more.  That's
not to say that they might not be good ideas, but they make it harder to
structure a discussion so as to knock down one question at a time.

[ Changed the subject to more accurately represent the topic. ] [ I don't have a strong opinion on whether we should have this discussion on the mailing list or on Github, but it should probably only be one of the two. ] On 8/16/2025 1:33 PM, Cory Cross wrote: > I don't think OpenSCAD should have complex OO, I'm not sure, but I think what you suggested is approximately three times as complex as anything we've discussed before... You're introducing type safety, at least sort of, and I have very mixed feelings about that.  On the one hand I like type safety. On the other hand, it's entirely new to OpenSCAD. > which is what worries me about the `this` discussion and all the > complexities of scoping and such you have brought up. You propose an answer to the "this" questions; you just aren't explicit about it.  The answer that you propose is that "this" must be explicitly specified as the first parameter to the method.  Yes, that addresses the scoping problems.  We didn't like that it adds boilerplate and makes the argument list for a call be different from the parameter list for a declaration, and so were trying to avoid it.  (But, combined with the idea that calling a method automatically includes the object as the first argument, and calling that same function not as a method does not, it does tie up a number of loose ends.) > Define maps/structs/records/whatever-you-call-it as only containing > Values (ints, lists, lambdas, other records, etc) and having one parent. Why do you need this "parent" concept?  I don't think it adds anything to what we're got today with object(), and it makes iteration behavior less obvious.  (And seems to get drop the ability to merge objects.)  I think maybe you expect it to make `super` more possible, but that would seem to require mechanisms not yet discussed. (Note that object() has a feature that looks sort of like your parentage, but does not establish any sort of parentage relationship; instead, it copies the object into the object being constructed.) > name mangling for namespaces Name mangling is for people who have linkers that don't support namespaces :-) > Go's philosophy is, in part, to notice people don't spend much time > typing, so don't focus on minimizing key strokes, instead focus on > readability and unit testability, even if that means you write a lot > of redundant things over and over again. I'm not familiar with Go, so I can only respond to your comment. If you write a lot of redundant things, that reduces readability and increases the opportunity for error.  Are you suggesting that DRY <https://en.wikipedia.org/wiki/Don%27t_repeat_yourself> is not a good philosophy? I certainly agree that words are better than punctuation, except in the most common of cases.  (But:  words can intrude on namespace, where punctuation doesn't.) >   function sum(o is obj) = o.something+o.some_default; >   module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); } I notice that because you do not have methods being members, you had to introduce "->" to distinguish between them, and that sum() sometimes takes one argument and sometimes takes none. (Yes, I see the pattern, but it still seems weird.) > module nut(o is MyClass::obj, normal_arg) { I am confused about whether obj is a variable or a data type, or what it means if it is both.  (Maybe it means "has obj in its parentage chain".) > > module nut(o is list, a_different_arg_is_possible) { >   obj_o = MyClass::coerce(o); >   assert(!is_undef(obj_o)); >   nut(o, normal_arg=a_different_arg_is_possible); > } > }} I think you meant that call to be to MyClass::nut(). >   assert(on arg); Is this a typo?  If not, I have no idea what it means. > bosl2::threading::nut_builder::new(required, args, > here)->optional_generic_arg(its_value)->reify(); I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all.  Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function? If the former, I don't understand what it means.  If the latter, are you seriously suggesting this as a replacement for threaded_nut(required, args, here, optional_generic_arc=its_value); ? It seems like we were struggling with one or two questions, and you've implicitly answered that question but then added several more.  That's not to say that they might not be good ideas, but they make it harder to structure a discussion so as to knock down one question at a time.
CC
Cory Cross
Mon, Aug 18, 2025 9:54 PM

On 8/16/25 8:49 AM, Jordan Brown wrote:

Go's philosophy is, in part, to notice people don't spend much time
typing, so don't focus on minimizing key strokes, instead focus on
readability and unit testability, even if that means you write a lot
of redundant things over and over again.

I'm not familiar with Go, so I can only respond to your comment.  If
you write a lot of redundant things, that reduces readability and
increases the opportunity for error.  Are you suggesting that DRY
https://en.wikipedia.org/wiki/Don%27t_repeat_yourself is not a good
philosophy?

I am saying the Go philosophy is about not adding features to reduce the
amount of characters written. You see this a million times:

thing, err := function_which_returns_thing_or_error()
if err != nil {
  return nil, err
}
thing2, err := function_using_thing(thing)
if err != nil {
  return nil, err
}

In Rust and others, you have the ? operator which you can tack onto
anything which can return either a value or an error, and simply return
the error from the current function if the thing is an error.

thing2 := function_using_thing(function_which_returns_thing_or_error()?)?

Go is not going to add that.

The Go philosophy is also that if you can write something with a for
loop in 5 lines, then there shouldn't be a function for it. So if you
wanted type safe accesses for a data structure not builtin, you had to
write the methods yourself (i.e. contains()). (Luckily generics finally
got added). I don't like this.

I do agree with WET (write everything twice) instead of DRY, but I have
a lot of issues with some aspects of Go (especially that it values unit
testability, but being able to write integration tests makes your code
garbage (you can't mock concrete types from other packages, so you have
to use interfaces for every library you use, if you want to test with a
fake or mock, it's Java-esque in indirection)).

On 8/16/25 8:49 AM, Jordan Brown wrote: >> Go's philosophy is, in part, to notice people don't spend much time >> typing, so don't focus on minimizing key strokes, instead focus on >> readability and unit testability, even if that means you write a lot >> of redundant things over and over again. > > I'm not familiar with Go, so I can only respond to your comment.  If > you write a lot of redundant things, that reduces readability and > increases the opportunity for error.  Are you suggesting that DRY > <https://en.wikipedia.org/wiki/Don%27t_repeat_yourself> is not a good > philosophy? > I am saying the Go philosophy is about not adding features to reduce the amount of characters written. You see this a million times: thing, err := function_which_returns_thing_or_error() if err != nil {   return nil, err } thing2, err := function_using_thing(thing) if err != nil {   return nil, err } In Rust and others, you have the ? operator which you can tack onto anything which can return either a value or an error, and simply return the error from the current function if the thing is an error. thing2 := function_using_thing(function_which_returns_thing_or_error()?)? Go is not going to add that. The Go philosophy is also that if you can write something with a for loop in 5 lines, then there shouldn't be a function for it. So if you wanted type safe accesses for a data structure not builtin, you had to write the methods yourself (i.e. contains()). (Luckily generics finally got added). I don't like this. I do agree with WET (write everything twice) instead of DRY, but I have a lot of issues with some aspects of Go (especially that it values unit testability, but being able to write integration tests makes your code garbage (you can't mock concrete types from other packages, so you have to use interfaces for every library you use, if you want to test with a fake or mock, it's Java-esque in indirection)).
CC
Cory Cross
Mon, Aug 18, 2025 10:22 PM

On 8/16/25 8:49 AM, Jordan Brown wrote:

On 8/16/2025 1:33 PM, Cory Cross wrote:

I don't think OpenSCAD should have complex OO,

I'm not sure, but I think what you suggested is approximately three
times as complex as anything we've discussed before...

There's simple-to-use and simple-to-implement. What I think you are
suggesting is simple-to-implement. I'm suggesting simple-to-use instead :).

You're introducing type safety, at least sort of, and I have very
mixed feelings about that.  On the one hand I like type safety. On the
other hand, it's entirely new to OpenSCAD.

No, no type safety. Just pattern matching and checking pointer equivalence.

which is what worries me about the this discussion and all the
complexities of scoping and such you have brought up.

You propose an answer to the "this" questions; you just aren't
explicit about it.  The answer that you propose is that "this" must be
explicitly specified as the first parameter to the method.  Yes, that
addresses the scoping problems.  We didn't like that it adds
boilerplate and makes the argument list for a call be different from
the parameter list for a declaration, and so were trying to avoid it. 
(But, combined with the idea that calling a method automatically
includes the object as the first argument, and calling that same
function not as a method does not, it does tie up a number of loose
ends.
)

(bolding mine) :-)

Define maps/structs/records/whatever-you-call-it as only containing
Values (ints, lists, lambdas, other records, etc) and having one parent.

Why do you need this "parent" concept?  I don't think it adds anything
to what we're got today with object(), and it makes iteration behavior
less obvious.  (And seems to get drop the ability to merge objects.)

You have object-related methods like "has_key". Instead of putting them
in the global namespace, you put it in the equivalent of Java's Object
(i.e. the object that all objects eventually descend from). This is also
the logical place for any other related methods like iterators for just
keys, just values, or key-value pairs [key value]; or a tostring method;
etc. If you think you're going to have methods and people won't want
these, well, they're going to!

With immutable data types, parentage as pointer or merge is actually an
implementation detail. The semantics should be the same, otherwise
methods on parents are quite limited if they can't reference values
overridden in a child.

I think maybe you expect it to make super more possible, but that
would seem to require mechanisms not yet discussed.

I think the lesson learned from trying to change parent libraries is
using super() or explicitly naming the superclass is not a consequential
cost of refactoring, so super() does not help enough over explicitly
naming the desired method.
So no super(). Just use parentClassNamespace::method(self).

...
I am confused about whether obj is a variable or a data type, or what
it means if it is both.  (Maybe it means "has obj in its parentage
chain".)

"has obj in its parentage chain" <= bingo. Eliminates the need for
class keyword by using namespaces. Simpler.

The parent relationship enables -> to work without keeping a per-object
vtable of all inherited methods. You could just as well collect all
methods (or at least all namespaces) on object creation, but it should
be more efficient to not. Or, at the very least, easier to implement :).
Caching should solve most theoretical performance issues.

(Note that object() has a feature that looks sort of like your
parentage, but does not establish any sort of parentage relationship;
instead, it copies the object into the object being constructed.)

I think the merge semantics should be retained regarding the keys and
values.

  function sum(o is obj) = o.something+o.some_default;
  module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); }

I notice that because you do not have methods being members, you had
to introduce "->" to distinguish between them, and that sum()
sometimes takes one argument and sometimes takes none. (Yes, I see the
pattern, but it still seems weird.)

This also eliminates the need to make first-class modules i.e. being
able to define them anywhere but the top-level or assign them as
variables. This is a lot of implementation complexity that can be
avoided and isn't currently being discussed.

This is like Python if you want to call an explicit superclass method,
you add this-equivalent yourself: SuperClassName.method_name(self, args)

The reason for -> is, yes, for looking up in the namespace and not
members. This, among other things, allows you to release just object()
as it currently works.

module nut(o is list, a_different_arg_is_possible) {
  obj_o = MyClass::coerce(o);
  assert(!is_undef(obj_o));
  nut(o, normal_arg=a_different_arg_is_possible);
}
}}

I think you meant that call to be to MyClass::nut().

Nope, the other "nut()" is also a module in bosl2::threading. It's
placed there as a demonstration that generic functions/modules can
dispatch on any type in any namespace (unlike a Python method: a Python
method can only be dispatched on objects from its class or a class which
descends from it).

  assert(on arg);

Is this a typo?  If not, I have no idea what it means.

typo/just being vague. Just meant "this is where you'd write asserts"

I'll answer the rest in another email, struggling a little to finish it up.

-Cory Cross

On 8/16/25 8:49 AM, Jordan Brown wrote: > On 8/16/2025 1:33 PM, Cory Cross wrote: >> I don't think OpenSCAD should have complex OO, > I'm not sure, but I think what you suggested is approximately three > times as complex as anything we've discussed before... There's simple-to-use and simple-to-implement. What I think you are suggesting is simple-to-implement. I'm suggesting simple-to-use instead :). > You're introducing type safety, at least sort of, and I have very > mixed feelings about that.  On the one hand I like type safety. On the > other hand, it's entirely new to OpenSCAD. No, no type safety. Just pattern matching and checking pointer equivalence. >> which is what worries me about the `this` discussion and all the >> complexities of scoping and such you have brought up. > > You propose an answer to the "this" questions; you just aren't > explicit about it.  The answer that you propose is that "this" must be > explicitly specified as the first parameter to the method.  Yes, that > addresses the scoping problems.  We didn't like that it adds > boilerplate and makes the argument list for a call be different from > the parameter list for a declaration, and so were trying to avoid it.  > (But, combined with the idea that calling a method automatically > includes the object as the first argument, and calling that same > function not as a method does not, it does *tie up a number of loose > ends.*) (bolding mine) :-) >> Define maps/structs/records/whatever-you-call-it as only containing >> Values (ints, lists, lambdas, other records, etc) and having one parent. > > Why do you need this "parent" concept?  I don't think it adds anything > to what we're got today with object(), and it makes iteration behavior > less obvious.  (And seems to get drop the ability to merge objects.) You have object-related methods like "has_key". Instead of putting them in the global namespace, you put it in the equivalent of Java's Object (i.e. the object that all objects eventually descend from). This is also the logical place for any other related methods like iterators for just keys, just values, or key-value pairs [key value]; or a tostring method; etc. If you think you're going to have methods and people won't want these, well, they're going to! With immutable data types, parentage as pointer or merge is actually an implementation detail. The semantics should be the same, otherwise methods on parents are quite limited if they can't reference values overridden in a child. > I think maybe you expect it to make `super` more possible, but that > would seem to require mechanisms not yet discussed. I think the lesson learned from trying to change parent libraries is using super() or explicitly naming the superclass is not a consequential cost of refactoring, so super() does not help enough over explicitly naming the desired method. So no super(). Just use parentClassNamespace::method(self). > ... > I am confused about whether obj is a variable or a data type, or what > it means if it is both.  (Maybe it means "has obj in its parentage > chain".) "has obj in its parentage chain" <= bingo. Eliminates the need for `class` keyword by using namespaces. Simpler. The parent relationship enables -> to work without keeping a per-object vtable of all inherited methods. You could just as well collect all methods (or at least all namespaces) on object creation, but it should be more efficient to not. Or, at the very least, easier to implement :). Caching should solve most theoretical performance issues. > (Note that object() has a feature that looks sort of like your > parentage, but does not establish any sort of parentage relationship; > instead, it copies the object into the object being constructed.) I think the merge semantics should be retained regarding the keys and values. >>   function sum(o is obj) = o.something+o.some_default; >>   module fat_cylinder(o is obj) { cylinder(d=o->sum()*2); } > > I notice that because you do not have methods being members, you had > to introduce "->" to distinguish between them, and that sum() > sometimes takes one argument and sometimes takes none. (Yes, I see the > pattern, but it still seems weird.) This also eliminates the need to make first-class modules i.e. being able to define them anywhere but the top-level or assign them as variables. This is a lot of implementation complexity that can be avoided and isn't currently being discussed. This is like Python if you want to call an explicit superclass method, you add `this`-equivalent yourself: `SuperClassName.method_name(self, args)` The reason for `->` is, yes, for looking up in the namespace and not members. This, among other things, allows you to release just `object()` as it currently works. >> module nut(o is list, a_different_arg_is_possible) { >>   obj_o = MyClass::coerce(o); >>   assert(!is_undef(obj_o)); >>   nut(o, normal_arg=a_different_arg_is_possible); >> } >> }} > > I think you meant that call to be to MyClass::nut(). Nope, the other "nut()" is also a module in bosl2::threading. It's placed there as a demonstration that generic functions/modules can dispatch on any type in any namespace (unlike a Python method: a Python method can only be dispatched on objects from its class or a class which descends from it). >>   assert(on arg); > > Is this a typo?  If not, I have no idea what it means. typo/just being vague. Just meant "this is where you'd write asserts" I'll answer the rest in another email, struggling a little to finish it up. -Cory Cross
CC
Cory Cross
Tue, Aug 19, 2025 5:47 AM

On 8/16/25 8:49 AM, Jordan Brown wrote:

bosl2::threading::nut_builder::new(required, args,
here)->optional_generic_arg(its_value)->reify();

I'm very sympathetic to the desire to reduce repetition in argument
handling, but I'm not understanding what that means at all.  Partly
that's presentation; is this intended to be how the library would say
something, or how the caller would invoke the function?

If the former, I don't understand what it means.  If the latter, are
you seriously suggesting this as a replacement for

 threaded_nut(required, args, here, optional_generic_arc=its_value);

I am suggesting it as a replacement for the latter; not because it's
better for the user, but because it's better for the maintainers and not
worse for the users. (I would assume we'd add using bosl2::threading
to shorten names, at some point).

As a practical example, here is a invocation of a threaded module in my
code:

buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK);

here's how I would do it with the builder pattern and the suggested OO
approach:

buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify;

You don't have to use the builder pattern. You could choose to use
objects as a replacement for Python's **kwargs:

buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK));

In this case, buttress_threaded_nut's implementation would change from
50 lines
https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/threading.scad#L1242
to 11:

module buttress_threaded_nut(kwargs) {
    profile = [
        [  -1/2, -0.77],
        [ -7/16, -0.75],
        [  5/16,  0],
        [  7/16,  0],
        [  7/16, -0.75],
        [  1/ 2, -0.77],
    ];
    generic_threaded_nut(struct(kwargs, profile=profile));
}

Of course, this isn't OO and makes it harder to detect argument name
typos and such.

The builder pattern could be implemented as so:

// namespace for buttress_threaded_nut_builder
obj = struct(generic_threaded_nut_builder::new());
function new() =
    let ( profile = [
        [  -1/2, -0.77],
        [ -7/16, -0.75],
        [  5/16,  0],
        [  7/16,  0],
        [  7/16, -0.75],
        [  1/ 2, -0.77],
    ])
    struct(obj)->profile(profile);

so not any more or less difficult to write. What do profile and
positioning look like?

// namespace of generic_threaded_rod_builder
function profile(o is builder_obj, profile) =
    assert(is_list(profile)) // And other tests independent of other values
    struct(o,profile=profile);

// namespace of attachable_builder
function positioning(o is attachable_builder_obj, anchor, spin, orient) =
    assert(/* tests related to the parameters*/)
    let(
        l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
           is_undef(spin) ? [] : [["spin", spin]],
           is_undef(orient) ? [] : [["orient", orient]])
        )
    struct(o,l);

Okay, not in love with the repetition in there. But there are some
improvements here:

  1. Some validation can now stop cluttering the top of
    https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L353
    so many functions/modules.
  2. The get_radius
    https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L2461
    function doesn't need its args filled out every time
  3. We're reusing attachable instead of needing to redundantly pass it so
    many args every
    https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L2469
    single
    https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/threading.scad#L2067
    time
    https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/walls.scad#L69
    and in every function and module signature.
  4. We can match on the old types and convert to objects as needed

Unsolved issue: why would -> method invocation not look at the namespace
of positioning? I didn't intend it to, but it would do the wrong thing
as written; only the calls in the new() methods should add the namespace
to the method lookup. This might be best as object vs struct keywords.
Maybe attachable should be a mixin instead of in the class hierarchy.

So how would I write this with this as currently proposed?

function buttress_threaded_nut_builder =
    let ( profile = [
        [  -1/2, -0.77],
        [ -7/16, -0.75],
        [  5/16,  0],
        [  7/16,  0],
        [  7/16, -0.75],
        [  1/ 2, -0.77],
    ])
    object(new_generic_threaded_rod_builder().set_profile(profile), /*
all methods on buttress_threaded_nut_builder must be defined here */);

// in method list of generic_threaded_rod_builder
function set_profile(profile) =
    assert(is_list(profile)) // And other tests independent of other values
    object(this,profile=profile);

// in method list of attachable_builder
function set_positioning(anchor, spin, orient) =
    assert(/* tests related to the parameters*/)
    let(
        l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
           is_undef(spin) ? [] : [["spin", spin]],
           is_undef(orient) ? [] : [["orient", orient]])
        )
    struct(this,l);

module reify_buttress_threaded_nut(obj) {
    reify_threaded_nut(obj);
}

module reify_threaded_nut(obj) {
    reify_generic_threaded_nut(obj);
}

reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK));

...

Less different than I thought. Cascading explicit reify modules are
annoying. For this usage the inability to write your own generic methods
dispatching on other object types does not hinder anything. The shared
namespace means methods and values must have unique names. I think this
is supposed to solve name conflicts by having you only need one unique
name per file and you put all your constants in there? And methods, I
guess, so at the top of f.ex. bosl2/threading.scad there would be:

bosl2_threading = object(
    top_level_constant = 27,
    function buttress_threaded_nut_builder() =
...
);

module reify_buttress_threaded_nut(obj) {
    bosl2_threading.top_level_constant; // for whatever reason
    reify_threaded_nut(obj);
}

I've seen worse. I certainly can't say this rules out this.

On 8/16/25 8:49 AM, Jordan Brown wrote: >> bosl2::threading::nut_builder::new(required, args, >> here)->optional_generic_arg(its_value)->reify(); > > I'm very sympathetic to the desire to reduce repetition in argument > handling, but I'm not understanding what that means at all.  Partly > that's presentation; is this intended to be how the library would say > something, or how the caller would invoke the function? > > If the former, I don't understand what it means.  If the latter, are > you seriously suggesting this as a replacement for > > threaded_nut(required, args, here, optional_generic_arc=its_value); > I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add `using bosl2::threading` to shorten names, at some point). As a practical example, here is a invocation of a threaded module in my code: buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK); here's how I would do it with the builder pattern and the suggested OO approach: buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify; You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs: buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK)); In this case, buttress_threaded_nut's implementation would change from 50 lines <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/threading.scad#L1242> to 11: module buttress_threaded_nut(kwargs) {     profile = [         [  -1/2, -0.77],         [ -7/16, -0.75],         [  5/16,  0],         [  7/16,  0],         [  7/16, -0.75],         [  1/ 2, -0.77],     ];     generic_threaded_nut(struct(kwargs, profile=profile)); } Of course, this isn't OO and makes it harder to detect argument name typos and such. The builder pattern could be implemented as so: // namespace for buttress_threaded_nut_builder obj = struct(generic_threaded_nut_builder::new()); function new() =     let ( profile = [         [  -1/2, -0.77],         [ -7/16, -0.75],         [  5/16,  0],         [  7/16,  0],         [  7/16, -0.75],         [  1/ 2, -0.77],     ])     struct(obj)->profile(profile); so not any more or less difficult to write. What do profile and positioning look like? // namespace of generic_threaded_rod_builder function profile(o is builder_obj, profile) =     assert(is_list(profile)) // And other tests independent of other values     struct(o,profile=profile); // namespace of attachable_builder function positioning(o is attachable_builder_obj, anchor, spin, orient) =     assert(/* tests related to the parameters*/)     let(         l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],            is_undef(spin) ? [] : [["spin", spin]],            is_undef(orient) ? [] : [["orient", orient]])         )     struct(o,l); Okay, not in love with the repetition in there. But there are some improvements here: 1. Some validation can now stop cluttering the top of <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L353> so many functions/modules. 2. The `get_radius` <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L2461> function doesn't need its args filled out every time 3. We're reusing attachable instead of needing to redundantly pass it so many args every <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/shapes3d.scad#L2469> single <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/threading.scad#L2067> time <https://github.com/BelfrySCAD/BOSL2/blob/e940b69f554d499b3d24b09615d597ec59139e6b/walls.scad#L69> and in every function and module signature. 4. We can match on the old types and convert to objects as needed Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as `object` vs `struct` keywords. Maybe attachable should be a mixin instead of in the class hierarchy. So how would I write this with `this` as currently proposed? function buttress_threaded_nut_builder =     let ( profile = [         [  -1/2, -0.77],         [ -7/16, -0.75],         [  5/16,  0],         [  7/16,  0],         [  7/16, -0.75],         [  1/ 2, -0.77],     ])     object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */); // in method list of generic_threaded_rod_builder function set_profile(profile) =     assert(is_list(profile)) // And other tests independent of other values     object(this,profile=profile); // in method list of attachable_builder function set_positioning(anchor, spin, orient) =     assert(/* tests related to the parameters*/)     let(         l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],            is_undef(spin) ? [] : [["spin", spin]],            is_undef(orient) ? [] : [["orient", orient]])         )     struct(this,l); module reify_buttress_threaded_nut(obj) {     reify_threaded_nut(obj); } module reify_threaded_nut(obj) {     reify_generic_threaded_nut(obj); } reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK)); ... Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be: bosl2_threading = object(     top_level_constant = 27,     function buttress_threaded_nut_builder() = ... ); module reify_buttress_threaded_nut(obj) {     bosl2_threading.top_level_constant; // for whatever reason     reify_threaded_nut(obj); } I've seen worse. I certainly can't say this rules out `this`.
PK
Peter Kriens
Tue, Aug 19, 2025 8:40 AM

You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and object? Not good for my blood pressure! :-)

I like the builder approach and it is one of my drivers for the object work and $this.

BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ...

Peter Kriens

On 19 Aug 2025, at 07:47, Cory Cross via Discuss discuss@lists.openscad.org wrote:
On 8/16/25 8:49 AM, Jordan Brown wrote:

bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify();

I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all.  Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function?
If the former, I don't understand what it means.  If the latter, are you seriously suggesting this as a replacement for
threaded_nut(required, args, here, optional_generic_arc=its_value);

I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add using bosl2::threading to shorten names, at some point).

As a practical example, here is a invocation of a threaded module in my code:

buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK);

here's how I would do it with the builder pattern and the suggested OO approach:

buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify;

You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs:

buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK));

In this case, buttress_threaded_nut's implementation would change from 50 lines to 11:

module buttress_threaded_nut(kwargs) {
profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
];
generic_threaded_nut(struct(kwargs, profile=profile));
}

Of course, this isn't OO and makes it harder to detect argument name typos and such.

The builder pattern could be implemented as so:

// namespace for buttress_threaded_nut_builder
obj = struct(generic_threaded_nut_builder::new());
function new() =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
struct(obj)->profile(profile);

so not any more or less difficult to write. What do profile and positioning look like?

// namespace of generic_threaded_rod_builder
function profile(o is builder_obj, profile) =
assert(is_list(profile)) // And other tests independent of other values
struct(o,profile=profile);

// namespace of attachable_builder
function positioning(o is attachable_builder_obj, anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(o,l);

Okay, not in love with the repetition in there. But there are some improvements here:

  1. Some validation can now stop cluttering the top of so many functions/modules.
  2. The get_radius function doesn't need its args filled out every time
  3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature.
  4. We can match on the old types and convert to objects as needed

Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as object vs struct keywords.
Maybe attachable should be a mixin instead of in the class hierarchy.

So how would I write this with this as currently proposed?

function buttress_threaded_nut_builder =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */);

// in method list of generic_threaded_rod_builder
function set_profile(profile) =
assert(is_list(profile)) // And other tests independent of other values
object(this,profile=profile);

// in method list of attachable_builder
function set_positioning(anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(this,l);

module reify_buttress_threaded_nut(obj) {
reify_threaded_nut(obj);
}

module reify_threaded_nut(obj) {
reify_generic_threaded_nut(obj);
}

reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK));

...

Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be:

bosl2_threading = object(
top_level_constant = 27,
function buttress_threaded_nut_builder() =
...
);

module reify_buttress_threaded_nut(obj) {
bosl2_threading.top_level_constant; // for whatever reason
reify_threaded_nut(obj);
}

I've seen worse. I certainly can't say this rules out this.


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and `object`? Not good for my blood pressure! :-) I like the builder approach and it is one of my drivers for the object work and $this. BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ... Peter Kriens > On 19 Aug 2025, at 07:47, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: > On 8/16/25 8:49 AM, Jordan Brown wrote: >>> bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify(); >> I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all. Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function? >> If the former, I don't understand what it means. If the latter, are you seriously suggesting this as a replacement for >> threaded_nut(required, args, here, optional_generic_arc=its_value); > I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add `using bosl2::threading` to shorten names, at some point). > > As a practical example, here is a invocation of a threaded module in my code: > > buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK); > > here's how I would do it with the builder pattern and the suggested OO approach: > > buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify; > > You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs: > > buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK)); > > In this case, buttress_threaded_nut's implementation would change from 50 lines to 11: > > module buttress_threaded_nut(kwargs) { > profile = [ > [ -1/2, -0.77], > [ -7/16, -0.75], > [ 5/16, 0], > [ 7/16, 0], > [ 7/16, -0.75], > [ 1/ 2, -0.77], > ]; > generic_threaded_nut(struct(kwargs, profile=profile)); > } > > Of course, this isn't OO and makes it harder to detect argument name typos and such. > > The builder pattern could be implemented as so: > > // namespace for buttress_threaded_nut_builder > obj = struct(generic_threaded_nut_builder::new()); > function new() = > let ( profile = [ > [ -1/2, -0.77], > [ -7/16, -0.75], > [ 5/16, 0], > [ 7/16, 0], > [ 7/16, -0.75], > [ 1/ 2, -0.77], > ]) > struct(obj)->profile(profile); > > so not any more or less difficult to write. What do profile and positioning look like? > > // namespace of generic_threaded_rod_builder > function profile(o is builder_obj, profile) = > assert(is_list(profile)) // And other tests independent of other values > struct(o,profile=profile); > > // namespace of attachable_builder > function positioning(o is attachable_builder_obj, anchor, spin, orient) = > assert(/* tests related to the parameters*/) > let( > l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], > is_undef(spin) ? [] : [["spin", spin]], > is_undef(orient) ? [] : [["orient", orient]]) > ) > struct(o,l); > > Okay, not in love with the repetition in there. But there are some improvements here: > > 1. Some validation can now stop cluttering the top of so many functions/modules. > 2. The `get_radius` function doesn't need its args filled out every time > 3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature. > 4. We can match on the old types and convert to objects as needed > > Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as `object` vs `struct` keywords. > Maybe attachable should be a mixin instead of in the class hierarchy. > > So how would I write this with `this` as currently proposed? > > function buttress_threaded_nut_builder = > let ( profile = [ > [ -1/2, -0.77], > [ -7/16, -0.75], > [ 5/16, 0], > [ 7/16, 0], > [ 7/16, -0.75], > [ 1/ 2, -0.77], > ]) > object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */); > > // in method list of generic_threaded_rod_builder > function set_profile(profile) = > assert(is_list(profile)) // And other tests independent of other values > object(this,profile=profile); > > // in method list of attachable_builder > function set_positioning(anchor, spin, orient) = > assert(/* tests related to the parameters*/) > let( > l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], > is_undef(spin) ? [] : [["spin", spin]], > is_undef(orient) ? [] : [["orient", orient]]) > ) > struct(this,l); > > module reify_buttress_threaded_nut(obj) { > reify_threaded_nut(obj); > } > > > module reify_threaded_nut(obj) { > reify_generic_threaded_nut(obj); > } > > reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK)); > > > ... > > Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be: > > bosl2_threading = object( > top_level_constant = 27, > function buttress_threaded_nut_builder() = > ... > ); > > module reify_buttress_threaded_nut(obj) { > bosl2_threading.top_level_constant; // for whatever reason > reify_threaded_nut(obj); > } > > I've seen worse. I certainly can't say this rules out `this`. > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org
CC
Cory Cross
Wed, Aug 20, 2025 11:55 PM

Okay, I think I've identified the biggest problem with the proposed implementation of this: shadowed methods cannot be called on a child. Methods declared in a child can call a parent method on the parent object, but not with any values that may be defined or updated by the child constructor.

I also dislike that you must define all methods in one context and file location. What I've proposed gives the users the same power as the library writers, while this method locks ownership of objects to the original author. (Obviously, yes, we can all edit any code, compiled or otherwise, on our machines, but it's not afforded by the language and it's a lot harder to ship patches to someone else's library than to ship your own file that adds a method to a namespace).

As far as future optimization, because of the dynamic context, can this approach be optimized well or is it like Python and doomed to always be slow? Well, lua is ultra flexible and luajit was apparently pretty amazing. So it might not be impossible.

I like the builder approach and it is one of my drivers for the object work and $this.

Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions:

function lookup(plist, pname, idx=0) =
let ( kv = plist[idx] ) is_undef(kv) ? kv : kv[0] == pname ? kv[1] : lookup(plist, pname, idx+1);

function call(obj, fname, args_plist) =
let( f = lookup(obj, fname) ) is_function(f) ? f(obj, args_plist) : undef;

function any_method(obj, args_plist) =
let( actual_arg = lookup(args_plist, "actual_arg"),
member = lookup(obj, "member"),
) implementation;

x=call(myobj, "distance", [["p",other_point]]) // call method distance on my object to calculate the distance from other_point
x=myobj.distance(p=other_point); // doesn't look so different, does it?

So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer?

Actually, plists are better because you can get super methods :-)

I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors.

On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss discuss@lists.openscad.org wrote:

You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and object? Not good for my blood pressure! :-)

I like the builder approach and it is one of my drivers for the object work and $this.

BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ...

Peter Kriens

On 19 Aug 2025, at 07:47, Cory Cross via Discuss discuss@lists.openscad.org wrote:
On 8/16/25 8:49 AM, Jordan Brown wrote:

bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify();
I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all.  Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function?
If the former, I don't understand what it means.  If the latter, are you seriously suggesting this as a replacement for
threaded_nut(required, args, here, optional_generic_arc=its_value);
I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add using bosl2::threading to shorten names, at some point).

As a practical example, here is a invocation of a threaded module in my code:

buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK);

here's how I would do it with the builder pattern and the suggested OO approach:

buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify;

You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs:

buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK));

In this case, buttress_threaded_nut's implementation would change from 50 lines to 11:

module buttress_threaded_nut(kwargs) {
profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
];
generic_threaded_nut(struct(kwargs, profile=profile));
}

Of course, this isn't OO and makes it harder to detect argument name typos and such.

The builder pattern could be implemented as so:

// namespace for buttress_threaded_nut_builder
obj = struct(generic_threaded_nut_builder::new());
function new() =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
struct(obj)->profile(profile);

so not any more or less difficult to write. What do profile and positioning look like?

// namespace of generic_threaded_rod_builder
function profile(o is builder_obj, profile) =
assert(is_list(profile)) // And other tests independent of other values
struct(o,profile=profile);

// namespace of attachable_builder
function positioning(o is attachable_builder_obj, anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(o,l);

Okay, not in love with the repetition in there. But there are some improvements here:

  1. Some validation can now stop cluttering the top of so many functions/modules.
  2. The get_radius function doesn't need its args filled out every time
  3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature.
  4. We can match on the old types and convert to objects as needed

Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as object vs struct keywords.
Maybe attachable should be a mixin instead of in the class hierarchy.

So how would I write this with this as currently proposed?

function buttress_threaded_nut_builder =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */);

// in method list of generic_threaded_rod_builder
function set_profile(profile) =
assert(is_list(profile)) // And other tests independent of other values
object(this,profile=profile);

// in method list of attachable_builder
function set_positioning(anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(this,l);

module reify_buttress_threaded_nut(obj) {
reify_threaded_nut(obj);
}

module reify_threaded_nut(obj) {
reify_generic_threaded_nut(obj);
}

reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK));

...

Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be:

bosl2_threading = object(
top_level_constant = 27,
function buttress_threaded_nut_builder() =
...
);

module reify_buttress_threaded_nut(obj) {
bosl2_threading.top_level_constant; // for whatever reason
reify_threaded_nut(obj);
}

I've seen worse. I certainly can't say this rules out this.


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

Okay, I think I've identified the biggest problem with the proposed implementation of this: shadowed methods cannot be called on a child. Methods declared in a child can call a parent method on the parent object, but not with any values that may be defined or updated by the child constructor. I also dislike that you must define all methods in one context and file location. What I've proposed gives the users the same power as the library writers, while this method locks ownership of objects to the original author. (Obviously, yes, we can all edit any code, compiled or otherwise, on our machines, but it's not afforded by the language and it's a lot harder to ship patches to someone else's library than to ship your own file that adds a method to a namespace). As far as future optimization, because of the dynamic context, can this approach be optimized well or is it like Python and doomed to always be slow? Well, lua is ultra flexible and luajit was apparently pretty amazing. So it might not be impossible. > I like the builder approach and it is one of my drivers for the object work and $this. Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions: function lookup(plist, pname, idx=0) = let ( kv = plist[idx] ) is_undef(kv) ? kv : kv[0] == pname ? kv[1] : lookup(plist, pname, idx+1); function call(obj, fname, args_plist) = let( f = lookup(obj, fname) ) is_function(f) ? f(obj, args_plist) : undef; function any_method(obj, args_plist) = let( actual_arg = lookup(args_plist, "actual_arg"), member = lookup(obj, "member"), ) implementation; x=call(myobj, "distance", [["p",other_point]]) // call method distance on my object to calculate the distance from other_point x=myobj.distance(p=other_point); // doesn't look so different, does it? So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer? Actually, plists are better because you can get super methods :-) I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors. On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss <discuss@lists.openscad.org> wrote: >You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and `object`? Not good for my blood pressure! :-) > >I like the builder approach and it is one of my drivers for the object work and $this. > >BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ... > > Peter Kriens > >> On 19 Aug 2025, at 07:47, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: >> On 8/16/25 8:49 AM, Jordan Brown wrote: >>>> bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify(); >>> I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all. Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function? >>> If the former, I don't understand what it means. If the latter, are you seriously suggesting this as a replacement for >>> threaded_nut(required, args, here, optional_generic_arc=its_value); >> I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add `using bosl2::threading` to shorten names, at some point). >> >> As a practical example, here is a invocation of a threaded module in my code: >> >> buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK); >> >> here's how I would do it with the builder pattern and the suggested OO approach: >> >> buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify; >> >> You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs: >> >> buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK)); >> >> In this case, buttress_threaded_nut's implementation would change from 50 lines to 11: >> >> module buttress_threaded_nut(kwargs) { >> profile = [ >> [ -1/2, -0.77], >> [ -7/16, -0.75], >> [ 5/16, 0], >> [ 7/16, 0], >> [ 7/16, -0.75], >> [ 1/ 2, -0.77], >> ]; >> generic_threaded_nut(struct(kwargs, profile=profile)); >> } >> >> Of course, this isn't OO and makes it harder to detect argument name typos and such. >> >> The builder pattern could be implemented as so: >> >> // namespace for buttress_threaded_nut_builder >> obj = struct(generic_threaded_nut_builder::new()); >> function new() = >> let ( profile = [ >> [ -1/2, -0.77], >> [ -7/16, -0.75], >> [ 5/16, 0], >> [ 7/16, 0], >> [ 7/16, -0.75], >> [ 1/ 2, -0.77], >> ]) >> struct(obj)->profile(profile); >> >> so not any more or less difficult to write. What do profile and positioning look like? >> >> // namespace of generic_threaded_rod_builder >> function profile(o is builder_obj, profile) = >> assert(is_list(profile)) // And other tests independent of other values >> struct(o,profile=profile); >> >> // namespace of attachable_builder >> function positioning(o is attachable_builder_obj, anchor, spin, orient) = >> assert(/* tests related to the parameters*/) >> let( >> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >> is_undef(spin) ? [] : [["spin", spin]], >> is_undef(orient) ? [] : [["orient", orient]]) >> ) >> struct(o,l); >> >> Okay, not in love with the repetition in there. But there are some improvements here: >> >> 1. Some validation can now stop cluttering the top of so many functions/modules. >> 2. The `get_radius` function doesn't need its args filled out every time >> 3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature. >> 4. We can match on the old types and convert to objects as needed >> >> Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as `object` vs `struct` keywords. >> Maybe attachable should be a mixin instead of in the class hierarchy. >> >> So how would I write this with `this` as currently proposed? >> >> function buttress_threaded_nut_builder = >> let ( profile = [ >> [ -1/2, -0.77], >> [ -7/16, -0.75], >> [ 5/16, 0], >> [ 7/16, 0], >> [ 7/16, -0.75], >> [ 1/ 2, -0.77], >> ]) >> object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */); >> >> // in method list of generic_threaded_rod_builder >> function set_profile(profile) = >> assert(is_list(profile)) // And other tests independent of other values >> object(this,profile=profile); >> >> // in method list of attachable_builder >> function set_positioning(anchor, spin, orient) = >> assert(/* tests related to the parameters*/) >> let( >> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >> is_undef(spin) ? [] : [["spin", spin]], >> is_undef(orient) ? [] : [["orient", orient]]) >> ) >> struct(this,l); >> >> module reify_buttress_threaded_nut(obj) { >> reify_threaded_nut(obj); >> } >> >> >> module reify_threaded_nut(obj) { >> reify_generic_threaded_nut(obj); >> } >> >> reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK)); >> >> >> ... >> >> Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be: >> >> bosl2_threading = object( >> top_level_constant = 27, >> function buttress_threaded_nut_builder() = >> ... >> ); >> >> module reify_buttress_threaded_nut(obj) { >> bosl2_threading.top_level_constant; // for whatever reason >> reify_threaded_nut(obj); >> } >> >> I've seen worse. I certainly can't say this rules out `this`. >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to discuss-leave@lists.openscad.org > >_______________________________________________ >OpenSCAD mailing list >To unsubscribe send an email to discuss-leave@lists.openscad.org
CC
Cory Cross
Thu, Aug 21, 2025 6:01 AM

Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions:

Bah, these are alists, of course, and not plists. Either could be used though.

>Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions: Bah, these are alists, of course, and not plists. Either could be used though.
PK
Peter Kriens
Thu, Aug 21, 2025 1:57 PM

On 21 Aug 2025, at 01:55, Cory Cross via Discuss discuss@lists.openscad.org wrote:

Okay, I think I've identified the biggest problem with the proposed implementation of this: shadowed methods cannot be called on a child. Methods declared in a child can call a parent method on the parent object, but not with any values that may be defined or updated by the child constructor.

Objects do NOT have parents so I am not sure what you're talking about? An object is a flat set of key-value pairs. There is no hierarchy. We are very intentionally not implementing a full blow OO system to keep OpenSCAD as simple as possible, but not simpler.

I also dislike that you must define all methods in one context and file location. What I've proposed gives the users the same power as the library writers, while this method locks ownership of objects to the original author. (Obviously, yes, we can all edit any code, compiled or otherwise, on our machines, but it's not afforded by the language and it's a lot harder to ship patches to someone else's library than to ship your own file that adds a method to a namespace).

As far as future optimization, because of the dynamic context, can this approach be optimized well or is it like Python and doomed to always be slow? Well, lua is ultra flexible and luajit was apparently pretty amazing. So it might not be impossible.

I like the builder approach and it is one of my drivers for the object work and $this.

Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions:

function lookup(plist, pname, idx=0) =
let ( kv = plist[idx] ) is_undef(kv) ? kv : kv[0] == pname ? kv[1] : lookup(plist, pname, idx+1);

function call(obj, fname, args_plist) =
let( f = lookup(obj, fname) ) is_function(f) ? f(obj, args_plist) : undef;

function any_method(obj, args_plist) =
let( actual_arg = lookup(args_plist, "actual_arg"),
member = lookup(obj, "member"),
) implementation;

x=call(myobj, "distance", [["p",other_point]]) // call method distance on my object to calculate the distance from other_point
x=myobj.distance(p=other_point); // doesn't look so different, does it?

So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer?

Slightly??? We must live in another universe. There are few types of code I'd like to write less than this kind of boiler plate code. I wrote several of these 'OO' systems but they were quite ugly.

This PR added methods to object(). Torsten then raised the issue how callbacks should be handled. There are various way to handle this but that raised the question what should be the default: method bound to its object by default or through some keyword/operator/function. It was that simple.

Then you threw in an a-bom ... and another one. I am getting a bit desperate and feel this is going way off the track and taking way too much of my time ...

I am new here so I might not understand the mores in this project. However, in other projects I am used that if you want to derail a PR you make a fully working counter PR so people can play with the proposals and compare. I find that you're now just dropping disruptive ideas ...

The danger here is that we spend a lot of time talking back and forth and then nothing happens again because everything got so complicated. I think this partly happened with OEP8 and that spent a lot of time in discussion. There are very good, some crucial, ideas in that PR that has been idling since 2023.

I know I can be a bit blunt but you can blame it on my Dutch citizenship ;-)

Peter

Actually, plists are better because you can get super methods :-)

I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors.

On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss discuss@lists.openscad.org wrote:

You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and object? Not good for my blood pressure! :-)

I like the builder approach and it is one of my drivers for the object work and $this.

BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ...

Peter Kriens

On 19 Aug 2025, at 07:47, Cory Cross via Discuss discuss@lists.openscad.org wrote:
On 8/16/25 8:49 AM, Jordan Brown wrote:

bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify();

I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all.  Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function?
If the former, I don't understand what it means.  If the latter, are you seriously suggesting this as a replacement for
threaded_nut(required, args, here, optional_generic_arc=its_value);

I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add using bosl2::threading to shorten names, at some point).

As a practical example, here is a invocation of a threaded module in my code:

buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK);

here's how I would do it with the builder pattern and the suggested OO approach:

buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify;

You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs:

buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK));

In this case, buttress_threaded_nut's implementation would change from 50 lines to 11:

module buttress_threaded_nut(kwargs) {
profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
];
generic_threaded_nut(struct(kwargs, profile=profile));
}

Of course, this isn't OO and makes it harder to detect argument name typos and such.

The builder pattern could be implemented as so:

// namespace for buttress_threaded_nut_builder
obj = struct(generic_threaded_nut_builder::new());
function new() =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
struct(obj)->profile(profile);

so not any more or less difficult to write. What do profile and positioning look like?

// namespace of generic_threaded_rod_builder
function profile(o is builder_obj, profile) =
assert(is_list(profile)) // And other tests independent of other values
struct(o,profile=profile);

// namespace of attachable_builder
function positioning(o is attachable_builder_obj, anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(o,l);

Okay, not in love with the repetition in there. But there are some improvements here:

  1. Some validation can now stop cluttering the top of so many functions/modules.
  2. The get_radius function doesn't need its args filled out every time
  3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature.
  4. We can match on the old types and convert to objects as needed

Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as object vs struct keywords.
Maybe attachable should be a mixin instead of in the class hierarchy.

So how would I write this with this as currently proposed?

function buttress_threaded_nut_builder =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */);

// in method list of generic_threaded_rod_builder
function set_profile(profile) =
assert(is_list(profile)) // And other tests independent of other values
object(this,profile=profile);

// in method list of attachable_builder
function set_positioning(anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(this,l);

module reify_buttress_threaded_nut(obj) {
reify_threaded_nut(obj);
}

module reify_threaded_nut(obj) {
reify_generic_threaded_nut(obj);
}

reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK));

...

Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be:

bosl2_threading = object(
top_level_constant = 27,
function buttress_threaded_nut_builder() =
...
);

module reify_buttress_threaded_nut(obj) {
bosl2_threading.top_level_constant; // for whatever reason
reify_threaded_nut(obj);
}

I've seen worse. I certainly can't say this rules out this.


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

> On 21 Aug 2025, at 01:55, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: > > Okay, I think I've identified the biggest problem with the proposed implementation of this: shadowed methods cannot be called on a child. Methods declared in a child can call a parent method on the parent object, but not with any values that may be defined or updated by the child constructor. Objects do NOT have parents so I am not sure what you're talking about? An object is a flat set of key-value pairs. There is no hierarchy. We are very intentionally not implementing a full blow OO system to keep OpenSCAD as simple as possible, but not simpler. > > I also dislike that you must define all methods in one context and file location. What I've proposed gives the users the same power as the library writers, while this method locks ownership of objects to the original author. (Obviously, yes, we can all edit any code, compiled or otherwise, on our machines, but it's not afforded by the language and it's a lot harder to ship patches to someone else's library than to ship your own file that adds a method to a namespace). > > As far as future optimization, because of the dynamic context, can this approach be optimized well or is it like Python and doomed to always be slow? Well, lua is ultra flexible and luajit was apparently pretty amazing. So it might not be impossible. > >> I like the builder approach and it is one of my drivers for the object work and $this. > > Well... as I think about it further, we don't even need objects at all to do the builder pattern, since, as implemented, they're equivalent in power to a plist, you can do everything proposed with lists, concat, a plist lookup and a plist call functions: > > function lookup(plist, pname, idx=0) = > let ( kv = plist[idx] ) is_undef(kv) ? kv : kv[0] == pname ? kv[1] : lookup(plist, pname, idx+1); > > function call(obj, fname, args_plist) = > let( f = lookup(obj, fname) ) is_function(f) ? f(obj, args_plist) : undef; > > function any_method(obj, args_plist) = > let( actual_arg = lookup(args_plist, "actual_arg"), > member = lookup(obj, "member"), > ) implementation; > > x=call(myobj, "distance", [["p",other_point]]) // call method distance on my object to calculate the distance from other_point > x=myobj.distance(p=other_point); // doesn't look so different, does it? > > So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer? Slightly??? We must live in another universe. There are few types of code I'd like to write less than this kind of boiler plate code. I wrote several of these 'OO' systems but they were quite ugly. This PR added methods to object(). Torsten then raised the issue how callbacks should be handled. There are various way to handle this but that raised the question what should be the default: method bound to its object by default or through some keyword/operator/function. It was that simple. Then you threw in an a-bom ... and another one. I am getting a bit desperate and feel this is going way off the track and taking way too much of my time ... I am new here so I might not understand the mores in this project. However, in other projects I am used that if you want to derail a PR you make a fully working counter PR so people can play with the proposals and compare. I find that you're now just dropping disruptive ideas ... The danger here is that we spend a lot of time talking back and forth and then nothing happens again because everything got so complicated. I think this partly happened with OEP8 and that spent a lot of time in discussion. There are very good, some crucial, ideas in that PR that has been idling since 2023. I know I can be a bit blunt but you can blame it on my Dutch citizenship ;-) Peter > > Actually, plists are better because you can get super methods :-) > > > I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors. > > > > On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss <discuss@lists.openscad.org> wrote: >> You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and `object`? Not good for my blood pressure! :-) >> >> I like the builder approach and it is one of my drivers for the object work and $this. >> >> BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ... >> >> Peter Kriens >> >>> On 19 Aug 2025, at 07:47, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: >>> On 8/16/25 8:49 AM, Jordan Brown wrote: >>>>> bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify(); >>>> I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all. Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function? >>>> If the former, I don't understand what it means. If the latter, are you seriously suggesting this as a replacement for >>>> threaded_nut(required, args, here, optional_generic_arc=its_value); >>> I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add `using bosl2::threading` to shorten names, at some point). >>> >>> As a practical example, here is a invocation of a threaded module in my code: >>> >>> buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK); >>> >>> here's how I would do it with the builder pattern and the suggested OO approach: >>> >>> buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify; >>> >>> You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs: >>> >>> buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK)); >>> >>> In this case, buttress_threaded_nut's implementation would change from 50 lines to 11: >>> >>> module buttress_threaded_nut(kwargs) { >>> profile = [ >>> [ -1/2, -0.77], >>> [ -7/16, -0.75], >>> [ 5/16, 0], >>> [ 7/16, 0], >>> [ 7/16, -0.75], >>> [ 1/ 2, -0.77], >>> ]; >>> generic_threaded_nut(struct(kwargs, profile=profile)); >>> } >>> >>> Of course, this isn't OO and makes it harder to detect argument name typos and such. >>> >>> The builder pattern could be implemented as so: >>> >>> // namespace for buttress_threaded_nut_builder >>> obj = struct(generic_threaded_nut_builder::new()); >>> function new() = >>> let ( profile = [ >>> [ -1/2, -0.77], >>> [ -7/16, -0.75], >>> [ 5/16, 0], >>> [ 7/16, 0], >>> [ 7/16, -0.75], >>> [ 1/ 2, -0.77], >>> ]) >>> struct(obj)->profile(profile); >>> >>> so not any more or less difficult to write. What do profile and positioning look like? >>> >>> // namespace of generic_threaded_rod_builder >>> function profile(o is builder_obj, profile) = >>> assert(is_list(profile)) // And other tests independent of other values >>> struct(o,profile=profile); >>> >>> // namespace of attachable_builder >>> function positioning(o is attachable_builder_obj, anchor, spin, orient) = >>> assert(/* tests related to the parameters*/) >>> let( >>> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >>> is_undef(spin) ? [] : [["spin", spin]], >>> is_undef(orient) ? [] : [["orient", orient]]) >>> ) >>> struct(o,l); >>> >>> Okay, not in love with the repetition in there. But there are some improvements here: >>> >>> 1. Some validation can now stop cluttering the top of so many functions/modules. >>> 2. The `get_radius` function doesn't need its args filled out every time >>> 3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature. >>> 4. We can match on the old types and convert to objects as needed >>> >>> Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as `object` vs `struct` keywords. >>> Maybe attachable should be a mixin instead of in the class hierarchy. >>> >>> So how would I write this with `this` as currently proposed? >>> >>> function buttress_threaded_nut_builder = >>> let ( profile = [ >>> [ -1/2, -0.77], >>> [ -7/16, -0.75], >>> [ 5/16, 0], >>> [ 7/16, 0], >>> [ 7/16, -0.75], >>> [ 1/ 2, -0.77], >>> ]) >>> object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */); >>> >>> // in method list of generic_threaded_rod_builder >>> function set_profile(profile) = >>> assert(is_list(profile)) // And other tests independent of other values >>> object(this,profile=profile); >>> >>> // in method list of attachable_builder >>> function set_positioning(anchor, spin, orient) = >>> assert(/* tests related to the parameters*/) >>> let( >>> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >>> is_undef(spin) ? [] : [["spin", spin]], >>> is_undef(orient) ? [] : [["orient", orient]]) >>> ) >>> struct(this,l); >>> >>> module reify_buttress_threaded_nut(obj) { >>> reify_threaded_nut(obj); >>> } >>> >>> >>> module reify_threaded_nut(obj) { >>> reify_generic_threaded_nut(obj); >>> } >>> >>> reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK)); >>> >>> >>> ... >>> >>> Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be: >>> >>> bosl2_threading = object( >>> top_level_constant = 27, >>> function buttress_threaded_nut_builder() = >>> ... >>> ); >>> >>> module reify_buttress_threaded_nut(obj) { >>> bosl2_threading.top_level_constant; // for whatever reason >>> reify_threaded_nut(obj); >>> } >>> >>> I've seen worse. I certainly can't say this rules out `this`. >>> _______________________________________________ >>> OpenSCAD mailing list >>> To unsubscribe send an email to discuss-leave@lists.openscad.org >> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to discuss-leave@lists.openscad.org > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org
CC
Cory Cross
Thu, Aug 21, 2025 9:18 PM

On August 21, 2025 9:57:30 AM EDT, Peter Kriens via Discuss discuss@lists.openscad.org wrote:

On 21 Aug 2025, at 01:55, Cory Cross via Discuss discuss@lists.openscad.org wrote:

Objects do NOT have parents so I am not sure what you're talking about? An object is a flat set of key-value pairs. There is no hierarchy.

Then they shouldn't be called objects. At least 99% of people who have or will use OpenSCAD will associate objects with the mainstream object-oriented languages which all put inheritance front-and-center. It's day-one "learning Python" material. I don't think there's a single language with a "this" keyword that doesn't have inheritance. You're setting up people for confusion.

We are very intentionally not implementing a full blow OO system to keep OpenSCAD as simple as possible, but not simpler.

Following this logic, not adding it is simpler.

What do you want to be simple: writing SCAD or the implementation of OpenSCAD? There's often (but not always) a trade-off. Brainfuck is very simple to implement. You can solve some very complicated analyses in a single line of Mathematica.

So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer?

Slightly??? We must live in another universe. There are few types of code I'd like to write less than this kind of boiler plate code. I wrote several of these 'OO' systems but they were quite ugly.

"this" refers to the proposal to add a "this" keyword. But what I wrote shows that, with a couple helper functions, the proposed object() does not result in substantially simpler code and by the maxim "keep OpenSCAD as simple as possible, but not simpler", shouldn't be added.

Then you threw in an a-bom ... and another one. I am getting a bit desperate and feel this is going way off the track and taking way too much of my time ...

I am new here so I might not understand the mores in this project.

I am new as well (though a user for many years).

However, in other projects I am used that if you want to derail a PR you make a fully working counter PR so people can play with the proposals and compare. I find that you're now just dropping disruptive ideas ...

I've not found any other discussion of OEP8 and wasn't active at the time anyway. I am discussing now because now is when I'm here.

It's my impression the "this" keyword is just being added because people are unfamiliar with other systems. If SCAD was a hybrid procedural/OO lisp with mutable values like JavaScript, then it'd be fine to copy their semantics. But it's not and I think you're going down the wrong road.

I'm trying to prove it by picking bosl2 and showing how I'd refactor it using the proposed "this" approach or the one I'm proposing, because ultimately what we want is what makes it easier and faster to write correct code, right?

Everything I've proposed is quite easy to implement and I'll be happy to do it and/or collaborate on it.

The danger here is that we spend a lot of time talking back and forth and then nothing happens again because everything got so complicated. I think this partly happened with OEP8 and that spent a lot of time in discussion. There are very good, some crucial, ideas in that PR that has been idling since 2023.

I also think it's important to keep momentum up.

I know I can be a bit blunt but you can blame it on my Dutch citizenship ;-)

I can be a bit blunt but you can blame it on my Dutch ancestry :-).

  • Cory Cross
Peter

Actually, plists are better because you can get super methods :-)

I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors.

On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss discuss@lists.openscad.org wrote:

You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and object? Not good for my blood pressure! :-)

I like the builder approach and it is one of my drivers for the object work and $this.

BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ...

Peter Kriens

On 19 Aug 2025, at 07:47, Cory Cross via Discuss discuss@lists.openscad.org wrote:
On 8/16/25 8:49 AM, Jordan Brown wrote:

bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify();
I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all.  Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function?
If the former, I don't understand what it means.  If the latter, are you seriously suggesting this as a replacement for
threaded_nut(required, args, here, optional_generic_arc=its_value);
I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add using bosl2::threading to shorten names, at some point).

As a practical example, here is a invocation of a threaded module in my code:

buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK);

here's how I would do it with the builder pattern and the suggested OO approach:

buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify;

You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs:

buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK));

In this case, buttress_threaded_nut's implementation would change from 50 lines to 11:

module buttress_threaded_nut(kwargs) {
profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
];
generic_threaded_nut(struct(kwargs, profile=profile));
}

Of course, this isn't OO and makes it harder to detect argument name typos and such.

The builder pattern could be implemented as so:

// namespace for buttress_threaded_nut_builder
obj = struct(generic_threaded_nut_builder::new());
function new() =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
struct(obj)->profile(profile);

so not any more or less difficult to write. What do profile and positioning look like?

// namespace of generic_threaded_rod_builder
function profile(o is builder_obj, profile) =
assert(is_list(profile)) // And other tests independent of other values
struct(o,profile=profile);

// namespace of attachable_builder
function positioning(o is attachable_builder_obj, anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(o,l);

Okay, not in love with the repetition in there. But there are some improvements here:

  1. Some validation can now stop cluttering the top of so many functions/modules.
  2. The get_radius function doesn't need its args filled out every time
  3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature.
  4. We can match on the old types and convert to objects as needed

Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as object vs struct keywords.
Maybe attachable should be a mixin instead of in the class hierarchy.

So how would I write this with this as currently proposed?

function buttress_threaded_nut_builder =
let ( profile = [
[  -1/2, -0.77],
[ -7/16, -0.75],
[  5/16,  0],
[  7/16,  0],
[  7/16, -0.75],
[  1/ 2, -0.77],
])
object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */);

// in method list of generic_threaded_rod_builder
function set_profile(profile) =
assert(is_list(profile)) // And other tests independent of other values
object(this,profile=profile);

// in method list of attachable_builder
function set_positioning(anchor, spin, orient) =
assert(/* tests related to the parameters*/)
let(
l = concat(is_undef(anchor) ? [] : [["anchor", anchor]],
is_undef(spin) ? [] : [["spin", spin]],
is_undef(orient) ? [] : [["orient", orient]])
)
struct(this,l);

module reify_buttress_threaded_nut(obj) {
reify_threaded_nut(obj);
}

module reify_threaded_nut(obj) {
reify_generic_threaded_nut(obj);
}

reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK));

...

Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be:

bosl2_threading = object(
top_level_constant = 27,
function buttress_threaded_nut_builder() =
...
);

module reify_buttress_threaded_nut(obj) {
bosl2_threading.top_level_constant; // for whatever reason
reify_threaded_nut(obj);
}

I've seen worse. I certainly can't say this rules out this.


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

On August 21, 2025 9:57:30 AM EDT, Peter Kriens via Discuss <discuss@lists.openscad.org> wrote: >> On 21 Aug 2025, at 01:55, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: >Objects do NOT have parents so I am not sure what you're talking about? An object is a flat set of key-value pairs. There is no hierarchy. Then they shouldn't be called objects. At least 99% of people who have or will use OpenSCAD will associate objects with the mainstream object-oriented languages which all put inheritance front-and-center. It's day-one "learning Python" material. I don't think there's a single language with a "this" keyword that doesn't have inheritance. You're setting up people for confusion. > We are very intentionally not implementing a full blow OO system to keep OpenSCAD as simple as possible, but not simpler. Following this logic, not adding it is simpler. What do you want to be simple: writing SCAD or the implementation of OpenSCAD? There's often (but not always) a trade-off. Brainfuck is very simple to implement. You can solve some very complicated analyses in a single line of Mathematica. >> So we already have the power to do builders, it's just slightly uglier and slower. My question is while settle for "this" when generic functions are even nicer? > >Slightly??? We must live in another universe. There are few types of code I'd like to write less than this kind of boiler plate code. I wrote several of these 'OO' systems but they were quite ugly. "this" refers to the proposal to add a "this" keyword. But what I wrote shows that, with a couple helper functions, the proposed object() does not result in substantially simpler code and by the maxim "keep OpenSCAD as simple as possible, but not simpler", shouldn't be added. >Then you threw in an a-bom ... and another one. I am getting a bit desperate and feel this is going way off the track and taking way too much of my time ... > >I am new here so I might not understand the mores in this project. I am new as well (though a user for many years). > However, in other projects I am used that if you want to derail a PR you make a fully working counter PR so people can play with the proposals and compare. I find that you're now just dropping disruptive ideas ... I've not found any other discussion of OEP8 and wasn't active at the time anyway. I am discussing now because now is when I'm here. It's my impression the "this" keyword is just being added because people are unfamiliar with other systems. If SCAD was a hybrid procedural/OO lisp with mutable values like JavaScript, then it'd be fine to copy their semantics. But it's not and I think you're going down the wrong road. I'm trying to prove it by picking bosl2 and showing how I'd refactor it using the proposed "this" approach or the one I'm proposing, because ultimately what we want is what makes it easier and faster to write correct code, right? Everything I've proposed is quite easy to implement and I'll be happy to do it and/or collaborate on it. >The danger here is that we spend a lot of time talking back and forth and then nothing happens again because everything got so complicated. I think this partly happened with OEP8 and that spent a lot of time in discussion. There are very good, some crucial, ideas in that PR that has been idling since 2023. I also think it's important to keep momentum up. >I know I can be a bit blunt but you can blame it on my Dutch citizenship ;-) I can be a bit blunt but you can blame it on my Dutch ancestry :-). - Cory Cross > > Peter > > > >> >> Actually, plists are better because you can get super methods :-) >> >> >> I'm on my phone composing this without Internet access, so please forgive the formatting and mild syntax errors. >> >> >> >> On August 19, 2025 4:40:20 AM EDT, Peter Kriens via Discuss <discuss@lists.openscad.org> wrote: >>> You flabbergasted me with the completely different direction/syntax you took but then at the end I saw that you came to the conclusion you could do all this also with 'this' and `object`? Not good for my blood pressure! :-) >>> >>> I like the builder approach and it is one of my drivers for the object work and $this. >>> >>> BTW, notice that OEP8 also proposed to have modules as expression. I am currently working on a PR for this. This will allow builders to also call modules, have modules as variables, and hopefully modules as methods when we can finally close [the $]this discussion ... >>> >>> Peter Kriens >>> >>>> On 19 Aug 2025, at 07:47, Cory Cross via Discuss <discuss@lists.openscad.org> wrote: >>>> On 8/16/25 8:49 AM, Jordan Brown wrote: >>>>>> bosl2::threading::nut_builder::new(required, args, here)->optional_generic_arg(its_value)->reify(); >>>>> I'm very sympathetic to the desire to reduce repetition in argument handling, but I'm not understanding what that means at all. Partly that's presentation; is this intended to be how the library would say something, or how the caller would invoke the function? >>>>> If the former, I don't understand what it means. If the latter, are you seriously suggesting this as a replacement for >>>>> threaded_nut(required, args, here, optional_generic_arc=its_value); >>>> I am suggesting it as a replacement for the latter; not because it's better for the user, but because it's better for the maintainers and not worse for the users. (I would assume we'd add `using bosl2::threading` to shorten names, at some point). >>>> >>>> As a practical example, here is a invocation of a threaded module in my code: >>>> >>>> buttress_threaded_nut(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK); >>>> >>>> here's how I would do it with the builder pattern and the suggested OO approach: >>>> >>>> buttress_threaded_nut_builder::new()->nutwidth(10)->id(7+.17*4)->h(6)->shape("square")->positioning(orient=DOWN,anchor=TOP+BACK)->reify; >>>> >>>> You don't have to use the builder pattern. You could choose to use objects as a replacement for Python's **kwargs: >>>> >>>> buttress_threaded_nut(struct(nutwidth=10,id=7+.17*4,h=6,pitch=1,shape="square",orient=DOWN,anchor=TOP+BACK)); >>>> >>>> In this case, buttress_threaded_nut's implementation would change from 50 lines to 11: >>>> >>>> module buttress_threaded_nut(kwargs) { >>>> profile = [ >>>> [ -1/2, -0.77], >>>> [ -7/16, -0.75], >>>> [ 5/16, 0], >>>> [ 7/16, 0], >>>> [ 7/16, -0.75], >>>> [ 1/ 2, -0.77], >>>> ]; >>>> generic_threaded_nut(struct(kwargs, profile=profile)); >>>> } >>>> >>>> Of course, this isn't OO and makes it harder to detect argument name typos and such. >>>> >>>> The builder pattern could be implemented as so: >>>> >>>> // namespace for buttress_threaded_nut_builder >>>> obj = struct(generic_threaded_nut_builder::new()); >>>> function new() = >>>> let ( profile = [ >>>> [ -1/2, -0.77], >>>> [ -7/16, -0.75], >>>> [ 5/16, 0], >>>> [ 7/16, 0], >>>> [ 7/16, -0.75], >>>> [ 1/ 2, -0.77], >>>> ]) >>>> struct(obj)->profile(profile); >>>> >>>> so not any more or less difficult to write. What do profile and positioning look like? >>>> >>>> // namespace of generic_threaded_rod_builder >>>> function profile(o is builder_obj, profile) = >>>> assert(is_list(profile)) // And other tests independent of other values >>>> struct(o,profile=profile); >>>> >>>> // namespace of attachable_builder >>>> function positioning(o is attachable_builder_obj, anchor, spin, orient) = >>>> assert(/* tests related to the parameters*/) >>>> let( >>>> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >>>> is_undef(spin) ? [] : [["spin", spin]], >>>> is_undef(orient) ? [] : [["orient", orient]]) >>>> ) >>>> struct(o,l); >>>> >>>> Okay, not in love with the repetition in there. But there are some improvements here: >>>> >>>> 1. Some validation can now stop cluttering the top of so many functions/modules. >>>> 2. The `get_radius` function doesn't need its args filled out every time >>>> 3. We're reusing attachable instead of needing to redundantly pass it so many args every single time and in every function and module signature. >>>> 4. We can match on the old types and convert to objects as needed >>>> >>>> Unsolved issue: why would -> method invocation not look at the namespace of positioning? I didn't intend it to, but it would do the wrong thing as written; only the calls in the new() methods should add the namespace to the method lookup. This might be best as `object` vs `struct` keywords. >>>> Maybe attachable should be a mixin instead of in the class hierarchy. >>>> >>>> So how would I write this with `this` as currently proposed? >>>> >>>> function buttress_threaded_nut_builder = >>>> let ( profile = [ >>>> [ -1/2, -0.77], >>>> [ -7/16, -0.75], >>>> [ 5/16, 0], >>>> [ 7/16, 0], >>>> [ 7/16, -0.75], >>>> [ 1/ 2, -0.77], >>>> ]) >>>> object(new_generic_threaded_rod_builder().set_profile(profile), /* all methods on buttress_threaded_nut_builder must be defined here */); >>>> >>>> // in method list of generic_threaded_rod_builder >>>> function set_profile(profile) = >>>> assert(is_list(profile)) // And other tests independent of other values >>>> object(this,profile=profile); >>>> >>>> // in method list of attachable_builder >>>> function set_positioning(anchor, spin, orient) = >>>> assert(/* tests related to the parameters*/) >>>> let( >>>> l = concat(is_undef(anchor) ? [] : [["anchor", anchor]], >>>> is_undef(spin) ? [] : [["spin", spin]], >>>> is_undef(orient) ? [] : [["orient", orient]]) >>>> ) >>>> struct(this,l); >>>> >>>> module reify_buttress_threaded_nut(obj) { >>>> reify_threaded_nut(obj); >>>> } >>>> >>>> >>>> module reify_threaded_nut(obj) { >>>> reify_generic_threaded_nut(obj); >>>> } >>>> >>>> reify_buttress_threaded_nut(buttress_threaded_nut_builder().set_nutwidth(10)->set_id(7+.17*4)->set_h(6)->set_shape("square")->set_positioning(orient=DOWN,anchor=TOP+BACK)); >>>> >>>> >>>> ... >>>> >>>> Less different than I thought. Cascading explicit reify modules are annoying. For this usage the inability to write your own generic methods dispatching on other object types does not hinder anything. The shared namespace means methods and values must have unique names. I think this is supposed to solve name conflicts by having you only need one unique name per file and you put all your constants in there? And methods, I guess, so at the top of f.ex. bosl2/threading.scad there would be: >>>> >>>> bosl2_threading = object( >>>> top_level_constant = 27, >>>> function buttress_threaded_nut_builder() = >>>> ... >>>> ); >>>> >>>> module reify_buttress_threaded_nut(obj) { >>>> bosl2_threading.top_level_constant; // for whatever reason >>>> reify_threaded_nut(obj); >>>> } >>>> >>>> I've seen worse. I certainly can't say this rules out `this`. >>>> _______________________________________________ >>>> OpenSCAD mailing list >>>> To unsubscribe send an email to discuss-leave@lists.openscad.org >>> >>> _______________________________________________ >>> OpenSCAD mailing list >>> To unsubscribe send an email to discuss-leave@lists.openscad.org >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to discuss-leave@lists.openscad.org >_______________________________________________ >OpenSCAD mailing list >To unsubscribe send an email to discuss-leave@lists.openscad.org