From: Tamas K Papp on
Sometimes I need a lot of symbols from another package which are not
exported. Eg if I have unit tests in a separate package, but I want
to test internal functions in a library, I am forced to either import
the symbols, or write

(addtest (optimization-tests)
minmax-tests
(ensure-same (cl-random::minmax 1 2 3) 2)
(ensure-same (cl-random::minmax 2.2 2 3) 2.2)
(ensure-same (cl-random::minmax 4 2 3) 3))

I thought that it would be great if I could import symbols "locally",
but AFAIK CL offers no such facility. But it turns out that it is not
needed in this case. I came up with the following macro to get what I want:

(defmacro use-locally (package-symbols-list &body body)
`(progn
,@(sublis (iter
(for (package . symbols) :in package-symbols-list)
(dolist (symbol symbols)
(collecting (cons symbol
(find-symbol (symbol-name symbol) package)))))
body :test #'eq)))

so now I can write

(addtest (optimization-tests)
minmax-tests
(use-locally ((cl-random minmax))
(ensure-same (minmax 1 2 3) 2)
(ensure-same (minmax 2.2 2 3) 2.2)
(ensure-same (minmax 4 2 3) 3)))

Is there a better way?

Thanks,

Tamas
From: Helmut Eller on
* Tamas K Papp [2010-06-24 12:41] writes:

> (addtest (optimization-tests)
> minmax-tests
> (use-locally ((cl-random minmax))
> (ensure-same (minmax 1 2 3) 2)
> (ensure-same (minmax 2.2 2 3) 2.2)
> (ensure-same (minmax 4 2 3) 3)))
>
> Is there a better way?

(addtest (optimization-tests)
minmax-tests
(macrolet ((minmax (&rest stuff) `(cl-random:minmax . ,stuff)))
(ensure-same (minmax 1 2 3) 2)
(ensure-same (minmax 2.2 2 3) 2.2)
(ensure-same (minmax 4 2 3) 3)))

Helmut
From: Captain Obvious on
TKP> I thought that it would be great if I could import symbols "locally",
TKP> but AFAIK CL offers no such facility. But it turns out that it is not
TKP> needed in this case. I came up with the following macro to get what I
TKP> want:

