2022/05/25

PythonでRustのOption型のようなものを作ってみる

RustのOption型とは

Rustでは列挙型のOptionというものがあります。これはRustでは結構よく使うもので、値がある場合はSome(値)、無い場合はNoneというような使い方をします。

ポインタを扱える言語では、nullポインタ参照が未定義の動作で、問題を起こす可能性があるということで、さまざまな対策が必要なのですが、これを回避すべく機能としてOption型が存在します。

PythonはNoneがありますし、ポインタを扱う言語ではないですし、オブジェクトはすべて参照なので、Option型のようなものは必要ないように思います。が……

Python3.10からなんとパターンマッチング構文なるものが導入されて、近年の型アノテーションをつける流れもあり、今後、Option型のようなものがあってもいいんじゃない?という、考えに至ったので作ってみました。

久々のPythonなので、色々忘れていることもありましたが、とりあえず形にしてみました。コードはこちら

実例

Python3.10でパターンマッチング構文を使うと、以下のようなことができます。


>>> import py_option as op
>>> s = op.Option.new(123)
>>> n = op.Option.new()
>>> def f(option):
...     match option:
...         case (op.OptionType.Some, x): return x
...         case (op.OptionType.Non,): return 0
...         case _: raise TypeError
... 
>>> f(s)
123
>>> f(n)
0
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in f
TypeError

Python3.10のパターンマッチングがどれくらい使えるものなのかまだ検証しておりませんが、OCamlやRustなどの関数型言語が割と好きな私としては、うれしい機能であります。ただ、Pythonはどちらかというと関数型寄りではないので、検証次第では……

今回Optionクラスというものを作成しましたが、その際、初期化メソッドなし、すべてstaticmethodにしてみました。というのも、OCamlのモジュールのような使い方をしたかったからです。これが結構使いやすいように感じました。OCamlに慣れてきたせいかもしれませんが......

staticmethodにすることで、機能の目的が明確になるのではないかと思います。

例えば、通常のクラスのようにインスタンスを生成して使う場合、


>>> some = Option(123)
>>> some.is_some()

というような形になり、変数名次第でちょっと分かりにくくなるような気がします。複数定義する場合、変数の命名も限界になってくるように思います。しかし、staticmethodを使用している場合、どんな変数名でも必ずOption.is_some(x)というように、関数の頭にOption.が付き、その部分のコードを見ただけで、「あ、xはOptionTypeのなにかなんだな」と察しがつきます。

今後

本来Pythonでは必要ない機能かもしれません。しかし、あったらあったで何かと使えそうな気がして作ったpy_optionですが、内部ではパターンマッチ構文を使っていません。

今後パターンマッチ構文が標準的に使われるようになってきたら、そのあたりも書き換えていこうと思います。そうすると、コードもちょっとすっきりするように思います。