OCaml の警告オプション(の一部)

szktty

SUZUKI Tetsuya

Posted on January 23, 2020

OCaml の警告オプション(の一部)

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


警告、してますか?

こんにちは、 OCaml タグの投稿が少ないのをいいことに好き勝手書いてる OCaml ビギナーです。私しか OCaml 書く人がいないんで、仕事でも好き勝手書いてます。書く人がいなければなかなかレビューを受ける機会もないので、独り言が増えました。ぼっちレビューです。

「ちょっとアンタ、ナニよこれ? 関数名もコンストラクタ名も CamelCase で書いてあるじゃない! 関数名は snake_case 、コンストラクタ名は先頭大文字の Snake_case に直しなさいヨ!」
「イヤァヨ、そんなのキモいじゃない。モジュールとコンストラクタで先頭を大文字にすんなら、ぜーんぶ CamelCase で揃えるのがモプ、すなわちモダン・プログラマーなのヨォ。あらヤダ、これって駱駝っぽくてカッコよくなくない?」
「このバカチン! 修正コミットしたらガイドラインを百遍音読しておきなさいヨ!(日本語訳はココヨ!)」
「ナニサ、アンタたちだってぶれっぶれじゃないの。モジュール名を原則 Snake_case にしてたり、コンストラクタ名を CamelCase で通す人も見たわヨォ。やぁねェ、いい歳してカビ臭いガイドラインで束縛したがる男って。 30 インチのワイドディスプレイがデフォルトの時代に 80 カラム幅って何? アンタら加齢臭がキツイのヨッ!」
「ダマらっしゃい、ミドル脂臭風情がッ! アンタみたいなペーペーはね、十年ゲザってアタシのありがたーいアドバイスを聞いてりゃいいのヨ! だいたいアンタはねェ……あらまァ、この open の羅列はナニ? これじゃどこの馬の骨に依存してんだかわかりゃしない。いい加減に警告オプションを指定おしッ! すべての open を駆逐ヨッ!」
「ハイハイ、ワカリマシタ……アァーン、警告ちゃんったら 45 もあるじゃないのォ。ンもぉメンドくさくなっちゃったわアタシぃ。こうなったらゼンブ無視よ無視」
「マッタク、最近の若いコときたら! 先人の血と汗とヒゲを何だと思ってるのかしら。ブツブツ……」

アッハイ、余計な前説にカマってないで先を進めます。バージョン 4.01 で利用できる警告の数は 45 + アルファベットの数だけあります(アルファベットは数字の別名またはグループ化です)。 45 も警告があると調べるのがしんどくて、個人的に気になったやつを調べるだけで力尽きました。

【すべてのエラーを】警告 A【消し去りたい】

すべての警告を有効にします。

……

はい死んだ! 今君のソースコード死んだよ! 「とりあえず全部エラーにしとけ」と思って -warn-error A ってやっただろ! ずらずら出てきた警告を潰そうとして手をつけたのはいいが、メッセージの意味がわかんなくて直しきれないんだろ! ビルドできなくなっちゃっただろ! あぁん? 怒ってないから正直に言ってみろ!

ハァハァ……き、貴様のようなビギナーはすべての警告を有効にしつつ、そこから厳し過ぎてコーディングがしんどいものを除くのが吉です。

警告に関するオプションは -w と -warn-error があります。オプション引数には有効にしたい警告のリストを指定するのですが…… -w のフォーマットが少し風変わりでして、任意の警告番号の範囲をまとめて有効化/無効化したり、 -warn-error を使わずに警告をエラー扱いにできます。詳しい解説書なら事細かに説明するところかもしれ……しないか。ただ、お前らビギナーはしちめんどくさいフォーマットを覚える必要はなく、消去法で警告を指定するやり方のみ知っておけばいいと思います。フォーマットはこうです。

-w A-?-?-... -warn-error A

無効にしたい警告を - で区切ります(- は警告を無効にする指示子です)。例えば、私は次のオプションを指定しています。まあ、ほぼ受け売りなんですけど(い、一応自分でも調べて選んだんだからねっ!)。

-w A-4-9-40-42-44-45 -warn-error A

このオプションは 4, 9, 40, 42, 44, 45 を 除く すべての警告を有効にし、かつエラーにします。これに 6 を加える場合もあるようですが、プロジェクトに応じて判断してください。

ちなみに、デフォルトの警告設定はこうです。

-w +a-4-6-7-9-27-29-32..39-41..42-44-45

