Discussion:
CLOS Design Question
(too old to reply)
jeosol
2019-12-02 23:16:53 UTC
Permalink
Good morning all,


I have a CLOS design question and wanted to see if there is a better way of doing what I'm trying to do and doing already. I'll keep it brief.

Basically, I have 3 concepts (A,B,C) and each can take different possibilities (or sub concepts). For each subconcept, some slots take a particular value (e.g., simply setting the to T or nil or some value). For example imagine B is a concept on status (ON/OFF) with subconcepts B1 (ON) and B2 (OFF).

For the testing, I'll need to get the options/or settings for all 3 concepts, A-B-C, that then fully specifies the case.

What I have done so far is as follows:

defclass base-class ()
((... add all the slots ...))

(defclass A-B-C (base-class) () ...)

(defclass A1-B-C (A-B-C) () ...)

you probably get my point. I am creating specific cases for the combination (handful) but it's getting messy.

I use :default-initargs appropriately to initialize slots as needed.

-----
Alternative approach (not tested or scoped out).


(defclass A1-B-C (A B C) () ...) and the slots are combined from the base classes of A, B, C. (Note A, B, C are different and are only useful when combined).



----------
For my testing, I need to be able to create an instance of a class for a specific case: e.g., A1-B2-C3

For that I have a generic create-object-method with (type :name), for example,

(defmethod create-object ((obj-type (eql :A1-B2-C3)) ..) to create the cases (not for all class combinations) but as much as needed to be able allow adequate differentiation (e.g., A1-B2-C3 may be have same slots and settings for A1-B2-C2) but is treated differently based on class name.


My approach follows the file streams example in Sonja Keene's text book.

What do you guys think? Comments?
t***@google.com
2019-12-03 17:31:24 UTC
Permalink
On Monday, December 2, 2019 at 3:16:59 PM UTC-8, jeosol wrote:

As an over-arching comment, I think that finding yourself in a situation where
you are creating classes that represent the cross-product of a set of other
classes is not really what you want to be doing.

One of the points of using CLOS instances is to be able to treat the instances
as interchangeable. You may, in fact, be doing this if you have a certain set
of methods that are differentiated by class, but it seems rather counter to a
lot of class design to require the user to know about all of these combinatoric
subclasses in order to create the instances.

One should also remember that subclassing is not the only way to organize a
set of objects of different types. Sometimes doing an aggregation of different
types and using delegation can make more sense.

You may also need to provide a more concrete example of what you want to get
from these classes and their instances, as the abstract description doesn't
really give me a good idea of what final behavior you are trying to achieve.
Post by jeosol
Good morning all,
I have a CLOS design question and wanted to see if there is a better way of doing what I'm trying to do and doing already. I'll keep it brief.
Basically, I have 3 concepts (A,B,C) and each can take different possibilities (or sub concepts). For each subconcept, some slots take a particular value (e.g., simply setting the to T or nil or some value). For example imagine B is a concept on status (ON/OFF) with subconcepts B1 (ON) and B2 (OFF).
What behavior do you want if you change the slot value on one of these
instances? Do you want it to change class? Or are the slot values immutable?
Post by jeosol
For the testing, I'll need to get the options/or settings for all 3 concepts, A-B-C, that then fully specifies the case.
defclass base-class ()
((... add all the slots ...))
(defclass A-B-C (base-class) () ...)
(defclass A1-B-C (A-B-C) () ...)
you probably get my point. I am creating specific cases for the combination (handful) but it's getting messy.
I use :default-initargs appropriately to initialize slots as needed.
-----
Alternative approach (not tested or scoped out).
(defclass A1-B-C (A B C) () ...) and the slots are combined from the base classes of A, B, C. (Note A, B, C are different and are only useful when combined).
If the classes are only useful when combined, why do you separate them out?
Perhaps a simpler solution than trying to do all combinations is to create a
single class that has slots for A, B and C.

Also, do you want instances of A1-B-C to be able to play the role of just an A
or just a B, etc? Remember that subclasses should semantically be sub-types. If
it is some other form of relationship, you should use a different mechanism.

You could then have process the class based on various attributes of the
classes. Or define predicates for combinations that make some semantic sense
and use those for guiding your processing?

I suppose that the fundamental question is what benefit to you seek from
generating all of these combination classes?
Post by jeosol
----------
For my testing, I need to be able to create an instance of a class for a specific case: e.g., A1-B2-C3
For that I have a generic create-object-method with (type :name), for example,
(defmethod create-object ((obj-type (eql :A1-B2-C3)) ..) to create the cases (not for all class combinations) but as much as needed to be able allow adequate differentiation (e.g., A1-B2-C3 may be have same slots and settings for A1-B2-C2) but is treated differently based on class name.
This seems to be a bit of a reinvention of MAKE-INSTANCE. It would seem that
you would not need to introduce this new function, but just be able to create
instances and have the appropriate initialization methods run for your classes.
Post by jeosol
My approach follows the file streams example in Sonja Keene's text book.
What do you guys think? Comments?
Perhaps what you want to use is something like a more fluid concept-based
knowledge representation system such as the old Loom(R) and more recent
PowerLoom(R) systems from the University of Southern California's Information
Sciences Institute. These are open-source knowledge representation systems
available in Common Lisp.
* https://www.isi.edu/isd/LOOM/
* https://www.isi.edu/isd/LOOM/PowerLoom/

They allow you to define concepts (similar to classes) where the membership can
be determined by the value of relations (similar to slots) dynamically. Loom
supports methods, although PowerLoom does not.
jeosol
2019-12-03 19:38:41 UTC
Permalink
@tar

Thank you for your very detailed reply. I will look into the suggestions and recommendations you made.

I apologize if my explanation was not clear enough.

I could have explained things better.


For the 3 classes A,B,C, these are concepts that when combined they fully define a system.

I will go to Sonja Keene's example (Chapter 11: Developing an Advanced CLOS Program: Streams) as being close to what I am trying to model. In the book she

She organized the classes in terms of:
1) Type of stream : input, output, bidirection
2) Type of data: character stream, byte stream,
3) Type of device: disk, tape

