Top > OCaml > guideline
Counter: 14689, today: 1, yesterday: 1

Caml Programming Guideline 和訳

  • 原文

    本ページの和訳は未許可翻訳ドラフト版です。

    This Japanese translation document is a draft, which is not yet permitted to make public by original authors.

Japanese translation by M.Ukai (ukai at sansu dot org).


Caml プログラミングガイドライン

これはCamlプログラムの体裁を整えるための、 ベテランのCamlプログラマのコンセンサスを反映したガイドライン集です。 が、誤りや漏れの指摘は喜んで受け入れます。 コメントはここに送ってください: webmaster @ caml.inria.fr

オリジナルの英訳者: Ruchira Datta

このページに批評を加えてくれた人たちに感謝します:

Daniel de Rauglaudre, Luc Maranget, Jacques Garrigue, Damien Doligez, Xavier Leroy, Bruno Verlyck, Bruno Petazzoni, Francois Maltey, Basile Starynkevitch, Toby Moth, Pierre Lescanne.

プログラムを書くときの一般的なガイドライン

単純に、読みやすく

プログラムをタイプする時間は、それを読む時間と比較すると無視できるので、 可読性を最適にするよう努める程時間を節約できる。

今日プログラムをシンプルにするための時間を無駄にすると、その100倍の時間が 将来の非可算ともいえる多くの修正やプログラムの解読 (最初のデバッグも含めて)となって跳ね返ってくるだろう。

  • プログラムを書く法則: プログラムは1回書き、10回修正し、100回読む。プログラムをシンプルに書き、今後の変更を常に記憶にとどめ、可読性を決して犠牲にしないこと。

プログラムの書式に関するガイドライン

文法の慣習

  • スペースの法則もどき: プログラムにスペースを入れて単語を分割することをためらわないように。スペースバーはキーボード上のもっとも見つけやすいキーだから、必要に応じてどんどん叩け!

デリミタ

スペースは常に識別子のデリミタとして使うべきだ。 またスペースは演算子記号を囲うべきだ。 活版印刷でテキストを読むのを簡単にするためにスペースで単語を分離する(分かち書き)のはすばらしい進化だった。 プログラムの可読性を上げるためにこれと同じようにしよう。

組の書き方

タプルは括弧で括り、中のコンマ(デリミタ)にはスペースを後置する:

(1, 2)
let triplet = (x, y, z) ...
  • 一般に許容される例外:
    • 1組のコンポーネントの定義 : let (x, y) = ... のかわりに let x, y = ... と書いてよい。
      • 正当な理由: これはタプルを構成するのではなく、同時に複数の値を定義するから。さらに、let と = の間にあるから。
    • 同時に値のマッチングをとるとき:同時に値のマッチをとるとき、n-タプルを括る括弧は省略してよい。
      • 妥当性: これはタプルを構成するのではなく、並列にマッチをとっているから。さらに、それぞれ match と with の間、| と -> の間にあるから。
             match x, y with
             | 1, _ -> ...
             | x, 1 -> ...
             | x, y -> ...

リストの書き方

x :: 1

のように :: の両側にスペースを書く(:: は中置演算子なのでスペースで囲うべき)。 また

[1; 2; 3] 

と書く(; はデリミタなのでスペースを後置すべき)。

演算子の書き方

演算子シンボルはスペースできちんと区切るよう注意しよう。 数式が読みやすくなるだけでなく、 複数文字から構成される演算子との混同を避けられる。 (この規則の自明な例外: !と.演算子は引数との間を区切らない)

例: x + 1 や x + !y と書く。

  • 妥当性: 空白を省くと、x+1 ならわかるが x+!y は '+!' という2字の演算子(マルチキャラクタシンボル)と解釈されてしまう。
  • 批判: 相対的な演算子の優先順位を反映させるケースでは、演算子の回りのスペースを省くと式の可読性が上がる。例えば x*y + 2*z と書けば、掛け算が加算より優先するのが自明である。
  • 批判への応答: この考え方は良くない妄想だ。空白で式の意味を期待どおりに反映されることを言語は保証していないから。例えば x * z-1 と書いても (x * z) - 1 であって、 スペースをどのように解釈しても x * (z - 1) を示すことはないだろう。さらにマルチキャラクタシンボルがあり、一定の方法でこれを使う決まりを作るには問題がある: 乗算子の前後のスペースを排除すると x*!y + 2*!z となるなど。つまりスペースの操作は微妙で壊れやすい決まりであり、読み取りにくいサブリミナルメッセージのようなものだ。順序性を明白にしたいなら、言語が提供する式手段である括弧を書くことだ。
  • もうひとつ妥当性: 統一的に演算子を空白で囲うことで、複雑な特殊ケースがなくなり二項演算子を単純に扱えるようになる。実際、 (+)は空白なしに書けるが、(*)とはいかない。(* がコメントの開始と解釈されるからね。最低でも ( *) と書かないといけないし、さらに文脈によっては *) がコメントの終端と解釈されるから、それを避けるために * の後ろにもスペースを書かないといけなくなる。ここで提案した単純な規則「スペースで演算子を切り離せ」を採用するだけで、これらの困難すべてから解放される。事実、この規則に従うのが難しくないことにすぐ気づくだろう。スペースバーはキーボードの中でも最高最良の位置にあり、打鍵しやすく間違えようがないから!

長い文字列の書き方

1行に収まりきらない文字列は、慣用として各行の終端に文字列継続の識別子をおいてインデントする(行の終端の文字 \ により次の行の先頭の空白列が無視される)。

  let universal_declaration =
    "-1- Programs are born and remain free and equal under the law;\n\
     distinctions can only be based on the common good." in
  ...

(訳注: これは良くないと思う。\ の後ろに見えない空白を付けてしまったら何が起こるんだっけ。という話はC言語でもあった気がする。かわりに ^演算でくっつけるのが推奨。)

プログラムのインデント

この法則に付け足し:注意深くインデントを付けよ。本当にプログラムの意味を決めることがある。

プログラムのインデントとは、多くの強固な考え方を想起させるアートである。 ここで経験上得られた、がまだ厳しい批評にはさらされていない インデントスタイルを幾つか示す。

スタイルが妥当だとわたしが明白に思えたとき、その妥当性を示した。 一方で批判も記した。

なので、いつも提案したスタイルの中から選ぶこと。 まず絶対の規則を記した。

インデントの一貫性

インデントは一般に受け入れられているスタイルを選んで、 アプリケーション全体を通して統一的に使いなさい。

ページの幅

ページは80カラム幅です。

  • 妥当性: この幅はどの端末上でも、また標準用紙に読みやすい字体で印刷した場合でも読める幅だ。

ページの高さ

