コード記述コード:現代のメタプログラミングの理論と実践の紹介
公開: 2022-07-22マクロを説明する最良の方法を考えるときはいつでも、プログラミングを始めたときに書いたPythonプログラムを覚えています。 思い通りに整理できませんでした。 少し異なる関数をいくつか呼び出す必要があり、コードが煩雑になりました。 私が探していたのは、当時は知りませんでしたが、メタプログラミングでした。
メタプログラミング(名詞)
プログラムがコードをデータとして扱うことができる技術。
ペットの飼い主向けのアプリのバックエンドを構築していると想像することで、Pythonプロジェクトで直面したのと同じ問題を示す例を作成できます。 ライブラリのツールpet_sdk
を使用して、ペットの飼い主がキャットフードを購入できるようにPythonを記述します。
コードが機能することを確認した後、さらに2種類のペット(鳥と犬)に同じロジックを実装します。 また、獣医の予約をする機能を追加します。
Snippet 2の反復ロジックをループに凝縮するとよいので、コードの書き直しに着手しました。 各関数の名前が異なるため、ループ内で呼び出す関数( book_bird_appointment
、 book_cat_appointment
)を決定できないことがすぐにわかります。
必要な最終コードを自動的に生成するプログラムを作成できるPythonのターボチャージバージョンを想像してみましょう。このプログラムでは、プログラムをリスト、ファイル内のデータなどのように柔軟、簡単、流動的に操作できます。その他の一般的なデータ型またはプログラム入力:
これは、Rust、Julia、Cなどの言語で使用できるマクロの例ですが、Pythonは使用できません。
このシナリオは、独自のコードを変更および操作できるプログラムを作成することがどのように役立つかを示す優れた例です。 これはまさにマクロの描画であり、より大きな質問に対する多くの答えの1つです。プログラムに独自のコードをイントロスペクトさせ、データとして扱い、そのイントロスペクションに基づいて動作させるにはどうすればよいでしょうか。
概して、そのような内省を達成できるすべての技術は、「メタプログラミング」という包括的な用語に該当します。 メタプログラミングはプログラミング言語設計の豊富なサブフィールドであり、1つの重要な概念であるデータとしてのコードにまでさかのぼることができます。
リフレクション:Pythonを守るために
Pythonはマクロサポートを提供しないかもしれませんが、このコードを書くための他の多くの方法を提供していることを指摘するかもしれません。 たとえば、ここではisinstance()
メソッドを使用して、 animal
変数がインスタンスであるクラスを識別し、適切な関数を呼び出します。
このタイプのメタプログラミングリフレクションと呼びますが、後で戻ってきます。 Snippet 5のコードはまだ少し面倒ですが、リストされた動物ごとにロジックを繰り返したSnippet2のコードよりもプログラマーが書くのは簡単です。
チャレンジ
getattr
メソッドを使用して、前述のコードを変更し、適切なorder_*_food
関数とbook_*_appointment
関数を動的に呼び出します。 これは間違いなくコードを読みにくくしますが、Pythonをよく知っている場合は、 isinstance
関数の代わりにgetattr
を使用して、コードを単純化する方法を検討する価値があります。
同像性:Lispの重要性
Lispのようないくつかのプログラミング言語は、同像性を介してメタプログラミングの概念を別のレベルに引き上げます。
同像性(名詞)
コードとプログラムが動作しているデータの間に区別がないプログラミング言語の特性。
1958年に作成されたLispは、最も古い同像性言語であり、2番目に古い高級プログラミング言語です。 「LIStプロセッサ」からその名前が付けられたLispは、コンピュータの使用方法とプログラミング方法を深く形作ったコンピューティングの革命でした。 Lispがプログラミングにどのように根本的かつ明確に影響を与えたかを誇張するのは難しいです。
EmacsはLispで書かれています。Lispは美しい唯一のコンピュータ言語です。 ニールスティーブンソン
Lispは、FORTRANからわずか1年後、部屋を埋め尽くしたパンチカードと軍用コンピュータの時代に作成されました。 それでも、プログラマーは今日でもLispを使用して新しい最新のアプリケーションを作成しています。 Lispの主要な作成者であるJohnMcCarthyは、AIの分野のパイオニアでした。 長年、LispはAIの言語であり、研究者は独自のコードを動的に書き直す能力を高く評価していました。 今日のAI研究は、そのタイプの論理生成コードではなく、ニューラルネットワークと複雑な統計モデルを中心にしています。 しかし、Lispを使用してAIについて行われた研究、特に60年代と70年代にMITとスタンフォードで行われた研究は、私たちが知っているようにこの分野を生み出し、その多大な影響は続いています。
Lispの出現により、初期のプログラマーは、再帰、高階関数、リンクリストなどの実用的な計算の可能性に初めてさらされました。 また、ラムダ計算のアイデアに基づいて構築されたプログラミング言語の力を示しました。
これらの概念はプログラミング言語の設計に爆発的な影響を与え、コンピュータサイエンスの著名人の1人であるEdsger Dijkstraが述べたように、 「 […]これまで不可能だった考えを考える上で、最も才能のある多くの仲間を助けました。」
この例は、入力の階乗を再帰的に計算し、入力「7」でその関数を呼び出す関数「factorial」を定義する単純なLispプログラム(およびより馴染みのあるPython構文での同等のもの)を示しています。
舌足らずの発音 | Python |
---|---|
( defun factorial ( n ) ( if ( = n 1 ) 1 ( * n ( factorial ( - n 1 ))))) ( print ( factorial 7 )) | |
データとしてコード化
Lispの最も影響力があり結果的な革新の1つであるにもかかわらず、再帰や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
しようとすると、エラーが発生します。
同像性は強力な種類のメタプログラミングであり、プログラムがその場で適応できる斬新で複雑なプログラミングパラダイムを解き放ち、ドメイン固有の問題や遭遇した新しいデータ形式に適合するコードを生成します。
WolframAlphaの場合を考えてみましょう。同像性のWolfram言語は、信じられないほどの範囲の問題に適応するためのコードを生成できます。 WolframAlphaに「ニューヨーク市のGDPをアンドラの人口で割ったものは何ですか?」と尋ねることができます。 そして、驚くべきことに、論理的な応答を受け取ります。
このあいまいで無意味な計算をデータベースに含めることを誰もが考えそうにないようですが、Wolframはメタプログラミングとオントロジー知識グラフを使用して、この質問に答えるためのオンザフライコードを記述します。
Lispや他の同像性言語が提供する柔軟性とパワーを理解することは重要です。 さらに詳しく説明する前に、自由に使用できるメタプログラミングオプションのいくつかを検討しましょう。
意味 | 例 | ノート | |
---|---|---|---|
同像性 | コードが「ファーストクラス」のデータである言語特性。 コードとデータの間に分離がないため、この2つは同じ意味で使用できます。 |
| ここで、Lispには、Scheme、Racket、ClojureなどのLispファミリの他の言語が含まれています。 |
マクロ | コードを入力として受け取り、コードを出力として返すステートメント、関数、または式。 |
| (Cのマクロに関する次の注記を参照してください。) |
プリプロセッサディレクティブ(またはプリコンパイラ) | プログラムを入力として受け取り、コードに含まれているステートメントに基づいて、プログラムの変更されたバージョンを出力として返すシステム。 |
| Cのマクロは、Cのプリプロセッサシステムを使用して実装されますが、この2つは別個の概念です。 Cのマクロ( #define プリプロセッサディレクティブを使用)と他の形式のCプリプロセッサディレクティブ( #if や#ifndef など)の主な概念上の違いは、他の非#define を使用しながらマクロを使用してコードを生成することです。他のコードを条件付きでコンパイルするためのプリプロセッサディレクティブ。 この2つは、Cと他のいくつかの言語で密接に関連していますが、異なるタイプのメタプログラミングです。 |
反射 | 独自のコードを調べ、変更し、内省するプログラムの機能。 |
| リフレクションは、コンパイル時または実行時に発生する可能性があります。 |
ジェネリック | さまざまなタイプに有効なコード、または複数のコンテキストで使用できるが1か所に保存できるコードを作成する機能。 コードが明示的または暗黙的に有効であるコンテキストを定義できます。 | テンプレートスタイルのジェネリック:
パラメトリック多型:
| ジェネリックプログラミングはジェネリックメタプログラミングよりも幅広いトピックであり、2つの間の線は明確に定義されていません。 この著者の見解では、パラメトリック型システムは、静的に型付けされた言語である場合にのみメタプログラミングとしてカウントされます。 |
さまざまなプログラミング言語で記述された同像性、マクロ、プリプロセッサディレクティブ、リフレクション、およびジェネリックスの実践的な例をいくつか見てみましょう。
マクロ(Snippet 11のマクロなど)は、新世代のプログラミング言語で再び人気が高まっています。 これらをうまく開発するには、重要なトピックである衛生を考慮する必要があります。
衛生的および非衛生的なマクロ
コードが「衛生的」または「非衛生的」であるとはどういう意味ですか? 明確にするために、 macro_rules!
関数。 名前が示すように、 macro_rules!
定義したルールに基づいてコードを生成します。 この場合、マクロにmy_macro
という名前を付け、ルールは「コード行let x = $n
を作成する」です。ここで、 n
は入力です。
マクロを展開すると(マクロを実行して、その呼び出しを生成するコードに置き換える)、次のようになります。
どうやら、私たちのマクロは変数x
を3に再定義したので、プログラムが3
を出力することを合理的に期待するかもしれません。 実際、 5
を出力します! 驚いた? Rustでは、 macro_rules!
は識別子に関して衛生的であるため、その範囲外の識別子を「キャプチャ」することはありません。 この場合、識別子はx
でした。 マクロによってキャプチャされた場合、3に等しくなります。
衛生(名詞)
マクロの展開が、マクロのスコープを超えて識別子やその他の状態をキャプチャしないことを保証するプロパティ。 このプロパティを提供しないマクロおよびマクロシステムは、非衛生的と呼ばれます。
マクロの衛生は、開発者の間でやや物議を醸すトピックです。 支持者は、衛生状態がなければ、コードの動作を誤って微妙に変更するのは非常に簡単であると主張しています。 多くの変数やその他の識別子を持つ複雑なコードで使用されるSnippet13よりもはるかに複雑なマクロを想像してみてください。 そのマクロがコードと同じ変数の1つを使用していて、気づかなかった場合はどうなりますか?
開発者がソースコードを読まずに外部ライブラリのマクロを使用することは珍しいことではありません。 これは、マクロサポートを提供する新しい言語(RustやJuliaなど)で特に一般的です。
Cのこの非衛生的なマクロは、識別子のwebsite
をキャプチャし、その値を変更します。 もちろん、識別子の取得は悪意のあるものではありません。 これは、マクロを使用した場合の偶発的な結果にすぎません。
ですから、衛生的なマクロは良いですし、非衛生的なマクロは悪いですよね? 残念ながら、それはそれほど単純ではありません。 衛生的なマクロが私たちを制限しているという強い主張があります。 場合によっては、識別子のキャプチャが役立つことがあります。 pet_sdk
を使用して3種類のペットにサービスを提供するSnippet2をもう一度見てみましょう。 元のコードは次のように始まりました。
Snippet 3は、Snippet2の反復ロジックを包括的なループに凝縮する試みであったことを思い出してください。 しかし、コードがcats
とdogs
の識別子に依存していて、次のようなものを書きたい場合はどうでしょうか。
もちろん、Snippet 16は少し単純ですが、マクロでコードの特定の部分を100%書き込む場合を想像してみてください。 このような場合、衛生的なマクロが制限される可能性があります。
衛生的なマクロの議論と非衛生的なマクロの議論は複雑になる可能性がありますが、良いニュースは、それがあなたがスタンスをとらなければならないものではないということです。 使用している言語によって、マクロが衛生的か非衛生的かが決まるため、マクロを使用するときはそのことに注意してください。
現代のマクロ
マクロは今少し時間があります。 長い間、現代の命令型プログラミング言語の焦点は、機能の中核部分としてのマクロから離れ、他のタイプのメタプログラミングを支持するようになりました。
新しいプログラマーが学校で教えられていた言語(PythonやJavaなど)は、必要なのはリフレクションとジェネリックスだけだと言っていました。
時間が経つにつれて、これらの現代言語が普及するにつれて、マクロは、プログラマーがそれらにまったく気付いていたとしても、威圧的なCおよびC++プリプロセッサー構文に関連付けられるようになりました。
しかし、RustとJuliaの登場により、トレンドはマクロに戻りました。 RustとJuliaは、マクロの概念をいくつかの新しい革新的なアイデアで再定義して普及させた、2つの現代的でアクセスしやすく、広く使用されている言語です。 これは、使いやすい「バッテリーを含む」多目的言語としてPythonとRに取って代わる準備ができているように見えるJuliaで特にエキサイティングです。
「TurboPython」メガネを通してpet_sdk
を最初に見たとき、私たちが本当に欲しかったのはJuliaのようなものでした。 その同像性とそれが提供する他のメタプログラミングツールのいくつかを使用して、JuliaでSnippet2を書き直してみましょう。
Snippet17を分解してみましょう。
- 3つのタプルを繰り返し処理します。 これらの最初のものは
("cat", :clean_litterbox)
であるため、変数pet
は"cat"
に割り当てられ、変数care_fn
は引用符で囲まれた記号:clean_litterbox
に割り当てられます。 -
Meta.parse
関数を使用して文字列をExpression
に変換し、コードとして評価できるようにします。 この場合、文字列補間の力を使用して、ある文字列を別の文字列に配置して、呼び出す関数を定義します。 -
eval
関数を使用して、生成しているコードを実行します。@eval begin… end
は、コードの再入力を回避するためのeval(...)
を記述する別の方法です。@eval
ブロック内には、動的に生成して実行しているコードがあります。
ジュリアのメタプログラミングシステムは、私たちが望むものを私たちが望む方法で表現することを本当に自由にします。 リフレクション(Snippet 5のPythonなど)を含む、他のいくつかのアプローチを使用することもできます。 特定の動物のコードを明示的に生成するマクロ関数を作成することも、コード全体を文字列として生成してMeta.parse
またはこれらのメソッドの任意の組み合わせを使用することもできます。
ジュリアを超えて:他の最新のメタプログラミングシステム
ジュリアはおそらく現代のマクロシステムの最も興味深く説得力のある例の1つですが、決してそれだけではありません。 Rustもまた、プログラマーの前にマクロをもたらすのに役立ちました。
Rustでは、マクロはJuliaよりもはるかに中心的な機能を備えていますが、ここでは詳しく説明しません。 さまざまな理由から、マクロを使用せずに慣用的なRustを作成することはできません。 ただし、ジュリアでは、同像性とマクロシステムを完全に無視することを選択できます。
その中心性の直接的な結果として、Rustエコシステムは実際にマクロを採用しています。 コミュニティのメンバーは、データのシリアル化と逆シリアル化、SQLの自動生成、コードに残された注釈の別のプログラミング言語への変換など、すべてがコードで生成されるツールを含む、いくつかの非常に優れたライブラリ、概念の証明、およびマクロを使用した機能を構築しました。コンパイル時。
Juliaのメタプログラミングはより表現力豊かで無料かもしれませんが、Rustはおそらく、メタプログラミングを向上させる現代言語の最良の例です。これは、言語全体で大きく取り上げられているためです。
未来への目
今こそプログラミング言語に興味を持つ素晴らしい時です。 今日では、C ++でアプリケーションを作成してWebブラウザーで実行したり、JavaScriptでアプリケーションを作成してデスクトップや電話で実行したりできます。 参入障壁はかつてないほど低くなり、新しいプログラマーはかつてないほど簡単に情報を入手できます。
プログラマーの選択と自由のこの世界では、コンピューターサイエンスの歴史と以前のプログラミング言語から機能と概念を厳選した、豊かで現代的な言語を使用する特権がますます増えています。 この開発の波の中でマクロが取り上げられ、散らかっているのを見るのはエキサイティングです。 RustとJuliaがマクロを紹介するので、新世代の開発者が何をするのか楽しみです。 「データとしてのコード」は単なるキャッチフレーズではないことを忘れないでください。 これは、オンラインコミュニティや学術的な設定でメタプログラミングについて議論するときに覚えておくべきコアイデオロギーです。
「データとしてのコード」は単なるキャッチフレーズではありません。
メタプログラミングの64年の歴史は、今日私たちが知っているように、プログラミングの開発に不可欠です。 私たちが探求した革新と歴史はメタプログラミングの物語の一角に過ぎませんが、それらは現代のメタプログラミングの強力な力と有用性を示しています。