매크로 시스템을보다 쉽게 ​​구현할 수있는 LISP는 무엇입니까?


21

SICP 에서 Scheme을 배우고 있으며 Scheme을 만드는 많은 부분이 LISP를 특별하게 만드는 것이 매크로 시스템이라는 인상을 받고 있습니다. 그러나 매크로는 컴파일 타임에 확장되기 때문에 사람들이 C / Python / Java /에 대해 동등한 매크로 시스템을 만드는 이유는 무엇입니까? 예를 들어, python명령을 다른 것에 바인딩 할 수 expand-macros | python있습니다. 이 코드는 매크로 시스템을 사용하지 않는 사람들에게는 여전히 이식성이 좋으며 코드를 게시하기 전에 매크로를 확장하기 만하면됩니다. 그러나 C ++ / Haskell의 템플릿을 제외하고는 모릅니다. 실제로 동일하지 않습니다. 매크로 시스템을보다 쉽게 ​​구현할 수있는 LISP는 무엇입니까?


3
"이 코드는 매크로 시스템을 사용하지 않는 사람들에게는 여전히 이식성이 좋으며, 코드를 게시하기 전에 매크로를 확장하기 만하면됩니다." -당신에게 경고하기 위해, 이것은 잘 작동하지 않는 경향이 있습니다. 다른 사람들은 코드를 실행할 수 있지만 실제로는 매크로 확장 코드를 이해하기가 어렵고 대개 수정하기가 어렵습니다. 저자가 사람의 눈을 위해 확장 된 코드를 만들지 않았고 실제 소스를 조정했다는 의미에서 "잘못 작성된 것"입니다. 당신은 그들이 ;-) 설정 무슨 색깔 C 전처리 및 시계를 통해 자바 코드를 실행하는 자바 프로그래머가 말하는 시도
스티브 Jessop

1
매크로는 그 시점에서 이미 언어에 대한 통역사를 작성하고 있습니다.
Mehrdad

답변:


29

많은 Lispers는 Lisp를 특별하게 만드는 것이 homoiconicity 라고 말하는데 , 이는 코드의 구문이 다른 데이터와 동일한 데이터 구조를 사용하여 표현됨을 의미합니다. 예를 들어, 주어진 측면 길이를 가진 직각 삼각형의 빗변을 계산하는 간단한 함수 (구성표 구문 사용)는 다음과 같습니다.

(define (hypot x y)
  (sqrt (+ (square x) (square y))))

