코드 작성 코드: 현대 메타프로그래밍의 이론 및 실습 소개
게시 됨: 2022-07-22매크로를 설명하는 가장 좋은 방법을 생각할 때마다 프로그래밍을 처음 시작할 때 작성한 Python 프로그램이 생각납니다. 내가 원하는 대로 정리할 수 없었다. 약간 다른 여러 함수를 호출해야 했고 코드가 복잡해졌습니다. 내가 찾던 것은(당시에는 몰랐지만) 메타프로그래밍 이었습니다.
메타프로그래밍 (명사)
프로그램이 코드를 데이터로 취급할 수 있는 모든 기술.
애완 동물 소유자를 위한 앱의 백엔드를 구축한다고 상상하여 Python 프로젝트에서 직면한 것과 동일한 문제를 보여주는 예제를 구성할 수 있습니다. pet_sdk
라이브러리의 도구를 사용하여 애완 동물 소유자가 고양이 사료를 구매할 수 있도록 Python을 작성합니다.
코드가 작동하는지 확인한 후 두 종류의 애완 동물(새와 개)에 대해 동일한 논리를 구현합니다. 우리는 또한 수의사 약속을 예약하는 기능을 추가합니다:
Snippet 2의 반복적인 논리를 루프로 압축하는 것이 좋을 것이므로 코드를 다시 작성하기 시작했습니다. 각 함수의 이름이 다르게 지정되기 때문에 루프에서 호출할 함수(예: book_bird_appointment
, book_cat_appointment
)를 결정할 수 없다는 것을 빠르게 깨달았습니다.
우리가 원하는 최종 코드를 자동으로 생성하는 프로그램을 작성할 수 있는 터보차저 버전의 Python을 상상해 봅시다. 기타 일반적인 데이터 유형 또는 프로그램 입력:
이것은 몇 가지 예를 들자면 Rust, Julia 또는 C와 같은 언어에서 사용할 수 있는 매크로 의 예입니다. 그러나 Python은 아닙니다.
이 시나리오는 자체 코드를 수정하고 조작할 수 있는 프로그램을 작성하는 것이 얼마나 유용한지 보여주는 좋은 예입니다. 이것은 정확히 매크로의 묘미이며 더 큰 질문에 대한 많은 답변 중 하나입니다. 프로그램이 자체 코드를 내부 검사하고 이를 데이터로 취급한 다음 해당 내부 검사에 따라 조치를 취하는 방법은 무엇입니까?
대체로 그러한 내성을 달성할 수 있는 모든 기술은 포괄적인 용어 "메타프로그래밍"에 해당합니다. 메타프로그래밍은 프로그래밍 언어 디자인의 풍부한 하위 필드이며 데이터로서의 코드라는 중요한 개념으로 거슬러 올라갈 수 있습니다.
반성: 파이썬을 방어하기 위해
Python이 매크로 지원을 제공하지 않을 수 있지만 이 코드를 작성하는 다른 많은 방법을 제공한다는 점을 지적할 수 있습니다. 예를 들어, 여기서 isinstance()
메서드를 사용하여 animal
변수가 인스턴스인 클래스를 식별하고 적절한 함수를 호출합니다.
우리는 이러한 유형의 메타프로그래밍 리플렉션( reflection )이라고 부르며 나중에 다시 다루겠습니다. Snippet 5의 코드는 여전히 약간 번거롭지만 나열된 각 동물에 대해 논리를 반복한 Snippet 2보다 프로그래머가 작성하기가 더 쉽습니다.
도전
getattr
메서드를 사용하여 앞의 코드를 수정하여 적절한 order_*_food
및 book_*_appointment
함수를 동적으로 호출합니다. 이것은 틀림없이 코드를 읽기 어렵게 만들지만 Python을 잘 알고 있다면 isinstance
함수 대신 getattr
을 사용하고 코드를 단순화하는 방법에 대해 생각해 볼 가치가 있습니다.
동질성: Lisp의 중요성
Lisp와 같은 일부 프로그래밍 언어는 동질성( homoiconicity )을 통해 메타프로그래밍의 개념을 다른 수준으로 끌어 올립니다.
동질성 (명사)
코드와 프로그램이 작동하는 데이터 사이에 구분이 없는 프로그래밍 언어의 속성입니다.
1958년에 만들어진 Lisp는 가장 오래된 호모이코닉 언어이자 두 번째로 오래된 고급 프로그래밍 언어입니다. "LIST Processor"에서 이름을 따온 Lisp는 컴퓨터 사용 및 프로그래밍 방식을 결정짓는 컴퓨팅의 혁명이었습니다. Lisp가 프로그래밍에 얼마나 근본적이고 뚜렷하게 영향을 미쳤는지 과장하기는 어렵습니다.
Emacs는 유일하게 아름다운 컴퓨터 언어인 Lisp로 작성되었습니다. 닐 스티븐슨
Lisp는 펀치 카드와 군용 컴퓨터가 방을 가득 채우던 시대, FORTRAN 이후 1년 만에 만들어졌습니다. 그러나 프로그래머는 오늘날에도 여전히 Lisp를 사용하여 새롭고 현대적인 애플리케이션을 작성합니다. Lisp의 주요 제작자인 John McCarthy는 AI 분야의 선구자였습니다. 수년 동안 Lisp는 AI의 언어였으며 연구원들은 자신의 코드를 동적으로 다시 작성할 수 있는 능력을 높이 평가했습니다. 오늘날의 AI 연구는 그러한 유형의 논리 생성 코드보다는 신경망과 복잡한 통계 모델에 중점을 두고 있습니다. 그러나 Lisp를 사용한 AI에 대한 연구, 특히 60년대와 70년대에 MIT와 Stanford에서 수행된 연구는 우리가 알고 있는 분야를 만들어냈고 그 영향력은 계속되고 있습니다.
Lisp의 출현으로 초기 프로그래머는 재귀, 고차 함수 및 연결 목록과 같은 실용적인 계산 가능성에 처음으로 노출되었습니다. 또한 람다 미적분학의 아이디어를 기반으로 구축된 프로그래밍 언어의 힘을 보여주었습니다.
이러한 개념은 프로그래밍 언어 설계의 폭발적인 원인이 되었으며 컴퓨터 과학의 가장 위대한 이름 중 한 명인 Edsger Dijkstra가 말했듯이 " […
이 예제는 입력의 계승을 재귀적으로 계산하고 입력 "7"을 사용하여 해당 함수를 호출하는 "factorial" 함수를 정의하는 간단한 Lisp 프로그램(및 더 친숙한 Python 구문에서 이에 상응하는 프로그램)을 보여줍니다.
리스프 | 파이썬 |
---|---|
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 )) | |
데이터로서의 코드
Lisp의 가장 영향력 있고 결과적인 혁신 중 하나임에도 불구하고, 재귀 및 Lisp가 개척한 다른 많은 개념과 달리 동형이동성은 오늘날 대부분의 프로그래밍 언어에 포함되지 않았습니다.
다음 표는 Julia와 Lisp 모두에서 코드를 반환하는 동형 함수를 비교합니다. Julia는 여러 면에서 여러분에게 친숙한 고급 언어(예: Python, Ruby)와 유사한 동형 언어입니다.
각 예에서 구문의 핵심 부분은 인용 문자입니다. Julia는 인용할 때 :
콜론)을 사용하고 Lisp는 '
(작은따옴표)를 사용합니다.
줄리아 | 리스프 |
---|---|
function function_that_returns_code() return :(x + 1 ) end | |
두 예에서 기본 표현식( (x + 1)
또는 (+ x 1)
) 옆에 있는 따옴표는 직접 평가될 코드에서 우리가 조작할 수 있는 추상 표현식으로 변환합니다. 함수는 문자열이나 데이터가 아닌 코드를 반환합니다. 함수를 호출하고 print(function_that_returns_code())
를 작성하면 Julia는 x+1
로 문자열화된 코드를 인쇄할 것입니다(Lisp의 경우 해당). 반대로 :
(또는 Lisp에서 '
)가 없으면 x
가 정의되지 않았다는 오류가 발생합니다.
Julia 예제로 돌아가서 확장해 보겠습니다.
eval
함수는 프로그램의 다른 곳에서 생성한 코드를 실행하는 데 사용할 수 있습니다. 출력된 값은 x
변수의 정의를 기반으로 합니다. x
가 정의되지 않은 컨텍스트에서 생성된 코드를 eval
하려고 하면 오류가 발생합니다.
Homoiconicity는 강력한 종류의 메타프로그래밍으로, 프로그램이 즉석에서 적응할 수 있는 새롭고 복잡한 프로그래밍 패러다임을 풀 수 있고, 발생하는 도메인별 문제 또는 새로운 데이터 형식에 맞는 코드를 생성할 수 있습니다.
동일한 Wolfram 언어가 놀라운 범위의 문제에 적응할 수 있는 코드를 생성할 수 있는 WolframAlpha의 경우를 생각해 보십시오. WolframAlpha에게 "뉴욕시의 GDP를 안도라의 인구로 나눈 값은 얼마입니까?"라고 물어볼 수 있습니다. 그리고 놀랍게도 논리적인 응답을 받습니다.
누구도 이 모호하고 무의미한 계산을 데이터베이스에 포함할 생각을 하지 않을 것 같지만 Wolfram은 메타프로그래밍과 존재론적 지식 그래프를 사용하여 이 질문에 답하기 위해 즉석 코드를 작성합니다.
Lisp 및 기타 동음이의어가 제공하는 유연성과 기능을 이해하는 것이 중요합니다. 더 자세히 알아보기 전에 마음대로 사용할 수 있는 몇 가지 메타프로그래밍 옵션을 고려해 보겠습니다.
정의 | 예 | 메모 | |
---|---|---|---|
동질성 | 코드가 "일급" 데이터인 언어 특성입니다. 코드와 데이터의 구분이 없기 때문에 둘을 혼용하여 사용할 수 있습니다. |
| 여기서 Lisp는 Scheme, Racket 및 Clojure와 같은 Lisp 제품군의 다른 언어를 포함합니다. |
매크로 | 코드를 입력으로 사용하고 코드를 출력으로 반환하는 명령문, 함수 또는 표현식입니다. |
| (C의 매크로에 대한 다음 참고 사항을 참조하십시오.) |
전처리기 지시문(또는 전처리기) | 프로그램을 입력으로 사용하고 코드에 포함된 명령문을 기반으로 프로그램의 변경된 버전을 출력으로 반환하는 시스템입니다. |
| C의 매크로는 C의 전처리기 시스템을 사용하여 구현되지만 둘은 별개의 개념입니다. C의 매크로( #define 전처리기 지시문 사용)와 다른 형식의 C 전처리기 지시문(예: #if 및 #ifndef ) 간의 주요 개념적 차이점은 다른 비 #define 을 사용하는 동안 매크로를 사용하여 코드를 생성한다는 것입니다. 조건부로 다른 코드를 컴파일하기 위한 전처리기 지시문. 이 둘은 C 및 일부 다른 언어에서 밀접하게 관련되어 있지만 서로 다른 유형의 메타프로그래밍입니다. |
반사 | 자체 코드를 검사, 수정 및 검사할 수 있는 프로그램의 기능입니다. |
| 리플렉션은 컴파일 타임이나 런타임에 발생할 수 있습니다. |
제네릭 | 다양한 유형에 유효하거나 여러 컨텍스트에서 사용할 수 있지만 한 곳에 저장되는 코드를 작성하는 기능. 명시적이든 암시적이든 코드가 유효한 컨텍스트를 정의할 수 있습니다. | 템플릿 스타일 제네릭:
매개변수 다형성:
| 제네릭 프로그래밍은 제네릭 메타프로그래밍보다 광범위한 주제이며 둘 사이의 경계가 잘 정의되어 있지 않습니다. 이 저자의 관점에서 매개변수 유형 시스템은 정적으로 유형이 지정된 언어인 경우에만 메타프로그래밍으로 간주됩니다. |
다양한 프로그래밍 언어로 작성된 동질성, 매크로, 전처리기 지시문, 리플렉션 및 제네릭의 몇 가지 실습 예제를 살펴보겠습니다.
Snippet 11의 매크로와 같은 매크로는 새로운 세대의 프로그래밍 언어에서 다시 인기를 얻고 있습니다. 이를 성공적으로 개발하려면 위생이라는 핵심 주제를 고려해야 합니다.
위생 및 비위생 매크로
코드가 "위생적"이거나 "비위생적"이라는 것은 무엇을 의미합니까? 명확히 하기 위해 macro_rules!
기능. 이름에서 알 수 있듯이 macro_rules!
우리가 정의한 규칙에 따라 코드를 생성합니다. 이 경우 매크로의 이름을 my_macro
로 지정했으며 규칙은 "Create line of code let x = $n
"입니다. 여기서 n
은 입력입니다.
매크로를 확장할 때(매크로 호출을 생성하는 코드로 교체하기 위해 매크로 실행) 다음을 얻을 것으로 예상됩니다.
겉보기에는 매크로가 변수 x
를 3으로 재정의했기 때문에 프로그램이 3
을 인쇄할 것으로 합리적으로 기대할 수 있습니다. 사실, 그것은 5
를 인쇄합니다! 놀란? Rust에서 macro_rules!
식별자와 관련하여 위생적이므로 범위 외부의 식별자를 "캡처"하지 않습니다. 이 경우 식별자는 x
입니다. 매크로로 캡처했다면 3과 같았을 것입니다.
위생 (명사)
매크로의 확장이 매크로 범위를 벗어나는 식별자 또는 기타 상태를 캡처하지 않도록 보장하는 속성입니다. 이 속성을 제공하지 않는 매크로 및 매크로 시스템을 비위생적 이라고 합니다.
매크로의 위생은 개발자들 사이에서 다소 논란의 여지가 있는 주제입니다. 지지자들은 위생이 없으면 실수로 코드의 동작을 미묘하게 수정하기가 너무 쉽다고 주장합니다. 많은 변수와 기타 식별자가 있는 복잡한 코드에 사용되는 Snippet 13보다 훨씬 더 복잡한 매크로를 상상해 보십시오. 해당 매크로가 코드와 동일한 변수 중 하나를 사용했는데 눈치채지 못했다면?
개발자가 소스 코드를 읽지 않고 외부 라이브러리의 매크로를 사용하는 것은 드문 일이 아닙니다. 이것은 매크로 지원을 제공하는 새로운 언어(예: Rust 및 Julia)에서 특히 일반적입니다.
C의 이 비위생적인 매크로는 식별자 website
를 캡처하고 값을 변경합니다. 물론 식별자 캡처는 악의적이지 않습니다. 매크로 사용의 우연한 결과일 뿐입니다.
그래서 위생적인 매크로는 좋고 비위생적인 매크로는 나쁘죠? 불행히도, 그것은 그렇게 간단하지 않습니다. 위생적인 매크로가 우리를 제한하는 강력한 경우가 있습니다. 때로는 식별자 캡처가 유용합니다. pet_sdk
를 사용하여 세 종류의 애완 동물을 위한 서비스를 제공하는 Snippet 2를 다시 살펴보겠습니다. 원래 코드는 다음과 같이 시작되었습니다.
Snippet 3은 Snippet 2의 반복적인 논리를 포괄적인 루프로 압축하려는 시도였다는 것을 기억할 것입니다. 그러나 우리 코드가 cats
와 dogs
식별자에 의존하고 다음과 같은 것을 작성하고 싶다면 어떻게 될까요?
Snippet 16은 물론 약간 간단하지만 매크로가 코드의 주어진 부분을 100% 쓰기를 원하는 경우를 상상해 보십시오. 이러한 경우 위생적인 매크로가 제한될 수 있습니다.
위생적 대 비위생적인 거시적 논쟁은 복잡할 수 있지만 좋은 소식은 그것이 입장을 취해야 하는 사안이 아니라는 것입니다. 사용하는 언어에 따라 매크로가 위생적인지 비위생적인지 결정되므로 매크로를 사용할 때 이를 염두에 두십시오.
현대 매크로
매크로는 이제 약간의 시간을 보내고 있습니다. 오랫동안 현대 명령형 프로그래밍 언어의 초점은 기능의 핵심 부분인 매크로에서 벗어나 다른 유형의 메타프로그래밍을 선호했습니다.
새로운 프로그래머가 학교에서 가르치는 언어(예: Python 및 Java)는 리플렉션과 제네릭만 있으면 된다고 말했습니다.
시간이 지남에 따라 이러한 현대 언어가 대중화되면서 매크로는 프로그래머가 전혀 알고 있었다면 위협적인 C 및 C++ 전처리기 구문과 연결되었습니다.
그러나 Rust와 Julia의 등장으로 추세는 다시 매크로로 바뀌었습니다. Rust와 Julia는 새롭고 혁신적인 아이디어로 매크로 개념을 재정의하고 대중화한 두 가지 현대적이고 접근 가능하며 널리 사용되는 언어입니다. 이것은 사용하기 쉽고 "배터리 포함" 다용도 언어로 Python 및 R을 대신할 태세를 갖춘 Julia에서 특히 흥미진진합니다.
"TurboPython" 안경을 통해 pet_sdk
를 처음 보았을 때 우리가 정말로 원했던 것은 Julia와 같은 것이었습니다. 줄리아에서 Snippet 2의 동질성과 그것이 제공하는 다른 메타프로그래밍 도구를 사용하여 다시 작성해 보겠습니다.
Snippet 17을 분석해 보겠습니다.
- 우리는 세 개의 튜플을 반복합니다. 첫 번째는
("cat", :clean_litterbox)
이므로 변수pet
은"cat"
에 할당되고 변수care_fn
은 인용 부호:clean_litterbox
에 할당됩니다. -
Meta.parse
함수를 사용하여 문자열을Expression
으로 변환하여 코드로 평가할 수 있습니다. 이 경우, 우리는 어떤 함수를 호출할지 정의하기 위해 한 문자열을 다른 문자열에 넣을 수 있는 문자열 보간 기능을 사용하려고 합니다. -
eval
함수를 사용하여 생성 중인 코드를 실행합니다.@eval begin… end
는 코드 재입력을 피하기 위해eval(...)
을 작성하는 또 다른 방법입니다.@eval
블록 내부에는 우리가 동적으로 생성하고 실행 중인 코드가 있습니다.
Julia의 메타프로그래밍 시스템은 진정으로 우리가 원하는 방식으로 원하는 것을 표현할 수 있도록 해줍니다. 우리는 리플렉션(Snippet 5의 Python과 같은)을 포함하여 몇 가지 다른 접근 방식을 사용할 수 있었습니다. 특정 동물에 대한 코드를 명시적으로 생성하는 매크로 함수를 작성하거나 전체 코드를 문자열로 생성하고 Meta.parse
또는 이러한 방법의 조합을 사용할 수도 있습니다.
Julia를 넘어서: 다른 현대적인 메타프로그래밍 시스템
Julia는 아마도 현대 매크로 시스템의 가장 흥미롭고 설득력 있는 예 중 하나일 것입니다. 그러나 이것이 유일한 것은 아닙니다. 또한 Rust는 다시 한 번 프로그래머에게 매크로를 제공하는 데 중요한 역할을 했습니다.
Rust에서 매크로는 Julia에서보다 훨씬 더 중앙에서 기능하지만 여기에서 완전히 살펴보진 않겠습니다. 여러 가지 이유로 매크로를 사용하지 않고 관용적인 Rust를 작성할 수 없습니다. 그러나 Julia에서는 동질성과 거시 시스템을 완전히 무시하도록 선택할 수 있습니다.
이러한 중심성의 직접적인 결과로 Rust 생태계는 매크로를 실제로 수용했습니다. 커뮤니티 구성원은 데이터를 직렬화 및 역직렬화하고, SQL을 자동으로 생성하거나, 코드에 남아 있는 주석을 다른 프로그래밍 언어로 변환할 수 있는 도구를 포함하여 매크로를 사용하여 매우 멋진 라이브러리, 개념 증명 및 기능을 구축했으며, 모두 다음에서 코드에서 생성됩니다. 컴파일 시간.
Julia의 메타프로그래밍이 더 표현력이 풍부하고 자유로울 수 있지만 Rust는 아마도 메타프로그래밍을 향상시키는 현대 언어의 가장 좋은 예일 것입니다.
미래를 보는 눈
지금은 프로그래밍 언어에 관심을 가질 수 있는 놀라운 시기입니다. 오늘은 C++로 응용 프로그램을 작성하고 웹 브라우저에서 실행하거나 JavaScript로 응용 프로그램을 작성하여 데스크톱이나 전화에서 실행할 수 있습니다. 진입 장벽이 그 어느 때보다 낮았으며 새로운 프로그래머는 전례 없이 손쉽게 정보를 얻을 수 있습니다.
프로그래머의 선택과 자유가 있는 이 세상에서 우리는 컴퓨터 과학과 초기 프로그래밍 언어의 역사에서 특징과 개념을 선별한 풍부하고 현대적인 언어를 사용할 수 있는 특권을 점점 더 많이 누리고 있습니다. 이 개발 물결에서 매크로가 선택되고 먼지가 제거되는 것을 보는 것은 흥미진진합니다. 나는 새로운 세대의 개발자들이 Rust와 Julia가 그들에게 매크로를 소개할 때 어떤 일을 할 지 너무 기대됩니다. "데이터로서의 코드"는 단순한 캐치프레이즈 그 이상이라는 것을 기억하십시오. 온라인 커뮤니티나 학문적 환경에서 메타프로그래밍을 논의할 때 염두에 두어야 할 핵심 이념입니다.
'데이터로서의 코드'는 단순한 캐치프레이즈 그 이상입니다.
메타프로그래밍의 64년 역사는 오늘날 우리가 알고 있는 프로그래밍 개발에 필수적이었습니다. 우리가 탐구한 혁신과 역사는 메타프로그래밍 무용담의 한 부분에 불과하지만 현대 메타프로그래밍의 강력한 힘과 유용성을 보여줍니다.