Subject: bug#77725: 31.0.50; Add support for types accepted by `cl-typep' to cl-generic?

From: David Ponce <[email-masked]>
Date: Sun, 13 Apr 2025 16:45:06 +0200

Hi Stefan,

Sorry for this late answer. It took me a while to take it all in ;-)

On 2025-04-11 18:26, Stefan Monnier wrote:
>> Does this mean that for a `cl-deftype'-like generalizer to work
>> correctly, it must be part of the "typof" generalizer?
> 
> No, but it needs to be either always more precise or always less precise
> than `type-of`.
> 
> IOW, either:
> 
>      (eql (gtypeof-generalizer A) (gtypeof-generalizer B))
>      IMPLIES ((eql (gtypeof-generalizer A) nil)
>               OR (eql (cl-type-of A) (cl-type-of B)))
> 
> or the reverse.  That's a current limitation in the cl-generic system.
>
> One way to solve this is to use a generalizer that returns either nil or
> a pair containing both the `gtype-of` and the `cl-type-of`, so it's
> trivially always more precise than `cl-type-of`. But beware: these
> things are compared with `eql` so you need to "uniquify" them with
> a sort of hashconsing scheme.

I am not sure to understand this point.

AFAICS, gtypes always take precedence over other types.  So, provided
I correctly understand your point, the gtype generalizer is more precise
than `type-of`.

For instance, if you define an icon type like this:

(defgtype icon nil ()
   "Builtin icon type."
   '(and symbol (satisfies iconp)))

(gtype-of 'button)
=> icon
(cl-type-of 'button)
=> symbol

So you cannot have two methods with the same name, one to handle the
symbol type, and one to handle the icon type.  I agree, it is a
limitation.  Is this your point?

I wonder if in practice it is really inconvenient, and worth the
trouble of complicate more the implementation?  Isn't it clearer to
use different methods for icons and symbols? Aren't these types
fundamentally different, and isn't the fact that an icon is a symbol
just an implementation choice?

> 
> BTW if you do
> 
>      (defgtype cons-car-foo nil ()
>        "A cons with a `foo' car."
>        `(satisfies ,(lambda (x) (eq (car-safe x) 'foo))))
> 
>      (defgtype cons-cdr-foo nil ()
>        "A cons with a `foo' cdr."
>        `(satisfies ,(lambda (x) (eq (cdr-safe x) 'foo))))
> 
> what's the `(gtype-of '(foo . foo))` ?

Regarding this point, perhaps it would be possible to use an heuristic
like: "in case of conflict, the last defined type is chosen as the
most specific"?

I implemented this idea in gtype (attached) to compute the gtype
precedence list, by using a topologic sort with a tie-breaker to
decide which gtype will be the most specific between two candidates
that cannot be separated otherwise.  It should be easy to implement
different heuristics by providing different tie-breakers.

With your example, if you define `cons-cdr-foo' after `cons-car-foo':

(gtype-of '(foo . foo))
=> cons-cdr-foo

And, if you define `cons-car-foo' after `cons-cdr-foo':

(gtype-of '(foo . foo))
=> cons-car-foo

To avoid inconsistent method dispatching, the order of gtypes does not
change if you eval again the definition of an existing gtype.  But the
order will change if you first remove the gtype, then define it again.
  
>> Or am I missing something? --- Sorry, I only started studying
>> cl-generic relatively recently, and I probably don't understand
>> all its intricacies ;-)
> 
> Yeah, it's unsatisfactorily intricate, indeed.  It's designed first and
> foremost to keep the dispatch "simple and reasonable fast", at the cost
> of making `cl-generic-define-generalizer` a very sharp tool.  🙂
> 
> I have recently been thinking about how to make it more reliable (which
> would also make it more flexible/powerful, allowing the definition of
> both `and` and `or` specializers).  I have some vague idea, but there's
> no code at all yet, and it might come with some non-trivial tradeoffs
> (e.g. preloading the byte-compiler).

I can't wait to see this :-)

Thanks for your patience!