関数は1画面(およそ70行)に納めること。例外ケースは2つ、せいぜい3つ以内にする。それ以上は無茶。

  • 妥当性: 関数が1画面に収まらないと、その関数を細分化してそれぞれハンドリングする手間がかかる。画面に収まらなかった分は見失ってしまうし、折角のインデントも読めないし、正しいインデントにするのも困難だろう。

インデントの量

一般的に、プログラムの上下の行のインデント変化量はスペース1〜2個である。 インデント量を決めたらプログラム中でそれを変えないこと。

タブ

タブキャラクタ(ASCII文字9)を使用するのは、まるで推奨しない。

  • 妥当性: ある端末と別の端末でプログラムのインデントが全く違ってしまう。特にタブとスペースを混在させたインデントをすると全くおかしなものにさえなってしまう。
  • 批評: タブの目的は、プログラムの読み手がタブ送りの設定を変えるだけでインデント量を変化させられることにある。インデントは全体として正しいわけだから、読み手としてはインデント量を簡単にカスタマイズできるほうがうれしいだろう。
  • 答え: いつもタブでインデントするという、困難で不自然なことを強制しない限り、この手法はほとんど不可能だ。

大域 let ... ;; 定義のインデントの仕方

一般に、モジュール内でグローバル定義された関数本体は普通にインデントする。 しかし、このケースでは特に、定義をより強調するのも良い。

  • 普通に1つか2つのスペースでインデントする:
    • 妥当性: インデントの例外が無い
 let f x = function
   | C ->
   | D ->
   ...;;
 let g x =
  let tmp =
   match x with
   | C ->
   | x -> 0 in
  tmp + 1;;

他の慣習も許容されている。例えば :

  • 本体がパターンマッチングの時は左揃え
    • 妥当性: 定義の終わりではパターンマッチの縦棒がとぎれるので、次の定義にに移っていくのもまだ簡単だ。
    • 批評: 通常のインデントに対する不快な例外だ。
 let f x = function
 | C ->
 | D ->
 ...;;
  • 本体部は関数の定義名にぴったり揃える。
  • 妥当性: 定義の最初の行がうまく引き立つので、定義から次の定義へと簡単に移れる。
    • 批評: 右マージンをすぐに消費しきってしまう。
 let f x =
     let tmp = ... in
     try g x with
     | Not_found ->
     ...;;

let ... in のインデント

let での定義に続く式は、キーワードletと同じ位置にインデントし、 その定義に出てくるキーワード in は行末に書く。

 let expr1 = ... in
 expr1 + expr1

let 定義が続くときも、このルール、つまり 同じインデントレベルで定義を書くようにする。

 let expr1 = ... in
 let n = ... in
 ...
  • 妥当性: 「let ... in」構造の連続は、数学の教科書における仮定の集合に似ていると言えて、その仮定はみんな同じインデントレベルだから。

変化: キーワード in は計算の最後の式のあとに別個に一行で単独で書くことがある。

 let e1 = ... in
 let e2 = ... in
 let new_expr =
  let e1' = derive_expression e1
  and e2' = derive_expression e2 in
  Add_expression e1' e2'
 in
 Mult_expression (new_expr, new_expr);;
  • 批評: これは一貫性に欠ける。

if then else のインデント

複数の条件分岐

複数の条件分岐の条件が同じインデントレベルになるように書く。

     if cond1 ...
     if cond2 ...
     if cond3 ...
  • 妥当性: パターンマッチ節(これも同じインデントで揃える)と同じように扱う。

もし条件文の長さや式が短ければ例えば以下のように書く:

      if cond1 then e1 else
      if cond2 then e2 else
      if cond3 then e3 else
      e4

もし複数の条件分岐の中の式が囲いを必要とする場合(例えば複文を含むとき) はこう書く:

     if cond then begin
       e1
      end else
     if cond2 then begin
       e2
      end else
     if cond3 then ...

複数の条件分岐について他の方法も提案されている。 キーワード else から行を始める方法:

      if cond1 ...
      else if cond2 ...
      else if cond3 ...
  • 妥当性: else if を使うときには、多くの言語で使われている elsif キーワードを思い起こすようにインデントすれば良い。
  • 批評: これら条件節の取り扱いに一貫性がない。何故最初の条件だけが特別なのか?

繰り返すが、どれかスタイルを選んだら一貫して使うように。

* 単独の条件分岐

条件分岐が単独である場合については、条件式の長さや、 特に式に begin-end デリミタがあるかどうかにに応じて、 スタイルがいくつかある。

条件分岐にデリミタがある場合はいくつかのスタイルがある:

  • begin が行末にあるスタイル
       if cond then begin
        e1
       end else begin
        e2
       end
  • さもなくば、begin を行頭に持ってくる
       if cond then
        begin
         e1
        end else begin
         e2
        end

実際には、条件節のインデントは構成する式の長さに依存する。

  • cond, e1, e2 が短い時には単純に1行で書いてしまう。
     if cond then e1 else e2
  • 条件式が純粋に関数(副作用無し)で構成できる場合で1行では収まらない場合は、条件を let .. in で束縛することを主張する。
    • 妥当性: こうすれば1行の単純なインデントになってくれてもっとも読み易い。おまけに名前が付くことで読解の助けにもなる。
  • というわけで、条件式が単純に let ... in とは束縛できない、副作用をもつケースを考える。 1. e1 と cond が短いが e2 が大きい場合:
               if cond then e1 else
               e2
    2. e1 と cond が長いが e2 が小さい場合:
               if cond then
                e1
               else e2
    3. どの式も大きい場合
               if cond then
                e1
               else
                e2
    4. begin .. end デリミタがある場合
               if cond then begin
                e1
               end else begin
                e2
               end

5. e1 には begin .. end があるのに e2 が短い場合

           if cond then begin
            e1
           end else begin
            e2
           end

あるいは

           if cond then begin
            e1
           end else e2

パターンマッチングのインデント

* 大原則

パターンマッチの各節は、最初のも含め縦棒で始める。

  • 批評: 最初の縦棒は必須ではないから書く必要がない。
  • 答え: 最初の縦棒を省略するとインデントが不自然だと思う。つまり最初の場合分けのインデントが普通の行に必要な分より大きくなる。従って正しいインデントに対する役たたずな例外となる。さらにそれは、それらの節の集合全体に同じ文法を用いない、微妙に異なる例外的な文法で最初の節を書くと主張している。極めつけは、美的センスが酷く疑わしい(「疑わしい」じゃなく「醜い」と言う人もいるだろう)

全てのパターンマッチング節は、 最初の節も含め、縦棒で始め、その縦棒で揃える。

節の中の式が1行に収まらない場合は、節のなかにある矢印の直後で改行する。そして普通にその節のパターン部分の先頭に揃えてインデントする。

パターンマッチング節の矢印は並べない。

* match あるいは try