TKP> (defmacro use-locally (package-symbols-list &body body)
TKP> `(progn
TKP> ,@(sublis (iter
TKP> (for (package . symbols) :in package-symbols-list)
TKP> (dolist (symbol symbols)
TKP> (collecting (cons symbol
TKP> (find-symbol (symbol-name symbol) package)))))
TKP> body :test #'eq)))

I think it is more like a shadowing-import, because it completely discards
already accessible symbols.

TKP> (addtest (optimization-tests)
TKP> minmax-tests
TKP> (use-locally ((cl-random minmax))
TKP> (ensure-same (minmax 1 2 3) 2)
TKP> (ensure-same (minmax 2.2 2 3) 2.2)
TKP> (ensure-same (minmax 4 2 3) 3)))

TKP> Is there a better way?

It depends on what is better for you.
I think your macro might be optimal as long as we don't care about shadowing
and we want to mention symbols explicitly one by one.

If you do not like explicit mentioning part, I think it is possible to
devise some devious macro that will "import" (via sublis) everything
automatically according to some rules.
Kind of do-what-I-mean style.
Or that can be a reader macro.

From: Pascal J. Bourguignon on
Tamas K Papp <tkpapp(a)gmail.com> writes:

> Sometimes I need a lot of symbols from another package which are not
> exported. Eg if I have unit tests in a separate package, but I want
> to test internal functions in a library, I am forced to either import
> the symbols, or write
>
> (addtest (optimization-tests)
> minmax-tests
> (ensure-same (cl-random::minmax 1 2 3) 2)
> (ensure-same (cl-random::minmax 2.2 2 3) 2.2)
> (ensure-same (cl-random::minmax 4 2 3) 3))
>
> I thought that it would be great if I could import symbols "locally",
> but AFAIK CL offers no such facility.

However, it is easy enough to implement. With a reader macro.

{LOCALLY-IMPORTING-FROM ((P1 (S11 S12 ...))
(P2 (S21 S22 ...))
...)
BODY...}

The reader macro would then read the BODY by reading S11 as
P1::S11, etc.

Notice that you will want to read the symbols character by character to
be able to distinguish P0::S11 or :S11 from S11 that should be read as P1::S11
(assuming *PACKAGE* bound to P0).


> But it turns out that it is not
> needed in this case.

--
__Pascal Bourguignon__
http://www.informatimago.com
From: Pascal J. Bourguignon on
pjb(a)informatimago.com (Pascal J. Bourguignon) writes:

> Tamas K Papp <tkpapp(a)gmail.com> writes:
>
>> Sometimes I need a lot of symbols from another package which are not
>> exported. Eg if I have unit tests in a separate package, but I want
>> to test internal functions in a library, I am forced to either import
>> the symbols, or write
>>
>> (addtest (optimization-tests)
>> minmax-tests
>> (ensure-same (cl-random::minmax 1 2 3) 2)
>> (ensure-same (cl-random::minmax 2.2 2 3) 2.2)
>> (ensure-same (cl-random::minmax 4 2 3) 3))
>>
>> I thought that it would be great if I could import symbols "locally",
>> but AFAIK CL offers no such facility.
>
> However, it is easy enough to implement. With a reader macro.
>
> {LOCALLY-IMPORTING-FROM ((P1 (S11 S12 ...))
> (P2 (S21 S22 ...))
> ...)
> BODY...}
>
> The reader macro would then read the BODY by reading S11 as
> P1::S11, etc.
>
> Notice that you will want to read the symbols character by character to
> be able to distinguish P0::S11 or :S11 from S11 that should be read as P1::S11
> (assuming *PACKAGE* bound to P0).


Like this, for example.

To make it even easier, I would need to design an API for parsing
tokens that would allow to modify the parsing of each kind of token
more modularly. Here I shamelessly copy-and-paste-and-patched code
from my reader.


;;;------------------------------------------------------------------------

(cl:in-package :cl-user )
(asdf-load :com.informatimago.common-lisp)

(defpackage "BRACE-READER-MACRO"
(:NICKNAMES "BRM")
(:use "CL")
(:EXPORT "BRACE-READER-MACRO"
"LOCALLY-IMPORTING-FROM"))
(in-package "BRACE-READER-MACRO")

(defun brace-reader-macro (stream ch)
(declare (ignore ch))
(let ((brace-macro (read stream)))
(cond
((string= brace-macro 'locally-importing-from)
(locally-importing-from stream))
(t
(error "Unknonw brace macro: ~A" brace-macro)))))


(reader::defparser parse-symbol-token-as-string (token)
(let ((colon (position-if
(lambda (traits) (reader::traitp reader::+ct-package-marker+ traits))
(reader::token-traits token))))
(if colon
(let* ((double-colon (and (< (1+ colon) (reader::token-length token))
(reader::traitp reader::+ct-package-marker+
(reader::token-char-traits token (1+ colon)))))
(pname (subseq (reader::token-text token) 0 colon))
(sname (subseq (reader::token-text token)
(+ colon (if double-colon 2 1)))))
(when (position-if
(lambda (traits) (reader::traitp reader::+ct-package-marker+ traits))
(reader::token-traits token) :start (+ colon (if double-colon 2 1)))
(reader::reject t "Too many package markers in token ~S" (reader::token-text token)))
(if (find-package pname)
(multiple-value-bind (sym where) (find-symbol sname pname)
(declare (ignore sym))
(if where
(reader::accept 'string pname)
(reader::reject t "There is no symbol named ~S in ~
the package named ~S" sname pname)))
(reader::reject t "There is no package with name ~S" pname)))
;; no colon in token, let's just return the symbol name:
(reader::accept 'string (reader::token-text token)))))


(defun parse-token/symbol-as-string (token)
"
RETURN: okp ; the parsed lisp object if okp, or an error message if (not okp)
"
(let ((message nil))
(macrolet
((rom (&body body)
"Result Or Message"
(if (null body)
'nil
(let ((vals (gensym)))
`(let ((,vals (multiple-value-list ,(car body))))
(if (first ,vals)
(values-list ,vals)
(progn
(when (second ,vals)
(setf message (third ,vals)))
(rom ,@(cdr body)))))))))
(multiple-value-bind (ok type object)
(rom (READER::parse-decimal-integer-token token)
(READER::parse-integer-token token)
(READER::parse-ratio-token token)
(READER::parse-float-1-token token)
(READER::parse-float-2-token token)
(parse-symbol-token-as-string token))
(declare (ignorable type))
(values ok (if ok object message))))))