이제 호모 닉은 위의 코드가 실제로 Lisp 코드에서 데이터 구조 (특히 목록 목록)로 표현 될 수 있다고 말합니다. 따라서 다음 목록을 고려하여 이들이 어떻게 "접착"되는지 확인하십시오.

  1. (define #2# #3#)
  2. (hypot x y)
  3. (sqrt #4#)
  4. (+ #5# #6#)
  5. (square x)
  6. (square y)

매크로를 사용하면 소스 코드를 다음과 같이 처리 할 수 ​​있습니다. 이들 6 "하위 목록"은 각각 다른리스트에 하나의 포인터를 포함하고, 또는 심볼들 (이 예에서 : define, hypot, x, y, sqrt, +, square).


그렇다면 어떻게 호모 닉을 사용하여 구문을 "고르고"매크로를 만들 수 있습니까? 다음은 간단한 예입니다. let매크로를 다시 구현해 보겠습니다 my-let. 알림으로

(my-let ((foo 1)
         (bar 2))
  (+ foo bar))

로 확장해야

((lambda (foo bar)
   (+ foo bar))
 1 2)

Scheme "명시 적 이름 바꾸기"매크로 †를 사용한 구현은 다음과 같습니다 .

(define-syntax my-let
  (er-macro-transformer
    (lambda (form rename compare)
      (define bindings (cadr form))
      (define body (cddr form))
      `((,(rename 'lambda) ,(map car bindings)
          ,@body)
        ,@(map cadr bindings)))))

form매개 변수는 실제 형식에 바인딩, 그래서 우리의 예를 들어, 그것은 것입니다 (my-let ((foo 1) (bar 2)) (+ foo bar)). 따라서 예제를 살펴 보겠습니다.

  1. 먼저 폼에서 바인딩을 검색합니다. 양식 cadr((foo 1) (bar 2))일부를 잡습니다 .
  2. 그런 다음 양식에서 본문을 검색합니다. 양식 cddr((+ foo bar))일부를 잡습니다 . (이것은 바인딩 후 모든 하위 폼을 잡기위한 것이므로 폼이

    (my-let ((foo 1)
             (bar 2))
      (debug foo)
      (debug bar)
      (+ foo bar))
    

    몸이 될 것입니다 ((debug foo) (debug bar) (+ foo bar)).)

  3. 이제 우리는 실제로 결과 lambda표현을 만들고 수집 한 바인딩과 본문을 사용하여 호출합니다. 백틱 (backtick)은 "quasiquote"라고하며, 쿼마 (Quasiquote) 내부의 모든 것을 쉼표 뒤의 비트 ( "unquote")를 제외하고 리터럴 데이텀으로 취급 합니다.
    • (rename 'lambda)수단은 사용 lambda이 매크로가 될 때 힘에 바인딩을 정의 , 오히려 어떤 것보다 lambda이 매크로 때 주위에있을 수있는 바인딩을 사용 . (이것은 위생이라고 합니다.)
    • (map car bindings)반환 값 (foo bar): 각 바인딩의 첫 번째 데이텀.
    • (map cadr bindings)반환 값 (1 2): 각 바인딩의 두 번째 데이텀.
    • ,@ 리스트를 리턴하는 표현식에 사용되는 "접합"을 수행합니다.리스트 자체가 아니라리스트의 요소를 결과에 붙여 넣습니다.
  4. 그 결과, 목록으로 함께, 우리가 얻을 모든 것을, 퍼팅 (($lambda (foo bar) (+ foo bar)) 1 2), $lambda이름을 바꾼를 말한다 여기 lambda.

간단 하죠? ;-) (직접적이지 않다면 다른 언어의 매크로 시스템을 구현하는 것이 얼마나 어려울 지 상상해보십시오.)


따라서 소스 코드를 복잡하지 않은 방식으로 "분리"할 수있는 방법이 있다면 다른 언어에 대한 매크로 시스템을 가질 수 있습니다. 이것에 대한 몇 가지 시도가 있습니다. 예를 들어 sweet.js 는 JavaScript에 대해이 작업을 수행합니다.

† 노련한 Schemers가 이것을 읽으려고 의도적으로, defmacro다른 Lisp 방언이 사용하는 것 사이 의 중간 타협으로 명시 적 이름 바꾸기 매크로를 사용하기로 선택했습니다 syntax-rules. 나는 다른 Lisp 방언으로 글을 쓰고 싶지 않지만, 익숙하지 않은 비 화학자들을 소외하고 싶지 않습니다 syntax-rules.

참고로 다음과 같은 my-let매크로를 사용합니다 syntax-rules.

(define-syntax my-let
  (syntax-rules ()
    ((my-let ((id val) ...)
       body ...)
     ((lambda (id ...)
        body ...)
      val ...))))

해당 syntax-case버전은 매우 유사합니다.

(define-syntax my-let
  (lambda (stx)
    (syntax-case stx ()
      ((_ ((id val) ...)
         body ...)
       #'((lambda (id ...)
            body ...)
          val ...)))))

둘 사이의 차이는 즉 모든syntax-rules암시가 #'적용 당신이 할 수 있도록 의 패턴 / 템플릿 쌍을 syntax-rules따라서 완전히 선언이다. 반대로,에서 syntax-case패턴 뒤의 비트는 실제로 구문 #'(...)코드 ( ) 를 반환해야 하지만 다른 코드도 포함 할 수있는 실제 코드입니다 .


2
당신이 언급하지 않은 장점 : 예, JS에 대한 sweet.js와 같은 다른 언어로 시도가 있습니다. 그러나 lisps에서 매크로 작성은 함수 작성과 동일한 언어로 수행됩니다.
Florian Margaine

맞습니다. Lisp 언어로 절차 적 (대신 선언적) 매크로를 작성할 수 있습니다. 이것이 실제로 고급 작업을 수행 할 수있게 해줍니다. BTW, 이것이 Scheme 매크로 시스템에 대해 내가 좋아하는 것입니다. 간단한 매크로의 경우 syntax-rules순전히 선언적인을 사용합니다. 복잡한 매크로의 경우 syntax-case부분적으로 선언적이고 부분적으로 절차적인을 사용할 수 있습니다 . 그리고 명백한 이름 변경이 있습니다. 순전히 절차 적입니다. (대부분의 Scheme 구현은 하나 syntax-case또는 ER을 제공 할 것입니다. 두 가지를 모두 제공하는 것을 보지 못했습니다. 그것들은 동등한 것입니다.)
Chris Jester-Young

매크로가 AST를 수정해야하는 이유는 무엇입니까? 왜 더 높은 수준에서 일할 수 없습니까?
Elliot Gorokhovsky

1
그렇다면 왜 LISP가 더 낫습니까? LISP가 특별한 이유는 무엇입니까? js에서 매크로를 구현할 수 있다면 다른 언어로도 구현할 수 있습니다.
Elliot Gorokhovsky

3
@ RenéG는 첫 번째 의견에서 말했듯이 여전히 동일한 언어로 글을 쓰고 있다는 장점이 있습니다.
Florian Margaine

23

반대 의견 : Lisp의 동질성은 대부분의 Lisp 팬들이 생각하는 것보다 훨씬 유용하지 않습니다.

구문 매크로를 이해하려면 컴파일러를 이해하는 것이 중요합니다. 컴파일러의 역할은 사람이 읽을 수있는 코드를 실행 가능한 코드로 바꾸는 것입니다. 매우 높은 수준의 관점에서 볼 때, 파싱코드 생성의 두 가지 단계가 있습니다.

구문 분석 은 코드를 읽고 일련의 공식 규칙에 따라 해석하여 일반적으로 AST (Abstract Syntax Tree)라고하는 트리 구조로 변환하는 프로세스입니다. 프로그래밍 언어들 사이의 모든 다양성에있어서, 이것은 하나의 놀라운 공통점입니다. 본질적으로 모든 범용 프로그래밍 언어는 트리 구조로 파싱됩니다.

코드 생성 은 파서의 AST를 입력으로 사용하고 공식 규칙을 적용하여 실행 가능한 코드로 변환합니다. 성능 측면에서 볼 때 이것은 훨씬 간단한 작업입니다. 많은 고급 언어 컴파일러는 구문 분석에 75 % 이상의 시간을 소비합니다.

Lisp에 대해 기억해야 할 것은 매우 오래 되었다는 것입니다.. 프로그래밍 언어 중에서 FORTRAN 만 Lisp보다 오래되었습니다. 과거로 돌아가서, 파싱 (컴파일의 느린 부분)은 어둡고 신비로운 예술로 여겨졌습니다. Lisp 이론에 대한 John McCarthy의 원본 논문은 (실제로 실제 컴퓨터 프로그래밍 언어로 구현 될 수 있다고 생각하지 않았을 때의 아이디어 였을 때) 현대의 "모든 곳의 S- 표현식"보다 다소 복잡하고 표현적인 구문을 설명합니다. "표기법. 사람들이 실제로 그것을 구현하려고 할 때 그것은 나중에 나왔습니다. 파싱은 당시에 잘 이해되지 않았기 때문에 기본적으로 파서를 펀칭하고 파서의 작업을 완전히 사소하게 만들기 위해 구문을 균질 트리 구조로 어둡게 만들었습니다. 최종 결과는 개발자 (개발자)가 많은 파서를 수행해야한다는 것입니다. 공식적인 AST를 코드에 직접 작성하면됩니다. Homoiconicity는 "매크로를 훨씬 더 쉽게 만들지"않습니다.

이것의 문제는 특히 동적 타이핑의 경우 S- 표현식이 많은 의미 정보를 전달하기가 매우 어렵다는 것입니다. 모든 구문이 동일한 유형의 항목 (목록 목록) 인 경우 구문에서 제공하는 컨텍스트 방식이 많지 않으므로 매크로 시스템은 거의 작업하지 않습니다.

컴파일러 이론은 Lisp가 발명 된 1960 년대 이래 먼 길을 걸어 왔으며, 그 일이 인상적이었던 것은 오늘날에는 다소 원시적 인 것처럼 보입니다. 최신 메타 프로그래밍 시스템의 예를 보려면 (슬프게도 과소 평가 된) 부 언어를 살펴보십시오. Boo는 정적으로 유형이 지정되고 객체 지향적이며 오픈 소스이므로 모든 AST 노드에는 매크로 개발자가 코드를 읽을 수있는 잘 정의 된 구조의 유형이 있습니다. 이 언어에는 Python에서 영감을 얻은 비교적 간단한 구문이 있으며,이 키워드로 작성된 트리 구조에 고유 한 의미 론적 의미를 부여하는 다양한 키워드가 있으며 메타 프로그래밍에는 직관적 인 준 따옴표 구문이있어 새로운 AST 노드 생성을 단순화합니다.

어제 BeginUpdate()UI 코드 try를 호출 하고 블록 에서 업데이트를 수행 한 다음 호출 하는 GUI 코드의 여러 위치에 동일한 패턴을 적용한다는 것을 알았을 때 어제 만든 매크로가 있습니다 EndUpdate().

macro UIUpdate(value as Expression):
    return [|
        $value.BeginUpdate()
        try:
            $(UIUpdate.Body)
        ensure:
            $value.EndUpdate()
    |]

macro명령 사실이다 매크로 자체 입력으로 매크로 본체 소요 매크로 처리 클래스를 생성 하나. 매크로의 이름을 MacroStatement매크로 호출을 나타내는 AST 노드를 나타내는 변수로 사용합니다 . [| ... |]은 인용 부호 블록으로, 부호에 해당하는 AST를 생성하고, 인용 부호 블록 내부에서 $ 기호는 "unquote"기능을 제공하여 지정된대로 노드를 대체합니다.

이것으로 다음과 같이 쓸 수 있습니다 :

UIUpdate myComboBox:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0

다음으로 확장하십시오.

myComboBox.BeginUpdate()
try:
   LoadDataInto(myComboBox)
   myComboBox.SelectedIndex = 0
ensure:
   myComboBox.EndUpdate()

이러한 방식으로 매크로를 표현하는 것은 Lisp 매크로보다 간단하고 직관적입니다. 개발자는 구조 와 속성이 작동 MacroStatement하는 방식을 알고 있으며 고유 한 지식을 사용하여 매우 직관적 인 개념을 표현할 수 있기 때문입니다. 방법. 컴파일러는의 구조를 알고 있기 때문에 더 안전 합니다. 유효하지 않은 것을 코딩하려고 하면 컴파일러가 즉시 잡아서 무언가가 나올 때까지 알지 못하는 대신 오류를보고합니다. 실행 시간.ArgumentsBodyMacroStatementMacroStatement

Haskell, Python, Java, Scala 등에 매크로를 접목하는 것은 어렵지 않습니다. 언어는 언어를 위해 설계되지 않았기 때문에 어렵고, 언어의 AST 계층 구조가 처음부터 매크로 시스템에 의해 검사 및 조작되도록 설계 될 때 가장 잘 작동합니다. 처음부터 메타 프로그래밍을 염두에두고 설계된 언어로 작업 할 때 매크로는 훨씬 단순하고 사용하기 쉽습니다!


4
읽는 기쁨, 감사합니다! Lisp 이외의 매크로가 구문을 변경하는 한 확장됩니까? Lisp의 강점 중 하나는 구문이 모두 동일하므로 모두 동일하므로 함수, 조건문을 쉽게 추가 할 수 있습니다. Lisp 이외의 언어를 사용하는 경우 한 가지는 다른 것과 다릅니다 if.... 예를 들어 함수 호출처럼 보이지 않습니다. Boo를 모르지만 Boo에 패턴 일치가 없다고 생각하면 매크로와 같은 고유 구문을 사용하여 패턴을 도입 할 수 있습니까? 내 요점은-Lisp의 새로운 매크로는 100 % 자연스럽고 다른 언어에서는 작동하지만 스티치를 볼 수 있다고 생각합니다.
greenoldman

4
내가 항상 읽었던 이야기는 조금 다릅니다. s-expression에 대한 대체 구문이 계획되었지만 프로그래머가 이미 s-expression을 사용하기 시작하고 편리해 졌기 때문에 이에 대한 작업이 지연되었습니다. 따라서 새로운 구문에 대한 연구는 결국 잊혀졌습니다. 컴파일러 이론의 단점을 나타내는 소스를 s-expressions를 사용하는 이유로 인용 할 수 있습니까? 또한 Lisp 가족은 수십 년 동안 발전해 왔으며 (Scheme, Common Lisp, Clojure) 대부분의 방언은 s- 표현을 고수하기로 결정했습니다.
Giorgio

5
"더 간단하고 직관적": 죄송하지만 방법을 모르겠습니다. "Updating.Arguments [0]"은 의미 가 없습니다 . 이름이 지정된 인수가 있고 인수 수가 일치하는지 컴파일러가 확인하도록하겠습니다. pastebin.com/YtUf1FpG
coredump

8
"성능 측면에서 볼 때 이것은 훨씬 간단한 작업입니다. 많은 고급 언어 컴파일러는 구문 분석에 75 % 이상의 시간을 소비합니다." 나는 대부분의 시간을 차지하기 위해 최적화를 찾고 적용 할 것으로 예상했을 것입니다 (그러나 실제 컴파일러 는 작성하지 않았습니다 ). 여기에 뭔가 빠졌습니까?
Doval

5
불행히도 귀하의 예는 그것을 보여주지 않습니다. 매크로를 사용하여 Lisp에서 구현하는 것이 기본입니다. 실제로 이것은 가장 원시적 인 매크로 중 하나입니다. 이것은 당신이 Lisp의 매크로에 대해 많이 모른다고 생각합니다. "Lisp의 구문은 1960 년대에 고착되어 있습니다."실제로 Lisp의 매크로 시스템은 1960 년 이후 많은 진전을 이루었습니다 (1960 년 Lisp에는 매크로도 없었습니다!).
Rainer Joswig

3

SICP에서 Scheme을 배우고 있으며 Scheme을 만드는 많은 부분이 LISP를 특별하게 만드는 것이 매크로 시스템이라는 인상을 받고 있습니다.

어떻게 요? SICP의 모든 코드는 매크로가없는 스타일로 작성되었습니다. SICP에는 매크로가 없습니다. 373 페이지의 각주에만 매크로가 언급되었습니다.

그러나 매크로는 컴파일 타임에 확장되므로

반드시 그런 것은 아닙니다. Lisp는 인터프리터와 컴파일러 모두에서 매크로를 제공합니다. 따라서 컴파일 타임이 없을 수 있습니다. Lisp 인터프리터가 있으면 실행시 매크로가 확장됩니다. 많은 Lisp 시스템에는 컴파일러가 내장되어 있으므로 런타임에 코드를 생성하고 컴파일 할 수 있습니다.

Common Lisp 구현 인 SBCL을 사용하여 테스트 해 봅시다.

SBCL을 인터프리터로 전환 해 봅시다 :

* (setf sb-ext:*evaluator-mode* :interpret)

:INTERPRET

이제 매크로를 정의합니다. 매크로는 코드 확장을 위해 호출 될 때 무언가를 인쇄합니다. 생성 된 코드가 인쇄되지 않습니다.

* (defmacro my-and (a b)
    (print "macro my-and used")
    `(if ,a
         (if ,b t nil)
         nil))

이제 매크로를 사용하자 :

MY-AND
* (defun foo (a b) (my-and a b))

FOO

만나다. 위의 경우 Lisp는 아무것도하지 않습니다. 매크로는 정의시 확장되지 않습니다.

* (foo t nil)

"macro my-and used"
NIL

그러나 런타임에 코드를 사용하면 매크로가 확장됩니다.

* (foo t t)

"macro my-and used"
T

다시, 런타임에 코드가 사용될 때 매크로가 확장됩니다.

SBCL은 컴파일러를 사용할 때 한 번만 확장됩니다. 그러나 다양한 Lisp 구현은 SBCL과 같은 통역사도 제공합니다.

Lisp에서 매크로가 쉬운 이유는 무엇입니까? 글쎄, 그들은 쉽지 않습니다. 리스프에만 있고 매크로 지원 기능이 내장 된 많은 리스프가 있습니다. 많은 리스프에는 매크로를위한 광범위한 기계가 함께 제공되므로 쉬운 것처럼 보입니다. 그러나 매크로 메커니즘은 매우 복잡 할 수 있습니다.


SICP뿐만 아니라 웹 주변의 Scheme에 대해 많이 읽었습니다. 또한 Lisp 표현식이 해석되기 전에 컴파일되지 않습니까? 적어도 파싱해야합니다. 따라서 "컴파일 타임"이 "파싱 타임"이어야한다고 생각합니다.
Elliot Gorokhovsky 0시

@ RenéG Rainer의 요점은 Lisp 언어로 코딩 eval하거나 load코드를 작성하면 매크로가 처리된다는 것입니다. 질문에 제안 된대로 전처리 시스템을 사용하는 경우 등 eval은 매크로 확장의 이점을 얻지 못합니다.
Chris Jester-Young

@ RenéG 또한 "구문 분석"은 readLisp에서 호출 됩니다. 이 구별은 eval텍스트 형식이 아닌 실제 목록 데이터 구조 (내 대답에 언급 된대로)에서 작동 하기 때문에 중요 합니다. 따라서 (eval '(+ 1 1))2를 사용 하고 다시 얻을 수는 있지만 (문자열) (eval "(+ 1 1)")돌아옵니다 "(+ 1 1)". (7 개의 문자열)에서 (한 개의 기호와 두 개의 고정 번호가있는 목록 ) read으로 가져 오는 데 사용 합니다 . "(+ 1 1)"(+ 1 1)
Chris Jester-Young

@ RenéG 이러한 이해를 바탕으로 매크로는 작동하지 않습니다 read. 컴파일 타임에 코드가 있으면 코드가 실행될 때마다가 아니라 코드가로드 될 때마다 코드가 한 번만 (and (test1) (test2))확장됩니다 (if (test1) (test2) #f)(구성표에서). (eval '(and (test1) (test2)))런타임에 해당 표현식을 적절하게 컴파일하고 매크로 확장합니다.
Chris Jester-Young

@ RenéG Homoiconicity는 Lisp 언어가 텍스트 형식이 아닌 목록 구조에서 평가 될 수있게하며 실행 전에 이러한 목록 구조가 (매크로를 통해) 변환되도록합니다. 대부분의 언어 eval는 텍스트 문자열에서만 작동하며 구문 수정 기능은 훨씬 부족하고 번거 롭습니다.
Chris Jester-Young

1

동질성으로 인해 매크로를 훨씬 쉽게 구현할 수 있습니다. 코드가 데이터이고 데이터가 코드라는 아이디어는 ( 위생적 매크로로 해결 된 우발적 인 식별자 캡처를 제외하고 ) 서로를 자유롭게 대체 할 수있게합니다. Lisp와 Scheme은 균일하게 구조화되어 Syntactic Macros 의 기초를 형성하는 AST로 쉽게 전환 할 수있는 S- 표현 구문을 사용하여이를보다 쉽게 ​​만듭니다 .

S- 표현식이나 동음이없는 언어는 구문 매크로를 구현하는 데 여전히 어려움을 겪을 수 있습니다. 프로젝트 케플러 는 예를 들어 스칼라에 소개하려고합니다.

비 동질성 외에도 구문 매크로 사용에서 가장 큰 문제는 임의로 생성 된 구문 문제입니다. 그것들은 엄청난 유연성과 힘을 제공하지만 소스 코드가 더 이상 이해하거나 유지하기 쉽지 않을 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.