The classes from above are then (from book)
1) stream - foundation for the streams
2) directional streams: input-stream, output-stream, bidirectional-stream
3) element type stream: character-stream, byte-stream
4) device stream: disk-stream

I'll skip the rest of the details, but the organization then allows defining a specific case, e.g.,

(defclass 32-bit-word-disk-output-stream (...) ())

So, it my case, the combination of the concepts A, B and C, now completely define the case just like in the Stream example above.

My approach follows this but it seems a bit complicated and I was hoping there could be simpler/better way to organize the classes and overall concerns.
Post by t***@google.com
This seems to be a bit of a reinvention of MAKE-INSTANCE. It would seem that
you would not need to introduce this new function, but just be able to create
instances and have the appropriate initialization methods run for your classes.
This correct, what I mentioned is a thin wrapper around make-instance just to get a uniform constructor.
Post by t***@google.com
Perhaps what you want to use is something like a more fluid concept-based
knowledge representation system such as the old Loom(R) and more recent
PowerLoom(R) systems from the University of Southern California's Information
Sciences Institute. These are open-source knowledge representation systems
available in Common Lisp.
* https://www.isi.edu/isd/LOOM/
* https://www.isi.edu/isd/LOOM/PowerLoom/
They allow you to define concepts (similar to classes) where the membership can
be determined by the value of relations (similar to slots) dynamically. Loom
supports methods, although PowerLoom does not.
Thanks, I will take a look at the LOOM recommendation.
jeosol
2019-12-03 20:00:54 UTC
Permalink
Post by t***@google.com
If the classes are only useful when combined, why do you separate them out?
Perhaps a simpler solution than trying to do all combinations is to create a
single class that has slots for A, B and C.
They are not separated out. I am using them as being grouped together A-B-C.
Post by t***@google.com
Also, do you want instances of A1-B-C to be able to play the role of just an A
or just a B, etc? Remember that subclasses should semantically be sub-types. If
it is some other form of relationship, you should use a different mechanism.
A1-B-C represents a type of the A-B-C, in this case but even this will be an abstract and not one to be instantiated. The ones the user specifies will have a spec for all the concepts, e.g., A1-B2-C1, or A1-B1-C1.
Post by t***@google.com
You could then have process the class based on various attributes of the
classes.
I believe I am doing something like this, not sure.
Post by t***@google.com
Or define predicates for combinations that make some semantic sense
and use those for guiding your processing?
Will look into this
Post by t***@google.com
I suppose that the fundamental question is what benefit to you seek from
generating all of these combination classes?
I don't generate all possible combinations only as much as is necessary to treat the cases appropriately.