match や try はその構文の最初と節を揃える:

    match lam with
    | Abs (x, body) -> 1 + size_lambda body
    | App (lam1, lam2) -> size_lambda lam1 + size_lambda lam2
    | Var v -> 1
    try f x with
    | Not_found -> ...
    | Failure "not yet implemented" -> ...

キーワード with は行末に置く。with の前の式が1行に収まらないときには 1 行に with だけを置く:

    try
     let y = f x in
     if ...
    with
    | Not_found -> ...
    | Failure "not yet implemented" -> ...
  • 妥当性: キーワード with がある行で、プログラムはパターンマッチ部になることがわかる。

* 節の中の式のインデント

パターンマッチの矢印の右の式が長い時は矢印の後ろで切る。

   match lam with
   | Abs (x, body) ->
      1 + size_lambda body
   | App (lam1, lam2) ->
      size_lambda lam1 + size_lambda lam2
   | Var v -> 1

プログラマによっては式のうち1つでも溢れたら ルールを一般化して全ての節に適用する。すると最後の節はこうなる。

   | Var v ->
      1

他には、これをもっと押し進めて、 このルールをどのパターンマッチの節に対しても統一的に適用する。

  let rec fib = function
    | 0 ->
       1
    | 1 ->
       1
    | n ->
       fib (n - 1) + fib ( n - 2);;
  • 批評: これは単純なパターンマッチ(または複雑なマッチの中でも単純な節)においてはコンパクトじゃないし、可読性も全く向上しない。
  • (批判の)妥当性: コードの行数に比例して給料が支払われる以外にこのルールのもっともと思える理由がわからない。もしそうならCamlプログラムではバグを埋め込まないように気を付けてこのルールを採用してじゃんじゃん金を稼いでくれ。

* 匿名関数の中のパターン・マッチング

function で始まる無名関数のマッチはmatch や try と同様、 function キーワードに揃えてインデントする。

   map
    (function
     | Abs (x, body) -> 1 + size_lambda 0 body
     | App (lam1, lam2) -> size_lambda (size_lambda 0 lam1) lam2
     | Var v -> 1)
    lambda_list

* 名前つき関数のパターンマッチング

let や let rec で定義される関数中のパターンマッチングでは、 さきほどのパターンマッチングのルール(匿名関数でのもの) に従うような 妥当なスタイルが幾つかある。 インデントスタイルは大域定義の項で説明している。 どれかよいものを選べば良いが、いつも同じスタイルを使うように!

  let rec size_lambda accu = function
    | Abs (x, body) -> size_lambda (succ accu) body
    | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2
    | Var v -> succ accu
  let rec size_lambda accu = function
  | Abs (x, body) -> size_lambda (succ accu) body
  | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2
  | Var v -> succ accu

パターンマッチングでの悪いインデント

* 関数と場合分けのインデントを悲惨にをしない。

これは前述指導した通り、 match や function キーワード揃えるようなインデントを含む。 こんな風に書いてはいけない:

   let rec f x = function
                 | [] -> ...
                  ...

そうではなく、let キーワードに揃えたインデントをする:

   let rec f x = function
     | [] -> ...
                  ...
  • 正当な理由: 右マージン不足に行きあたる。美的センスも疑わしい。

* パターンマッチングの各節での -> 識別子で揃えるような悲惨なことをしない

以下のコードで例示するような パターンマッチングの矢印でいちいち揃えるのは悪い習慣だと思う:

   let f = function
     | C1          -> 1
     | Long_name _ -> 2
     | _           -> 3;;
  • 妥当性: これではプログラムのメンテナンスが面倒だ。(場合分けを追加したりするとそれまであった部分のインデントも全部変えないといけなくなる...途中で揃えるのが嫌になってしまうから、最初っから矢印で揃えない方がいいのだ!)

関数呼び出しのインデント

* 関数名でインデント:

大量の引数または複雑な引数(1行に収まらない)を伴う関数を除き、問題は起きない。 式は関数名のところ(慣習によりスペース1個か2個付け足しても良い) にインデントする。小さい引数は同一行に書いて、引数の頭で改行する。

複雑な式で構成される引数はなるべく避けること: そういう場合はでかい引数は let 構文で定義する。

  • 妥当性: インデントの問題がない。式に名前をつけることは重要なことであり、コードはもっと読みやすくなる。

追加の妥当性: 引数の評価時に副作用が発生する場合は、評価順序をわかりやすく定めるために実際 let 束縛が必要だ。

* 複雑な引数には名前を付ける

以下のかわりに、

    let temp =
     f x y z
      ``large
      expression''
      ``other large''
      expression'' in
    ...

こう書く

    let t =
      ``large
      expression''
    and u =
      ``other large''
      expression'' in
    let temp =
     f x y z t u in
    ...

* 匿名関数に名前を付ける

複雑な関数を引数に取るイテレータの場合、 let 束縛で関数を定義するほうが賢明だ。

以下のかわりに

   List.map
    (function x ->
      blabla
      blabla
      blabla)
    l

こう書く

   let f x =
    blabla
    blabla
    blabla in
   List.map f l
  • 妥当性: 特に関数に名前を付けることは重要で、よりわかりやすい。

演算子のインデント

演算子が複雑な引数を取るとき、または同じ演算子を何度も呼び出すときは、 演算子が行末にくるようにし、残りの演算はインデントしない。 例えば (末尾に付いている垂直線 | で示したところが行の右端であるとする)、

x + y + z + |
t + u       |
  • 妥当性: 演算子が行末にあれば、演算が次の行まで続くことは自明だ。

このような演算列において「長い式」がある場合は、 そこでインデントするよりも、その「長い式」を let...in 構文で定義するほうが望ましい。

以下のかわりに

 x + y + z +        |
 ``large            |
   expression''     |

こう書く

 let t =
  ``large           |
    expression'' in |
 x + y + z + t      |

演算を組み合わせる場合、 一つの演算に書き下せない程長い式になってしまうときは 確実に束縛すること。 こんな読みにくい式ではなく

 (x + y + z * t) /    |
 (``large             |
   expression'')      |
 

こう書く

 let u =
  ``large             |
   expression'' in    |
 (x + y + z * t) / u  |

この指針は全部の演算子に拡張される。例えば

 let u =
  ``large          |
   expression'' in |
 x :: y ::         |
 z + 1 :: t :: u   |

プログラミングガイドライン

プログラミングの方法

   Always put your handiwork back on the bench,
   and then polish it and re-polish it. 
   手作りの作品は常に作業台の後ろにおいておき、
   磨いて磨いてまた磨くのだ。

簡潔明瞭なプログラムを書く

書いたら読み返して、もっとシンプルに明解にする。 創造するどの段階でも頭を使え!

プログラムは小さな関数に細分化する

小さい関数のほうがより把握しやすい。

別々の関数で繰り返し定義されているコードを括り出す

