pexels-photo-3993247.jpeg

PythonでPhantom Type(幽霊型)を使って静的にプログラムの欠陥を発見する

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

PythonでのPhantom Typeの実現

はじめに

本記事では、HaskellやScalaなどの静的型付け言語で用いられるテクニックであるPhantom TypeをPythonのType Hintシステムを利用して実現できることを示します。具体的には、mypyやpyrightなどのType Checkerを通して、Phantom Typeを活用して静的検査を行う方法について説明します。

環境

  • Python 3.8
  • PyrightのStrict Mode

Phantom Typeの簡単な解説

Phantom Typeとは、組み込み型(例: intやstr)とは異なり、コンパイルタイム(Pythonの場合はType CheckerによるType Check時)にのみ影響を与える型を、プレースホルダ的に使用するテクニックです。型による検査を利用することで、プログラム内のある種の欠陥を実行時ではなくプログラム中に発見することができます。

しかし、この説明だけではわかりにくいかもしれません。実際のユースケースを通じて、Phantom Typeの目的とその実現方法を見ていきましょう。

例と説明

やりたいこと

HTTPリクエストで何らかのデータをPOSTしたいと考えます。POSTなので、リクエスト先のURLとBodyが必要になります。

def post(url: str, body: Dict[str, Any]):
    ...

例えば/usersというURLには{name: str, age: int}/articlesというURLには{title: str, body: str}という形のBodyを送りたいとします。この場合、URLとそのBodyの形の整合性を取る必要が生じます。

通常、間違ったURLとBodyの組み合わせを弾くためにif文などで実行時に検査を行いますが、Phantom Typeを使用すると型によって静的検査で宣言的に弾くことができます。

型によるPOSTのモデリング

まず、POSTにはURLとBodyという2つの値が出てくるので、これらをモデリングします。

Body = TypeVar("Body", bound=TypedDict)

class Url(str, Generic[Body]):
    pass

URLの実態はstrなので、継承させるだけで他には何も実装しません。ただし、一つ違うのがGeneric[_]を継承させることで、型パラメータを取ることが可能な型として宣言します(この場合はBodyというパラメータ名で多相化しています)。この型パラメータはプログラムの実行には何も影響を与えず、単なるプレースホルダ=何かしらのマーキングとしてのみ振る舞います。今回はURLに対応するBodyの型をマーキングするために使用されます。また、Bodydictに厳格に型をつけるためにTypedDictを上限境界として持っています。

次に、Urlを実際のURLごとにインスタンス化します。

UsersBody = TypedDict("UsersBody", name=str, age=int)
ArticlesBody = TypedDict("ArticlesBody", title=str, body=str)

UsersUrl: Url[UsersBody] = Url("/users")
ArticlesUrl: Url[ArticlesBody] = Url("/articles")

これはUsersUrlを使用する際にはUsersBody型のdictをBodyにしなければならないということを示しています。ArticlesUrlについても同様です。

URLとBodyについてのマーキングが済んだので、これを使用して実際にPOSTを実装してみましょう。

B = TypeVar("B", bound=TypedDict)

def post(url: Url[B], body: B):
    # implementations
    ...

このメソッドのシグネチャはUrl[B]のときには同時にBodyとしてBも渡さなければならないということを宣言しています。つまり、先程用意したURLとBodyの対応付けがここで生きてくるわけです。

実際にpostをType Checkしてみましょう。

def main():
    post(UsersUrl, {"name": "john", "age": 20}) # => ok
    post(ArticlesUrl, {"title": "daily", "body": "hello"}) # => ok
    post(UsersUrl, {"name": "john", "age": "hello"}) # => bad
    post(UsersUrl, {"title": "daily", "body": "hello"}) # => bad
    post(ArticlesUrl, {"title": "daily"}) # => bad

Type Checkerで検査すると、ちゃんと対応として宣言されたdictのみを受け付けるようになっていることが分かります。

おわり

Pythonもちゃんとやればちゃんとなる。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。