DEV Community

SUZUKI Tetsuya
SUZUKI Tetsuya

Posted on

2 1

OCaml の記号あれこれ

※この記事は 2014年03月23日 に Qiita に投稿したものです。

※ちょっと見出しと構成の一部を変えました。


おはようございます。今日も一日、よろしくお願いします。

おはようございます。今日も一日、よろしくお願いします。

大事なことなので二度コピペしました。おはようございます、 OCaml ビギナーです。先日、私のコードの diff を眺めていた CVO(最高美夢責任者)が

「くっ、我がダークヴィムマスターの戦闘力を以てしても皆目検討が付かぬ……これが神の創りし大駱駝(※ OCaml のことらしい)の力……」

とつぶやかれておりましたので、

「っふ……駱眼(※ OCaml のコードを見抜く第三の目。慈悲はない)を持たぬ者にはわからんだろう……ククク、俺の右眼が疼くわ!(花粉の季節はつらいです)」

と突き放しておきましたが、はて具体的に何がわからなかったのか聞きそびれました。ところで私は、 OCaml の実用的なコードに含まれる見慣れない記号の多さに面食らった覚えがあります。最近になってやっとだいたいの記号の意味がつかめてきましたので、メモしておきます。

以下、並び順は適当な個人的指標です。 実際の遭遇傾向とまったく関係ない のであしからず。 # で始まるサンプルコードは ocaml (トップレベル対話環境)の入力です。

レアリティ:N

(* ... *) (** ... *)

コメントです。アスタリスク二つで始まるコメント (** ... *)Ocamldoc で抽出できるらしいです。例えばこんな感じ(たいした意味もなく Real World OCaml より抜粋):

(** Bump the frequency count for the given string. *)
val touch : (string * int) list -> string -> (string * int) list

[ ... ]

リスト(list 型)を生成します。要素の区切り文字には ; を使います。

例:

# let words = ["foo"; "bar"; "baz"];;
val words : string list = ["foo"; "bar"; "baz"]

配列やリストの要素の区切りが , である言語に慣れていると、思わぬタイミングではまることがあるので要注意です。

,

タプルを生成します。ただし、タプルの型は各要素を * で区切って表します。型の定義と値の生成とで使う記号が異なるので、私はしばらくの間ときどき間違えました。

(* 型の定義 *)
type name = string * string

(* タプルの生成 *)
let johndoe = ("John", "Doe")

(* パターンマッチ *)
match s with
| (first, last) -> Printf.printf "my name is %s %s" first last

;

文の区切りです。と言っても OCaml の文法の大半はこの区切りが不要なので、コードの短い関数ではあまり使いません。すでに上で触れているように、リスト(以下)やレコードの要素を区切るのにもこの記号を使います。

しかし、なぜ区切り記号が ; だったり , だったりするのか? これには一貫性があるらしいですよ。なるほど、そう言われるとそうですね。

&&

論理積(AND)の演算子です。まあ想像つきますよね。 and もありますが、こちらは別の目的で使われる文法です。

||

論理和(OR)の演算子です。 or も予約語に含まれていますが、現在は deprecated です。

::

リストを合成する演算子です。 x :: xsxs の先頭に x を追加したリストを生成します。

'

シングルクオートは識別子に使えます。 x' と言う変数名も、 f' と言う関数名も OK です。

ですが、 'a の形の変数は定義できません。これは型変数と言って(Web で検索!)、型の表記に使われます。

|

バリアント型とパターンマッチで、複数のパターンを区切って書くのに使います。これはコードを見るのが早いですね(どれもか)。

(* バリアント型の定義 *)
type foobar =
    Foo (* | Foo としてもよい *)
  | Bar
  | Baz

(* パターンマッチ *)
match v with
| Foo -> ...
| Bar -> ...
| Baz -> ...

->

パターンマッチのガードまたは型の表記に使われます。例えば、文字列の特定の位置の文字を取得する関数 String.get の型は string -> int -> char です。

(* パターンマッチ *)
match v with
| Foo -> ...
| Bar -> ...
| Baz -> ...

()

括弧の間には何も入りません。これは unit 型と呼ばれる何も持たないタプルで……他の言語で言う void ですね。

引数に () を指定している関数を見つけたら、それは特に引数を必要としない関数です。関数は何かしら引数をつけないと呼び出せないので、型が unit -> で始まる関数は () を指定すれば呼び出せます。例えばこんなの:

# Sys.getcwd ();; (* カレントディレクトリを取得する *)
- : string = "/Volumes/Users/szktty"

