内藤 裕二/ 12月 5, 2020/ C#, Python, 雑記

承前

BASICからC言語, C++での教育を経てC#を扱っていた旅人が、ふと迷い込んだPythonの国で不思議な出来事と出会う。
そんなポエム。

C#er, カタナシと出会う

コンピュータの中にはメモリという連続した箱があり、その箱に何かを入れることができる。
箱の大きさは決まっているが、いくつかの箱を組み合わせて使うことでひとつの箱に入らない大きさのものを入れることができる。
箱の中に何をどのように入れたかは外からではわからないので、あらかじめ箱の中にどのようにものを入れるかを決めておく必要がある。

箱につけた名前を「変数」といい、どのようにものを入れるかを「型」という。

旅人の習った教育では、これが世界の常識であり、すべての基礎となる知識だった。
だが、ふと迷い込んだPythonの国で、旅人は目を見張った。

def add(param1, param2):
    return param1 + param2

なんだこれは。
いや、関数定義であるのはわかる。
だが、引数であるparam1とparam2は どのような型 なのだ。
演算子’+’で演算をしているが、param1とparam2に対してこの演算子を適用して良いかどうか、 このコードだけでは判断できない ではないか。
眼前に現れた型のないソースコードに、旅人はしばし途方にくれた。

必死にgrepすると、どうやらこのコードを呼び出しているすべての個所で、int型の引数しか渡してはいないようだった。
だが、それが 偶然ではない という証拠はどこにもない。

反応を見てみる

旅人はこの関数と対話を試みることにした。

>>> print(add(1, 1))
2

関数は即座に答えを返した。2だ。
なるほど、int型の定数を引数に渡せば、演算子’+’によってint型の答えが返される。

>>> print(add(1.0, 1.0))
2.0

またも関数は即座に答えを返した。2.0だ。
ふむふむ、float型の定数を引数に渡せば、演算子’+’によってfloat型の答えが返される。

>>> print(add(1, 1.0))
2.0

関数は淡々と答えを返した。2.0だ。
int型とfloat型の定数を引数に渡せば、演算子’+’によってより大きな型であるfloat型の答えが返される。

>>> print(add('a', 'b'))
ab

関数の反応は変わらない。’ab’だ。
str型の定数を引数に渡せば、演算子’+’によって文字列連結されたstr型の答えが返される。
理屈としてはわかる。わかるが・・・
先ほどまで数値を受け入れていた関数が平然と文字列を受け入れるのを見ると、旅人は得体のしれない悪寒を感じた。  

もしかすると、この関数はすべてを受け入れる、万能な関数なのだろうか・・・
だとすると・・・
しばしの黙考のあと、旅人はを決して再び関数と対話した。

>>> print(add('a', 1))

TypeError: can only concatenate str (not "int") to str

なんと、関数は 例外を吐いて死んでしまった!
型の異なる引数を渡しただけで例外を吐くとは、なんと脆い関数だ!
こんな脆弱な関数は、 中身を確認しなければ恐ろしくて呼び出せない ではないか。

よみがえる思い出

旅人は、関数の吐いた例外を眺めながら、故郷の国でも似たような現象を見たことがあるのを思い出した。
あれはまだ若き日のこと。
どうしても複数の型の引数を受け付ける関数を書かなければならなかった旅人は、すべての基底クラスであるObject型を引数にとる関数を生み出した。

static Object add(Object param1, Object param2) {
    return param1 + param2;
}

旅人の作ったこの関数は大抵の場合において望んだ答えを返したが、関数の戻り値を再びキャストしなおさなければならなかったため、呼び出し側からは大変不評だった。
また、引数にはすべての型を渡すことができたため、演算子’+’が適用できない変数を渡すと、同じように例外を吐いて死んだものだった。

もちろん、いまではこんな関数は書かない。
最低限、Genericを使用して以下のような関数を書くだろう。

static T add<T>(T param1, T param2) {
    return param1 + param2;
}

この関数は、コンパイラによって、param1, param2, そして関数の戻り値が同一の型であることが保証される。
演算子’+’が適用できない型を渡せば例外を吐くだろうが、すくなくともString型とInt型を引数に渡すことはできないことは 関数の定義を見るだけでわかる

コンパイラ・・・
旅人は、久しく聞いていなかったその名を繰り返した。
故郷にいたときは、細かなtypoまであげつらう様に何度腹を立てたことか。
だが常識の一切通じない異国の空の下では、その愚直なまでの融通のきかなさも懐かしい。

蛇の国の流儀

通りがかる人影に、旅人は我に返った。
人影は、どうやらこの国の住人のようだった。

過去を振り返るよりも、いまはこの脆弱な関数との付き合い方を考えなければ。
旅人は相手の時間を奪ってしまうことを詫びながら、現在直面している問題について、どのように対処すれば良いか尋ねた。
住人は慣れた手つきで、関数の定義をこう書き換えた。

def add(param1: int, param2: int) -> int:
    return param1 + param2

なるほど、蛇の国にも型情報を付け加える手段はあったのだ。
旅人は、己の無知を恥じた。
この仕組みは Type Hints と呼ぶらしい。

住民は、親切にも、説明を加えてくれた。

言語仕様に 数年前に追加された 仕組みである。
複数の型を受け付ける事を示すためにはimport Typingして“`union“を記述する必要がある。
この記述によって mypy などの ツールを使用して 実行前に不正な呼び出しをチェックできる・・・

旅人は胸騒ぎを覚えて尋ねた。
ツールを使用していない場合は、実行前のチェックはできないということか、と。
住民はさも当たり前のように首肯し、旅人はめまいを覚えた。

それでは、ツールを使用していない場合、正しく関数を呼び出すためには、その関数の内容を確認しなければならないではないか。
途方にくれる旅人を、住民は憐れむように眺めながら、口を開いた。

そもそもint型しか引数に取らないのであれば、そのような関数名とするべきである。

旅は続く

住人の去ったあと、旅人はすぐにmypyをインストールした。
型のない国を何のよりどころもなく歩く自信など、どこにもなかった。

参考URL