1

Suppose we have two packages, each defines a class and exports symbols for slots/generic methods with identical names.

(defpackage pkg1 (:export _class1 _slot _reader _method))
(in-package pkg1)
(defclass _class1 () ((_slot :initform "SLOT111" :initarg :slot :reader _reader)))
(defmethod _method ((self _class1)) (format t "SLOT-: ~a~%" (_reader self)))

(defpackage pkg2 (:export _class2 _slot _reader _method))
(in-package pkg2)
(defclass _class2 () ((_slot :initform "SLOT222" :initarg :slot :reader _reader)))
(defmethod _method ((self _class2)) (format t "SLOT=: ~a~%" (_reader self)))

How do we import those symbols in some third package, successfully merging (not shadowing) generics?

(defpackage test)
(in-package test)
... ; here we somehow import symbols _slot, _reader and _method
    ; from both packages, so they get merged (like in 'GNU Guile' or 'Gauche')
(defvar v1 (make-instance '_class1))
(defvar v2 (make-instance '_class2))
(_reader v1) (_method v1) ; both must work
(_reader v2) (_method v2) ; and these too
AlexDarkVoid
  • 485
  • 3
  • 12

2 Answers2

2

I'm really a noob when it comes to CLOS so I did the same experiment last year. My findings is that CL doesn't really export methods or merge methods. It exports symbols, that might have bindings. Thus you need to make a package with the symbols that they should share and perhaps put the documentation there:

;; common symbols and documantation
(defpackage interface (:export _slot _reader _method))
(in-package interface)
(defgeneric _method (self)
  (:documentation "This does this functionality"))
(defgeneric _reader (self)
  (:documentation "This does that functionality"))

(defpackage pkg1 (:use :cl :interface) (:export _class1 _slot _reader _method))
(in-package pkg1)
(defclass _class1 () ((_slot :initform "SLOT111" :initarg :slot :reader _reader)))
(defmethod _method ((self _class1)) (format t "SLOT-: ~a~%" (_reader self)))

(defpackage pkg2 (:use :cl :interface) (:export _class2 _slot _reader _method))
(in-package pkg2)
(defclass _class2 () ((_slot :initform "SLOT222" :initarg :slot :reader _reader)))
(defmethod _method ((self _class2)) (format t "SLOT=: ~a~%" (_reader self)))

(defpackage test (:use :cl :pkg1 :pkg2))
(in-package test)
(defvar v1 (make-instance '_class1))
(defvar v2 (make-instance '_class2))
(_reader v1) ; ==> "SLOT111"
(_method v1) ; ==> nil (outputs "SLOT-: SLOT111")
(_reader v2) ; ==> "SLOT222"
(_method v2) ; ==> nil (outputs "SLOT-: SLOT222")

You can from test check out what has happened:

(describe '_method) 

_METHOD is the symbol _METHOD, lies in #<PACKAGE INTERFACE>, is accessible in 
4 packages INTERFACE, PKG1, PKG2, TEST, names a function.
Documentation as a FUNCTION:
This does this functionality

 #<PACKAGE INTERFACE> is the package named INTERFACE.
 It imports the external symbols of 1 package COMMON-LISP and 
 exports 3 symbols to 2 packages PKG2, PKG1.

 #<STANDARD-GENERIC-FUNCTION _METHOD> is a generic function.
 Argument list: (INTERFACE::SELF)
 Methods:
 (_CLASS2)
 (_CLASS1)

(describe '_reader) 

_READER is the symbol _READER, lies in #<PACKAGE INTERFACE>, is accessible in 
 4 packages INTERFACE, PKG1, PKG2, TEST, names a function.
Documentation as a FUNCTION:
This does that functionality

 #<PACKAGE INTERFACE> is the package named INTERFACE.
 It imports the external symbols of 1 package COMMON-LISP and 
 exports 3 symbols to 2 packages PKG2, PKG1.

 #<STANDARD-GENERIC-FUNCTION _READER> is a generic function.
 Argument list: (INTERFACE::SELF)
 Methods:
 (_CLASS2)
 (_CLASS1)

This has the side effect that importing pkg1 _method will work on pkg2 instances should you get such instance from a package that uses pkg2.

Now there is an elephant in this room. Why not define a base class in interface and add it as the parent class of both _class1 and _class2. You can easily do that with just a few changes, however that wasn't what you asked for.

Sylwester
  • 47,942
  • 4
  • 47
  • 79
  • 2
    Defining a protocol (/interface) like this makes sense if the methods are conceptually the same operation for different classes (as a heuristic, if one docstring can usefully describe both methods). If the methods are entirely different operations, it would be better to just use a package qualified name, or prefix the method names with suitable (different) protocol names. – jkiiski Mar 02 '17 at 13:44
  • Thanks, but even if this solution works, it is completely wrong from a _design_ standpoint. What if both packages **_pkg1** and **_pkg2** are written and maintained by different developers? So in order to work both should be aware of each other? Why **_class1** and **_class2** must share some common ancestor, if they are totally unrelated? – AlexDarkVoid Mar 02 '17 at 18:26
  • @AlexDarkVoid I agree. If I were to design a lisp OOP system the import would merge dispatchers with the same imported name iff their signatures are compatible and the join will affect the libraries they were imported from so that both supports all types the library they are imported in support. It gets messy unless you have a compiled language though. – Sylwester Mar 02 '17 at 19:47
  • I think this problem can be solved by writing a custom macro that defines a generic in the current package and fills it with methods from desired packages using `add-method` like this: `(in-package test) (merging-import _method _pkg1:_method _pkg2:_method) (merging-import _reader _pkg1:_reader _pkg2:_reader)`. But there is still a question: how to get a list of methods from a generic function? – AlexDarkVoid Mar 03 '17 at 07:45
0

After trying to solve this task via MOP I came up with a much simplier workaround:

(defmacro wrapping-import
          (sym-name &rest sym-list)
  `(defmethod ,sym-name
              (&rest args)
     (loop for sym in '(,@sym-list) do
           (let ((gf (symbol-function sym)))
             (if (compute-applicable-methods gf args)
               (return (apply gf args)))))
     (error "No applicable method found in ~A" ',sym-name)))

Example:

(defpackage p1 (:export say-type))
(in-package p1)
(defmethod say-type ((v integer)) "int")

(defpackage p2 (:export say-type))
(in-package p2)
(defmethod say-type ((v string)) "str")

(in-package cl-user)
(wrapping-import say-type p1:say-type p2:say-type)

(say-type "") ; -> "str"
(say-type 1) ; -> "int"

Also, here's the original solution:

(defmacro merging-import
          (sym-name &rest sym-list)
  (let ((gf-args (clos:generic-function-lambda-list
                  (symbol-function (first sym-list)))))
    `(progn
       (defgeneric ,sym-name ,gf-args)
       (loop for sym in '(,@sym-list) do
             (loop for meth
                   in (clos:generic-function-methods (symbol-function sym))
                   do
                   (add-method #',sym-name
                               (make-instance 'clos:standard-method
                                              :lambda-list  (clos:method-lambda-list  meth)
                                              :specializers (clos:method-specializers meth)
                                              :function     (clos:method-function     meth)))))))) 

Note that wrapping-import works even when signatures of generic functions don't match, while merging-import requires their lambda-lists to be equal.
Now I wonder: why we have to invent such things in 2017? Why those aren't in the standard yet?

And just in case someone needs it - a macro, which works like from pkg import * in Python:

(defmacro use-all-from
          (&rest pkg-list)
  `(loop for pkg-name in '(,@pkg-list) do
         (do-external-symbols
          (sym (find-package pkg-name))
          (shadowing-import (read-from-string (format nil "~a:~a"
                                                      pkg-name sym))))))
AlexDarkVoid
  • 485
  • 3
  • 12