エントリポイントの let () = ... もこの記号ですね。こちらは引数ではなくパターンマッチですけど。

^

文字列を連結する演算子です。 OCaml の文字列は可変ですが、拡張はできません。この演算子を使うと新しい文字列が生成されるので、連結の繰り返しには Buffer を使いましょう(これを書いているヤツは、ここに来て「アッ、俺がコピペしてた Buffer はこれだったのか!」と気づいた)。

+. -. *. /.

浮動小数点数同士を加算する演算子です。 OCaml では多重定義が禁止されているので〜と言った理屈はともかく、整数には整数同士の、浮動小数点数には浮動小数点数同士の演算子を使わなければならないため、 1 + 0.1 とか 1.5 + 3.2 のような式は書けません。

_

識別子に使える記号です。パターンマッチではワイルドカードと呼ばれますが、実は他の変数と比べて特殊な働きをするわけではありません。 _ としても v としても、任意の値にマッチします:

match exp with
| 0 -> ...
| v -> ... (* 0 以外の値にマッチする *)

ただし、この記号を変数名(または関数名)の先頭につけると、その変数はある警告をすり抜けます。通常は定義した変数をまったく使わないと未使用の変数としてコンパイラに警告されますが、そのチェックが行われません。つい let _ = と書かないように気をつけましょう。

match exp with
| 0 -> 0
| v -> 1 (* v を使っていないため、未使用の変数として警告される *)

match exp with
| 0 -> 0
| _ -> 1 (* "_" に対しては警告されない *)

もう一つ、これは別段言語仕様でも強制でもありませんが、命名の慣習として識別子(特にレコードのフィールド名)の最後に _ をつける場合があります。よく見る/やるのは end_to_ ですね。理由は単純で、同名の予約語 end to があるからです。頭ひねって別名考えても回りくどいだけだし、二文字三文字の単語を省略するとわかりにくいし、それじゃあお尻に _ をくっつけとけば……良心が疼くほどでもないっしょ?

type location = {
  start : int;
  end_ : int;
}

レアリティ:R

[| ... |]

配列(array 型)を生成する文法です。これで [...] との区別は完璧ですね!

# [|1; 2; 3|];;
- : int array = [|1; 2; 3|]

:= <-

OCaml にも破壊的代入ってやつがあります。ただしどの変数でもと言うわけにはいかず、 ref キーワードを指定した変数か、 mutable キーワードを指定した(レコードの)フィールドに限られます。前者と後者で記号がなぜだか異なりますが、見りゃわかると思うのでもう何も怖くない!

= <> == !=

等値演算子です。 Jane Street Core ライブラリを使っている場合、 ==!= を使うと次のエラーメッセージが表示されます。