When I use the code, I specify the concepts in A, B, C and then the system should instantiate the right class and the slots will have right default values and others as initialized from the constructor.

My use of the word concept makes it look like it's a KRR system (may be it is) hence your suggestion of LOOM.

I am just get way to model this different combinations and also have the appropriate variables initialized so they are processed correctly downstream. This process differs for the type of A-B-C.
t***@google.com
2019-12-03 20:24:30 UTC
Permalink
Post by jeosol
I am just get way to model this different combinations and also have the appropriate variables initialized so they are processed correctly downstream. This process differs for the type of A-B-C.
OK. So based on your description of the streams (I have not read that work)
with orthogonal characteristics, it seems like what you have is perhaps
some abstract classes A, B, C which each describe some orthogonal part of
your space. Each of these classes will have some disjoint subclasses out
of which you will build your classes. [*]

In that case, I would suggest either one of these approaches:

(Common) Create your concrete classes A1, A2, ...; B1, B2, ...; etc.
If it makes sense you could make each set of concrete classes share a common
parent. But that is pretty much optional.

(1) Following the streams example, you could build concrete composite classes
that use inheritance for getting their behavior:
(defclass A1-B2-C1 (A1 B2 C1) ...)
You would create the composite classes out of the concrete parts and not
bother with any intermediate classes. Make the combined classes each be
a direct subclass of the concrete parts. Now this may or may not work depending
on the degree to which there is interaction between the individual parts.
The problem is that if you need a method which depends on more than one of the
superclasses, you can't get that by the standard method dispatch. You would
instead have to define it for the new class you have written.

(2) Following an aggregation approach, you could have a class Z with slots for
the A, B and C types. Each slot would hold an instance of a concrete subclass
of the particular type. The methods on class Z would comprise the API for your
combined class and would dispatch to methods of the underlying instances and do
whatever combination of results would be needed. This would be the preferred
design if the actions of the characteristics are largely or wholly independent
of each other.
It may also be a reasonable strategy if there are interactions, because you
could define multi-dispatch methods and pass in the individual instances to
select the appropriate handling. So this might produce the most reasonable
class + method structure.


[*] I'll note that CLOS does not have any built-in support for abstract classes
or for disjoint subclasses. Although you could program something like that if
you want to make sure you can't try to build something like
(defclass bleah (a1 a2 b3 c4) ...)
jeosol
2019-12-03 22:06:25 UTC
Permalink
Thanks again for your reply.
Post by t***@google.com
OK. So based on your description of the streams (I have not read that work)
with orthogonal characteristics, it seems like what you have is perhaps
some abstract classes A, B, C which each describe some orthogonal part of
your space. Each of these classes will have some disjoint subclasses out
of which you will build your classes. [*]
Yes, the A,B,C class are orthogonal and disjoint in general.
Post by t***@google.com
(Common) Create your concrete classes A1, A2, ...; B1, B2, ...; etc.
If it makes sense you could make each set of concrete classes share a common
parent. But that is pretty much optional.
I am doing a bit of this, but not the separate concrete classes.
Post by t***@google.com
(1) Following the streams example, you could build concrete composite classes
(defclass A1-B2-C1 (A1 B2 C1) ...)
You would create the composite classes out of the concrete parts and not
bother with any intermediate classes. Make the combined classes each be
a direct subclass of the concrete parts. Now this may or may not work depending
on the degree to which there is interaction between the individual parts.
The problem is that if you need a method which depends on more than one of the
superclasses, you can't get that by the standard method dispatch. You would
instead have to define it for the new class you have written.
This is approach is interesting and similar to the one I do, except the classes are not derived from the concrete abstract ones: A1, B2, ...
This could potentially work because the methods are defined on the aggregate/composite classes. The only thing is I can't take advantage of a composite class that could be similar, where I don't have to define specific method and have the class just call a less specific one that is available.

