チュートリアル:マクロ

あなたのDSLR、人々をバストしてください。マクロを詳しくチェックする時です!

マクロ命令とは何ですか?

一般に、プログラミングマクロは、マクロが評価された後に、少量のコードをより大きな式、構文木、コードオブジェクトなどに置き換える特別な種類の構文です。実際には、マクロは、それらに含まれるコードの通常の解析と評価を一時停止します。これは、完全な入力で拡張を実行できるようにするためです。おおまかに言えば、マクロを実行するアルゴリズムは次のとおりです。

  1. マクロの開始、一時停止、または通常の解析のスキップ
  2. マクロ入力を文字列として収集する
  3. 入力によるマクロの評価
  4. 通常の解析と実行を再開します。

このメタプログラミングですか?もちろんです!

マクロはいつどこで使用されますか?

マクロは、多くのプログラミング言語の実用的なビート純度の特徴です。言語に応じて、通常の構文解析サイクルから抜け出すことができるため、それらを使って本当に野生的なことを行うことができます。しかし、マクロは実際にユーザーと開発者が書かなければならないボイラープレートコードの量を減らすために存在します。

CおよびC ++(およびFortran)では、Cプリプロセッサcppはマクロ評価エンジンです。たとえば、#includeor が表示されるたびに#ifdef、これはcpp動作しているマクロシステムです。これらの言語では、マクロは技術的には現在の言語の定義の外にあります。さらに、cppコードを1回だけ通過させる必要があるため、書き込むことのできるマクロの種類 cppは比較的簡単です。

一方、Rustは、通常の機能によく似た、見た目と感触が高いファーストクラスのマクロを持っています。Rustのマクロは、引数から型情報を引き出し、戻り値が消費されないようにすることができます。

Lisp、Forth、Juliaのような他の言語もマクロシステムを提供しています。再構成されたテキスト(rST)指令でさえ、マクロと見なすことができます。Haskellやその他のより純粋に機能的な言語はマクロを必要としません(評価は怠けているので)。

これらはPythonの世界に慣れていないように見える場合は、そのJupyterとIPython魔法を注意%して%%マクロです!

関数マクロ

