プログラミング
空間言語:コードを2Dで書く
Spatial languages: Writing code in 2D (shukla.io)
要約
本記事では、従来の線形的なコード記述を超え、2次元空間でコードを書くという概念を探求します。3項演算子 `@@` を導入し、空間的な配置が3方向トグルや量子コンピューティング回路のような複雑なロジックを、垂直連鎖や演算のよりクリーンな表現を可能にすることで、どのように簡略化できるかを示しています。
全文翻訳
式は必ずしも左から右へ、一行で読む必要はないと思います。a < b を垂直に書くことはできないでしょうか?
a
<
b
これまでの間、私たちは1次元空間で式を書いてきましたが、 extra の次元を解き放つとどうなるでしょうか?もちろん、IDEとパーサーはすべてがうまく機能するように協力する必要があります。それでも、そのポイントは何でしょうか?さて、以下の例を見てみましょう。
3項関数
私たちは3項関数をあまり多くのinfix表記で見かけることはありません。例えば、Python(およびほとんどの他の言語)では、一度に3つ以上のものを扱うことができます。
x and y and z
厳密には、それは複数の二項関数を組み合わせただけです。
(a < b) and (b < c) を簡略化するためのPythonのショートカットでさえ、同じことの繰り返しです。
a < b < c
さて、私が遊びで使っている andFlip という面白い小さな演算子を紹介しましょう。これは、真の3項infix表記の必要性を動機づけます。まず、関数の定義は次のとおりです。
def andFlip(args):
if args[0] and args[1]:
args[2] = not args[2]
return args[2]
これは2つの入力を受け取り、両方の入力が真であればターゲットを反転させます。infix表記でこれを書く良い方法はありません。しかし、とにかく試してみましょう! andFlip 演算子のinfix記号として @@ を定義します。実際にはどのように見えるでしょうか?
方法1:標準的な方法
1つの選択肢は andFlip をカリー化することです:infix @@ は2つの引数を取り、3番目の引数を待つ関数を返します。たとえば次のようになります。
(a1 @@ a2) a3
ええ、それはまあまあです。機能します。しかし…
方法2:奇妙な方法
しばらくの間、コードについて知っていることを忘れてください。もし私が魔法の杖を振ることができたら、3項表記が次のように見えるように願うでしょう。
a3
a1 @@ a2
y軸を活用するのも良いでしょう、そう思いませんか?連鎖がずっと簡単になります。
t1 = 0
t2 = 0
t1 t2 (x @@ y) @@ z
連鎖では、x @@ y は t1 を切り替え、t1 は連鎖 t1 @@ z を継続し、t2 を切り替えます。これは成功した3方向トグルであり、ターゲットを切り替えるには x、y、z のすべてが真である必要があります。
ちなみに、私たちは今2D空間で作業しているので、次の式、t1 x @@ y は次のものと同等です。
x @@ y
t1
例:私の鶏小屋のドア
私が鶏小屋に設置した自動ドアを考えてみましょう。
自動ドア(右)とその制御ボックス
光センサーと内部時計だけで、マイクロコントローラーは次のものを追跡します。
x – 光センサーが夜明け/夕暮れのしきい値を超えた
y – 読み取りが5分間安定している
z – ゲートが最近切り替えられていない
ドアは、これら3つすべてが一致した場合にのみ切り替わります。通り過ぎる雲は x をトリップさせますが、y より長く持続することはないため、ドアはそのままです。
小屋のコントローラーは、上記の連鎖式と同じです。
t1 = 0
t2 = 0
t1 t2 (x @@ y) @@ z
なぜ単に x and y and z と書かないのか疑問に思うかもしれません。なぜなら、その場合、ドアはセンサーをミラーリングし、条件が消えるとすぐにスイングバックするからです。@@ はその場で切り替わります:ドアは一度動き、そのまま残り、部分的な答えはスクラッチビットである t1 に格納されます。
空間言語は、垂直連鎖を導入するとさらに魅力的になります。
垂直連鎖
その可変変数 t1 は、式を「純粋関数」と呼ぶ資格を失わせます。しかし、それを偽装することはできるでしょうか?同じ式内で t1 をリセットすることはできるでしょうか?2つのパズルを通して垂直連鎖を紹介したいと思います。
パズル1:t1 を 0 にリセットする
前述のように、上記の連鎖例では、変数 t1 は可変です。ここにパズルがあります:それを 0 にリセットするにはどうすればよいですか?式は不変に見えるようにするにはどうすればよいですか?
単に t1 = 0 を追加して終わりにする誘惑に駆られるかもしれませんが、パズルの目標はすべて同じ式内で行うことです。スクロールダウンする前に試してみてください!
答えは次のとおりです。
t1 = 0
t2 = 0
(x @@ y) @@ z
t1 t2 (x @@ y)
おそらくあなたの頭は吹き飛んだでしょう。データフローは、プログラミング言語で予想されるように、左から右、上から下へと続いています。x と y が真の場合、最初の x @@ y は t1 を切り替え、次に2番目の x @@ y はそれをリセットします。そうでなければ、どちらも t1 に影響を与えません。
このパズルは、量子コンピューティングの CCCX ゲートを実際にモデル化しており、中間値 t1 のアンコンピュートを同じ回路内で行うことは重要な要件です。
パズル2:一時変数を元の値に戻す
t1 が未知のブール値 n で開始された場合、さらに複雑になります。
t1 = n
t2 = 0
t1 t2 (x @@ y) @@ z
t1 を元の値に戻すための上記と同じトリックは機能しません。自分で確認してください:t1 = 1 で、x、y、z がすべて真の場合、t2 は 0 のままですが、これは正しくありません。
t1 = 1
t2 = 0
(1 @@ 1) @@ 1
t1 t2 (1 @@ 1)
では、答えは何でしょうか?タダ!
t1 = n
t2 = 0
t1 t2 (x @@ y) @@ z
t1 t2 (x @@ y) @@ z
絶対に伝説的です。この単一の2D式は、t2 の正しい結果を生成しながら、t1 を n にリセットします。
このパズルは、量子コンピューティングで貸し借りしたアンシラの概念に触発されています。t1 は「借りた」または「汚れた」値であり、変更されません。
同じ回路が Qiskit で表示されます。
from qiskit import QuantumCircuit, QuantumRegister
x = QuantumRegister(1, "x")
y = QuantumRegister(1, "y")
z = QuantumRegister(1, "z")
t1 = QuantumRegister(1, "t1") # starts in unknown state n
t2 = QuantumRegister(1, "t2") # target, starts at 0
qc = QuantumCircuit(x, y, z, t1, t2)
qc.ccx(x, y, t1)
qc.ccx(t1, z, t2)
qc.ccx(x, y, t1)
qc.ccx(t1, z, t2)
Qiskit の実装は、変数の追跡が前後するため、追跡が難しいと私は主張します。ちなみに、対応する量子回路図は次のとおりです。
ワイヤー:x、y、z、t1 = n、t2 = 0
出力:x、y、z、n、x·y·z
ccx x y t1
ccx t1 z t2
ccx x y t1
ccx t1 z t2
ある意味では、この回路図は理解しやすいですが、おそらくスケーラブルではありません。空間言語の構文は、両方の世界の最良のものです。
新しい演算子の定義
まず、@@ の弟を紹介します。単一の @ は、2つではなく1つの条件を受け取ります:x が真の場合、x @ y は y を反転させます。
@ と @@ を手に入れれば、新しい演算子を空間的に定義できます。 := の左側は形状であり、右側はその形状が書き換えられるものです。
b maj c
(a @ b) @@ (a @ c)
a := a
この奇妙な演算子は、a が真の場合に b と c の両方を反転させ、両方の側が真になった場合に a を反転させます。これは a ^ ((a^b) & (a^c)) を a に計算します。これは偶然にも3つの入力の多数と等しくなります。そのため、この演算子は maj と呼ばれます。そして maj は任意の選択ではありません。これは古典的な量子リップルキャリーアダーの MAJ ゲートであり、(carry, bit, bit) の多数が次のキャリーです。
maj は b と c を汚れたままにし、a^b と a^c を保持します。これは、貸し借りした t1 と同じ種類の混乱です。クリーンアップするために、uma も導入します。この名前は、同じアダー論文「UnMajority and Add」に由来しています。これは c をアンメジャーし、a を復元し、合計を b に追加します。
c uma b
((b @@ a) @ a) @ b
a := c
それらを配線したときに何が起こるか見てみましょう。
3ビットアダー
3つの maj を使用して3ビットアダーを構築できます。これにより、キャリーが a ワイヤーを介してリプルアップし、最後の @ が c3 にキャリーアウトをドロップし、最後に1行の uma がすべてのスクラッチワイヤーをリセットします。
c0 = 0
c3 = 0
(b2 maj (b1 maj (b0 maj c0))) @ c3
a2 a1 a0
(( . uma b2) uma b1) uma b0 c0
これは、自己の後始末も行う、単一の2D式での完全な3ビットアダーです。b ワイヤーには a + b が格納され、c3 にはキャリーアウトが格納され、c0 と a ワイヤーは元の状態に戻ります。(c0 = 1 に設定すると、a + b + 1 が無料で得られます。)
2つの線が互いをミラーリングしていることに注意してください。上部は右にネストして b0 まで下がり、キャリーを上にリプルバックします。下部は左にネストして b2 まで下がり、外側にアンワインドします。共有されている a1 と a0 のラベルはピボットであり、それぞれが上の maj と下の uma を務めます。そして . は垂直パイプであり、直接上にあるワイヤー、ここでは a2 を表します。キャリーチェーンは垂直にステッチされています:アンコンピュートはコンピュートに触れます、パズルのように。
他の空間言語のスピードランニング
いくつかの他の空間言語の簡単なツアーです。
Befunge
2D言語の祖父(Befunge、1993):命令ポインタが物理的に移動します