2022/01/20

Pythonを使っている人のためのOCaml入門

Pythonで型を意識するなら

近年、Pythonではプリミティブな型やtypingで型アノテーションを記述し、mypyで型チェックするという流れができているようです。例えばint型を2つ受け取り、int型を返すplus関数がある場合、以下のように型アノテーションを書きます。


>>> def plus(x: int, y: int) -> int:
...     return x + y
...

しかし、これはあくまでアノテーションでしかなく、ドキュメントと同じようなもので、型を指定しているわけではありません。実際使ってみますと、


>>> plus(1, 2)
3
>>> plus("A", "B")
'AB'
>>> plus([1], [2])
[1, 2]

+演算子が使える型ならば、問題なく動作します。したがって、以下のように、引数の型を精査するように関数を書き換えると、型を指定することができます。


>>> def plus2(x: int, y: int) -> int:
...     if not (isinstance(x, int) and isinstance(y, int)):
...         raise TypeError
...     else:
...         return x + y
... 
>>> plus2(1, 2)
3
>>> plus2("A", "B")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in plus2
TypeError
>>> plus2([1], [2])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in plus2
TypeError

OCamlではどうなるでしょうか?

OCamlではdefではなく、letを使います。


# let plus x y = x + y ;;
val plus : int -> int -> int = <fun>

+演算子はint型でしか使えないので、引数の型はわざわざ書かなくてもOCamlの型推論機構によってint型であると確定します。したがって、int型以外を渡すとエラーになります。

上記の関数は以下のように書いても問題ありません。


# let plus (x : int) (y : int) : int = x + y ;;
val plus : int -> int -> int = <fun>

ちなみにOCamlでは、変数も関数もletで定義します。


# let x = 0 ;;
val x : int = 0

関数も実は以下のような変数と同じなのです。


# let plus = fun -> x (fun y -> x + y) ;;
val plus : int -> int -> int = <fun>
# plus 1 2 ;;
- : int = 3
# let plus_1 = plus 1 ;;
val plus_1 : int -> int -> <fun>
# plus_1 2 ;;
- : int = 3

Pythonで書くならば、以下のような形になるでしょう。


>>> plus = lambda x: lambda y: x + y
>>> plus(1)(2)
3
>>> plus_1 = plus(1)
>>> plus_1(2)
3
>>> plus_1(3)
4

浮動小数点数はどうなんだ?

Pythonを含め、多くの言語では整数も浮動小数点数も四則演算の演算子は同じですが、OCamlははっきりと分けられています。整数の四則演算は+ - * /で、浮動小数点数の四則演算は+. -. *. /.と、演算子にも小数点が付きます。では冪乗は****.なのか?

実はこれが違うのです。標準では冪乗は浮動小数点数のみを扱うようになっていて、**.という演算子はありません。おかしい。

ということで、標準ライブラリの諸々のおかしいやら不便やらを解決しているのが、baseライブラリで、baseをより多機能に拡張したものがcoreライブラリとなっています。

余談ではありますが、皆さん「累乗」と「冪乗」の違いご存知でしたか?私は知りませんでした。「累乗」は、指数が自然数(つまり1以上の正の整数)の場合、「冪乗」は指数に自然数以外もいけるやつだそうです。


# 2 ** 3 ;;
Error: This expression has type int
       but an expression was expected of type float
  Hint: Did you mean `2.'?
# open Base ;;
# 2 ** 3 ;;
- : int = 8
# 2. **. 3. ;;
- : float = 8.

「Real World OCaml」では基本的にbaseやcoreを使って章が進んでいきます。

リスト操作の違い

Pythonではリスト内包表記というものがあります。


>>> l = [x for x in range(10)]
>>> l
[0,1,2,3,4,5,6,7,8,9]

OCamlではどうなるでしょうか?

残念ながらリスト内包表記はないようです。したがって、上記と同じことをするには自前で何とかします。range関数を作成したり、例えば、


# let l = List.init 10 (fun y -> y) ;;
val l : int list = [0;1;2;3;4;5;6;7;8;9]

baseを使うなら、


# open Base ;;
# let l = List.init 10 ~f:(fun y -> y + 1) ;;
val l : int list = [0;1;2;3;4;5;6;7;8;9]

というような方法もあります。標準ライブラリとbaseでのちょっとした違いに、関数のラベル付き引数があるように思います。上記のコードでは~f:...のところです。私はラベル付き引数のほうがわかりやすくていいなと思っています。

まとめ

今回はPythonを比較対象としつつ、他言語と比較しつつOCamlを魅力を伝えていくシリーズを始めてみました。またRustやCommon Lispなどとの比較も面白いと思うので、今後うまくシリーズ化できるかはまだわかりませんが、ゆるゆると書いていこうと思います。