common-lisp
매크로
수색…
비고
매크로의 목적
매크로는 코드 생성, 코드 변환 및 새로운 표기법을 제공하기위한 것입니다. 이러한 새로운 표기법은 예를 들어 도메인 수준의 구조 또는 완전히 새로운 임베디드 언어를 제공하는 등 프로그램을 더 잘 표현하는 데 더 적합 할 수 있습니다.
매크로는 소스 코드를 더 자명하게 만들 수 있지만 디버깅을 더 어렵게 만들 수 있습니다. 일반적인 규칙에 따라 매크로를 사용해서는 안됩니다. 당신이 그들을 사용할 때, 일반적인 함정을 피하고, 일반적으로 사용되는 패턴과 명명 규칙을 따르십시오.
매크로 확장 주문
함수와 비교하여 매크로는 역순으로 확장됩니다. 가장 먼저, 가장 마지막에. 즉, 기본적으로 내부 매크로를 사용하여 외부 매크로에 필요한 구문을 생성 할 수 없습니다.
평가 주문
때때로 매크로는 사용자가 제공 한 양식을 이동해야합니다. 하나는 그들이 평가되는 순서를 변경하지 않도록해야합니다. 사용자는 순서대로 발생하는 부작용에 의존하고있을 수 있습니다.
한 번만 평가
매크로를 확장하면 종종 동일한 사용자 제공 양식의 값을 두 번 이상 사용해야합니다. 양식에 부작용이있을 수도 있고 값 비싼 기능을 호출 할 수도 있습니다. 따라서 매크로는 이러한 양식을 한 번만 평가해야합니다. 일반적으로이 값은 로컬 변수 ( GENSYM
ed라는 이름)에 값을 할당하여 수행됩니다.
EVAL-WHEN을 사용하여 매크로가 사용하는 함수
복잡한 매크로는 종종 로직의 일부가 별도의 기능으로 구현됩니다. 그러나 실제 코드가 컴파일되기 전에 매크로가 확장된다는 것을 기억해야합니다. 파일을 컴파일 할 때 기본적으로 동일한 파일에 정의 된 함수와 변수는 매크로를 실행하는 동안 사용할 수 없습니다. 매크로가 사용하는 동일한 파일의 모든 함수 및 변수 정의는 EVAL-WHEN
-form 형식으로 묶어야합니다. 로드 및 런타임 중에 동봉 된 코드도 평가해야하는 경우 EVAL-WHEN
은 세 번 모두 지정해야합니다.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
이것은 매크로의 확장에서 호출 된 함수에는 적용되지 않으며 매크로 자체에서 호출 된 함수에만 적용됩니다.
일반적인 매크로 패턴
TODO : 설명을 비고로 옮기고 예제를 별도로 추가 할 수 있습니다.
푸프
Common Lisp에는 일반화 된 참조 개념이 있습니다. 그들은 프로그래머가 변수 인 것처럼 다양한 "장소"에 값을 설정할 수 있습니다. 이 기능을 사용하는 매크로에는 이름에 F
자리 표시자가있을 수 있습니다. 일반적으로 매크로의 첫 번째 인수입니다.
표준의 예 : INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
어리석은 예제, 장소에 숫자 저장소의 부호를 뒤집는 매크로.
(defmacro flipf (place)
`(setf ,place (- ,place)))
WITH FOO
자원을 획득하고 안전하게 릴리스하는 매크로는 대개 WITH-
-prefix로 명명됩니다. 매크로는 일반적으로 다음과 같은 구문을 사용해야합니다.
(with-foo (variable details-of-the-foo...)
body...)
표준으로부터의 예 : WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
, WITH-OUTPUT-TO-STRING
.
이름 오염과 의도하지 않은 다중 평가의 함정을 피할 수있는 이런 유형의 매크로를 구현하는 한 가지 접근법은 먼저 기능 버전을 구현하는 것입니다. 예를 들어 with-widget
을 안전하게 생성하고 이후에 정리하는 with-widget
매크로를 구현하는 첫 번째 단계는 함수 일 수 있습니다.
(defun call-with-widget (args function)
(let ((widget (apply #'make-widget args))) ; obtain WIDGET
(unwind-protect (funcall function widget) ; call FUNCTION with WIDGET
(cleanup widget) ; cleanup
이것은 함수이기 때문에 함수 또는 공급자 내의 이름 범위에 대한 우려는 없으며 해당 매크로를 쉽게 작성할 수 있습니다.
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
할 ~ 푸
무언가를 반복하는 매크로는 흔히 DO
prefix로 명명됩니다. 매크로 구문은 일반적으로 형식이어야합니다.
(do-foo (variable the-foo-being-done return-value)
body...)
표준의 예 : DOTIMES
, DOLIST
, DO-SYMBOLS
.
FOOCASE, EFOOCASE, CFOOCASE
특정 경우에 대한 입력과 일치하는 매크로는 종종 CASE
postfix로 명명됩니다. E...CASE
는 종종 있습니다 E...CASE
입력이 어떤 경우에도 일치하지 않으면 오류를 표시하는 변수, C...CASE
는 연속 오류를 신호합니다. 그들은 같은 구문을 가져야한다.
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
표준의 CASE
: CASE
, TYPECASE
, HANDLER-CASE
.
예를 들어 문자열을 정규 표현식과 비교하고 레지스터 그룹을 변수에 바인드하는 매크로입니다. 정규 표현식에 CL-PPCRE 를 사용합니다.
(defmacro regexcase (input &body cases)
(let ((block-sym (gensym "block"))
(input-sym (gensym "input")))
`(let ((,input-sym ,input))
(block ,block-sym
,@(loop for (regex vars . body) in cases
if (eql regex 'otherwise)
collect `(return-from ,block-sym (progn ,vars ,@body))
else
collect `(cl-ppcre:register-groups-bind ,vars
(,regex ,input-sym)
(return-from ,block-sym
(progn ,@body))))))))
(defun test (input)
(regexcase input
("(\\d+)-(\\d+)" (foo bar)
(format t "Foo: ~a, Bar: ~a~%" foo bar))
("Foo: (\\w+)$" (foo)
(format t "Foo: ~a.~%" foo))
(otherwise (format t "Didn't match.~%"))))
(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.
DEFINE-FOO, DEFFOO
사물을 정의하는 매크로는 보통 DEFINE-
또는 DEF
-prefix로 명명됩니다.
표준의 예 : DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
애너픽 매크로
Anaphoric 매크로 는 사용자 제공 양식의 결과를 캡처하는 변수 (종종 IT
)를 소개하는 매크로입니다. 일반적인 예는 정규 IF
와 유사한 Anaphoric If이지만 테스트 형식의 결과를 나타 내기 위해 변수 IT
를 정의합니다.
(defmacro aif (test-form then-form &optional else-form)
`(let ((it ,test-form))
(if it ,then-form ,else-form)))
(defun test (property plist)
(aif (getf plist property)
(format t "The value of ~s is ~a.~%" property it)
(format t "~s wasn't in ~s!~%" property plist)))
(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!
MACROEXPAND
매크로 확장은 매크로를 실제 코드로 바꾸는 과정입니다. 이것은 대개 컴파일 프로세스의 일부로 발생합니다. 컴파일러는 실제로 코드를 컴파일하기 전에 모든 매크로 폼을 확장합니다. 매크로 확장은 Lisp 코드의 해석 중에도 발생합니다.
매크로 폼이 무엇으로 확장되는지 보려면 수동으로 MACROEXPAND
를 호출 할 수 있습니다.
CL-USER> (macroexpand '(with-open-file (file "foo")
(do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
(UNWIND-PROTECT
(MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
(WHEN FILE (CLOSE FILE :ABORT #:G725))))
MACROEXPAND-1
은 동일하지만 한 번만 확장됩니다. 이것은 다른 매크로 폼으로 확장되는 매크로 폼을 이해하려고 할 때 유용합니다.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
MACROEXPAND
나 MACROEXPAND-1
은 모든 레벨에서 Lisp 코드를 확장하지 않는다. 최상위 매크로 폼 만 확장합니다. 모든 레벨에서 폼을 매크로 확장하려면 코드 보행기 가 필요합니다. 이 기능은 Common Lisp 표준에서는 제공되지 않습니다.
역 인용 부호 - 매크로 용 코드 템플릿 작성
매크로는 코드를 반환합니다. Lisp의 코드는리스트로 구성되어 있기 때문에 일반리스트 조작 함수를 사용하여이를 생성 할 수있다.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
이것은 종종 매우 긴 매크로에서 읽기가 매우 어렵습니다. Backquote reader 매크로를 사용하면 요소를 선택적으로 평가하여 채워진 인용 템플릿을 작성할 수 있습니다.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
이 버전은 일반 코드와 거의 유사합니다. 쉼표는 FORM
을 평가하는 데 사용됩니다. 그 외 모든 것은 그대로 반환됩니다. ',form
의 작은 따옴표는 쉼표 바깥에 있으므로 리턴됩니다.
또한 ,@
를 사용하여 해당 위치의 목록을 이어 붙일 수 있습니다.
(defmacro echo (&rest forms)
`(progn
,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
,@forms))
(macroexpand '(echo (+ 3 4)
(print "foo")
(random 10)))
;=> (PROGN
; (FORMAT T "Form: ~a~%" (+ 3 4))
; (FORMAT T "Form: ~a~%" (PRINT "foo"))
; (FORMAT T "Form: ~a~%" (RANDOM 10))
; (+ 3 4)
; (PRINT "foo")
; (RANDOM 10))
Backquote는 외부 매크로에서도 사용할 수 있습니다.
매크로에서 이름 충돌을 방지하는 고유 한 기호
매크로를 확장하면 종종 사용자가 인수로 전달하지 않은 기호 (예 : 로컬 변수의 이름)를 사용해야합니다. 이러한 기호가 주변 코드에서 사용중인 기호와 충돌 할 수 없도록해야합니다.
이것은 일반적으로 GENSYM
을 사용하여 수행됩니다. GENSYM은 새로운 기호가없는 기호를 반환합니다.
나쁜
아래의 매크로를 고려하십시오. DOTIMES
-loop은 몸체의 결과를 목록으로 모으고, 마지막에 반환됩니다.
(defmacro dotimes+collect ((var count) &body body)
`(let ((result (list)))
(dotimes (,var ,count (nreverse result))
(push (progn ,@body) result))))
(dotimes+collect (i 5)
(format t "~a~%" i)
(* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)
이것은이 경우에 효과가있는 것으로 보입니다. 그러나 사용자가 본문에서 사용하는 RESULT
라는 변수 이름을 사용하면 결과가 사용자가 기대하는 바가 아닐 수도 있습니다. 최대 N
까지의 모든 정수의 합계 목록을 수집하는 함수를 작성하려는 시도를 생각해보십시오.
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
좋은
문제를 해결하려면 GENSYM
을 사용하여 매크로 확장에서 RESULT
GENSYM
의 고유 한 이름을 생성해야합니다.
(defmacro dotimes+collect ((var count) &body body)
(let ((result-symbol (gensym "RESULT")))
`(let ((,result-symbol (list)))
(dotimes (,var ,count (nreverse ,result-symbol))
(push (progn ,@body) ,result-symbol)))))
(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)
TODO : 문자열에서 기호를 만드는 방법
TODO : 다른 패키지의 기호 문제 해결
if-let, when-let, 매크로
이러한 매크로는 제어 흐름과 바인딩을 병합합니다. 그들은 개발자가 명명을 통해 의미를 전달할 수 있기 때문에 애매한 무언가 매크로보다 개선 된 기능입니다. 이와 같이 그들의 용도는 그들의 조상 대응 물보다 권장된다.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
매크로는 하나 이상의 변수를 바인딩 한 다음 해당 변수 ( IF
, WHEN
)의 테스트 양식으로 사용합니다. 여러 변수가 AND
결합됩니다. 선택된 브랜치는 바인딩이 유효한 상태로 실행됩니다. IF-LET
의 간단한 변수 구현은 다음과 같이 보일 수 있습니다.
(defmacro if-let ((var test-form) then-form &optional else-form)
`(let ((,var ,test-form))
(if ,var ,then-form ,else-form)))
(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
(format t "A: ~a~%" a)
(format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
; (IF A
; (FORMAT T "A: ~a~%" A)
; (FORMAT T "Not found.~%")))
여러 변수를 지원하는 버전은 Alexandria 라이브러리에서 사용할 수 있습니다.
매크로를 사용하여 데이터 구조 정의
매크로의 일반적인 사용은 일반적인 규칙을 따르지만 다른 필드를 포함 할 수있는 데이터 구조에 대한 템플릿을 만드는 것입니다. 매크로를 작성함으로써, 보일러 플레이트 코드를 반복하지 않고도 데이터 구조의 세부 구성을 지정할 수 있으며 프로그래밍에서 단순화하기 위해 메모리에 덜 효율적인 구조 (예 : 해시)를 사용하지 않아도됩니다.
예를 들어, getter와 setter를 사용하여 다양한 속성의 범위를 가진 여러 클래스를 정의하고자한다고 가정합니다. 또한 일부 속성 (전부는 아님)에 대해 setter가 속성을 변경했음을 알리는 객체에 대한 메소드를 호출하도록하고 싶습니다. Common LISP는 이미 getter와 setter를 작성하는 약식을 가지고 있지만, 표준 사용자 정의 setter를 작성하는 것은 일반적으로 모든 setter에서 통지 메소드를 호출하는 코드를 복제해야하므로 많은 수의 속성이 관련되어있는 경우 문제가 될 수 있습니다 . 그러나 매크로를 정의하면 훨씬 쉽게 될 수 있습니다.
(defmacro notifier (class slot)
"Defines a setf method in (class) for (slot) which calls the object's changed method."
`(defmethod (setf ,slot) (val (item ,class))
(setf (slot-value item ',slot) val)
(changed item ',slot)))
(defmacro notifiers (class slots)
"Defines setf methods in (class) for all of (slots) which call the object's changed method."
`(progn
,@(loop for s in slots collecting `(notifier ,class ,s))))
(defmacro defclass-notifier-slots (class nslots slots)
"Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
`(progn
(defclass ,class ()
( ,@(loop for s in nslots collecting `(,s :reader ,s))
,@(loop for s in slots collecting `(,s :accessor ,s))))
(notifiers ,class ,nslots)))
우리는 이제 (defclass-notifier-slots foo (bar baz qux) (waldo))
작성하고 정규 슬롯 waldo
(스펙을 가진 매크로의 두 번째 부분에 의해 생성 된 클래스 (defclass-notifier-slots foo (bar baz qux) (waldo))
(waldo :accessor waldo)
로 클래스 foo
를 즉시 정의 할 수있다) 및 슬롯 bar
, baz
및 qux
부르는 세터와 changed
(게터는 매크로의 첫 부분에 의해 정의되는 방식 (bar :reader bar)
및 호출하여 세터 notifier
매크로).
이런 방식으로 동작하는 여러 클래스를 빠르게 정의 할 수있을뿐 아니라 반복하지 않고 많은 수의 속성을 사용하여 코드 재사용의 이점을 얻을 수 있습니다. 나중에 알리미 메서드의 작동 방식을 변경하면 간단히 매크로를 사용하고 그것을 사용하는 모든 클래스의 구조가 변경됩니다.