コードの共有化は、 コード修正や改良すればそれがプログラム全体にに勝手にひろがるわけなので、 メンテナンスの容易性を獲得する。 さらに、 コード部分を分離して命名する、そんな単純なことで、 気づかなかった特徴を特定できるようになる。

コードをコピペしない

コードを貼り付ける行為は、ほぼ確実に、 コード共有化の不履行あるいは、 役に立つ補助関数を書いたり確認したりするのを放棄する ことを示している... 従ってこの行為は、プログラムのコード共有が失われていることを意味する。 コード共有をサボることはその後に控えるメンテナンスでより多くの問題を 抱えることを暗示している。 ペーストしたコードにバグがあれば、 バグが出るたびにコピーしたコード全部をそれぞれ修正しないといけない!

そのうえ10行で構成されたコードがプログラム中で20回も繰り返されていたら、 特定するのも難しい。 それとは対照的に、 その10行を補助関数として定義すれば、それらがどこで使われるのかを見つけるのは いともたやすい。単純な関数呼び出しになってるから。 もしいたるところでコードをコピペしようものなら、 プログラムの理解はさらに難しい。

結論としては、コピペしたコードは、 プログラムを読むのもメンテナンスするのも難しくするから、 追放しないとけない。

プログラムのコメントの付け方

難しい箇所があるときにはコメントを遠慮なく付ける

全然難しくないならコメントする意味はない。

関数の本体内でのコメントは避ける

関数の最初に一つコメントを付けるのが好ましい。

その関数で使われているアルゴリズムがどう動くのか説明する。 繰り替え図が、全然難しくないのならコメントする意味はない。

有毒なコメントを避ける

有毒なコメントとは、なんの付加価値もないコメント、つまり トリビアな情報しかない状態だ。 有毒なコメントには明らかに利子がない。 無駄に読者の興味を逸らすから迷惑だ。

ソフトウェアメソトロジーと呼ばれる類の奇妙な評価基準、 で計ることがしばしばある。 例えば コメントの量/コードの行数 の比は完璧に計測できるが、 私はこれの理論的、実用的な解釈を知らない。 有毒なコメントを絶対に避けること。

避けるべき例: 以下のコメントは、専門用語を使い、 関心を示す追加情報もなく、結果として本当のコメントのふりをしている。

(*
 function print_lambda:
 prints a lambda-expression passed as an argument.

 Arguments: lam, a lambda expression of any kind.
 Results: none.

 Note: print_lambda can only be used for its side effect.
*)
let rec print_lambda lam =
 match lam with
 | Var s -> printf "%s" s
 | Abs l -> printf "\\ %a" print_lambda l
 | App (l1, l2) ->
    printf "(%a %a)" print_lambda l1 print_lambda l2;;

モジュールインタフェースの使いかた

関数の使いかたは、プログラムのなかの実装に表れるのではなく、 外部とやりとりするモジュールインタフェースに表れなければならない。

Caml システムのインタフェースモジュールのようなコメントをつけること。 そうすると後でモジュールインタフェースの文書化の際に、 必要に応じて自動的に展開してくれるかもしれない。

assert を使う

できる限り assert を使う: 実行時に有益な検証をしてくれる上に冗長なコメントをしなくて済む。

例えば関数の引数の条件の有効性は assert により有効に検証される。

let f x =
 assert (x >= 0);
 ...

また assert はコメントよりも信頼できることが多いのに注意。 assert は実行時にその度に検証するので、適切であることを強制されるが、 コメントはすぐに時代おくれになり、その後のプログラムの読解に 有害になってしまう。

命令型コードには1行ずつコメントを付ける

難しいコード、特に 大量にメモリの破壊型変更(配列などのデータ構造のような物理的変更)をする 命令型のコードを書くときは、 コーディングしたアルゴリズムの実装の説明 をその関数本体内にコメントすべきだ。 さもなくばその関数のメンテナンスのために不変式の連続した変更を 余儀なくされる。 繰り返すが難しいところがあるなら、 コメントを必要に応じてプログラムの各行に書くべきだ。

識別子の付け方

プログラムの該当部分の意味を喚起するような 識別子の名前を付けるのは大変だ。 用語の明解さと規則性を強調することを特に注意しないといけない。

グローバル名に略語を使わない

グローバルな識別子(特に関数名)は長くて良い。 関数定義されたところから遠くはなれたところで使うときに、 その関数の目的を理解するために重要だ。

単語の区切りはアンダースコア (intOfString ではなく int_of_string)

Camlでは大文字小文字の変更は意味がある。 大文字で始まる単語は OCaml では コンストラクタとモジュール名に予約されており、反対に 普通の変数(関数や識別子)は小文字で始まらないといけない。 これらの規則により、識別子における単語の区切りに大文字を使えない (最初の単語は識別子の始まりだ)。 ゆえに小文字であるべきであり、関数名に IntOfString を選択するのは禁止。

同じ意味の関数引数には同じ名前を常に付ける

必要に応じ、用語体系をファイルの先頭でコメントに明示する。 同じ意味の複数の引数があるなら、それらに数字でサフィックスをつける。

ローカルの識別子は短くてよく、別の関数に使いまわせるように。

これはスタイルの規則性を推進する。 l や O のような、1 や 0 と見間違えやすい識別子は避ける。

例:

let add_expression expr1 expr2 = ...
let print_expression expr = ...

識別子における単語の大文字区切り非推奨の例外として、 この命名規則を使う既存のライブラリとのインタフェースを取る場合には 許容される。 そのライブラリを使う Caml ユーザーは、 オリジナルのライブラリのドキュメントから容易に順応できる。

式中の括弧の使いかた

括弧は重要だ:普通ではない優先順位を用いる必要があることを示している。 だから賢く使うべきであり、 プログラム中に手当たり次第にまき散らすべきではない。 そのためには、普通の優先順位、すなわち、 括弧が不要な演算の組み合わせを知る必要がある。 とても幸いなことに、 ちょっと数学を知っているか、以下の規則に従うよう努力すれば、 これは複雑なことではない。

算術演算子: 数学での規則と同じ

例: 1 + 2 * x は 1 + (2 * x) の意味。

関数の適用: 数学で三角関数を使うときと同じ

数学で sin x と書けば sin (x) のことだ。 同様に sin x + cos x は (sin x) + (cos x) であって sin (x+(cos x)) ではない。 Caml でもこれと同じ約束事が使える: f x + g x は (f x) + (g x) のことだ。