(defvar *imports* '())

(reader::defparser parse-symbol-token (token)
"symbol ::= symbol-name
symbol ::= package-marker symbol-name
symbol ::= package-marker package-marker symbol-name
symbol ::= package-name package-marker symbol-name
symbol ::= package-name package-marker package-marker symbol-name
symbol-name ::= {alphabetic}+
package-name ::= {alphabetic}+ "
(let ((colon (position-if
(lambda (traits) (reader::traitp reader::+ct-package-marker+ traits))
(reader::token-traits token))))
(if colon
;; With a colon, we leave it alone.
(let* ((double-colon (and (< (1+ colon) (reader::token-length token))
(reader::traitp reader::+ct-package-marker+
(reader::token-char-traits token (1+ colon)))))
(pname (subseq (reader::token-text token) 0 colon))
(sname (subseq (reader::token-text token)
(+ colon (if double-colon 2 1)))))
(when (position-if
(lambda (traits) (reader::traitp reader::+ct-package-marker+ traits))
(reader::token-traits token) :start (+ colon (if double-colon 2 1)))
(reader::reject t "Too many package markers in token ~S" (reader::token-text token)))
(when (zerop colon)
;; Keywords always exist, so let's intern them before finding them.
(setf pname "KEYWORD")
(intern sname pname))
;; The following form thanks to Andrew Philpot <philpot(a)ISI.EDU>
;; corrects a bug when reading with double-colon uninterned symbols:
(if (find-package pname)
(if double-colon
(reader::accept 'symbol (intern sname pname))
(multiple-value-bind (sym where) (find-symbol sname pname)
(if (eq where :external)
(reader::accept 'symbol sym)
(reader::reject t "There is no external symbol named ~S in ~
the package named ~S" sname pname))))
(reader::reject t "There is no package with name ~S" pname)))
;; no colon in token, let's see if it's an import.
(loop
:with sname = (reader::token-text token)
:for (package symbols) :in *imports*
:do (when (member sname symbols :test (function string=))
(reader::accept 'symbol (intern sname package)))
:finally (reader::accept 'symbol (intern sname *package*))))))


(defun parse-token/importing-symbols-from (token)
"
RETURN: okp ; the parsed lisp object if okp, or an error message if (not okp)
"
(let ((message nil))
(macrolet
((rom (&body body)
"Result Or Message"
(if (null body)
'nil
(let ((vals (gensym)))
`(let ((,vals (multiple-value-list ,(car body))))
(if (first ,vals)
(values-list ,vals)
(progn
(when (second ,vals)
(setf message (third ,vals)))
(rom ,@(cdr body)))))))))
(multiple-value-bind (ok type object)
(rom (READER::parse-decimal-integer-token token)
(READER::parse-integer-token token)
(READER::parse-ratio-token token)
(READER::parse-float-1-token token)
(READER::parse-float-2-token token)
(parse-symbol-token token))
(declare (ignorable type))
(values ok (if ok object message))))))




(defun locally-importing-from (stream)
(let ((READER:*readtable* (READER:copy-readtable nil)))
(setf (READER:readtable-parse-token READER:*readtable*)
(function parse-token/symbol-as-string)) ; to read the imports
(let ((*imports* (READER:READ stream)))
(setf (READER:readtable-parse-token READER:*readtable*)
(function parse-token/importing-symbols-from)) ; to read the body
`(progn ,@(READER:READ-DELIMITED-LIST #\} stream)))))




(in-package :cl-user)
(set-macro-character #\{ (function brm:brace-reader-macro))

;;;------------------------------------------------------------------------

CL-USER> {LOCALLY-IMPORTING-FROM ((P1 (S11 S12))
(P2 (S21 S22)))
'(:s11 s11 cl-user::s12 s12 s21 s22 )}
(:S11 P1::S11 S12 P1::S12 P2::S21 P2::S22)


--
__Pascal Bourguignon__ http://www.informatimago.com/