Xonshは通常のPython呼び出し可能コードに基づいたRustのようなマクロをサポートしています。マクロはxonshに特別な定義を必要としません。ただし、Rustの場合と同様に!、呼び出し可能なカッコと開始カッコの間に感嘆符を付けてコールする必要があります(マクロ引数は,、通常のPython関数のように最上位コンマ分割されますたとえば、関数fとがあるとしgます。これらの関数のマクロ呼び出しは、次のように実行できます。

# No macro args
f!()

# Single arg
f!(x)
g!([y, 43, 44])

# Two args
f!(x, x + 42)
g!([y, 43, 44], f!(z))

それほど悪くないよね?では、マクロ呼び出しで使用されたときに実際に引数に何が起こりますか?まあ、それは関数の定義に依存します。特に、マクロ呼び出しの各引数は、呼び出し可能なシグネチャ内の対応するパラメータ注釈と照合されます。たとえばidentity()、唯一の引数に文字列として注釈を付ける関数があるとします。

def identity(x : str):
    return x

これを普通に呼び出すと、たとえそのオブジェクトが文字列でなくても、戻ってくるオブジェクトを取得するだけです:

>>> identity('me')
'me'

>>> identity(42)
42

>>> identity(identity)
<function __main__.identity>

ただし、マクロ呼び出しを実行する場合は、マクロ呼び出しのソースコードの文字列を取得することが保証されています。

>>> identity!('me')
"'me'"

>>> identity!(42)
'42'

>>> identity!(identity)
'identity'

また、各マクロ引数はマクロ自体に渡す前に削除されます。これは一貫性のために行われます。

>>> identity!(42)
'42'

>>> identity!(  42 )
'42'

重要なのは、ソースコードをキャプチャして評価していないため、マクロ呼び出しには通常の構文を超えた入力が含まれる可能性があります。実際には、それは全体のポイントのようなものです。ギアの回転を開始するいくつかのケースがあります:

>>> identity!(import os)
'import os'

>>> identity!(if True:
>>>     pass)
'if True:\n    pass'

>>> identity!(std::vector<std::string> x = {"yoo", "hoo"})
'std::vector<std::string> x = {"yoo", "hoo"}'

あなたはあなたですidentity()

関数マクロの呼び出し

マクロを呼び出す際には、いくつかの点を考慮する必要があります。1つは、名前で引数を渡すと、期待どおりに動作しないということです。これは<name>=、マクロ自体によってキャプチャされるためです。identity()関数を使う

>>> identity!(x=42)
'x=42'

マクロ呼び出しを実行すると、引数の順序だけが値を渡します。

さらに、マクロ呼び出しは、最上位のカンマでのみ引数を分割します。最上位のコンマは引数には含まれません。これは、通常のPython関数呼び出しと同様に動作します。たとえば、次のg()2つの引数を受け入れる関数があるとします。

def g(x : str, y : str):
    print('x = ' + repr(x))
    print('y = ' + repr(y))

次に、各マクロ引数での分割とストリッピングの動作を確認できます。

>>> g!(42, 65)
x = '42'
y = '65'

>>> g!(42, 65,)
x = '42'
y = '65'

>>> g!( 42, 65, )
x = '42'
y = '65'

>>> g!(['x', 'y'], {1: 1, 2: 3})
x = "['x', 'y']"
y = '{1: 1, 2: 3}'

時には最初のいくつかの引数をマクロ引数として渡し、残りを通常のPython引数として扱いたい場合もあります。通常、xonshのマクロ呼び出し側は*、マクロ引数と正規の引数を分割するために孤立した引数を探します。したがって、たとえば:

>>> g!(42, *, 65)
x = '42'
y = 65

>>> g!(42, *, y=65)
x = '42'
y = 65

上記でxは、まだマクロ引数として取得されていることに注意してください。しかし、すべての後に*、すなわちy、それは通常の関数呼び出しに渡された場合で評価されます。これはマクロ引数としてほんの一握りのargsしか必要としない大規模なインタフェースに有効です。

うまくいけば、今大きな絵が見えます。

関数マクロの記述¶

任意の関数(または呼び出し可能)をマクロとして使用することはできますが、この関数はおそらく関数がマクロとして設計されている場合に最も便利です考慮すべきマクロ設計には、引数注釈と呼び出しサイト実行コンテキストの2つの主な側面があります。

マクロ注釈

マクロには6種類の注釈が​​あります。

注釈の種類
カテゴリー オブジェクト フラグ モード 返品
文字列 str 's''str'または'string'   引数のソースコードは文字列(デフォルト)です
AST ast.AST 'a' または 'ast' 'eval'(デフォルト)、'exec'または'single' 引数の抽象構文木。
コード types.CodeType または compile 'c''code'または'compile' 'eval'(デフォルト)、'exec'または'single' 引数のコンパイルされたコードオブジェクト。
評価 eval または None 'v' または 'eval'   議論の評価。
幹部 exec 'x' または 'exec' 'exec' (デフォルト)または 'single' 引数を実行し、Noneを返します。
タイプ type 't' または 'type'   引数が評価された後の引数の型。

これらのアノテーションを使用すると、コンパイルの任意の段階に自由にアクセスできます。注釈タイプへの変換の前に、引数の文字列形式を分割して(前述のように)削除することに注意することが重要です。

各引数には、それぞれ独自のタイプの注釈を付けることができます。アノテーションは、オブジェクトとして、または上記の表に見られる文字列フラグとして提供されることがあります。文字列フラグは大文字と小文字を区別しません。引数に注釈がない場合strは、選択されます。これにより、マクロ関数呼び出しは、以下のサブプロセスマクロおよびコンテキストマネージャマクロのように動作します。例えば、

def func(a, b : 'AST', c : compile):
    pass

のマクロ呼び出しではfunc!()

  • astr注釈が提供されていないために評価され、
  • b 構文木ノードに解析され、
  • c組み込みcompile() 関数が注釈として使用されたため、コードオブジェクトにコンパイルされます

さらに、特定の種類の注釈には、引数の解析、コンパイル、および実行に影響を与える異なるモードがあります。賢明なデフォルトが提供されていますが、独自のものを提供することもできます。これは、(kind、mode)タプルで注釈を付けることによって行われます。最初の要素は、任意の有効なオブジェクトまたはフラグです。2番目の要素は文字列として対応するモードでなければなりません。例えば、

def gunc(d : (exec, 'single'), e : ('c', 'exec')):
    pass

したがって、のマクロ呼び出しではgunc!()

  • d (exec-modeではなく)シングルモードでexec'dされます。
  • e exec-mode(eval-modeではなく)でコンパイルされます。

exec、eval、およびsingleモードの違いについては、Pythonのドキュメントを参照してください。

マクロ関数の実行コンテキスト

マクロ引数を持つことと同様に重要なのは、マクロ呼び出し自体の実行コンテキストを知ることです。マクロをフレームで囲む​​のではなく、コールサイトのグローバルとローカルの両方を提供します。これらは、アクセス可能macro_globalsmacro_localsマクロが実行されている間マクロ関数自体の属性。

たとえば、すべてのリテラル1桁をリテラルで置き換え2、変更を評価し、結果を返すマクロを考えてみましょう評価するために、マクロはグローバルとローカルを引き離す必要があります:

def one_to_two(x : str):
    s = x.replace('1', '2')
    glbs = one_to_two.macro_globals
    locs = one_to_two.macro_locals
    return eval(s, glbs, locs)

これをいくつかの異なる入力で実行すると、次のようになります。

>>> one_to_two!(1 + 1)
4

>>> one_to_two!(11)
22

>>> x = 1
>>> one_to_two!(x + 1)
3

もちろん、ユースケースに応じて他の多くの洗練されたオプションが利用できます。

サブプロセスマクロ

上記の関数マクロと同様に、サブプロセスマクロを使用すると、サブプロセスモードを終了する準備ができるまでパーサーを一時停止することができます。関数マクロとは異なり、マクロ引数は1つしかなく、そのマクロ型は常に文字列です。これは、通常、文字列以外の引数をコマンドに渡すのは意味がないためです。そして、そうなったら、@()構文があります!

最も単純なケースでは、サブプロセスマクロは、それらの関数マクロの同等物と同じように見えます。

>>> echo! I'm Mr. Meeseeks.
I'm Mr. Meeseeks.

ここでも、右側のすべてが最後の1つの引数としてコマンドに!渡され echoます。引用符で囲むような、スペースを節約します。

# normally, xonsh will split on whitespace,
# so each argument is passed in separately
>>> echo x  y       z
x y z

# usually space can be preserved with quotes
>>> echo "x  y       z"
x  y       z

# however, subprocess macros will pause and then strip
# all input after the exclamation point
>>> echo! x  y       z
x  y       z

しかし、マクロは、パスや環境変数の拡張を含め、引用符でさえ存在する可能性のあるすべてを一時停止します。例えば:

# without macros, environment variable are expanded
>>> echo $USER
lou

# inside of a macro, all additional munging is turned off.
>>> echo! $USER
$USER

先頭と末尾の空白を除く、感嘆符の右側のすべてが、書かれたとおりに直接コマンドに渡されます。これにより、クォートやパイピングがより重荷になる場合に、特定のコマンドを機能させることができます。このtimeitコマンドは、単純な構文が失敗することが多いが、マクロとして簡単に実行できる素晴らしい例です。

# fails normally
>>> timeit "hello mom " + "and dad"
xonsh: subprocess mode: command not found: hello

# macro success!
>>> timeit! "hello mom " + "and dad"
100000000 loops, best of 3: 8.24 ns per loop

感嘆符の左にあるすべての式は通常通り渡され、特殊なマクロ引数として扱われません。これにより、単純なコマンドライン引数と複雑なコマンドライン引数を混在させることができます。たとえば、実際に別の言語でコードを記述したい場合があります。

# don't worry, it is temporary!
>>> bash -c ! export var=42; echo $var
42

# that's better!
>>> python -c ! import os; print(os.path.abspath("/"))
/

関数マクロと比較すると、サブプロセスマクロは比較的簡単です。しかし、彼らはまだ非常に表現力豊かなことがあります!

コンテキストマネージャのマクロ

今、私たちはマクロ表現のような人生を見ることができたので、マクロステートメントを紹介しましょうwith!With-Bangは、既存のPythonコンテキストマネージャの上にマクロを提供します。これは、xonshに匿名ブロックと匿名ブロックの両方を提供します。

コンテキストマネージャマクロの構文は、Pythonの通常のwith-statementと同じですが、with単語と最初のコンテキストマネージャ式の間に感嘆符が追加されています。簡単な例として、

with! x:
    y = 10
    print(y)

上の例では、コロン(x)の左側のすべてが正常に評価されます。しかし、ボディは実行されyず、定義もプリントもされません。この場合、本文は入力される前に、グローバルとローカルと一緒にxに文字列としてアタッチされます。その後、本文はpassステートメントに置き換えられます。上記は次のように変形されていると考えることができます:

x.macro_block = 'y = 10\nprint(y)\n'
x.macro_globals = globals()
x.macro_locals = locals()
with! x:
    pass

これについて通知する重要な点がいくつかあります。

  1. macro_block文字列がdedentedされ、
  2. macro_*属性が設定されている前のように、コンテキストマネージャが入力されている__enter__()方法は、それらを使用することができ、
  3. macro_*コンテキストマネージャは、オブジェクトが終了した後も、それらを使用できるように属性が自動的にクリーンアップされていません。 __exit__()必要に応じて、この方法は、これらの属性をクリーンアップすることがあります。

デフォルトでは、マクロブロックは文字列として返されます。しかし、関数のマクロ引数のように、種類はmacro_block特殊な注釈によって決まります。この注釈は__xonsh_block__、コンテキストマネージャ自体の属性を介して与えられます。これにより、ブロックはAST、バイトコンパイルされたものなどと解釈されます。

この構文についての便利な部分は、マクロブロックがレベルの後ろに割り当てられていることがわかると、マクロブロックだけが終了するということですwith!その他のコードはすべて無差別にスキップされます!これにより、一時停止することなくxonsh以外の言語でコードブロックを書くことができます。

たとえば、単純なXMLマクロ・コンテキスト・マネージャを考えてみましょう。これは、解析されたXMLツリーをマクロブロックから返します。コンテキストマネージャ自体は次のように記述することができます。

import xml.etree.ElementTree as ET

class XmlBlock:

    # make sure the macro_block comes back as a string
    __xonsh_block__ = str

    def __enter__(self):
        # parse and return the block on entry
        root = ET.fromstring(self.macro_block)
        return root

    def __exit__(self, *exc):
        # no reason to keep these attributes around.
        del self.macro_block, self.macro_globals, self.macro_locals

上記のクラスは、つぎのようにwith-bangで使用できます。

with! XmlBlock() as tree:
    <note>
      <to>You</to>
      <from>Xonsh</from>
      <heading>Don't You Want Me, Baby</heading>
      <body>
        You know I don't believe you when you say that you don't need me.
      </body>
    </note>

これを実行すると、treeオブジェクトが実際に解析されたXMLオブジェクトであることがわかります。

>>> print(tree.tag)
note

だから、およそ8行のxonshコードで、別の、まったく異なる言語とシームレスにインターフェースすることができます。

この可能性は、単なるマークアップ言語やその他の当事者の手口に限定されません。あなたは、SSH、RPC、dask / distributedなどを介してリモート実行インターフェイスになることができます。コンテキストマネージャマクロの本当の利点は、xonsh言語自体の一部としていつ、どこで、どのコードが実行されるかを選択できることです。

権力はそこにある。予約なしでそれを使用してください!

逃げる

うまくいけば、この時点で、いくつかの適切に配置されたマクロは、どのプロジェクトにとっても非常に便利で貴重なものになることがわかります。