すべての警告から 4, 6, 7, 9, 27, 29, 32 から 39, 41 から 42, 44, 45 を除くもの、つまり 1, 2, 3, 5, 8, 10 から 26, 30 から 31, 40, 43 がデフォルトで有効です。何言ってんだかわからないと思いますが、デフォルトの設定では結構適当なコードが書けちゃいます。

ちなみに、 Real World OCaml のコードのコンパイルに使われている警告設定はこうです("Compiler Warnings" のところ)。あくまで本のための警告設定ですから、実務でもこれでいいのかは知りません。

-w @A-4-33-41-42-43-34-44

【ワイルドカード】警告 4 または E【禁止令】

バリアント型に後からコンストラクタが追加されても成立するパターンマッチに対して警告します、とあります。早い話が、ワイルドカードを使った手抜きのパターンマッチを見つけるとぶん殴りに来るようです。(ドキュメントではこういうパターンマッチを "fragile pattern matching" と呼んでいるようですが……どこかで定義されているのかな?)

例:

type t =
  | A
  | B
  | C

let is_a t =
  match t with
  | A -> true
  | _ -> false

コンパイル結果:

File "e.ml", line 7, characters 2-47:
Warning 4: this pattern-matching is fragile.
It will remain exhaustive when constructors are added to type t.

関数 is_a のパターンマッチでは、 A 以外のコンストラクタをワイルドカードで無視しています。例えばコードを書き進めるうちに t 型には D も必要だったとわかったとします。

type t =
  | A
  | B
  | C
  | D

このように書き足したとしても、 is_a を変更する必要はないですね。ワイルドカードで無視しちゃうんで。この警告を有効にすると、ワイルドカードを使わずにこのように書かないといけません。

let is_a t =
  match t with
  | A -> true
  | B -> false
  | C -> false
  | D -> false

逐一こう書くようにすれば、後から追加したコンストラクタがワイルドカードによっていい加減に扱われることで発生するバグを防げそうです。しかし反比例してタイプ量が相当増えます。めんどくさいですね。めんどくさいですが、とてつもなく厳密なコーディングが要求されるような大人数参加のプロジェクトでは有用なのかもしれません(そうは言っても、パターンマッチを適当に実装されれば無駄ですが)。でもやっぱりあまりにもめんどくさいんで、私は無効にしてます。

ところで、 Menhir(OCaml 用の yacc) を使う場合には注意が必要です。 Menhir がこの警告に引っかかるコードを生成するので、 -warn-error でこの警告をエラー扱いにするとビルドが止まってしまいます。それもあって、私は無効にしてます。

【ラベル嫌いを】警告 6 または L【誅したい】

この警告を有効にすると、ラベル付き引数に対してラベルなしの引数を与えた場合に警告されます。例によって適当な例:

# let test ~x y = x * y;;
val test : x:int -> int -> int = <fun>
# test 1 2;;
Warning 6: labels were omitted in the application of this function.
- : int = 2

デフォルトではこの警告は無効にされており、警告 A を指定してもこの警告を外す人もいます。なんでしょうね、この避けられっぷりは。古参の OCaml ユーザーにはラベルが体に馴染まない方が多いんでしょうか。ただ、引数の順序を間違えるとバグにつながるからラベルを使うという側面もあるので、特に理由がないのなら、私はこの警告を有効にしておくのがいいと思います。

【俺の名前を】警告 9 または R【言ってみろ!】

レコードをパターンマッチする際、フィールドをすべて指定しないと警告されます。

とありますが……私がぼっちプログラマーだからか、めんどくささとのトレードオフを考えたときのメリットがわからないんですよね。コンパイルできた時点でレコードの型は特定されてるんじゃないの? と思ったんで、一部のフィールドの型と名前が共通するレコード型を複数用意して、デフォルトの警告セットでパターンマッチを試してみますと……

# type a = {
    a : int;
    b : int;
  };;
type a = { a : int; b : int; }

# type b = {
   a : int;
   c : int;
  };;
type b = { a : int; c : int; }

# let test { a = i } j = i + j;;
val test : b -> int -> int = <fun>

あらま、通っちゃったよ。確かにコンパイル時にレコードの型は特定されたようですが、 b が選択されています。俺が期待してたのは a なんだよ!(無茶振り失礼)

ちっ。すべての警告を有効にして再び実行してみます:

# let test { a = i } j = i + j;;
Warning 41: these field labels belong to several types: b a
The first one was selected. Please disambiguate if this is wrong.
Warning 9: the following labels are not bound in this record pattern:
c
Either bind these labels explicitly or add '; _' to the pattern.
val test : b -> int -> int = <fun>

