型
型は境界に書きます。 関数の中は推論に任せます。 どちらでも決まらない型は、ビルドが断って、代わりに書くものを教えます。
規則はこれだけです。 以下では、境界がどこを指すのか、推論がどこまで効くのか、検査は何を見ているのかを順に説明します。
どこに書くか
値が、それを作った関数より長く生きるとき。 あるいは方言の外に出るとき。 そこが境界です。
| 境界 | 書き方 |
|---|---|
| モジュールの 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 どうしの算術
推論はその場かぎりで、足りない情報を推測で埋めることはしません。 右辺が型を教えてくれないときは、こちらが書きます。
関数の中で注釈を書く場面は、たいていこれです。 空の入れ物から始めるときだけ、と言ってもいいくらいです。
型の一覧
| 型 | 補足 |
|---|---|
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 を実行すると、これが何になるか見られます。
インターフェースで束縛した型パラメータです。 ディスパッチは静的で、呼ばれた型ごとにコンパイラが実体を一つずつ生成します。 実行時に呼び先を探すこともなければ、型が消えることもありません。
型引数は書きません。
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 との行き来にまとめてあります。