diff --git a/leopard_syntax.md b/leopard_syntax.md index 72b1f72..b894acc 100644 --- a/leopard_syntax.md +++ b/leopard_syntax.md @@ -1,13 +1,37 @@ # OCamlの文法とか +論文じゃないのでだらだらと書きますんで、ポイントだけでも簡潔に: + +* OCamlの文法の初心者殺しは主に「終端の開いた文法」によるもの +* インデントルールの文法は開いているようで至るところ閉じている +* OCamlの文法にインデントルールを入れてみました +* 言語に限らず後方互換製を保ったまま、簡単にインデントルールを入れられる方法 +* 実装あるよ + +# OCamleopard + +この数年、私の余暇のOCaml遊びは主にPPXをいじることだったのですが、 +PPXは手軽ですが限界があって、思いついたアイデアがPPXだけでは難しかったり、 +実装できなかったりしました。 + +そこで、今年から、またOCamlコンパイラ本体を細々といじるようになりました。 +プロジェクト名は OCamleopard (和名は「大麒麟(おおきりん)」)。 + +紹介するインデントルールは数年前にOCaml 4.02.1でまず実装したもので、 +それを引っ張り出してきてOCaml 4.05.0の改造であるOCamleopard 4.05.0に適用、 +それからOCaml4.06.0が出てしまったのでOCamleopard 4.06.0にポートしたものです。 + +OCamleopardには、こんなのがOCamlにあったらいいなと言うもので +わりと楽に実装できるが、多分本家には入らないだろうな、というものを +思いついてはちまちまと実装しているのですが、 +その紹介はまた別の機会にしたいと思います。 + # Reasonが流行っているようです -私、"OCaml"のTwitter検索タブを作って話題を監視しておるんですが、 +私、"OCaml"のTwitter検索タブを作って話題を監視しているんですが、 今年2017年のOCamlタブではReasonがわりと流行ってました。 -といっても、OCaml界隈本流ではほとんど誰も話題にしないという -変な流行りかた。 -(Reason流行ってないとか言わないでください。OCamlの流行り方と比べると -無視できないくらい流行ってます) +(Reason流行ってないとか言わないでください。 +OCamlの流行り方と比べると無視できないくらい流行ってます) Reasonって何やろ? https://reasonml.github.io/ によると、 @@ -24,20 +48,19 @@ Reasonって何やろ? https://reasonml.github.io/ によると、 * `npm`でインストールできる事にはじまって、JavaScript周りのツーリングを用意してある * OCamlの元の文法は捨てて、JavaScriptの文法に寄せてある -というものです。それ以上でも以下でもないです。まあAltJSの一つですよね多分。 +というものです。AltJSの一つですよね多分。 上記ホームページにはReasonは言語であるとは一言も書いてありません。 -中身は本当にOCamlと何も違わないので言語と名乗るのは流石に気がひけるのでしょうか。 +中身は本当にOCamlと何も違わないので、独自言語と名乗るのは流石に気がひけるのだと思います。 -OCamlの文法をJavaScriptに寄せてそのままAltJSにしてしまう、というのは -わりとアイデア賞だと思います。これは多分、非純粋で、またあまり型を書かないでよい -OCamlだからできた事なのかなあと思います。 +OCamlの文法をJavaScriptに寄せてそのままAltJSにしてしまう、というのはアイデア賞だと思います。 +非純粋、eagerで、あまり型を書かないでよいOCamlだからできた事なのかなあと思います。 -Reasonがわりと流行っているのを見ていると、機能や理論をよく知らないから新しいプログラミング言語を触らないのではなくて、単に文法が自分の慣れ親しんでいる物から離れているからという理由で忌避する人がわりといるんだなあと思いました。私はわりと多言語を触っていて、機能が魅力的であれば文法に好き嫌いはあっても拒否感までは持たないようになってしまっていたので、興味深く思いました。 +Reasonが流行っているのを見ていると、機能や理論をよく知らないから新しいプログラミング言語を触らないのではなくて、単に文法が自分の慣れ親しんでいる物から離れているからという理由で忌避する人がわりといるんだなあと思いました。私はわりと多言語を触っていて、機能が魅力的であれば文法に好き嫌いはあっても拒否感までは持たないようになってしまっていたので、興味深く思います。 -この分だと、OCamlの文法をRubyに似せたら案外使う人が出るかもしれませんね。 -ほんまかいな。 +この分だと、OCamlの文法をRubyに似せて名前を変えたら案外使う人が出るかもしれませんね! +(ほんまかいな。) -# 醜い?OCamlの文法 +# OCamlの文法ってそんなに醜いの? さて、このReasonの事が本題ではなくて、ですね。 @@ -49,9 +72,8 @@ Reasonがわりと流行っているのを見ていると、機能や理論を > OCamlの文法は見ると目が潰れる -とか自由に呟いておられたのが観測されました。 -Facebookの中の人は流石にOCamlの文法に慣れ親しんでいる人達でしょうから -ここまで言わないと思いますが、その一方で、どういう宣伝をしたのかな?と思います。 +とフリーダムに呟いておられたのが観測されました。 +どういう宣伝をしたらユーザー様はこういうこと言いだすのかな?と思います。 そんなにOCamlの文法って醜いですかね。 OCamlは現役のML実装の中で最もよくオリジナルのLCF MLの文法を守っているんですよ。 @@ -72,18 +94,23 @@ MLの授業ではエディタに書いた複数行のコードをREPLに入れ 例えば `;` は逐次評価 `e;e` に使われているから宣言終了には使えません。 `.` だと英語だと文の最後に使うものですからちょうどいいのですけれど、 浮動小数点数他に使ってしまっていて使えません。 -(その「点」PrologやErlangは頑張ってますね。No **pun** intended.)。 +(その「**点**」PrologやErlangは頑張ってますね。No **pun** intended.)。 となると句読点は全て使い切っていますので一文字では無理ってことになります。 そうなると式を逐次実行するための区切り記号`;`なのですから `;;`を逐次実行される宣言の区切りにするのはわりと理にかなっていると思うのですが。 -# 対岸のReason +# Reasonは**私には**嬉しくない じゃあ逆に私にとってReasonはどうなんだ、と言うと、これが全く響かない。 -OCamlネイティブの人にとっては、慣れ親しんでいる文法をよく知らないJavaScript風に -変えただけだし、言語機能も全く同じなので何の価値もありません。 -JavaScriptバックエンドはBuckleScriptもjs_of_ocamlも普通のOCamlから普通に -使えますし、`npm`なんか使わないし知らんガナ…というのが正直なところです。 +OCamlネイティブの人にとっては、 + +* 慣れ親しんでいる文法が、よく知らないJavaScript風に変わっただけ +* 言語機能はOCamlと全く同じなので何のプラス価値もない +* `npm`とか全く使わないので、ビルド作法からファイル配置からエコが全部わからん。わかりたくなる動機がない +* BuckleScriptは普通のOCaml文法でも使えるけど、bscコンパイラはやはり`npm`を仮定しているので既存資産を簡単にコンパイルできない(https://github.com/BuckleScript/bucklescript/issues/706) +* 既存のOCamlエコツーリングでJavaScriptバックエンドが欲しければjs\_of\_ocamlでよい + +という理由で触って嬉しいことがなにもない。 実際OCamlコミュニティ本流でReasonが話題になることってないし、 みなさんそうなんじゃないでしょうか。 @@ -92,35 +119,37 @@ JavaScriptバックエンドはBuckleScriptもjs_of_ocamlも普通のOCamlから * OCamlの構造比較`=`がReasonでは`==` * OCamlのポインタ比較`==`がReasonでは`===` -になっているのを見たところで私にはReasonは読めない書けないなと判断しました。ひっかけ問題かよ。 +になっているのを見たところで私にはReasonは読めない書けないなと判断しました。 +ひっかけ問題かよ。 JavaScriptには`==`と`===`があるから、みたいですけど、 -Reasonの`==`と`===`はOCamlの`=`と`==`の意味なんだから、 -JavaScriptに寄せたからって全然意味違うんだしやばくないですかね。 +Reasonの`==`と`===`の意味はOCamlの`=`と`==`の意味なんだから、 +JavaScriptの演算子とも意味違うんだし、やばくないですかね。 私は日頃StackOverflowでOCamlタグがついた未解答質問には出来るだけ解答を つけようと思っているのですが、 (だからわりと気軽に質問書いてください。私じゃなくても誰かが答えますから。) このところ、質問がReasonで書いてあることが増えてきました。 多分こういうOCamlプログラムなんだろうなとは思っても確信が持てないので助けてあげられず、 -せっかく新しいユーザ入ってきてもコミュニティ的に -初めから分断されてるんじゃないかと思います。 +せっかく新しいユーザ入ってきてもコミュニティ的に分断されてるんじゃないかと思います。 # OCamlプログラマのための新文法は? もしReasonがOCamlの文法の醜さを解決しているとしても、 -もともとOCaml使っている人に魅力なかったら、その人たちにはあまり意味がない。 +もともとOCaml使っている人に魅力なかったら、その人たちには意味がない。 Reasonは関数型言語の文法がどうもという人に受けたのですから、 -じゃあ関数型言語の文法に慣れている人のための新しいOCamlの文法があってもいいんじゃないかしらん、と思いまして、 +じゃあ関数型言語の文法に慣れている人のための +新しいOCamlの文法があってもいいんじゃないかしらん、と思いまして、 今年は暇な時に昔作ったものを引っ張り出して遊んでいました。 この文章はそれを紹介しようと思って書いているのですが、 -まあそれだけ紹介してもつまらんですので、読み物として書いていると、 -前置きがどんどん長くなりますね。 +まあそれだけ直接紹介してもつまらんですので、前置きがどんどん長くなりますね。 ## 終端が閉じた文法要素、開いた文法要素 -OCamlには終端を書かなくて良い開いた文法と、 -終端を明示しなければいけない閉じた文法要素があるのが -わりと美しくないなあと思っています。 +OCamlには終端を書かなくて良い「開いた文法」と、 +終端を明示しなければいけない「閉じた文法」要素があるのが +わりと美しくない、と思います。 +(開いた文法、閉じた文法というのは私が今勝手に作った用語です。 +なんか適切な用語があったら教えてください。) OCamlでは、命令型の制御構文は`for i=0 to 10 do .. done`や `while .. do .. done`などの「閉じた」文法を採用しています。 @@ -129,36 +158,48 @@ OCamlでは、命令型の制御構文は`for i=0 to 10 do .. done`や 一方、関数型言語的な機能に関する構文は`fun ... -> e`, `let ... in ...`, や `if .. then .. else ..`など、終端を書かなくてよいLCF ML由来の「開いた」文法を採用しています。 -閉じていたり、開いていたり、統一感が無い気がしますね。 -そのあたりに醜さを感じます。 +型制限は`(e : t)`と書き、閉じた文法です。 +括弧なのに、いくら文面から明らかな場合でも、省略不可。 +その一方、`let`束縛のところでは `let p : t = e` と括弧を省略できる特例があります: + +```ocaml + let (x, y) : int * int = (1,1);; +``` + +閉じていたり、開いていたり、統一感が無いですね。このあたり、醜いね。 開いた文法はLCF MLが参考にしたISWIMからのものですが、この文法は、 できればプログラムを自然言語っぽく書きたい、閉じ記号を書きたくないのですね。 それに慣れると、閉じ記号必須の文法でプログラムを書いていて `end end .. end `や`)))))`や`fi; esac`とか書かないといけなくなると -無駄だなあと感じるようになります。 +面倒だなあと感じるようになります。 `for`や`while`とか、OCamlに慣れてくると滅多に書くことはないんですが、 こいつらの最後に`done`を書かないといけないのは格好が悪い。 -`for i = 0 to n do e` とか`while true do e`でいいと思うんです。 +`for i = 0 to n do e` とか`while true do e`にすればいいのに。 -ただ、開いた文法には一つ致命的な問題があります。それは閉じる記号が無い分、 -括弧を書かないといけない場合があることです。例えば、 +## 開いた文法の問題 -``` +でも、開いた文法には一つ問題があります。 +閉じる記号が無い分、場合によっては括弧を書かないといけない場合があるのです。 +例えば、 + +```ocaml map (fun x -> x * 2) xs ``` このコードは、`fun p -> e`が`fun p -> e end`のように終端記号が必要な文法だった場合、 -``` +```ocaml map fun x -> x *2 end xs ``` -と括弧なしで書けるのですね。え?余計気持ち悪い?この例ではそうですね。 +と括弧なしで書けるのです。え?余計気持ち悪い?この例ではそうですね。 + +### 入れ子ケース では次の例ではどうでしょう(OCamlのUnixライブラリから引用、自明な整形あり): -``` +```ocaml let system cmd = match fork() with | 0 -> @@ -170,28 +211,248 @@ let system cmd = | id -> snd (waitpid_non_intr id) ``` -これはよくある`fork`+`exec`の例です。でも間違っていますね。本当は、 +これはよくある`fork`+`exec`の例です。でも間違っています。本当は、 -``` +```ocaml let system cmd = match fork() with | 0 -> - begin try + begin try (* 🐫 *) execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] with | Error _ -> sys_exit 127 - end + end (* 🐫 *) | id -> snd (waitpid_non_intr id) ``` と、中の`try .. with ..`を括弧(ここでは`begin`,`end`)で囲まないといけません。 +そうしないと最後の`| id -> ..`のケースが`try`の例外処理の部分だとパースされてしまいます。 +この例では外側のケースのパターンの型(`int`)と内側のケースのパターンの型(`exn`)が +違うので型エラーになるって気が付けますが、両方の型が同じだとコンパイラエラーにならず、 +やっかいなバグになってしまう場合があります。 + +### `if-then-else`での逐次実行 + +`if-then`や`if-then-else`も終端が開いているので初心者はバグを作りやすい: + +```ocaml +let f greeting = + if greeting then + prerr_endline "hello in heaven" + else + prerr_endline "see you"; + prerr_endline " in hell" +;; +``` + +これだと`f true`が`hello in heaven in hell`になっちゃいますね。本当は、 + +```ocaml +let f greeting = + if greeting then + prerr_endline "hello in heaven" + else begin (* 🐫 *) + prerr_endline "see you"; + prerr_endline " in hell" + end (* 🐫 *) +;; +``` + +としなければいけません。 + +これはOCamlの一番の初心者殺しポイントで、StackOverflowのOCamlの質問の大体3%くらいは +ケースや条件分岐での開いた文法のみ理解に起因するコンパイルエラーや、 +意図と異なるプログラムの動作です。 + +この問題を避けるのは簡単です。 +インデントを機械に任せれば、問題の行のところのインデント変化で、 +人間の意図と機械の理解がずれていることがすぐに理解できるのです。 +けれども、初心者の人は大抵インデントツールをインストールすることも難しい人達。 +まず`vim`とか`emacs`インストールして、その使い方から、とか始めると、 +これはもう数週間戻ってこれません。 + +なので昔、こういうのを調べるツールを作りました: +https://bitbucket.org/camlspotter/ocaml-check-indent +インデントがプログラムのパース結果と齟齬があったら警告を発するツールです。 +ただこれは結局`lint`なので、初心者は使わないし、プロは使わないよね。 +そういう虚しさがありますね。 + +### 関数適用と二項演算子 + +関数適用にも初心者罠があります。 +これはOCamlに限らず関数適用に括弧がいらない言語全てにある問題だと思うのですが、 +`f(a)`って書かずに`f a`って書きますよね。 +カリー化された高階関数の連続適用に便利だというのでこうなっているのですが、 +これも閉じた文法から開いた文法になっている。この文法慣れていないと、 + +```ocaml +fib 1+2 +``` + +を + +```ocaml +fib (1+2) +``` + +だと思って書いてしまいます。四則演算に限らず、listのconsでもよく起こります +(例: `f x::acc xs`を`f (x::acc) xs`のつもりで書いてしまう。) +これもStackOverflowで頻出する初心者質問です。 +(StackOverflow上での特定プログラミング言語の質問をちゃんと分類、カウントすると、 +かなり意味があると思います。三回生あたりのプロジェクトとしていかがでしょう。) + +これを避ける簡単な方法は、 + +> 二項演算子には必ず前後にスペースを付ける + +というものです。`fib 1 + 2`と書けば`fib (1 + 2)`とは見えにくい(はずだ)。 +機械的にこれをチェックするには`e1 e2 binop e3`と機械がパースする式において、 +`binop`の前後にスペースがあるか調べればよいはずです。 +これをlint化したことはありません。lintは誰も使わないからです。 + +このように、閉じる文法から入ってきた初心者は開いた文法に慣れていないので、 +よく引っかかってバグを出します。 + +開いた文法の問題は初心者だけに限りません。慣れた人でも次のようなプログラムの +書き方をします: + +```ocaml +let system cmd = + match fork() with + | 0 -> + try + execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] + with + | Error _ -> + sys_exit 127 + |■ +``` + +これは、上の例と同じコードを書いているその途中の段階で、 +`match`のケースを書こうと思ったプログラマが、このままケースを書くと +`try`のものになってしまうのに気づいた瞬間です。 +ここからプログラムを正しくなおすためには、 + +* カーソルを`try`にまで持っていく +* `begin`を足す +* カーソルを元の位置に戻して`|`の前に`end`を書く +* 改行とリインデント + +しなければいけません。これは内部のケースのコードが長いと、 +カーソル移動に意外と時間がかかります。 +コードをのりにのって書いているときにはイライラしますね。 +これを避けるには + +* 内側の`match`や`try`を書く前からこうなることを予見して`begin match/try`と書く。 + (意識するとわりとできるようになります。コツは、意識してください。) +* 上の状態から自動的に`begin`と`end`を挿入するカッコイイ`Elisp`か`Vimscript`などを書く +* Twitterでボヤいて誰かがツールを作ってくれるのを待つ +* OCamlを捨てて`match`が閉じた文法になっているCoqを使う + +などありますが決定打はありません。 + +### トップレベル`let` + +トップレベルの`let p = e`も初心者殺しです。 +これも宣言の後が開いているので、そのまま後ろに別の宣言ではない式を書いてしまうと +バグになります: + +```ocaml +let f x = match x with + | 0 -> "0" + | _ -> "non zero" + +printf "%s" (f 0) +``` + +これは`;;`を使って`let`宣言の終了を明示するか、 + +```ocaml +let f x = match x with + | 0 -> "0" + | _ -> "non zero" +;; (* 🐫 *) + +printf "%s" (f 0) +``` -特に開いた文法は閉じる文法から入ってきた初心者が必ず引っかかってバグを出します +式を`let`宣言にしなければいけません: + +```ocaml +let f x = match x with + | 0 -> "0" + | _ -> "non zero" + +let () (* 🐫 *) = printf "%s" (f 0) +``` + +プロは後者を使ってコード中から`;;`を消すのを好みます。 +(正直どっちでもいいです。) + +### インデンテーションツールの煩雑化 + +最後に、開いた文法を使っていると、自動インデンテーションツールを書くのが比較的面倒になります。 + +終端が明示されていれば、終端記号を見たらインデントレベルを1下げればいいな、 +とか、対応する開始記号を探して、そのインデントレベルに戻せばいいな、 +とわかるものですが、明示されていない文法ではより複雑になります。例えば + +```ocaml +let f x = match x with + | 0 -> "0" + | _ -> "non zero" + +■ +``` + +ここで、カーソルのある行のインデントはこの時点ではわからない。 +次に来るトークンで決まります: + +* `let`や`type`などのキーワードが来れば、新しいトップレベル宣言。インデントは0char +* `;;`が来たらトップレベル宣言の終わり。インデントはやはり0char +* `|` が来れば新しいケース。インデントは2chars +* それ以外は`"non zero"`に続く式。インデントは9chars + +と大体四通りに別れます。 + +OCamlには昔からEmacsのためのインデントモードとしてocaml-modeとtuareg-modeというのが +ありました。tuareg-modeの方が当時のインデントスタイルに対応していたのでわりとみんな +使っていたのですが、OCamlにいくつか新しい文法要素が付け加わるに従って、 +だんだんコードが複雑になり、誰もメンテ出来ない(OCamlプログラマは虚弱なので +Elispは書けるが他人の書いた複雑なElispを見ると死ぬ)状態になりました。 +そんな時にSMIE(Simply Minded Indentation Engine)というのがEmacsに出てきて、 +tuareg-modeをこのSMIEで実装したものができました。 +これをみなさん使い始めたのですが、これが以前と全くの非互換で、 +提案してくるインデントが合わない人には徹底して合わなかったのです。 +これを手直ししようと思ってソースを見ても、これのどこがSimply Mindedやねんという状態。 +終端が開いているのが普通のOCamlの文法にはOperator precedence grammarを +使ったSMIEは間違っていたのではないかと思います。 + +私は長い間我慢して古いtuareg-modeを使っていましたが、我慢できなくなって +`ocaml-indent`というインデント用外部ツールを作りました。 +現在では同じような`ocp-indent`が普及したおかげで、OCamlのインデント問題は +ほぼ解消されましたが、当時は自動でインデントも出来ないのなら +自動インデント捨てて、その代わりにインデントルール採用したらいいんちゃうかとかまで +思っていたものです。(そしてこの原稿につながるわけですけれども) + +## 閉じた文法の問題 + +Coqの`match`のように、閉じた文法ではこのような問題はありません。 +一方、閉じた文法の問題はタイプ数が多くなってウザくなる、ただそれだけです。 + +ただ、ウザい、というのは人間の根本の感情なので、軽視してはいけない。 +放置すると、「End hellはリファクタリングシグナル」だとか、 +「括弧に埋没する快楽」とか現状肯定に走り(それ以上いけない。 + +# じゃあOCamlの開いた文法どうするの + +このOCamlの開いた文法を一部でもどうにかしたいと思いますが、 +その前に既存研究を見ていきましょう。 ## 既存研究: CamlP4 -実はOCamlの文法を変えたものは既にあるのです。 +実はOCamlの文法を変えたものはもう一つあるのです。 CamlP4のRevised syntaxと言うものが。 が、これは実に使いにくい。たとえば、この見慣れたOCamlプログラム @@ -213,51 +474,365 @@ value rec fold_left f accu l = ; ``` -こうなります。何だこりゃ。余分についた`[`と`]`ですが、Haskellの省略可能な`{`と`}`の記号が変わって省略不可になったものと思えば大体あっています。さらに各宣言の最後は`;`が必須です。Revised syntaxは普段使いとして使いやすいOCamlの文法を提供するのが目的ではなく、OCamlのASTを操作するためのDSLとして作られたもので、文法要素の開始と終了を明示していたるところ「閉じた」文法にすることでquasiquoteを使いやすくなる効果があります。OCamlの普通の文法ではうまく書けないクォートがRevisedだと書けたはずです。でもだからってなんでこんな文法を覚えなきゃいけないの、ということで、CamlP4関連以外では全く使われずに終わりました。 +こうなります。何だこりゃ。余分についた`[`と`]`ですが、Haskellの省略可能な`{`と`}`の記号が変わって省略不可になったものと思えば大体あっています。さらに各宣言の最後は`;`が必須です。簡単に言うと、OCamlの開いた文法をかなり閉じてきています。が、無駄に予約語を変えたりしているので、使いにくいんです。 +このRevised syntaxが面倒だったという黒歴史を覚えている人は、 +OCamlの新しい文法を作るのに躊躇します。 -で、Revised syntaxが面倒だったという黒歴史をみんな覚えているので、誰もOCamlの新しい文法を作ろうという人はあまりいなかったと記憶しています。 +Revised syntaxは普段使いとして使いやすいOCamlの文法を提供するのが目的ではなく、OCamlのASTを操作するためのDSLとして作られたもので、文法要素の多くを閉じることでquasiquoteを使いやすくなる効果があります。OCamlの普通の文法ではうまく書けないクォートがRevisedだと書けたはず。でもだからってなんでこんな文法を覚えなきゃいけないの、ということで、CamlP4関連以外では全く使われずに終わりました。 ## 既存研究: F# -OCaml文法の改変としてもう一つありました。OCamlの外に。 -F# の "light syntax" です。 +OCaml文法の改変としてもう一つありました。でもこれは、OCamlの外に。 +F# の "lightweight syntax" です。 +この文法はOCamlの文法にインデントルールを入れたもので、その結果、 +`;`や終端記号が大幅に省略できるようになったものです。 + +例えばF#の文法だと、次の(OCaml)プログラムは正しい意味になります: + +```ocaml +let system cmd = + match fork() with + | 0 -> + try + execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] + with + | Error _ -> + sys_exit 127 + | id -> snd (waitpid_non_intr id) +``` + +これはわりといいんじゃないかと思います。 + +インデントルールのおかげで、OCamlでは`;;`が必要なケースでも`;;`を省略できます: + +```F# +let f x = match x with + | 0 -> "0" + | _ -> "non zero" + +printfn "%s" (f 0) +``` + +OCamlだと`printfn`の前に`;;`を入れないと、前の続きだと思われます。 + +F#ではリストの改行があれば、区切りの`;`でさえ省略できます: + +```F# +printfn "%A" [ 1 + 2 + 3 + ] +``` + +ええと、これはわりと私には気持ち悪いのですが...他の`;`が省略できるなら +リストの区切りも省略できてしかるべきだろう、ということでしょうか。 +(じゃあなんで`match`とかのケース区切り`|`は省略可能にしないんだろう... +普通にできると思うんだけど。) + +ただしインデントがプログラムの意味を規定する文法ですから注意しなければ +いけないところもでてきます。例えば: + +```F# +if false then printfn "a"; printfn "b" +``` + +と + +```F# +begin if false then printfn "a" end; printfn "b" +``` + +は違いますね。 + +このようにインデントルールを採用すると、 +開いた文法の見た目のまま開いた文法の問題が解決できます。 + +実は、この文法、**見た目開いているんですが、至るところ閉じている**んです。 +構文の始めと終わりは目には目立たないがインデントとして明示されているのですね。 + +# ぼくのかんがえたさいきょうの + +今まで書いたことをまとめると、 + +* OCamlには終端が開いた文法構造と閉じた構造があり、見た目に統一されていない +* OCamlの開いた文法では、インデント支援を使って自分の書いたプログラム制御構造を確かめながら書くもの +* だが、初心者やHaskellから流れて来た人は、制御構造をインデント支援で確かめるのを知らない、でバグる +* OCamlに慣れた人でもnested casesがあるとカーソル移動量が多くなり文句を言う +* 初めから閉じていればこう言う問題は起こらない +* End hellはいや + +## インデントルール入れよう + +インデントルールを採用しないで開いた文法のままにしていると +前述の通りの初心者殺しができてしまいます。 +インデント支援で制御構造を確認しないのなら、複雑なインデント支援を実装しても +宝の持ち腐れだし、オレオレインデントにプログラムの構造の方を合わせた方が +いっそよいです。 + +インデントルールを採用しないで閉じた文法にすると +終了記号が目立ちすぎて慣れている人には目に煩い。 +これもインデントルールで終了記号を省略可能にすると解決できます。 + +インデントルール、わりとイケている、どころではなく、唯一の解なのでは? +とこの頃思うようになりました。 + +インデントルールを入れると起こる害は、 + +* 複数行をインデントし直す時面倒 +* プログラム自動生成するときインデント数えるのめんどくさい + +ですが、前者はOffside trap mode( http://d.hatena.ne.jp/camlspotter/20100531/1275317223 )で解決済み。 + +後者は、今時、他の言語ならともかく、 +OCamlでのコード自動生成はASTレベルでやるものです。 +コードをテキストレベルで`printf`とか`^`でつなぎ合わせてコード生成する時代ではない。 +せめてquasiquoteとかでやって欲しい、ということで蹴り飛ばします。 + +だから、ぼくのていしょうするさいきょうのOCaml新文法はインデントルール!!と決めました。 +(というか前にそういうのをすでに遊びで書いていたので、それをサルベージしただけですけど) + +## 後方互換性たもとう + +ふんじゃーOCamlにインデントルール入れました、終了めでたしめでたしパチパチ +だと至極簡単でいいんですが、まだ困ることがあります。 +インデントルールそのまま入れると前の文法と非互換になるのですね。 + +新文法作ったら新文法でコード書くための移行コストが無視できないので、 +完全上位互換にしておきたいのです。 + +F#には、インデントルールが入ったlightweightと +OCamlによりよく似たインデントを気にしないverboseの2つの文法モードがあって、 +OCamlのコードでも簡単なものならverboseモードを使えばコンパイルすることができますが、 +これらを同時に混ぜて書くことはできません。 +(`#light`ディレクティブを使えば切り替えることはできますが、 +トップレベル宣言一つの中に2つの文法を混ぜることはできませんよね) + +始めから新文法で100%コード書けと言われたら多分面倒くさくてダメです。 +新文法を作ろうとしている本人でさえそんな感じなので、誰も使ってくれないと思います。 +できれば、普段は普通の文法で書くんだけれども、`match`とか後で`begin`..`end` +入れなきゃいけないから面倒くさくなりそうだなあ、というところだけ、インデントルール +使えれば嬉しい。と思う。多分。 + +そこで参考にしたのがPythonのインデントルール方式です。 +Pythonではインデントルールを使ったコードブロックに入る時に`:`を使いますよね。 +その真似をして、キーワードの後に`:`が書いてある時だけインデントルールを使うようにすれば、 +オリジナル文法のコードと新文法のコードが混在できます。例えば、 + +```ocaml +let system cmd = + match fork() with + | 0 -> + try + execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] + with: (* <= このwithはインデントルール *) + | Error _ -> + sys_exit 127 + | id -> snd (waitpid_non_intr id) +``` + +このコードでは`try`の`with:`に関してだけインデントルールが入っています。 +もし、`with:`が現れたら、その直後に`begin`を挿入し、それ以降、 +インデントが`with:`のある行よりも左にいったら、 +その直前に`end`を入れてプログラムをパースします。 +つまり、上のプログラムは次のコードと同じになります: + +```ocaml +let system cmd = + match fork() with + | 0 -> + try + execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |] + with begin (* 🐫 *) + | Error _ -> + sys_exit 127 + end (* 🐫 *) + | id -> snd (waitpid_non_intr id) +``` + +`begin`..`end`で包むのは`try`以下ではなく、`with`以下になっています。 +OCamlには`try .. with begin .. end`という構文はありませんが、 +後方互換性は崩れないので、コソっと入れてあります。 +明示的に書けないRevised syntaxの`[`と`]`とだと思えば良いです。 + +次の例では`else:`のところにインデントルールが入ります: + +```ocaml +let f greeting = + if greeting then + prerr_endline "hello in heaven" + else: (* 🐫 *) + prerr_endline "see you"; + prerr_endline " in hell" +;; +``` + +インデントに従って`begin`と`end`が挿入されます。 + +```ocaml +let f greeting = + if greeting then + prerr_endline "hello in heaven" + else begin (* 🐫 *) + prerr_endline "see you"; + prerr_endline " in hell" + end (* 🐫 *) +;; +``` + +# いろんな言語にインデントルールを入れられる + +実装で面白いのは、この`begin`や`end`の自動挿入はパーサー部ではなくて、 +字句解析部とパーサー部の間に、トークン列の変換として実装できることです。 +基本的には: + +* 特殊な`:`付きトークンが来たら`:`なしのものに変更 +* 直後に`begin`を挿入 +* 特殊な`:`付きトークンのある行のインデントレベルをスタックにプッシュ +* 改行後のインデントレベルとスタック先頭のインデントレベルを比較し、改行後のレベルが小さければ、スタックをポップし、`end`を挿入 + +これだけです。 +`do:`には`begin`は挿入せず閉じる時は`end`ではなく`done`、だとか、 +ケース区切りの`|`のインデントの扱いはちょっと特別扱いするとか、少し例外はありますが、 +インデントルールの処理はOCamlの文法を理解する必要が全くありません。 +ですので、この処理は字句解析と構文解析から独立して実装できますし、 +多分同じようにしていろんな言語に、元文法との互換性を保ったまま、 +インデントルールを簡単に入れることができるはずです。 + +## できないこと + +インデントルールつきのキーワードは、改行してインデントを変える以外、 +閉じる事ができません: + +```ocaml +prerr_endline (match Random.int 2 with: + | 0 -> "head" + | _ -> "tail") +;; +``` + +上は次のコードに変換されるのでエラーになります。 + +```ocaml +prerr_endline (match Random.int 2 with begin + | 0 -> "head" + | _ -> "tail") +end +;; +``` +`)`を見た時に`(`以降に導入された`begin`を全て閉じればいいんですが +これをやりだすとOCaml文法の枠構造`[`..`]`とか`if`..`then`とか +`struct`..`end`とかを全部変換器に教えなきゃいけない。 +できないこともないし難しくも無いのですがが、 +構文解析と同じようなことを別の場所でやることになって、 +せっかくの簡単な実装方法が複雑になってしまいます。 + +# OCamleopard ですぐ遊べます + +## インストロール + +```shell +$ wget https://raw.github.com/ocaml/opam/master/shell/opam_installer.sh -O - | sh -s /usr/local/bin +$ opam update +$ opam switch 4.06.0 +$ opam pin add leopard 'git://github.com/camlspotter/ocaml#4.06.0+leopard' +``` + +参考リンク: +* https://opam.ocaml.org/doc/Install.html +## あそびかた -昔からOCaml周りの人のジョークに、「文法はHaskellで機能はOCamlのOCamlが欲しい」と言うものがあり度々言われていることですから、OCamlの文法が古びていることは否めません。 +```shell +$ leopard +# if true then: + prerr_endline "hello" + prerr_endline "world" + ;; +``` +```shell +$ cat > x.ml << EOF +if true then: + prerr_endline "hello"; + prerr_endline "world" +;; +EOF +$ leopardc x.ml +$ ./a.out +``` +## `xxx:` に対応しているキーワード -OCamlがHaskellの文法に求めるものというと、トップレベルのletを無くすだとか、type application の順序を後置にする(int list じゃなくて list intにする)とか、なんとかして;;を無くすとか、色々あると思うのですが +* `then:`,`else:` +* `function:` +* `match`と`try`の`with:` +* `lazy:` +* `for`と`while`の`do:` +* `struct:`と`sig:` +* `object:` +* アトリビュート + `[@..]`,`[@@..]`,`[@@@..]`のインデントルールバージョンとして、 + `:@`,`:@@`,`:@@@`が使えます -Imperativeな機能に関する構文はdo done などの明示的に閉じる文法 -Functionalな機能に関する構文はfun ... -> e や let ...などの開いた文法 -複数の宣言をまとめるモジュールやクラスに関する文法は閉じる +* エクステンションポイント -と、見る人が見ると法則があるのですが、どうも受けがよくありません。 + `[%..]`,`[%%..]`のインデントルールバージョンとして + `:%`,`:%%`が使えます -特に開いた文法は閉じる文法から入ってきた初心者が必ず引っかかってバグを出します +## 対応しない、まだ対応していないキーワード -match +* `begin` -これはfunctionやwithのケースの中にケースを入れ子にする場合のカッコの付け忘れに関する問題 + いや上使えば`begin:`いらないでしょ -これはinのないトップレベルのletとinがつくローカルのletの違いがわかってないことによる問題です +* `assert` -これらの問題はちゃんとしたインデント支援があれば簡単に見つかるのですが、初心者の人はそういうツールをインストールするのも一苦労なのでSOに今日もこういう相談が来るわけです。 + やってもいいですね。あまり便利そうではないけど -わかっている人でもケース構文が入れ子になったことに気づいてから中のケース公文をbegin..endで囲ってやるのは無駄にカーソルを移動させなければいけないので面倒です。じゃあelispかなんか書いて一発で囲めるようにしろよといつも言っているんですがどうも誰もelispが書けないのかVim使ってるようなので誰も書きません。 +* `let`とか -逆に、インデントから読み取れるプログラマの意図が実際の構文木と違う場合、警告を出すlintを作ると捗ると思って昔一度書いたことがあります。たとえば + `in`や`;;`がいらなくなりますが、そのためには`let`がtoplevelかそうでないか判断する必要があり、インデント解析がOCamlの文法をもう少し「知る」必要があります。OCamlの自動インデントを書くときにもこれが必要で面倒くさい。 -というコードの場合、最後のケースはインデントからするとプログラマは外側のmatchのケースだと思って書いているようだが実際のパース結果ではそうではない。ケースのインデントが一貫してない場合は警告を出すのですね。でこれわりといいかなと思ったのですが書いた本人がそもそもそんな間違いをしないし、lintなんて基本的に誰も使わないし、わりとfalse positiveがあって放置しています。 + `let module`とかもありますね。ちなみにOCamlは`let module`,`let exception`を足したのに`let type`が無い。そして`let open M`という英語としておかしいものがある。この辺の拡張方法がadhocで嫌い。 -今まで見てきたようにOCamlの文法として +### `;` について -インデント支援がしょぼい +`;`は省略不可です。これは好みです。私がF#のコードを見て空虚感にとらわれて +出家したいニャンになるのは見た目に`;`が無いのが主なのです。 +また、初心者が`;`を忘れたからバグったとかという話がありません。 +(逆にHaskellでは`do`を忘れて必要なところに`;`が挿入されずバグる事があります。) + +テクニカルな理由としては、`;`が省略できるとすると、例えば、 + +``` +let f () = + a + b + c -といくつか問題があると思うのですが、実はこれらはすべてインデントルール入れたら解決されるんじゃね?と思いつきました。 +let g () = .. +``` + +を + +``` +let f () = + a; + b; + c + +let g () = .. +``` -とはいえOCamlの文法をそのままHaskellのようなインデントルールを入れるとそのままF#になってlight構文となんとか構文と2つの互換性のない文法になってしまいます。インデントルールありで書いたコードが旧文法で別のプログラムにパースできてしまう場合があるので、それは避けたいところてます。(これは新文法!みたいなpragmaを書くのはダサい +に変換したい場合、`c`の後に`;`を入れてはいけませんが、 +それには変換器がトップレベル宣言の終了と開始を検知できる必要があります。 +文法を変換器に教える必要があり、実装が複雑になります。 +# 結論 +* OCamlの文法の初心者殺しは主に「終端の開いた文法」によるもの +* インデントルールの文法は開いているようで至るところ閉じている +* OCamlの文法にインデントルールを入れてみました +* 言語に限らず後方互換製を保ったまま、簡単にインデントルールを入れられる方法 +* 実装あるよ diff --git a/ppx.md b/ppx.md index d3a5a70..86b3301 100644 --- a/ppx.md +++ b/ppx.md @@ -181,8 +181,7 @@ Parsetree を弄るなら Ast_mapper。あれ? 4.01.0 では class 使って * -ppx で AST 書き換え。これが標準になるので OCaml でコンパイラでなんたらという若者はおさえておくこと * Ast_mapper に肝は用意されている -* `ocaml` では動かないので `ocamlc` で予習。 +* `ocaml` では動かないので `ocamlc` で予習。 ⇐今は大丈夫 以上 - - \ No newline at end of file +