警告 41 と警告 9 が出力されました。警告 41 のメッセージでは、パターンマッチに対応する型が複数見つかったために片方(b)を選択したとあります。

あん? レコードの型をきちんと指定させたいなら警告 41 を有効にすればいいのでは? 不備を指摘されたら型名を指定すればいいわけだし:

# let test ({ a = i } : a) j = i + j;;
val test : a -> int -> int = <fun>

いけますね。一方、警告 9 のメッセージには "Either bind these labels explicitly or add '; _' to the pattern." とある通り、抜け道があるようです。どうやらワイルドカードは使えるんですね。やってみます。

# let test { a = i; _ } j = i + j;;
val test : b -> int -> int = <fun>

……何も注意されない。この警告、有効にする意味があるんですかね?

いや待て、この警告は 9 で先の警告は 41 です。数字の差が広いってことは……ぐぐってみますと、 9 はバージョン 3.12.0 で、 41 は 4.00.0 で追加されたようです。もしかすると、 9 のよりよい警告が 41 なのかもしれません。うん? それなら若者( OCaml 歴的な意味で)は、いまさら 9 を使わずともええんちゃう?

ふむ、しかし何かが引っかかる……

うむ、どうも私はひでぇ思い違いをしているのかもしれない。もしかしてこの警告は、「複数あるレコード型のうち、いずれか一つを特定できないパターンマッチを指摘するために使う警告 ではない 」のではないでしょうか!

……。

てへぺろ☆ (・ω<)

【アウトオブ範囲だけど】警告 40【型指定さえあれば問題ないよねっ】

ドキュメントには "Constructor or label name used out of scope." とあり、その通りに読むならスコープ外のコンストラクタやラベル名が使われていれば警告します。でもここで言われているラベル名とは、どうもラベル付き引数のラベルではなく、レコードのフィールド名らしいです。私のコードではコンストラクタよりもレコードのフィールドで引っかかる場合が多かったです。

例えば、このようなモジュール定義があったとして:

module M = struct
  type t = 
    | Foo 
    | Bar 
    | Baz 
end

モジュール M とは異なる名前空間で( module M ... end 外で)、適当な変数に Foo をセットしてみますと:

# let x = Foo;;
Error: Unbound constructor Foo

コンストラクタ Foo はモジュール M 以下の名前空間にあるので失敗します。なので、 Foo にモジュール M を修飾してやればコンパイルにパスします(または open M でもいいですが、それだと名前空間を汚すのでここでは置いときます):

# let x = M.Foo;;
val x : M.t = M.Foo

しかし Foo が一箇所のみならこれでいいんですが、式中に何度も現れるといちいち修飾するのが面倒です。だったら、あらかじめ x の型を M.t に指定しておけばなんとかなりそうですよね。こうとか:

let x : M.t = ...

これなら右辺の式で Foo, Bar, Baz が使えそうです。するとどうなるか:

# let x : M.t = Foo;;
Warning 40: Foo was selected from type M.t.
It is not visible in the current scope, and will not 
be selected if the type becomes unknown.
val x : M.t = M.Foo

おっと、警告 40 はここで出るんですね。「 xM.t だと宣言してるんだから、 FooM.Foo なのは自明でしょ」と言いたくなりますが、それは利便性を優先したいユーザーの理屈であって、コンパイラはコンパイラで問題を把握しています。「べっ、別にあんたの指定した型を信用してないわけじゃないんだからねっ! ただ、 Foo がスコープ内に見当たらなくって、あんたの指定した型から仕方なく選んで……そ、そう、仕方なくよっ!」みたいな。「スコープ内に見当たらない」から 「ちゃんと名前空間を指定せい」 というのがこの警告の趣旨なんでしょうね。……多分。

私は当初この警告も指定していたのですが、修正がめんどくさくなって止めました。自分のコードでは、この警告を無視しても致命的な問題はなさそうだったし。まあ、もしかしたら私がこの警告の重要性を理解しないばかちんであるだけかもしれませんが。