It is this feature than enables me not define the methods for every case, because at some level, a defmethod on a class X (some A?-B?-C?) will work for every class that's derived from X.
Post by t***@google.com
(2) Following an aggregation approach, you could have a class Z with slots for
the A, B and C types. Each slot would hold an instance of a concrete subclass
of the particular type. The methods on class Z would comprise the API for your
combined class and would dispatch to methods of the underlying instances and do
whatever combination of results would be needed. This would be the preferred
design if the actions of the characteristics are largely or wholly independent
of each other.
It may also be a reasonable strategy if there are interactions, because you
could define multi-dispatch methods and pass in the individual instances to
select the appropriate handling. So this might produce the most reasonable
class + method structure.
If I understand this correctly, I will have slots that the types of A, B, and C on the main class Z.
For the technique is interesting and different from what I use now (currently I have slots representing the values in the A, B, C, all under some base class like Z, but separated into separate classes A,B,C).

I imagine the methods will be defined on Z and within Z, call the different functions specalized on A, B, C (extracted from slots from Z in the parent defmethod). I hope I'm interpreting that correctly.

If this approach gives better class+method structure, and allows for extensibility, then I can explore it more. The structure A1-B2-C3, will be harder to manage as I have to remember the correct A?B?C? to use as the parent class for new classes.
Post by t***@google.com
[*] I'll note that CLOS does not have any built-in support for abstract classes
or for disjoint subclasses. Although you could program something like that if
you want to make sure you can't try to build something like
(defclass bleah (a1 a2 b3 c4) ...)
Interesting point. I should only build a case that combines unique cases of a,b,c.

The intent at the end it to have a CL list with all possible types (think of it like a drop-down, but on CL side), that I can then pick one specific member that corresponds to a class to create an object.

It's helpful discuss as design considerations have consequences down the line so I wanted to explore alternative ideas.
Madhu
2019-12-04 03:16:04 UTC
Permalink
[ignore if not useful]

* jeosol <e67af00a-b44a-47d9-89c7-***@googlegroups.com> :
Wrote on Tue, 3 Dec 2019 14:06:25 -0800 (PST):

If I understand correctly (and I also haven't read Keene's book) your
method differs from Keene's approach in specifying all (or most) of the
slots in a single base class. Subclasses are "classed" according to
which particular options/slots they set. This means that you have
decided that the "disjoint" concepts aren't disjoint at all.


