コンテンツにスキップ

型は境界に書きます。 関数の中は推論に任せます。 どちらでも決まらない型は、ビルドが断って、代わりに書くものを教えます。

規則はこれだけです。 以下では、境界がどこを指すのか、推論がどこまで効くのか、検査は何を見ているのかを順に説明します。

どこに書くか

値が、それを作った関数より長く生きるとき。 あるいは方言の外に出るとき。 そこが境界です。

境界 書き方
モジュールの state count: State[int] = State(0)
store と model のフィールド total: int = 0
値クラスのフィールド x: float
ヘルパーの引数と戻り値 def area_of(s: Shape) -> float:
コンポーネントの引数 def card(title: str, n: int):
@py の署名 引数すべてと戻り値

理由はどれも同じです。 コンパイル済みのアプリは、動き出す前にメモリの配置を決めてしまいます。 State[int] は 64 ビット整数を一つ置くセルであって、あとから中身を見て大きさを決める箱ではありません。

どこまで推論されるか

ローカル変数の型は、代入された値から決まります。 次のコードには注釈が一つもありませんが、型はすべて確定しています。

def restock():
    n = count()                 # int   count が State[int] なので
    label = f"{n} left"         # str   f 文字列は str
    names = items()             # list[str]
    first = names[0]            # str   list[str] の要素だから
    total = n * 2 + 1           # int   int どうしの算術

推論はその場かぎりで、足りない情報を推測で埋めることはしません。 右辺が型を教えてくれないときは、こちらが書きます。

out: list[str] = []             # 空のリストからは要素型が分からない

関数の中で注釈を書く場面は、たいていこれです。 空の入れ物から始めるときだけ、と言ってもいいくらいです。

型の一覧

補足
int float str bool 64 ビット整数と倍精度浮動小数点数。str は Python の文字列
list[T] T はスカラー、値クラス、enum、リストのいずれか
dict[str, T] キーは文字列。どちらの実行でも入れた順に回る
tuple[A, B] 要素の型を書き出した組
@value クラス データ。フィールドだけを持ち、同一性はない
@model クラス 観測され共有されるもの。参照で辿る
Enum Python の enum.Enum
Protocol インターフェース。下のジェネリクスを参照
T | None オプショナル。比較か match で取り出す
State[T] セル
Weak[T] 所有しない参照。型チェッカーからは T | None に見える

状態の持ち方は State@store@model の三つです。 どれを選ぶかは言語ツアーが案内します。

三つの検査

型を見るものは三つあり、返ってくるタイミングも、答える問いも違います。

エディタ。 型スタブが wheel に同梱されているので、pyright や Pylance は Yokan のアプリを普通の Python として扱えます。 @store はシングルトンをクラス名に束縛し、@value@model はフィールドからコンストラクタを組み立て、Weak[Node]Node | None として読めます。 書いている最中に Python の型システムがそのまま働く、ということです。

yokan check app.py この形をコンパイラが受け取れるかどうかを見ます。 コンパイラを起動しないので一秒ほどで返ってきます。 返ってくるのは、ファイル名と行と桁、それに代わりに書くものです。

$ yokan check app.py
app.py:14:5: not in the dialect — a `list[str]` local starts from a
list literal or another list of the same type
    part = line.split("\t")
           ^

ビルド。 方言の下にはコンパイラ自身の型チェッカーがあります。 前の二つがすり抜けた型は、ここで止まります。

三つのどれもゲートではありません。 yokan gate が比べるのは振る舞いです。 同じクリックを開発ビルドとリリース用のバイナリの両方で再生し、画面をバイト単位で突き合わせます。 型が答えるのは「コンパイルが通るか」、ゲートが答えるのは「二つの実行が一致するか」です。

ジェネリクス

インターフェースは typing.Protocol で書きます。 それを引数に取るヘルパーは、ジェネリック関数になります。

class Shape(Protocol):
    def area(self) -> float: ...


@model
class Circle(Shape):
    r: float = 1.0

    def area(self) -> float:
        return self.r * self.r * 3.0


def area_of(s: Shape) -> float:
    return s.area()

yokan translate app.py を実行すると、これが何になるか見られます。

fn area_of<P0: Shape>(s: P0) Float {

インターフェースで束縛した型パラメータです。 ディスパッチは静的で、呼ばれた型ごとにコンパイラが実体を一つずつ生成します。 実行時に呼び先を探すこともなければ、型が消えることもありません。

型引数は書きません。 area_of(circle) と書けば、呼び出しごとに引数から P0 が決まります。

list[T]dict[str, T]State[T] のような入れ物も型引数を取りますが、こちらは書き出します。 要素の型まで決まって、はじめて入れ物の大きさが決まるからです。

推論しないもの

次の四つは推測せずに断ります。 理由はどれも同じで、コンパイル済みのアプリが黙って選んでしまうと、その選択が CPython の答えとずれることがあるからです。

  • 空の入れ物。 要素型が分からないので、注釈を付けます。
  • 片方の枝でしか代入していないローカル。 その枝を通らなかったら Python は NameError になります。 if と else の両方で代入すれば読めます。
  • 穴に置けないローカルf"{x}" の中の float と bool は、State に持つか .2f のように桁を指定してください。 float に対して Python の str() が返すものは仕様であって、表示の都合で変えられるものではないからです。
  • T | None を返すメソッド。 store と model のメソッドが返せるのは、スカラー、リスト、値クラス、enum です。 オプショナルの戻り値はまだ入っていません。

理由付きの一覧は、ツアー末尾の今できないことにあります。

Python が要るとき

方言に収まらない関数は、@py を付ければ本物の Python のまま残せます。 そのとき値は境界を渡ります。 何が渡れるかは Python との行き来にまとめてあります。