On Fri, Dec 2, 2022 at 1:33 AM Dmitry Gutov <dgutov@yandex.ru> wrote:

So, what you are asking for is perfectly reasonable in theory. Also the
current theory of OOP suggests that type inheritance comes with its
downsides as well. For instance, the straightforward way Joao was
suggesting (create a defclass) will mean nailing down the internal
structure of a type which would make it more difficult to create
internal changes to it -- because somebody might have inherited from it,
and might be using the exposed fields, etc.

I'm sorry, but it is imperative that this misconception be dispelled.

Just by doing:

  (defclass project-foo-project ()
    ((impl-detail :initform 42))

One is in no way exposing that implementation detail to the
subclassing user (if one is concerned the user uses slot-value
to access impl-detail's value, one may name it with the '--' 
double-dash convention or even make it a keyword, but I would say 
that's completely unnecessary here)

If one creates a slot with an reader:

  (defclass project-foo()
    ((some-implementation-detail-slot :initform 42)
     (root :initarg :root :reader project-root)))

then one _does_ expose an interface, in that one is adding a 
method to the generic function project-root, which is exactly 
what is desired.  Likewise for :writer and :accessor.

So, long after someone has inherited from project-foo one may 
as well erase 'root' and represent the root of a foo project in 
Morse code or anything like that just by reimplementing the 
project-root method for project-foo.

Thus, it's false that using defclass means "nailing down the internal
structure of a type".  It's just not true for CLOS (or any other OO 
system I know, for that matter).

As Stefan highlighted earlier, the generic functions are the 
protocol.  Offering classes and inheritance doesn't change that: 
it only makes them more powerful and simpler to instantiate.

And if one wants to add some pre-, post- or around- processing
to the `project-root` method created by defclass, one need
only use :before, :after or :around specifiers.

In fact, even if one wanted to prevent instancing of a specific 
class (I don't think we should here, but let's say project-foo is 
abstract or a singleton) defclass is still the way to go.  One can use a 
specialization of make-instance or a add a method to initialize-instance
for that.  

In fact, the "nailing down" problem is precisely what the current
implementation promotes.  The strange technique (transient . "some/path") 
used here doesn't actively prevent any user from instantiating it 
because 'cons' and 'transient' are both perfectly accessible symbols.  Users 
will likely use them to work around the missing constructor.  And once
that happens, _that's_ when `project.el` is stuck with that internal
implementation.  But if one uses defclass instead, one can retain this
freedom and even control or monitor the creation of objects with 
make-instance and associated CLOS protocols  (initialize-instance, 
reinitialize-instance, etc).

Have a look at how eglot-lsp-server class hierarchy to see 
some of these concepts in action.  For more examples, I 
recommend any good CLOS book. 

Finally, I know eieio.el isnt CLOS: it doesn't perfectly replicate 
CLOS unfortunately, but it's decently close for these basic use 
cases in my personal experience.

João