ちなみに、この警告を修正しようとして open するモジュールを増やすのは逆効果です。修正するなら局所的な open を使うか( let open ... in てやつ)、素直にモジュール名を修飾しとくのがよさげです。ちなみにフィールド名を修飾するには data.Module.field と書きますが、実は Module.(data.field) とも書けるんですよ。知ってました?(ドヤァ

【そんな定義で大丈夫か?】警告 42【大丈夫だ、問題ない】

-- Disambiguated constructor or label name.

と書くとエラい人の格言みたいですね。どうでもいいって?

この警告は、一応型は決定できるけれども、同一の名前空間に同名のコンストラクタが存在する場合、または同名のフィールドが複数のレコードに存在する場合に出ます。警告 40 と同じく、ラベル名とはレコードのフィールド名のようです。

ここではレコードを例に挙げます。まったく同じフィールド名を持つレコードがいくつかあったとして(無理矢理に見えますけど実際そうしたい場合もあるんですよ! ソースは俺):

type t = {
  x : int;
  y : int;
}

type u = {
  x : int;
  y : int;
}

型を指定した上でフィールドを参照すると警告されます:

# let x (r:t) = r.x
Warning 42: this use of x required disambiguation.

(r:t) と型を指定しているのでコンパイルは通してくれますが(警告 40 が同時に指摘される場合もあります)、フィールド x は複数のレコード t u に定義されていますよ、という警告です。試しにここから型指定を外してみますと:

# let x r = r.x
Warning 41: x belongs to several types: u t

警告 41 が出て、複数の候補があると指摘されます。警告なしの設定ではコンパイル時に適当に型が選択されるわけですが、モテない男は目の前にチャンスが複数ぶら下がっていても選べない……ってやかましいわ!

……さて警告 42 を避ける方法ですが、

  • 適当な接頭辞をつけて名前の重複を避ける( t_x u_x など)か、
  • 異なる名前空間(モジュール)に振り分ける

のどちらかのようです。「いちいち型ごとにモジュールを作るの?」と訝しんだあなた、 1 モジュール 1 データ型スタイルというやり方もありますよ。 OCaml のことだし高尚な技法じゃないの?と怖がる必要はなく、「 OCaml じゃこう書いとけばメンテ楽じゃね?」程度の流行と捉えておけばいいんじゃねと思います。開発手法の流行り廃りってそんなもんでしょ?

ただし型名は修飾できないので( r.t.x とか書けない)、いくらかコードが膨らむと気軽に修正できなかったりするんですよね。実際私のコードでは時間的・能力的に修正できていません(ビギナーですからね!)。ので、現在私は無視しています。新規にプロジェクトを始めるときは有効にしておいてもいいかもしれませんね。

【お兄ちゃんどいて!】警告 44, 45【そいつ使えない!】

open したモジュールが既存の識別子(警告 44 )またはコンストラクタやラベル(警告 45 )を上書きしてしまう(重複する)場合に警告されます。例によって実用的に何の意味もない次の例では、 open した List.length によって length が上書きされてしまいます。

# let rec length l = 
    let open List in
    length l (* List.length が優先される *)
    ;;
Warning 44: this open statement shadows the value identifier length (which is later used)

# length [];;
- : int = 0

この警告、ぱっと見役立ちそうでしょ? Jane Street Core に依存している私の開発環境で有効にしてみますと、 @ 演算子を使っている箇所で引っかかります。

Warning 44: this open statement shadows the value identifier @ (which is later used)

これは Core で @ が上書きされているためで、 a @ b と書いた箇所を Core.Std.(@) a b と書き直せば修正できるのですが、今度は |> が引っかかりました。まあ Core 自体が標準ライブラリ を上書きするライブラリですから仕方がないのかもしれません。めんどくさいんで私はこの警告を無効にしてます。ついでに OCaml 4.01.0 変更点の 「 open が既存の名前を覆い隠す時に警告」の節も読んでみるといいと思います。

さて、以上は 44 の話です。 45 ではコンストラクタとラベルが対象です。あるモジュールで、私はこのような型を定義しました。

type my_result =
  | Success
  | Failure of my_error

SuccessFailure 、結果を示す型では使いたくなる名前です。ところが Failure は標準ライブラリで Failure of string と定義されているため、このモジュールを open すると警告 45 に引っかかります。

一応、 Failure を使っている箇所を モジュール名.Failure と書き換えれば修正できます。しかしそうなると、 open の利便性を捨ててモジュール名をあちこち修飾して回るか、 MySuccess とか MyFailure などの適当な接頭辞をつけて衝突を回避するか……めんどくさいんで私はこの警告を無効にしてます。が、依存するライブラリに影響がなければ有効にするのもいいのかもしれません。私はまだそこまで判断できませんので、一度試してみてはいかがでしょうか。

💖 💪 🙅 🚩
szktty
SUZUKI Tetsuya

Posted on January 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related