Error: This expression has type int but an expression was expected of type
               [ `Consider_using_phys_equal ]

なんのこっちゃ? と思っていたのですが、 OCaml では ==!= はポインタ同士を比較する演算子です。ですから、私のように文字列を == で比較してしまうと想定外の結果ではまります(これは Jane Street Core を使っていなくても同じです)。こればかりはコンパイラもバグかどうか判別できません。

(* true にならない! *)
# "a" == "a";;
- : bool = false

あなたが求めている演算子は =<> ですね。異論は認めない。恥辱に塗れた己の黒歴史と共に ==!= を封印してドヤリングするのです(誰に

~

ラベル付き引数で使われる記号です。ラベル付き引数は使おうが使うまいがあなたの好きにすればいい機能ですが、お前のラベル定義なんざ無視してもコンパイルが通ります(そう、デフォルトの警告設定ならね)。何が言いたいのかって……面倒臭いのでリンク先を読んでください。それも面倒臭ければ -warn-error L を指定しとけば損しないです。

ちなみに私もつい最近まで警告オプションを設定していなかったため、 API リファレンスにラベルが書いてあっても理解を後回しにして(=調べるのをさぼって)引数を指定していました。もし私が OCaml のエラい人とペアプロでもしようものなら、「うわっ…君の年収、低すぎ…」と蔑みの視線が向けられること確実です(俺の年収は関係ねえだろ!)。

?

オプショナル引数=省略できるラベル付き引数 で使われる記号です。 OCaml では関数に定義より少ない引数を与えると部分適用として扱われるため、オプショナル引数の使い方に注意が必要です。

私は引数を省略できると知って、さっそく引数いらずの関数を定義しようとしたのですが、

(* 警告されつつもコンパイルは通るけれど…… *)
# let test ?(x=0) = x;;
Warning 16: this optional argument cannot be erased.
val test : ?x:int -> int = <fun>

何やら警告が出ます。しかし、たかが初見の警告程度で怯む私ではありません。 OCaml ビギナーの考える引数いらずの関数呼び出しとは……これだっ!

(* 引数がなければ呼び出せません *)
# test;;
- : ?x:int -> int = <fun>

……ですよねー。うーん、じゃあこうかなとやってみる俺、さすがビギナーです。目のつけどころが違う。

(* ダメなものはダメです。型が違うだろ! *)
# test ();;
Error: The function applied to this argument has type ?x:int -> int
This argument cannot be applied without label

むむむ……仕方がない、妥協して引数を与えてやろう。これはどうだ?

(* ラベルなし引数を与えるとエラー *)
# test 0;;
Error: The function applied to this argument has type ?x:int -> int
This argument cannot be applied without label

うっ……結局、ラベルを指定すれば呼べました。でも、もはや引数省略の意味がないですね。

(* オプショナル引数とは何だったのか *)
# test ~x:0;;
- : int = 0

どうしてこうなるのかと言えば、 OCaml では関数を呼び出すのに必ず引数が必要だからです。前述の test () は C などのメジャーな言語にある test()() 的な呼び出しベルではなくて、 () という unit 型の値を引数として渡す式として解釈されます。すっかり身に付いてしまった手癖で引数ゼロとしたつもりが、実は引数を一つ与えていたんですね。

さて、オプショナル引数にはもう一つ注意すべき点があります。ご期待通り私はまんまとトラップに引っかかってしまうのですが……(まあこれが、知ったかぶりで走り出す私のビギナーたる所以です)最後の引数をオプショナル引数にはできません。やってみると、

(* 同じ警告が出る *)
# let test y ?(x=0) = x + y;;
Warning 16: this optional argument cannot be erased.
val test : int -> ?x:int -> int = <fun>

先程と同じ警告が出ます。そうなるとやはり、

(* これじゃだめ *)
# test 1;;
- : ?x:int -> int = <fun>

(* ラベルを指定すれば OK 、だけど…… *)
# test ~x:1 2;;
- : int = 3

ビギナーの思うようには動きません。オプショナル引数は最後から二番目までに定義しましょう。暇を見つけて「部分適用」をヒントに考えてみると、あなたの OCaml レベルキャップを引き上げられるかもしれません。

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

まあ実は、どちらのミスもコンパイラオプションの -warn-error に 16 か X を指定しておけば防げるんですけどね。デフォルトでオンにしてくれてもよさそうですけどね。

レアリティ:SR

`

多相バリアントを示す記号です。多相バリアントにはこの他にも [>[< の記号があって、一見するとまあわけがわからないので後回しにしたくなりますね。

でもこれ、早めに理解しておくとよさそうです。それまで私はバリアント型を定義するのにどうも身構えてしまって戻り値に bool を使いがちでしたが、多相バリアントなら気楽にわかりやすい戻り値を使えますしね。

@@

4.01.0 から導入された演算子で、貴様の眼に映るすべての括弧を吹き飛ばす暗黒 OCa……まあ、 Haskell で言うアレ($)です。たびたび使うので、レアな記号でもないんですけど。

さてビギナーの私は、「おお括弧を省ける」と喜び勇んでこう書いてみたわけですよ。みんな一度はやるよね? あれ、やらない?

`
(* 括弧を省け……ない……だと……? *)

Some @@ x + y;;

Error: The constructor Some expects 1 argument(s),
but is applied here to 0 argument(s)
`

Someoption 型のコンストラクタなんですが、 OCaml ではコンストラクタは関数ではないため @@ が使えません。この場合は横着せずに Some (x + y) と書くほかありません。

ちなみに、私はたまに @ と間違えて型エラーを指摘されます。二文字打つのも面倒くさいし、& を用意しようかな。

|>

@@ の逆を行く演算子です。例えば x と y を足すだけの単純な関数 test があったとして、

`

let test x y = x + y;;

val test : int -> int -> int =
`

@@ を使うとこう書けます。

`

test 1 @@ 2 + 3;;

  • : int = 6 `

|> を使うとこう書けます。

`

2 + 3 |> test 1;;

  • : int = 6 `

@@ はわかったけど、 |> はどういうときに使えばいいのかって? え……ビギナーの私にそれを聞きますか。あなたの実際書くコードから無限デジャブめいたエイジングスメルが漂い始めたら考えればいいんじゃないですか? 私も本気出した暁には考えます。

レアリティ:SSR

;;

私も ocaml トップレベルは使わないのでよーけ知らんで。

@

この記号を絶対に夜一人で見てはいけない……。いいか、絶対にだ!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay