コンテンツにスキップ

Python との行き来

@py を付けた関数は本物の Python のままです。 方言がコンパイルするのは呼び出しのほうで、中身は開発中もコンパイル後も CPython が動かします。

@py
def stats(xs: list[float]) -> list[float]:
    import numpy as np

    a = np.array(xs)
    return [float(a.mean()), float(a.std())]

境界を渡るものは二つです。 行きの引数と、帰りの戻り値です。 このページは、その二つに何が起きるかを追いかけます。

二つの実行は対称ではありません

開発中の @py は恒等関数です。 アプリの他の部分を動かしているのと同じ CPython が、あなたのオブジェクトをそのまま引数にして関数を呼びます。 変換は一切起きません。 注釈は実行時に読まれることすらありません。

コンパイルすると、関数のソースはバイナリの中に運ばれ、埋め込みの CPython の上で動きます。 ここでは呼び出し側がネイティブコードなので、値は行きも帰りも変換されます。 その変換を決めているのが注釈です。

署名が必須なのはこのためです。 型チェッカー向けの飾りではなく、コンパイル済みのアプリが実装する契約になっています。 その契約どおりに二つの実行が同じ答えを出したことは、yokan gate が確かめます。

渡れるもの

書き方 渡るもの Rust 側
int 整数 i64
float 倍精度浮動小数点数 f64
str 文字列 行きは &str、帰りは String
bool 真偽値 bool
list[T] スカラーのリスト Vec<T>
dict[str, T] 文字列キーの辞書 HashMap<String, T>
値クラス frozen=True の dataclass 同じフィールドを持つ構造体
T | None 値、または None Option<T>

T はいずれも intfloatstrbool です。 この表にない形は、黙って別の形に直されることはなく、理由を添えて断られます。

行き

valuesState[list[float]] だとして、アプリが stats(values()) を呼んだとします。

開発実行。 State が持っているのは Python のリストです。 それがそのまま渡ります。 以上です。

コンパイル済みの実行。 呼び出し側が持っているのは、コンパイラ側のリストです。 Python がそれを見る前に、生成されたクレートが Python のオブジェクトを組み立てます。

  • int は Python の int に、floatfloat に、boolbool になります
  • str は Python の str になります
  • list[T] は、要素を一つずつ変換した Python の list になります
  • dict[str, T] は Python の dict になります
  • 値クラスは dataclass のインスタンスになります。フィールド名が同じこの dataclass は、エスケープのモジュールがこのために宣言しています
  • NoneNone のまま、T | None のもう一方は値そのものが渡ります

関数の中で手にしているのは、普通の Python のオブジェクトです。 xs はスライスもできますし、numpy に渡すことも、for で回すこともできます。

帰り

戻り値の注釈は、逆向きに読まれます。

開発実行。 return したオブジェクトが、そのままハンドラに届きます。 ここでも変換はありません。

コンパイル済みの実行。 返ってきた Python のオブジェクトを、宣言された型に読み戻します。

  • Python の intfloatbool はそのまま数値と真偽値になります
  • str はコンパイラ側の文字列になります
  • list は要素を一つずつ変換して list[T] になります
  • dictdict[str, T] になります
  • dataclass のインスタンスは、フィールドごとに値クラスになります
  • NoneT | None の空のほうになります

注釈に合わないオブジェクトが返ってきたら、たとえば署名が int なのに str を返したら、読み戻しはそこで失敗します。 適当に解釈して先に進むことはありません。 何が起きるかは例外が投げられたときを見てください。

値クラスの往復

ここでは同じ形を二か所で宣言します。 その二つが一致している必要があるので、往復をまとめて見ておきます。

@value
class Reading:
    label: str
    value: float


@py
def normalise(r: Reading) -> Reading:
    return Reading(r.label.strip().lower(), r.value / 100.0)

Yokan 側の Reading は値クラスです。 フィールドが二つあるだけで、同一性は持ちません。 コンパイルすると、同じ二つのフィールドを持つ Rust の構造体になります。 そして、エスケープのモジュールには対になる宣言が置かれます。

@dataclass(frozen=True)
class Reading:
    label: str
    value: float

これがあるので、あなたの書いた Python は Reading を組み立てることも、フィールドを読むこともできます。 フィールドは intfloatstrbool のいずれかにしてください。 リストや別の値クラスを持つ値クラスはまだ渡れません。 その場合はエラーになります。

例外が投げられたとき

try で囲んでいなければ、例外は握りつぶされません。 インタプリタが出したトレースバックがそのまま表示され、エスケープを呼んだ文はそこで終わります。 止まるのは例外を出したハンドラだけで、アプリ自体は動き続けます。

try で囲めば、どちらの実行でも同じ節が走ります。

def compute():
    try:
        n = parse(text())
    except ValueError as e:
        error.set(f"{e}")
        return
    total.set(n)

開発実行では、これは Python 自身の try です。 コンパイル済みの実行では、コンパイラが生成したディスパッチャが埋め込みインタプリタの中で例外を受け止め、どの節に当たったかとメッセージを返します。 そのおかげで f"{e}" は両側で同じバイト列になります。 このディスパッチャは、あなたが書いた except 節から組み立てられます。

task の中で

エスケープには時間のかかるものもあります。 task に入れておけば、その間もウィンドウは描き続けます。

def start():
    xs = values()
    task(lambda: stats(xs), on_done=landed, on_progress=moved)

コンパイルすると、エスケープは await されます。 描画しているスレッドではなく、エンジンのスレッドプールの上で動くということです。 エスケープの中からは、report(fraction, note) がこの task の on_progress に届きます。

@py
def transcribe(path: str) -> list[str]:
    from yokan import report

    report(0.0, "loading the model")
    ...

この import は、どちらの実行でも同じ一行です。 開発実行が読み込むのは本物のモジュールです。 コンパイル済みのバイナリでは、埋め込みインタプリタに yokan のパッケージがそもそもないので、生成されたクレートが関数一つだけの yokan モジュールを用意して、同じ経路につなぎます。

ここでひとつ制約が付きます。 タスクの処理は、アプリの状態に触れません。 UI スレッドの外で走るため、そこから State は読めないからです。 必要な値は task を始める前に読んで引数で渡し、返ってきた値は on_done で書きます。 上の例の xs = values() がそれにあたります。

渡れないもの

次のものは yokan check の時点で断られます。 エラーには、代わりに使える形が並びます。

  • 入れ子の入れ物。 署名の中の list[list[int]]dict[str, list[…]] です。 行きも帰りも一段までです。
  • 値クラスのリスト。 値クラス単体なら渡れますが、そのリストはまだ渡れません。
  • @model クラス。 model は観測され共有されるものなので、その同一性をインタプリタの境界を越えて写すことはできません。
  • str 以外をキーにした辞書
  • スカラー以外のフィールドを持つ値クラス

配るとき

@py を一つも持たないアプリは、Python を一行も積みません。 一つでも持つアプリは CPython を中に積んで配ります。 --bundle はフォルダを、--onefile は単一ファイルを作り、--app はそのどちらかを macOS の .app にします。 受け取る人は何もインストールせずに起動できます。 詳しくは検証とリリースにあります。