Defmethod on Arbitrary Type Specifiers?
Asked Answered
B

3

7

What I'm trying to do is this:

(defgeneric fn (x))

(defmethod fn ((x (integer 1 *)))
    "Positive integer")

(defmethod fn ((x (integer * -1)))
    "Negative integer")

I want a generic function that works with arbitrary type specifiers, including the list-based ones such as (and x y), (or x y), (satisfies p), etc. Now when I attempt to run the above code, I get an "Invalid Specializer" error. A little bit of research reveals that defgeneric is designed to work with CLOS, not with arbitrary type specifiers. Is there a defgeneric-like system in Common Lisp that would get me the behavior I want for arbitrary type specifiers, not just classes?

Bruch answered 23/11, 2014 at 3:57 Comment(8)
Zero is positive and negative?Prickly
@RainerJoswig Fixed but it doesn't really change the purpose of the question.Bruch
true, but it exposes a problem of such a feature. If both methods match: which one to use? The methods can't be ordered... For zero, both methods were matching. Which one should have been used?Prickly
@RainerJoswig true, but this isn't a big problem. For example last or first specified on can be executed.Enphytotic
@coredump that would be more or less random... Does not sound like a useful behavior. redefine something without a semantic change and it then behaves differently? oh oh.. There is nothing in CLOS which preserves order of definition time...Prickly
Worst case, the system could go the C++ route and simply give an error if the call is ambiguous. Granted it wouldn't be as pretty as the pure-CLOS version, I wouldn't say a type-based defgeneric is completely infeasible.Bruch
@RainerJoswig of course there is nothing in CLOS like that, and like OP wants eather as far as I know. But I think that the idea by itself not really bad one. Even if it'll be an undefined behavior in situations like this with zero. The programmer just must define unambiguous argument specifiers, if he didn't he'll suffer - it's fair.Enphytotic
@ Coredump: these consequences are everywhere. One would need an order over the whole type system. It would completely change the nature of the system. It has nothing to do with zero. Any two types which match an object would need to be comparable wrt specifity or the behaviour would be random.Prickly
S
8

Common Lisp defines two hierarchies which are related but not identical: the type hierarchy and the class hierarchy. Every class is a type, but the converse is not true — there are types that are not classes. For example, integer and string are classes, and therefore also types. On the other hand, (integer 1 *) and (satisfies evenp) are types, but not classes.

> (type-of "toto")
(SIMPLE-BASE-STRING 4)
> (class-of "toto")
#<BUILT-IN-CLASS STRING>

Parameter specialisers — the things that you put after parameters in defmethod — can only be class names (or of the form (eql value)). Since (integer 1 *) is not a class name, your code is not allowed by Common Lisp. There is an excellent reason for that: the compiler is always able to determine the class hierarchy, while the type language is way too powerful for that:

(defun satisfies-the-collatz-conjecture (n)
  (cond
    ((<= n 1) t)
    ((evenp n) (satisfies-the-collatz-conjecture (/ n 2)))
    (t (satisfies-the-collatz-conjecture (+ 1 (* n 3))))))

(subtypep 'integer '(satisfies satisfies-the-collatz-conjecture))
NIL ;
NIL

If you really need your code to be modular, you will need to first classify your values into something that can be made into a specialiser, and then dispatch on that:

(defmethod fn-generic (x (sign (eql 'positive)))
  "Positive integer")

(defmethod fn-generic (x (sign (eql 'negative)))
  "Negative integer")

(defun classify (x)
  (cond
    ((< x 0) 'negative)
    ((= x 0) 'null)
    ((> x 0) 'positive)))

(defun fn (x)
  (fn-generic x (classify x)))
Synthiasyntonic answered 23/11, 2014 at 13:17 Comment(0)
P
9

There is nothing like CLOS which would provide such a feature.

It actually does not fit well into CLOS either. Think about the following, we have the following call of a generic function:

(generic-function-foo 2)

Now we have methods defined for the following types:

(integer 0 9)
(integer 1 9)
(integer 0 99)
(integer 1 99)
(integer -1000 1000)
(or (satisfies evenp) (integer 0 30))
(satisfies evenp)
(satisfies divisible-by-two)
(satisfies all-numbers-which-are-in-my-list-of-numbers)

Which of the methods which all match for 2 should run? If I call CALL-NEXT-METHOD, which one would be the next?

Now we can say order them by appearance in the source. But in Common Lisp you can add, remove or redefine methods at runtime. The behavior would be more or less random.

We would need some other conflict resolution scheme. For example:

  • manually declaring the precedence
  • some kind of priority value
  • runtime errors with the opportunity for the user to select one
  • a type language, which provides an order
  • giving up order all together

There have been attempts to provide more expressive dispatch to CLOS. I'm not aware of adding types to CLOS, though. See predicate dispatch and filtered dispatch.

Other than that I would look for a rule-based system, but that usually is very different from CLOS, the Common Lisp Object System, unless it is in some way integrated.

Prickly answered 23/11, 2014 at 9:14 Comment(0)
S
8

Common Lisp defines two hierarchies which are related but not identical: the type hierarchy and the class hierarchy. Every class is a type, but the converse is not true — there are types that are not classes. For example, integer and string are classes, and therefore also types. On the other hand, (integer 1 *) and (satisfies evenp) are types, but not classes.

> (type-of "toto")
(SIMPLE-BASE-STRING 4)
> (class-of "toto")
#<BUILT-IN-CLASS STRING>

Parameter specialisers — the things that you put after parameters in defmethod — can only be class names (or of the form (eql value)). Since (integer 1 *) is not a class name, your code is not allowed by Common Lisp. There is an excellent reason for that: the compiler is always able to determine the class hierarchy, while the type language is way too powerful for that:

(defun satisfies-the-collatz-conjecture (n)
  (cond
    ((<= n 1) t)
    ((evenp n) (satisfies-the-collatz-conjecture (/ n 2)))
    (t (satisfies-the-collatz-conjecture (+ 1 (* n 3))))))

(subtypep 'integer '(satisfies satisfies-the-collatz-conjecture))
NIL ;
NIL

If you really need your code to be modular, you will need to first classify your values into something that can be made into a specialiser, and then dispatch on that:

(defmethod fn-generic (x (sign (eql 'positive)))
  "Positive integer")

(defmethod fn-generic (x (sign (eql 'negative)))
  "Negative integer")

(defun classify (x)
  (cond
    ((< x 0) 'negative)
    ((= x 0) 'null)
    ((> x 0) 'positive)))

(defun fn (x)
  (fn-generic x (classify x)))
Synthiasyntonic answered 23/11, 2014 at 13:17 Comment(0)
M
2

What you actually seem to be looking for is pattern matching, like in ML or Erlang. That is a rather different concept from dispatch, although they have similar purposes.

One popular pattern matching library for Common Lisp is optima (available from Quicklisp).

Microwave answered 23/11, 2014 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.