この約束事は、全ての中置記法の演算子に一般化できる: f x :: g x は (f x) :: (g x) のこと、 f x @ g x は (f x) @ (g x) のこと、 failwith s ^ s' は (failwith s) ^ s' であって failwith (s ^ s') じゃない。

比較と論理演算子:

比較は中置演算子なのでさっきの規則が適用されるから、 f x < g x は (f x) < (g x) だ。 同じ形式で(他の解釈は存在しない)、 式 f x < x + 2 は (f x) < (x + 2) の意味になる。 同様に f x < x + 2 && x > 3 は ( (f x) < (x + 2) ) && (x > 3)だ。

論理演算子の優先順位は数学のそれと同じ

数学者はこの場合括弧を酷使する傾向があるが、 論理演算子の "or" は加算と同じようなもので、 "and"は乗算と同じようなものだ。 だから、1 + 2 * x が 1 + (2 * x) であるのとまさに同様に、 true || false && x は true || (false && x) のことだ。

デリミタ(終端子)のつけかた

プログラム中でデリミタ構文が必要であるときには、 括弧よりも begin ... end キーワードをデリミタに使う。 だが、一貫して、つまり統一的な方法で使うのであれば、括弧を使うのも許容される。

この構文の明示的な区切りは、本質的にはパターンマッチ構文、 あるいはif-then-else 構文に埋め込まれるシーケンス に関連する。

match 構文内のmatch 構文

パターンマッチング節のなかに match...with や try...with が現れるときは、 この埋め込まれた構文のデリミタが必須である (さもなくば、外側のパターンマッチング構文の従属節が自動的に 内側パターンマッチ構文に関連づけられてしまう) 。例えば:

match x with
| 1 ->
  begin match y with
  | ...
  end
| 2 ->
...

if の分岐内のシーケンス

同様に、条件文の then または else 節の中のシーケンスにも デリミタが必須。

if cond then begin
 e1;
 e2
end else begin
 e3;
 e4
end

モジュールの使いかた

モジュールの分割

プログラムは筋の良いモジュールに分割しなければならない。

モジュールのぞれぞれに、インタフェースを明示しなければならない。

インタフェースのそれぞれに、モジュールで定義された諸々 (関数、型、例外など)をドキュメント化しなければならない

モジュールのオープン

open ディレクティブは使わない。代わりに、識別子修飾記法を使う。 ゆえに、短いが意味の通ったモジュール名を好むことになるだろう。

  • 妥当性: 修飾されない識別子を使うとあいまいになり、意味上の誤りを検出するのが困難になる。
let lim = String.length name - 1 in
...

let lim = Array.length v - 1 in
...

... List.map succ ...

... Array.map succ ...

モジュールを閉じたままにするか open するか

環境を変更したり重要な関数群の別のバージョンを持ちこむ モジュールを open するのが普通かどうかを考えてみる。 例えば、Format モジュールは自動のインデント表示を供給する。 このモジュールは print_string, print_int, print_float などの 標準の表示関数を再定義する。 したがって、Format を用いるときはファイルの先頭で組織的に open する。 もし Format を open しない場合、 Format 内の多くの関数はデフォルトの環境(Pervasives)に相当のものがあるので、 表示関数の修飾を忘れてしまっても完全に平穏に見えてしまいます。 Format と Pervasives の表示関数の混在は微妙な表示バグの元となり、 追うのも大変です。 例えば、

let f () =
 Format.print_string "Hello World!"; print_newline ();;

は、いんちきである。というのも、プリティプリンタの キューを空にして "Hello World!" と表示するために Format.print_newline を呼び出していないから。 かわりに "Hello World!" はプリティプリンタのキューに積まれ、 Pervasis.print_newline が復改を標準出力に出力する...。 もし Format がファイルに出力し、かつ標準出力が端末である場合、 ユーザーはファイルに復改がない (そしてファイルの中身を表示させると奇妙なことになり Format.print_newline によって閉じられるべきものはオープンされたまま) ことに気づき、かわりに画面にニセの復改が出現してひどい目にあうだろう!

同じ理由から、 任意精度整数などのプログラム中で負担になる大きなライブラリは open する。

open Num;;

let rec fib n =
 if n <= 2 then Int 1 else fib (n - 1) +/ fib (n - 2);;
  • 妥当性: 全ての識別子を修飾しないといけない場合、可読性は低くなるだろう。

型定義が共有されるプログラムでは、 実装部を含めない(型定義のみを含む) 一つ以上のモジュール定義を集めるのは良いことだ。 この場合、型定義の共有されるモジュール間で統一的に open するのは許容される。

パターン・マッチング

パターン・マッチングの過剰使用を決して恐れるな!

* 一方、不完全なパターン・マッチング構文を避けるよう注意せよ

| _ -> ... や | x -> ... のような「全てを捕まえる」節を使わずに済む (例えばプログラム中で定義された具体型のマッチングの場合) なら使わずにおいて、慎重に完全な形にせよ。 次節のコンパイラの警告も見よ。

コンパイラの警告

コンパイラの警告は潜在的誤りを防いでくれるはずだ。 ゆえにコンパイル時にそのような警告が出たら、 絶対にそれらに注意しつつプログラムを修正しなければならない。 その上、コンパイル時に警告が出るプログラムは、あなたの仕事にまるで 似合わないアマチュア臭がする。

パターン・マッチングの警告

パターンマッチングに関する警告は、慎重に慎重を重ねて扱うこと:

  • 使われない節に関するものはもちろん排除する
  • 不完全なパターンマッチングにおいては、「全てを捕まえる」デフォルトの場合分け(| _ -> ... のような)を加えずに、かわりに構文中で残っているまだテストされていない明示的な構造リスト(例えば| Cn _ | Cn1 _ -> ...)を用いてパターンマッチング構文を組み立て、完全な形にする。
    • 妥当性: このように書くのは全然複雑ではないどころか、プログラムがより安全に発展することにもなる。マッチング対象のデータ型に新しいものが追加されても、新たな警告が出るのだから、警告が保証されてるならプログラマは新しい構文に対応する節を加えれば良い。逆に「全部を捕まえる」節だと、黙ってその関数はコンパイルされ、新しい構文はデフォルトケースと扱われて正しそうに見える挙動を示すだろう。
  • ガードを伴う節が引き起こす不完全なパターンマッチも修正する。典型的には、余分なガードを抑制している。(訳注:言っていることがわからん)

let束縛の構造分解

[英訳者註:「let束縛の構造分解」とは、同時に複数の式を複数の名前に束縛するものだ。タプルやリストのようなものに集めて束縛して名前をパックする。let 束縛が評価されるときにその集めたものが全部アンパックされて、式それぞれに対応する名前が束縛される。例えば let x, y = 1, 2 は let 束縛の構造分解によって let x=1 と let y=2 が同時に起きて束縛される。

let 束縛は単純な識別子の定義に限っていない。もっと複雑な。単純なパターンに使える。例えば

  • 複雑なパターンを伴う let
    let [x; y] as l = ...
    リスト l とその2つの要素 x、yを同時に定義する。
  • 簡単なパターンを伴う let let _ = ...は何も定義しない。= の右辺の式を評価するだけだ。

let の構造分解は完全であること

パターンマッチングが完全である(パターンマッチが失敗しない)ような場合だけ、 let 束縛の構造分解を用いる。 したがって典型的には、 生成型(タプル又はレコード)の定義 単一ケースのバリアント型の定義に限られる。 他はいかなる場合でも、match...with構文を明示する必要がある。

  • let ... in : letの構造分解で警告が出るときは、明示的なパターンマッチングに置き換えること。例えば let [x; y] as l = List.map succ (l1 @ l2) in 式のかわりに以下のように書く:
       match List.map succ (l1 @ l2) with
       | [x; y] as l -> expression
       | _ -> assert false
  • let 構造分解がグローバル定義になるときは、パターンマッチとタプルに書き換える。
       let x, y, l =
        match List.map succ (l1 @ l2) with
        | [x; y] as l -> x, y, l
        | _ -> assert false
    • 妥当性: グローバルの let 束縛構造分解を使うと、パターンマッチを完全化する手段がない。

シーケンスの警告と let _ = ...

シーケンス中の式の型に関する警告をコンパイラが出すときは、 式の結果を無視することを明示すること。 以下の通り:

  • 空束縛を使って、シーケンスの警告を抑えるには
      List.map f l;
      print_newline ()
    こう書く
      let _ = List.map f l in
      print_newline ()
  • ignore : 'a -> unit という引数を無視して unit を返す定義済み関数が使える。
      ignore (List.map f l);
      print_newline ()

どの場合でも、警告を抑える最善の方法は、何故コンパイラが警告したのかを理解することだ。コンパイラは、あなたの書いたコードは役に立たない結果を求めさせている(計算した直後に結果を消している)から警告しているわけだから、それが役に立っているということは、その副作用のためだけ実行されているわけだから、unit を返すべきだ。

たいてい、警告は関数を間違って使っていることを示しており、大抵は結果に意味のある関数と副作用だけを持つバージョンの(結果が無関係の手続的な)関数と混同している。前記の例のように、最初のものはうまくいっているが、 map の代わりに iter を呼ぶべきであり、こう簡単に書ける:

  List.iter f l;
  print_newline ()
 

実際のプログラムでは適切な(副作用だけの)関数が存在しないかも知れず、書かざるを得ないこともよくある。関数の手続き部分を関数的部分から注意深く手で分離すれば問題をエレガントに解決でき、できあがったプログラムの方が良く見える!例えば、この問題だらけの定義を:

 let add x y =
   if x > 1 then print_int x;
   print_newline ();
   x + y;;

より明確な別々の定義に直せて:

(この部分消失)

 let print_adder x =
   if x > 1 then print_int x;
   print_newline ();;
 let add x y = x + y;;

それに応じて古い呼び出しは add に変わる。

どの場合でも let _ = ... 構文はまさに結果を無視したい場合に使う。 この構文で画一的にシーケンスを置き換えてはならない。

  • 妥当性: シーケンスがよりわかりやすい! e1; e2; e3 と比較せよ。
          let _ = e1 in
          let _ = e2 in
          e3

(List.) hd と tl 関数

hdとtl関数は使わない。リストの引数のパターンマッチのほうが明解だ。

  • 妥当性: hd,tl 関数が上げてしまう例外を捕捉するために try .. with ... で囲う必要があるようなものを使うのより、はるかにわかりやすく簡潔である。

ループ

for ループ

配列もしくは文字列を単純になめる場合は for ループを使う。

for i = 0 to Array.length v - 1 do
 ...
done

ループが複雑かもしくは結果を返す場合は、再帰関数を使う。

let find_index e v =
 let rec loop i =
  if i >= Array.length v then raise Not_found else
  if v.(i) = e then i else loop (i + 1) in
 loop 0;;
  • 妥当性: 再帰関数はどんなループも、例えば複数の脱出点があったり、変わったインデックス(例えばデータ値に依存するような)があったりするような複雑なものでさえも、単純にできる。 そのうえ、再帰ループはループ内のどこでも(ループ外でも)変更できる破壊可能変数を使わずに済むかわりに、再帰ループでは再帰呼び出しの間に変更されうる値を引数として明示的に渡す。

while ループ

  • whileループの法則: 注意: ループ不変性が明示的に書かれている場合を除き、while ループは普通間違っている。

while ループの主な使い道は、無限ループ while true do ... である。(大抵はプログラムの中断による)例外により脱出する。

証明されたアルゴリズムを詰め込んだプログラムを持ち込むのでない限り、 他に while ループを使うのは困難である。

  • 妥当性: while ループは、ループ条件を変更してループを終わらせるための破壊可能変数を一つ以上必要とする。ゆえにその正当性を証明するためには、ループ不変性を見つけなければならず、面白いが難しいスポーツだ。

例外

自前の例外を定義するのを恐れないこと。 ただし、一方で、可能な限りシステムであらかじめ定義されている例外を使うこと。 例えば、探索関数で失敗したときには定義済である Not_found 例外を上げるべきだ。 関数呼び出しによって上がった例外の処理は try...with を用いて慎重にすること。

全ての例外を try ... with _ -> で処理するのは、通常、 プログラムのメインの関数に予約されている。 アルゴリズムの不変式を扱うために全ての例外を捕捉する必要があるときは 不変式をリセットした後に、注意深く例外に名前を付けて再度例外を上げる。 典型的には、

let ic = open_in ...
and oc = open_out ... in
try
 treatment ic oc;
 close_in ic; close_out oc
with x -> close_in ic; close_out oc; raise x
  • 妥当性: try ... with _ -> は黙って全部の例外(その計算ではなにもすることのないようなものでさえ)を捕まえてしまう (たとえば「中断」も捕捉されるが計算は続けられる!)。

データ構造

Caml の強力なところの一つに、定義可能なデータ構造のパワーと操る簡単さがある。 ゆえにこの利点を最大限利用しなければならない: 独自のデータ構造を遠慮せず定義すること。特に、 画一的に整数値で列挙したりブール値で2つの場合分けを列挙したりしないこと。 例:

type figure =
   | Triangle | Square | Circle | Parallelogram;;
type convexity =
   | Convex | Concave | Other;;
type type_of_definition =
   | Recursive | Non_recursive;;
  • 妥当性: たいていブール値は対応するコードの直感的理解を妨げる。例えば type_of_definiton がブール値でコーディングされていたら、true はなんの意味だ? 「普通の」定義 (つまり Non_recursive) か? Recursive か?

列挙型を整数でエンコードする場合、 受け付け可能な整数の範囲制限がとても困難である。 コンストラクタ関数を定義してプログラムの不変性を保証せねばならない(訳注:?) (直接構築している値がないことを後で検証する必要もある)。 あるいはプログラム中に assert を入れ、パターンマッチでガードするか。 これは良い習慣ではない。 この問題は要約型(和型)を定義すればエレガントに解決でき、 さらに特典として、 パターンマッチングとコンパイラによる徹底検証の力をまるごと享受できる。

  • 批評: 二値列挙は型をあてはめるブール値の意味をそのまま転用した述語として統一的に定義できる。例えば、文字 p で終わる取り決めを採用すると、type_of_definition という新しい要約型(和型)の定義のかわりに、recursivep という定義が再帰型なら true を返す述語関数を使うことができる。
  • 答え: この方法は二値列挙に特化しており簡単に拡張できない。その上、パターンマッチングにあまりそぐわない。例えば、 | Let of bool * string * expression という典型的なパターンマッチングのの定義のコーディングはこうなるだろう:
| Let (_, v, e) as def ->
   if recursivep def then ``code for recursive case''
   else ``code for non recursive case''

あるいは recursivep がブール値を適用可能なら

| Let (b, v, e) ->
   if recursivep def then``code for recursive case''
   else ``code for non recursive case''

明示的コーディングと対照的だ:

| Let (Recursive, v, e) -> ``code for recursive case''
| Let (Non_recursive, v, e) -> ``code for non recursive case''

二つのプログラムの違いは微妙で、単なる好みの問題と思うかもしれない。 だが、明示的なコーディングは変更に対してより強く、 言語に良くフィットしているのは決定的である。

反対に、true/false が構造的に明確にわかるようなブール値のフラグとして 新しい型を画一的に定義する必要はない。以下の型定義の有用性は疑問だ:

type switch = | On | Off;;
type bit = | One | Zero;;

同様の異論として整数で代用する列挙型も、 その整数がデータ表現を明確に表しているときは許容できる。

破壊可能変数の使いどころ

破壊可能変数(値) は有益で、ときどきは単純で明確なプログラムに不可欠となる。 にもかかわらず、Caml の普通のデータ構造は不変値であることを認識しつつ 使わねばならない。 このことは、明解で安全なプログラムのための許容範囲として好まれる。

イテレータ

Camlのイテレータは強力で役に立つ特徴だ。 しかし過剰に使ったり、逆に無視したりしてはならない。 イテレータはライブラリとして供給されており、 正確でありライブラリ作者が良く考え抜いているだろう。 だから再発明は無駄だ。

ゆえに、こう書く:

let square_elements elements = map square elements;;

これよりも

let rec square_elements = function
  | [] -> []
  | elem :: elements -> square elem :: square_elements elements;;

いっぽう、こう書いてはいけない

let iter f x l =
 List.fold_right (List.fold_left f) [List.map x l] l;;

こちらの方が良い

val iter : ('a list -> 'a -> 'a list) -> ('a -> 'a) -> 'a list -> 'a list =
  <fun>
# iter (fun l x -> x :: l) (fun l ->5; List.rev l) [[1; 2; 3]] ;;
- : int list list = [[1; 2; 3]; [3; 2; 1]]

必要に応じて慎重に説明のためのコメントを付けること: 絶対必要!というのが私の意見だ。

プログラムの最適化方法

  • 最適化の法則もどき: 演繹的な最適化はないし、帰納的な最適化もない。

何より単純明解なプログラムであること。 ボトルネック(一般には2、3のルーチン)が特定されるまで最適化を始めないこと。 そして最適化は何よりも使っているアルゴリズムの複雑度の変更からだ。 これはしばしば操作するデータ構造の再定義をすることになり、 プログラムの困難な該当部分を完全に書き直すことになる。

  • 妥当性: プログラムの明快さと正しさは優先される。さらに多くのプログラムにおいて、効率上もっとも重要な部分を先見的に特定するのは現実的には不可能だ。

クラスとモジュールの選択方法

継承、つまりデータや機能をインクリメンタルに洗練していくことが必要なら、 OCaml のクラスを使うべき。

パターンマッチングが必要なら、従来のデータ構造(特にバリアント型)を 使うべき。

データ構造が固定されており、その機能も同様に固定されているかもしくは 新しい関数を使うところで追加するので十分ならば、モジュールを使うべき。

Caml コードの透明さ

Caml言語には単純明解なプログラミングのための強力な構文がある。 極めて明瞭なプログラムを得るための問題はおもに、その構文を適切に使うことだ。

言語は多数のプログラミングスタイル(またはプログラミングパラダイム) によって特徴づけられる: 命令型プログラミング(状態と割り当ての概念に基づく)、 関数型プログラミング(関数、関数の結果、(λ)計算の概念に基づく)、 オブジェクト指向プログラミング( 状態と、状態を変更する手続きやメソッドをまとめたオブジェクトの概念に基づく)。 プログラマの最初の仕事は、当面の問題にもっとも当てはまるプログラミング パラダイムを選択することだ。 プログラミングパラダイムの中から一つを用いるときに難しいのは、 アルゴリズムを実装した計算するための自然で単純な式表現を、 その言語で構築することだ。

スタイルの危険性

プログラミングスタイルについて、普通2つの対照的な問題行動があり得る。 片方は「全部命令型」の手段(画一的にループと変数割り当てを使う)で、 もう片方が「純粋な関数型」の手段(ループも変数割り当ても一切無し)だ。 100%オブジェクト指向スタイルは将来現れるに違いないが、 (幸い)ここで論議するにはまだ新しすぎる。

  • 「命令型が多すぎる」危険:
    • 自然に再帰で書ける関数を命令型のスタイルでコーディングするのは悪い考え方だ。例えばリストの長さを求めるのにこう書いてはいけない:
 let list_length l =
  let l = ref l in
  let res = ref 0 in
  while !l <> [] do
   incr res; l := List.tl !l
  done;
  !res;;

かわりに以下の再帰関数で書けば単純明快だ。

 let rec list_length = function
   | [] -> 0
   | _ :: l -> 1 + list_length l;;

(この二つのバージョンが等価かどうかについては脚注を見よ)

  • 命令型の世界でのもう一つの一般的な「命令型の使い過ぎの誤り」とは、ベクトルの要素のイテレータに対して統一的に単純forループを使わず、一つか二つの参照を含む複雑な while ループを使うことだ。(あと、無駄に多く変数を割り付けたりしてエラーの機会を増やすとか)。
  • このカテゴリに属するプログラマは、レコード型定義のキーワード mutable が暗黙であるべきだと思っている
  • 「関数型が多すぎる」危険:
    • この病気に苦しむプログラマは、用心深く配列と割り当てを避けている。もっとも極端な症状は、命令型構文を書くのを(それが問題解決のもっともエレガントな方法であるのが自明であっても)一切拒絶するというものだ。
    • 独特の兆候: 画一的に for ループを再帰関数に書き換えたり、誰が見ても命令型のデータ構造であるべきと思える文脈でリストを使ったり、グローバル参照を使えばほとんど不変式であるような偽パラメータを避けてられるところを、大量の大域パラメータを関数に渡したりする。
    • このプログラマは、レコード型定義のキーワード mutable が言語から使えないようにすべきだと思っている。

一般に読めないと思われる Caml コード

Caml言語は簡潔明瞭なプログラミングのための強力な構文を備える。 しかし、これらの強力な構文はまた、無駄に複雑なコードを書いてしまい、 全く読めないプログラムにさえ行き着く。

幾つかこの手段が知られている:

  • 以下のような役に立たない(ゆえに可読性に対して有害な) if then else
   let flush_ps () =
     if not !psused then psused := true;;

または(まだ微妙)

   let sync b =
     if !last_is_dvi <> b then begin last_is_dvi := b end;;
  • ある構文を別の書き方をする。例えば無名関数の引数の適用のための let...in をこんな風に書く
   (fun x y -> x + y)
    e1 e2

これは以下のことだ。

   let x = e1
   and y = e2 in
   x + y
  • 画一的に let ... in 束縛でシーケンスをコーディングする
  • 特に関数呼び出しにおいて計算と副作用をごっちゃにする。 関数呼び出しの引数の評価順が不定であることを思いだすと、 関数呼び出しには計算と副作用を混ぜてはいけないことが示唆される。 だが、引数が一つだけなら、引数に副作用があることを利用するかもしれない。 プログラムの意味論としては危険性はなくとも、読者に取って非常に厄介だから絶対禁止。
  • イテレータと高階関数の誤用(すなわちそれらを使いすぎか使わなさ過ぎ)。例えば、 List.map や List.iter を使わずに、自前で特化した等価動作するインライン再帰関数を書く。もっとひどいのは、List.map や List.iter を使わずに List.fold_right や List.fold_left を使って等価に書く。
  • 他に読めないコードを書く効率的な方法は、これらの方法をいくつか、又は全部混ぜることだ。例えば:
       (fun u -> print_string "world"; print_string u)
        (let temp = print_string "Hello"; "!" in
         ((fun x -> print_string x; flush stdout) " ";
         temp));;

あなたが print_string "Hello world!" をこのような方法で自然に書き下せるのなら、 作品を Obfuscated Caml Contest に提出できるのは疑うべくもない。

プログラム開発の管理

小さいチームで大きく複雑なプログラムを開発する好例である コンパイラ開発に従事するベテランの Caml プログラマから秘訣をもらった。

プログラムの編集方法

多くの開発者はプログラムを書くのに使っている Emacs エディタ(GNU-Emacs) をある種の尊敬を抱いている。 Caml のソースコードを文法的に色分けしてくれる (違うカテゴリの単語、例えばキーワードをカラーでレンダリングする)ので、 エディタは言語にとても直結している。

以下の2つのコマンドが不可欠だと思われる:

  • CTRL-c CTRL-c か Meta-x compile : エディタから(make コマンドを使って)リコンパイルを起動する
  • CTRL-x-`: Caml コンパイラが指摘したエラーの場所にカーソルを移動させる(ファイル内)

この機能の使いかたを説明する: CTRL-c CTRL-c の組み合わせでアプリケーション全体をリコンパイルする。 エラーがあったら続けて CTRL-x-` コマンドによって 出力されたエラーを全部訂正できる。 そしたらまた CTRL-c CTRL-c でリコンパイルを起動して繰り返せば良い。

他のEmacsトリック

ESC-/ (dynamic-abbrev-expand) コマンドは、 編集中のファイル内に登場する単語のうちカーソルの直前に出てきた 単語を自動補完する。 これにより、プログラム中の長い名前をタイプするという退屈作業なしに、 いつも重要な識別子を選んで付けることができる: 最初の文字の後で ESC-/ とすれば簡単に識別子を補完できる。違う補完をしたときも、 続けて ESC-/ とすれば別の補完をしてくれる。

Unix 配下では、 CTRL-c CTRL-c か Meta-x compile の組み合わせに続けて CTRL-x -` がさらに Caml プログラムのなかにある特定文字列を見つけるのに使える。 make を起動してリコンパイルするかわりに grep を起動させれば、 grep の「エラーメッセージ」相当のものということで、 CTRL-x-' で自動的に見つけた文字列のところに飛んでくれる。

対話システムによる編集方法

Unix 配下の場合: ラインエディタ ledit は素晴らしい編集能力を持つ Emacs 風のエディタだ (ESC-/もある!)。以前にタイプしたコマンドを復元するヒストリ機能もあり、 別のセッションのコマンドも復元できる。 ledit は Caml で書かれており、ここからフリーでダウンロードできる。

コンパイル方法

make ユーティリティはプログラムのコンパイル、再コンパイルの管理に不可欠だ。 Makefile のサンプルは The Hump にあるし、Caml コンパイラの Makefile も 参考になるだろう。

チームでの開発方法: バージョン管理

CVSというソフトウェアバージョン管理システムは、 決して生産性の向上させるために走らるのではない。 このシステムはプログラマチームによる開発管理をサポートする。 チームの一貫性を強制し、ソフトウェア作製に関する多くの変更を管理する。 CVS はまた複数のチームによる同時開発もサポートしており、 インターネットでリンクされたサイト間で分散共有できる。 Caml コンパイラは全て CVS 配下である。

anonymous CVS サーバ (camlcvs.inria.fr)はCamlコンパイラの 開発コードや他の Caml 関連ソフトウェアのソフトを抱えている。 CVSサーバを利用するとコンパイラを手元にコピーできたり、 更新も簡単にできる(手元の Caml ソースディレクトリで cvs update と打てばよい)


脚注

命令型と関数型それぞれのバージョンの list_length

二つの list_length は複雑さの点では完全には一致しない。 命令型のバージョンは定数サイズのスタックを実行時に使うが、 関数型バージョンは中断した再帰呼び出し (最大値が引数のリストの長さに等しい)の戻りアドレスを格納する必要がある。 関数プログラムの実行時に定数サイズのスペースだけ消費してほしいのなら、 末尾再帰(tail-rec) で関数を書けば良い。 すなわち、関数が再帰呼び出しで終わるようなものだ (呼び出された再帰関数が戻る処理が要らない)。 中間結果としてアキュムレータ(積算変数)を用いるとこのように書ける

 let list_length l =
  let rec loop accu = function
  | [] -> accu
  | _ :: l -> loop (accu + 1) l in
  loop 0 l;;

この方法によって、 命令型プログラムと同様の特性を持つプログラムになった。 さらに パターンマッチングと再帰によって再帰の sum データ型に属する 引数をハンドリングしているというように、 アルゴリズムが、明快で自然に見えるようになった。


リロード   新規 下位ページ作成 編集 凍結 差分 添付 コピー 名前変更   ホーム 一覧 検索 最終更新 バックアップ リンク元   ヘルプ   最終更新のRSS
Last-modified: Mon, 04 Dec 2006 09:43:29 JST (6350d)