制御フローとデータ
ツアーの続きです。 ハンドラに書けること、値としての関数、CPython と同じ意味を持つ算術、リスト、チャート、辞書、タプルを見ます。
ハンドラと制御フロー
ハンドラは三つの形で渡せます。
lambda(複数の操作はタプル lambda: (a.set(x), b.set(y)))、モジュールレベルの def、そしてストアのメソッド参照(on_click=Cart.clear)です。
デコレータもコンパイルできます。 デコレートが起きるのはインポート時ですが、コンパイル済みのアプリはモジュールを実行しません。 そこでラッパは、デコレートされたハンドラの本体に畳み込まれます。
def announced(f):
def wrapper():
status.set("working")
f()
status.set("done")
return wrapper
@announced
def save():
fs.write_text(path, body())
デコレータは引数を一つ取る def で、受け取った関数をそのまま返すか、その関数を一度だけ呼ぶラッパを定義して返します。 デコレータ自身が引数を取る形、関数を二度呼ぶラッパ、値として使うラッパは断られます。
def ハンドラの中身は、if や for などの制御フローごとコンパイルされます。
def double(v: int) -> int: # 純粋ヘルパはネイティブの関数になる
return v * 2
def tally():
total.set(0)
for i in range(1, 6):
if i == 3:
continue
total.set(total() + double(i))
if / elif / else、while(while True: も含みます)、for(range()、リストの状態、リストのフィールド、リスト型の引数)、break / continue、ローカル変数(Python と同じく再代入可)が使えます。
print(...) は stdout に書きます。
sep= と end= も Python と同じように使えます。
log("…") のほうは stderr に一行書きます。
どちらの実行も同じバイト列を書き、ゲートは画面と同じようにアプリが書いた内容も比べます。
ヘッドレス実行のダンプは PIXIE_DUMP が指すファイルに出るので、二つの出力が混ざることはありません。
assert と raise は、Python の例外と同じようにその文を終わらせます(アプリは動き続けます)。
条件には bool をそのまま書けます(if on:)。
比較の連鎖(0 < n < 10、中央は一度だけ読みます)と、:= での束縛も使えます。
比較は値でもあります。
flag.set(n() > 1)、ok = a == b、bool を返すヘルパが書けます。
条件式(a if c else b)は Python で書ける場所ならどこでも書けます。
ハンドラの中でも、ビューの中でもです。
両の枝に置けるのは int、float、str、bool の値です。
純粋ヘルパ(引数と返り値を注釈し、return 式 で終わる関数)はハンドラからもビューのテキストからも呼べます。
分岐の途中で return してよく、自分自身も呼べます。
list[...] の引数と既定引数を取り、Value クラスやリストを返せます。
ストアやモデルのメソッドは T | None を返せます。
空のほうは return None と書き、受け取る側で絞り込みます(v = Bag.pick() のあと if v is not None:)。
if と else の両方で代入したローカルは、Python と同じように分岐の後でも読めます。
def judge():
n = score()
if n > 20:
verdict = "high"
else:
verdict = "low"
grade.set(verdict) # 分岐の後で読める
片方の分岐でしか代入していないローカルを後で読むことは断られます(その分岐が実行されなければ、Python では NameError になる形だからです)。
Optional の絞り込みは walrus で書きます。
関数を値として扱う
関数はここでは値です。
ローカル変数に入れたラムダ、入れ子の def、store に持たせるコールバック、別の関数が受け取る引数として使えます。
型は Callable で書き、コンパイルされるクロージャはその注釈から作られます。
本体から型を読み取ることはないので、クロージャは必ず「何を受け取るか」を書いてある場所に置きます。
from typing import Callable
@store
class Pipeline:
n: int = 1
step: Callable[[int], int] = lambda x: x + 1 # コールバックのフィールド
def advance(self) -> None:
f = self.step
self.n = f(self.n)
def harder(self) -> None:
self.step = lambda x: x * x # 別のものに差し替える
def apply(self, g: Callable[[int], int]) -> None:
self.n = g(self.n) # 引数として受け取る
def offset() -> None:
base = Pipeline.n
def add(x: int) -> int: # 入れ子の def
return x + base
Pipeline.n = add(10)
def doubled() -> None:
twice: Callable[[int], int] = lambda x: x * 2 # ローカル変数
total.set(sum(list(map(twice, xs())))) # map が呼び出す
xs.set(sorted(xs(), key=twice)) # key= も呼び出す
クロージャは、作られた場所で値を捕まえます。
上の base は、その時点でそこにあった数です。
Python は変数のほうを捕まえるので、二つの場合に食い違いが起こります。
どちらも名前を挙げて断ります。
クロージャがすでに捕まえたローカル変数への書き込みと、ループ変数を捕まえたクロージャがその回を超えて生き残ることです。
同じ回の中で呼ぶだけなら、どちらの実行も同じ値を読むので通ります。
クロージャの中で State やフィールドを読むと、読むのはクロージャが動く時点です。
これは Python でも同じです。
ビューからクロージャは呼べません。 画面を組み立てるのは読むだけの作業で、クロージャは書き換えられるからです。 ハンドラから呼んで、答えを State に置いてください。
算術
Python の算術演算子はそのまま使えます。
+、-、* に加えて、/(結果は常に float)、//(負の無限大方向への切り捨て)、%(結果は除数の符号)、** も使えます。
どれもPython と同じ結果を返すようにコンパイルされます。
ゼロ除算やオーバーフローが起きると、その文だけが中断されます。
開発中は Python の例外として見え、リリース版でも同じ文で止まります(どちらもアプリは落ちません)。
int ** int の指数は非負のリテラルで書きます。
指数が負だと、結果の型が実行時に変わってしまうからです(負の指数は、どちらかを float にすれば書けます)。
失敗しうる / // % ** はハンドラの中で計算し、ビューには結果を渡します。
and、or、not は条件の中でそのまま使えます。
bool の値としても使えます(both.set(hot() and not cold()))。
bool 以外の値を and / or でつなぐ書き方は断られます(Python では結果が真偽値ではなく、どちらかのオペランドそのものになるためです)。
文字列
文字列は Python と同じように扱えます。
メソッド、長さ、添字とスライス、in、型変換が使えます。
name.set(raw().strip().upper())
parts.set(raw().split(","))
name.set(", ".join(parts()))
first.set(raw()[0] + raw()[1:4]) # コードポイントとスライス
n.set(len(raw()) + raw().find("a"))
if "ada" in raw().lower():
tag.set("found")
n.set(int("42") + int(2.5) + round(2.5)) # round は Python と同じ偶数丸め
算術と同じく、ここは二つの実行が別のコードを使う場所です。
開発中に走るのは CPython のメソッド、コンパイル後に走るのは同じ答えを返すように書いた Rust の双子です。
失敗の仕方まで同じで、int("x") はどちらの実行でもその文を中断します。
その二つを突き合わせるのがゲートです。
書式指定も Python のものがそのまま使え、ビューでもハンドラでも同じように書けます。
text(f"{total():,}") # 1,234,567
text(f"{ratio():.1%}") # 12.5%
text(f"{name():>10}") # 10 桁で右寄せ
text(f"{value():.2e}") # 1.50e+00
リスト、チャート、仮想化リスト
リストへの追加は「連結して置き直す」形で書きます。 リリース版では 1 要素の追記にコンパイルされるので、コピーのコストはありません。
Python のリスト操作はハンドラの中でそのまま使えます。
in、スライス、sorted / min / max / sum、内包表記、enumerate と zip、step 付きの range、二つのリストの連結です。
ローカルのリストは、注釈で要素の型を書きます(コンパイル時の型はこの注釈から決まります)。
out: list[str] = []
for i, s in enumerate(items()):
if s != "":
out = out + [f"{i}: {s}"]
items.set(sorted(out))
best.set(max(scores()))
要素が何かを問わない操作(in、スライス、+、[::-1])は、要素が値クラスでもタプルでも、どんなリストにも使えます。
比べる操作には何を比べるかが要るので、sorted と min と max は key= を取り、sorted は reverse= も取ります。
key= に渡すのは、要素を一つ取るラムダか、そういうヘルパの名前です。
並べ替えは reverse=True でも安定で、キーが等しい要素は入ってきた順のまま残ります。
by_score = sorted(players(), key=lambda p: p.score, reverse=True)
leader = max(players(), key=lambda p: p.score)
names = [p.name for p in players()]
newest = entries()[::-1]
reversed(xs) は Python ではイテレータなので、for で後ろから回すのに使います。
リストがほしいところでは、Python でもリストになる xs[::-1] を書きます。
添字は Python と同じ意味で読めます。 負の添字は後ろから数え、範囲を外れた添字はどちらの実行でもその文を中断します。
first.set(names()[0]) # 状態を読んで添字を引く
tail.set(names()[-1]) # 最後の要素(短すぎればその文が中断)
for i in range(len(Cart.items)):
Cart.items[i] = "-" # ストアの中なら `self.xs[i]` も同じ
チャートは float か int のリストを描きます。
values: State[list[float]] = State([])
line_chart(values(), height=120.0)
bar_chart(Metrics.svc_reqs, labels=Metrics.svc_names, height=100.0)
bar_chart(Books.profit, labels=Books.months, axis=True) # 負の月は 0 の線の下に垂れる
line_chart(series=Traffic.lines, colors=["accent", "#f38ba8"], axis=True)
範囲はデータ全体から決まり、常に 0 を含みます。
そのため、負の値は 0 の線の下に垂れます。
min= / max= を与えれば、範囲を固定できます。
axis=True で目盛りのラベルとグリッド線が付きます。
チャートにポインタを載せると、その下の値が読めます。棒や点のラベル(ラベルがなければ #3 のような番号)と、系列ごとの数値です。
スクリプトでは hover:<i>(i 番目の点。n 番目のチャートなら hover@n:<i>、棒グラフと折れ線グラフを通して数えます)でポインタを載せ、hover: で外します。ダンプにその読み取りが載るので、ホバーで見えるものもクリックと同じように確かめられます。
series= は list[list[float]] のフィールドを取り、線やバーの組を複数描きます。
colors= は系列ごとの色、color= は単一系列の色です(demo/charts.py)。
progress(value) はトラックを埋めます。
width= / height= が大きさ、label= が見出しです。
indeterminate=True にすると、値の代わりに区画が往復します。
どれだけかかるか分からない作業のための表示です。
行数の多いリストは list_view に渡します。
仮想化されていて、行を作る関数 row(i) は見えている範囲についてだけ呼ばれます(10 万行でも十数回)。
def row(i):
return text(items()[i])
list_view(len(items()), row, item_height=22.0, height=200.0)
list_view(len(items()), row, item_height=22.0, grow=1.0) # 親の残り高さを埋める
selected= と on_select は、表と同じようにリストでも使えます。
選ばれている行はアプリが持つ番号で、行をクリックすると、選択が勝手に動くのではなく、アプリに番号を動かしてくれと頼みます。
スクリプトは、行が表示している文字で行を選びます。
行の中身は行を作る関数が返したものなので、その行のどこかにある最初のテキストが目印になります。
on_select の無いリストはただのリストで、select: のステップはそれを数えません。
list_view(len(names()), line, item_height=28.0, height=180.0,
selected=picked(), on_select=picked.set, scroll_to=picked())
scroll_to= は、見えるところに出したい行の番号です。
スクロールの位置そのものは、スクロールしている人のものなので、アプリが言えるのはこの一言だけです。
リストは番号が変わったときにだけ従い、それ以外では表示位置に触りません。
誰かがスクロールしたリストが、画面を組み直すたびに引き戻されることはありません。
-1 は何も頼まないという意味です。
スクリプトにスクロールのステップが無いのも同じ理由です。
ダンプに出るのは頼んだ番号で、二つの実行が突き合わされるのもそれだからです。
表は、ヘッダーと列トラックを持つ list_view です。
table(columns, count, row) は、見えている行についてだけ row(i) を呼びます。
行を作る関数は、セルを列ごとに一つずつ並べた row を返します。
widths= はトラックの比率です。
selected= はその番号の行を塗り、on_select はクリックされた行のインデックスを受け取ります。
sort= / descending= はヘッダーの矢印を描き、on_sort はクリックされた列のインデックスを受け取ります。
並べ替えそのものは、アプリが自分のリストに対して行います。
スクリプトでは select:<先頭セル> で行を選び、click:<列名> でソートします(demo/roster.py)。
def cells(i: int):
return row(text(Roster.names[i]), text(f"{Roster.scores[i]}"))
table(["member", "score"], len(Roster.names), cells, widths=[2.0, 1.0],
selected=Roster.sel, on_select=Roster.pick,
sort=Roster.sort_col, descending=Roster.desc, on_sort=Roster.sort_by, grow=1.0)
行番号は int としてそのまま使えます。 テキストの中でも、条件でも、その行のハンドラの中でも読めます。
def line(i):
with row(spacing=6):
text(f"{i + 1}. {items()[i]}")
if i == Sel.idx:
text("*")
button("delete", on_click=lambda: Sel.drop(i))
list_view(len(items()), line, item_height=24.0, height=200.0)
キャンバス
キャンバスは、仮想的なピクセルを一つずつ描いていく格子です。
width と height がそのピクセル数で、scale は仮想の1ピクセルが論理ピクセル何個分かを表します。
canvas(160, 120, scale=4) は画面上で 640x480 を占めます。
描画命令はブロックの中に並べます。
with canvas(160, 120, scale=4, background=0, palette=Game.palette):
rect(Game.x, Game.y, 8, 8, 7)
circle(30, 20, 4, 12)
pixel_text(4, 4, f"SCORE {Game.score}", 7)
色はすべて数値です。
アプリが宣言した palette(16進の色のリスト)の何番目かを指す番号です。
色をパレットの番号で指すのは、ドット絵を扱う道具に共通のやり方です。
そうした道具のために書かれた描画コードは、番号を書き換えずにそのまま移せます。
範囲を越えた番号は最後の色で描きます。
見えなくなるより、間違った色が見えるほうが直せるからです。
palette が空のキャンバスはマゼンタで描きます。
命令は pixel、line、rect、rect_outline、circle、circle_outline、triangle、triangle_outline、sprite、pixel_text です。
座標は整数だけです。
ピクセルの格子に半分のピクセルはないので、浮動小数点数は断り、int(...) を書くように言います。
sprite(x, y, source, u, v, w, h) は PNG の一部を切り出して置きます。
colkey= は写さない色の番号で、flip_x= と flip_y= は左右と上下の反転です。
pixel_text はキャンバス自身が持つ 4x6 のフォントで、ピクセルの格子の上に文字を書きます。
キャンバスの中の for は普通のループです。
ループの本体が描いたものが、その場所でフレームに加わります。
with canvas(160, 120, scale=4, palette=Game.palette):
for e in Game.enemies:
sprite(e.x, e.y, "assets/sheet.png", 0, 16, 8, 8, colkey=0)
回せるのはビューが直接読めるリスト(State のセル、ストアのフィールド、モデル自身のフィールド)で、その要素はスカラーか value クラスです。
for i, e in enumerate(...) と書けば、要素と一緒に添字も受け取れます。
for i in range(2): も書けます。
こちらはその場で展開されるので、範囲には数値をそのまま書きます(64 個まで)。
展開した結果が、そのまま子要素の並びになるからです。
これらのループはキャンバスに限らず、どのコンテナの中でも同じように書けます。
描画命令は要素ではありません。
共通のプロパティは一つも取れません。
キャンバスの中のものはクリックできず、アクセシビリティのツリーではキャンバス全体が1枚の画像です。
何を描いているかを伝える手段は a11y_label= だけです。
ダンプに出るのはフレームそのもので、1命令が1行になります。
だから yokan gate は、両方の実行が描こうとした絵を比べられます。
Canvas(160x120, scale=4, bg=#000000)[
Rect(56, 100, 8, 8, #eeeeee)
PixelText(4, 4, "SCORE 1250", #eeeeee)
]
辞書
読みは .get、書きはキー単位、数えるのは len で、回し方は Python の辞書と同じです。
キーには str なら何でも書けます(リテラル、状態の読み、ループ変数)。
prices["cherry"] = 200 # キー単位の書き込み
picked.set(prices().get("apple", -1)) # 読み: 無いときは default
if "cherry" in prices(): ... # 所属
len(prices()) # 件数
def scan():
for k in prices(): # 挿入順、Python が回るのと同じ順
last.set(k)
for v in prices().values(): # 同じ順
total.set(total() + v)
for k in sorted(prices()): # キー順で回りたいとき
first.set(k)
コンパイル後の辞書はキーを入れた順を覚えているので、回すと Python と同じ順に並びます。
辞書はローカル変数にも置けます。
型を書いておくのが条件で、counts: dict[str, int] = {} と書いてから counts[k] = counts.get(k, 0) + 1 のように使います。
d[k] をそのまま読むと、無いキーのとき Python は KeyError を投げます。
だから書き方は二つです。
無いときに何を返すかを書く .get(key, default) か、Python と同じようにキーの不在を捕まえる try です。
try が捕まえるのは、名前に束ねた読みそのものです。
try:
gold = counts["gold"]
found.set(f"gold {gold}")
except KeyError as e:
found.set(f"no {e}") # e は Python と同じく 'gold' と読めます
.items() はキーと値の対で回ります。
こちらの順序も挿入順です。
リストを値に持つ辞書でグループ分けができます。
groups: State[dict[str, list[str]]] = State({})
for w in words():
groups[w[0]] = groups().get(w[0], []) + [w]
タプル
タプルは、いくつかの値を一つにまとめたものです。 Python と同じ書き方で書き、同じ読み方で読みます。
pair: State[tuple[str, int]] = State(("momo", 4))
rows: State[list[tuple[str, int]]] = State([])
def measure(word: str) -> tuple[str, int]:
return (word.upper(), len(word))
def scan():
label, n = measure("hello") # 分解
first = pair()[0] # 位置はリテラル
whole, rest = divmod(n, 3)
for name, count in rows(): # 行ごとに対
total.set(total() + count)
for key, value in prices().items(): # 辞書も対で回る
seen.set(seen() + key)
部分はそれぞれ自分の型を持つので、位置はリテラルで書きます。 計算した位置だと型が一つに決まりません。 部分は二つ以上です。 同じ形を、state にもフィールドにもリストの要素にも、引数にも返り値にも置けます。