[Attrib Added]
[snip]
Post by jeosol
Post by t***@google.com
(1) Following the streams example, you could build concrete composite
classes that use inheritance for getting their behavior: (defclass
A1-B2-C1 (A1 B2 C1) ...) You would create the composite classes out
of the concrete parts and not bother with any intermediate
classes. Make the combined classes each be a direct subclass of the
concrete parts. Now this may or may not work depending on the degree
to which there is interaction between the individual parts. The
problem is that if you need a method which depends on more than one
of the superclasses, you can't get that by the standard method
dispatch. You would instead have to define it for the new class you
have written.
This is approach is interesting and similar to the one I do, except
the classes are not derived from the concrete abstract ones: A1, B2,
... This could potentially work because the methods are defined on
the aggregate/composite classes. The only thing is I can't take
advantage of a composite class that could be similar, where I don't
have to define specific method and have the class just call a less
specific one that is available.
It is this feature than enables me not define the methods for every
case, because at some level, a defmethod on a class X (some A-B-C)
will work for every class that's derived from X.
Post by t***@google.com
(2) Following an aggregation approach, you could have a class Z with
slots for the A, B and C types. Each slot would hold an instance of a
concrete subclass of the particular type. The methods on class Z
would comprise the API for your combined class and would dispatch to
methods of the underlying instances and do whatever combination of
results would be needed. This would be the preferred design if the
actions of the characteristics are largely or wholly independent of
each other. It may also be a reasonable strategy if there are
interactions, because you could define multi-dispatch methods and
pass in the individual instances to select the appropriate
handling. So this might produce the most reasonable class + method
structure.
If I understand this correctly, I will have slots that the types of A,
B, and C on the main class Z. For the technique is interesting and
different from what I use now (currently I have slots representing the
values in the A, B, C, all under some base class like Z, but separated
into separate classes A,B,C).
I imagine the methods will be defined on Z and within Z, call the
different functions specalized on A, B, C (extracted from slots from Z
in the parent defmethod). I hope I'm interpreting that correctly.
If this approach gives better class+method structure, and allows for
extensibility, then I can explore it more. The structure A1-B2-C3,
will be harder to manage as I have to remember the correct A-B-C to
use as the parent class for new classes.
Again just a comment: I'd think you would only be aggregating A B or C
instances (which are distinct disjoint concepts) in the slots of your Zs
and not combinations of those.
Post by jeosol
Post by t***@google.com
[*] I'll note that CLOS does not have any built-in support for
abstract classes or for disjoint subclasses. Although you could
program something like that if you want to make sure you can't try to
build something like (defclass bleah (a1 a2 b3 c4) ...)
Interesting point. I should only build a case that combines unique cases of a,b,c.
The intent at the end it to have a CL list with all possible types
(think of it like a drop-down, but on CL side), that I can then pick
one specific member that corresponds to a class to create an object.
It's helpful discuss as design considerations have consequences down
the line so I wanted to explore alternative ideas.
jeosol
2019-12-04 16:43:40 UTC
Permalink
Post by Madhu
[ignore if not useful]
If I understand correctly (and I also haven't read Keene's book) your
method differs from Keene's approach in specifying all (or most) of the
slots in a single base class. Subclasses are "classed" according to
which particular options/slots they set. This means that you have
decided that the "disjoint" concepts aren't disjoint at all.
The concepts are disjoint but I admit that my initial design is poor. And yes, it does not strictly follow Keene's approach in the sense that I didn't create some aggregate class from the components e.g., (defclass agg (A B C) ...)
Post by Madhu
Again just a comment: I'd think you would only be aggregating A B or C
instances (which are distinct disjoint concepts) in the slots of your Zs
and not combinations of those.
if I understand you correctly, Z has no base class but contains instances of A, B, C as slots in Z.

The treatment (defmethods) considers considers (A,B,C), so my comment was how to define that in terms of Z but delegate to other functions specialized on A,B,C -- Still messy I think.

Or defmethods specialized on Z, and inside the method add conditionals based on A, B, C instances.
t***@google.com
2019-12-04 17:31:00 UTC
Permalink
Post by jeosol
Post by Madhu
Again just a comment: I'd think you would only be aggregating A B or C
instances (which are distinct disjoint concepts) in the slots of your Zs
and not combinations of those.
if I understand you correctly, Z has no base class but contains instances of
A, B, C as slots in Z.
Yes.
Post by jeosol
The treatment (defmethods) considers considers (A,B,C), so my comment was how
to define that in terms of Z but delegate to other functions specialized on
A,B,C -- Still messy I think.
Well, one way to do this would be something like:

(defmethod do-this ((self Z))
(do-this-internal (a self) (b self) (c self)))
(defmethod do-that ((self Z))
(do-that-internal (a self) (c self)))

And then define internal methods for

(defmethod do-this-internal ((a A1) (b B1) c)
;; Handling for doing this to A1 and B1 and any C.
)
(defmethod do-this-internal ((a A1) (b B2) c) ...)


Of course, depending on the method, you may not need to pass all of the A,B,C
instances as arguments. But that would be an internal detail of the method and
of no particular interest to the API user.
Post by jeosol
Or defmethods specialized on Z, and inside the method add conditionals based
on A, B, C instances.
jeosol
2019-12-05 04:15:47 UTC
Permalink
Post by t***@google.com
(defmethod do-this ((self Z))
(do-this-internal (a self) (b self) (c self)))
(defmethod do-that ((self Z))
(do-that-internal (a self) (c self)))
And then define internal methods for
(defmethod do-this-internal ((a A1) (b B1) c)
;; Handling for doing this to A1 and B1 and any C.
)
(defmethod do-this-internal ((a A1) (b B2) c) ...)
Ok. that's what I understood. It certainly should required fewer methods I think to take care of the main cases. In addition, it should hopefully be much smaller and better organized.
Post by t***@google.com
Of course, depending on the method, you may not need to pass all of the A,B,C
instances as arguments. But that would be an internal detail of the method and
of no particular interest to the API user.
Loading...