読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

GHCの使い方を調べてみた。(その6)

昨日はGHCiでの式の評価と変数への束縛について説明した。

今日はそれに関連して、複数行の入力について。
あと、オフサイドルールについても、自分が理解したことを書いていく。

複数行の入力

関数の定義などで複数行の入力を行いたい場合、改行したときに式の評価や変数への束縛がされてしまうのは、都合が悪い。
そういう場合、:{コマンドと:}コマンドで囲うと、改行しても式の評価や変数への束縛がされなくなり、複数行の入力が出来るようになる。

Prelude> :{  -- 複数行の入力を開始
Prelude| -- ここから複数行の入力を行っていく
Prelude| -- ...
Prelude| :}  -- 複数行の入力が終わり、式が評価される

ちなみに、複数行の入力中は、デフォルトではプロンプトが>から|に変わるようになっている。

さて、以下では複数行入力の具体的な例を見てみる。

例えば、GHCiで次のような関数を定義したいとする:

fibo 1 = 1
fibo 2 = 1
fino n = fibo (n - 2) + fibo (n - 1)

まず、次のようにやってしまうとアウト。

Prelude> let fibo 1 = 1
Prelude> let fibo 2 = 1
Prelude> let fibo n = fibo (n - 2) + fibo (n - 1)
Prelude> fibo 5
^CInterrupted.  -- 終わらないのでCtrl+Cで終了
Prelude> 

なぜfibo 5の計算が終わらないのかというと、変数fiboへの新しい束縛によって、以前の束縛が隠されてしまうから。

fibo 5 = fibo 3 + fibo 4
       = (fibo 1 + fibo 2) + fibo 4
       = ((fibo -1 + fibo 0) + fibo 2) + fibo 4 -- fibo 1の定義は隠されてしまっている
       = ...

なので、この3行はまとめて束縛する必要がある。

そこで、次のように:{:}を使う。

Prelude> :{
Prelude| let fibo 1 = 1
Prelude|     fibo 2 = 1
Prelude|     fibo n = fibo (n - 2) + fibo (n - 1)
Prelude| :}
Prelude> fibo 5
5
Prelude> 

こうすると、問題なくfibo 5の計算が出来ていることが分かる。

複数行入力を容易にする

一行入力して式を評価や変数への束縛をさせるよりも、複数行入力して式を評価や変数への束縛をさせることが多い場合、毎回:{:}を使うのは大変になる。
その場合、:set +mコマンドを使うと、複数行入力が容易になる。

この状態のときには、式を評価できなかったり、もしくは、文が続く可能性がある場合、式の評価や変数への束縛が行われず、次の行の入力を待つようになる。

例えば、以下のようになる。

Prelude> :set +m
Prelude> let fibo 1 = 1
Prelude| 

上の場合、まだ文が続く可能性があるので改行をしても変数への束縛は行われず、次の行の入力を待つ動作になる。

そして、式の評価や変数への束縛が行われるのは、次のいずれかの条件を満たしたとき。

  • 空行で改行されたとき
  • 式が評価できるとき
  • 文が続く可能性がないとき

まず、空行で改行されたとき。

これは簡単で、例えば次のようになる。

Prelude> let a = 1
Prelude|     b = 2
Prelude| 
Prelude> a
1
Prelude> b
2
Prelude> 

空行で改行したときに複数行の入力が終わりとみなされていて、変数aとbにそれぞれ1と2が束縛されている。

次に、式が評価できるとき。

改行が押された時点で式の評価が可能になっていれば、評価が行われる。

例えば、次のようになる。

Prelude> 1 + 2
3
Prelude> (1 + 2
Prelude| + 3)
6
Prelude> let a = 1
Prelude|     b = 2
Prelude| in a + b
3
Prelude> 

いくつか例を出しているけど、いずれの場合も、式の評価が可能になった時点で評価が行われている。

最後に、文が続く可能性がないとき。

例えば、letによる束縛を次のように波括弧を使って書くと、閉じる波括弧のあとに文が続く可能性はないので、束縛が行われる。

Prelude> let {a = 1
Prelude|     ;b = 2
Prelude|     }
Prelude> a
1
Prelude> b
2
Prelude> 

なお、元の動作に戻したい場合には、:unset +mコマンドを使えばいい。

Prelude> :unset +m
Prelude> let a = 1
Prelude> 

オフサイドルールについて

ところで、複数行入力するときに関係してくるのが、オフサイドルール。

波括弧やbegin-endといったキーワードを使う代わりに、レイアウトを使って文のまとまりを表すことを、オフサイドルールと言ったりする。
Haskellでもオフサイドルールは採用されているけど、正直、分かりにくい・・・

実際に自分がやった失敗として、次のようなものがある。

main関数をGHCiの中で定義しようと思って、次のように入力した:

Prelude> :{
Prelude| let main = do
Prelude|   a <- getLine
Prelude|   putStrLn $ "input: " ++ a
Prelude| :}

そしたらエラー。

おかしいなと思って、次のようなファイルを作ってロードしたあと、実行してみる。

-- Test.hs

main = do
  a <- getLine
  putStrLn $ "input: " ++ a
Prelude> :l Test
[1 of 1] Compiling Main             ( Test.hs, interpreted )
Ok, modules loaded: Main.
*Main> main
hoge  -- ここは文字列を入力した
input: hoge
*Main> 

すると、見てのとおり、問題なくコンパイル、実行できている。

GHCiから入力するのとファイルからロードするのとで何が違うのかと、あれこれ試行錯誤したんだけど、よく分からずに困ってしまった。

結論から言えば、オフサイドルールの規則をちゃんと理解できていなかったのがエラーの原因。
次のようにすれば、問題なく定義できる:

Prelude> :{
Prelude| let main = do
Prelude|       a <- getLine
Prelude|       putStrLn $ "input: " ++ a
Prelude| :}
Prelude> main
hoge
input: hoge
Prelude>

以下のようにするのもOK。

Prelude> :{
Prelude| let
Prelude|   main = do
Prelude|     a <- getLine
Prelude|     putStrLn $ "input: " ++ a
Prelude| :}
Prelude> main
hoge
input: hoge
Prelude> 

最初のがダメで、なんで2番目や3番目ならOKなのかを理解するには、オフサイドルールの規則を理解していないといけない。
それについて、ちょっと説明したい。

オフサイドルールに対する考え方

Haskellオフサイドルールを理解するには、次の考え方をした方がいいと思う:

Haskellオフサイドルールは、複文を書くための糖衣構文。

ここでいう複文というのは、{文1; 文2; ... }というふうに、複数の文がセミコロンで区切られ、全体を波括弧で囲まれているかたまりのこと。

複文が出てくるのは、次の4つのケース:

  • whereの直後
  • ofの直後
  • letの直後
  • doの直後

例えば、分かりやすい例としてletを考えてみると、

let a = 1
    b = 2
in a + b

というのは、実際には

let {a = 1; b = 2} in a + b

と書くのと等価になっている。

ここで重要なのが、「前者(オフサイドルールを使った書き方)は後者(波括弧とセミコロンを使った書き方)の書き方も出来る」と考えるのではなく、「後者の書き方は、前者の書き方も出来る」と考えること。
大抵の本だと前者の書き方がされているので、前者の書き方が本来の書き方で、後者の書き方が糖衣構文だと思ってしまいがちだけど、そうじゃなくて、後者の書き方が本来の書き方で、前者の書き方の方こそが糖衣構文なんだと考えた方がいい。

というのは、Haskellソースコードは、オフサイドルールを使わないで書いた場合、C言語などと同じように、改行やインデントの深さに依存しないから。

例えば、次のように、オフサイドルールを使わないで書いたモジュールを用意してみる。

-- Layout.hs

module Layout where {
  main = do {
    a <- getLine;
    putStrLn $ "input: " ++ a;
  };

  f x = x + 2;
}

これを、次のようにゴチャゴチャのレイアウトにしてみる。

-- Layout.hs

module Layout
        where {
  main =
      do { a
            <- getLine; putStrLn $
"input: "
        ++ a; };
              f x
            = x +
  2; }

普通のHaskellソースコードを見慣れていると、こんな先頭の位置が揃っていないコードでまともにコンパイルが通るのかと思うだろうけど、問題なくコンパイル出来る。
先頭の位置を揃えないといけないのは、オフサイドルールによって適切な位置にセミコロンが挿入され、そして波括弧が閉じられるようにするためであって、オフサイドルールを使わないのであれば、そもそもそんなことを考える必要はない。

オフサイドルールの規則

とはいえ、オフサイドルールに従ってきちんとレイアウトされたソースコードと、そうでないソースコードで、どちらが見やすいかといえば、それは当然前者なので、オフサイドルールに従ってコードは書いていきたい。

ここで、オフサイドルールが複文を書くための糖衣構文なんだという考え方をしておけば、オフサイドルールで書かれたコードがどのように波括弧とセミコロンで書かれたコードに変換されるのかという規則を把握することで、間違った書き方をしなくて済むようになる。

さて、肝心の規則は、次のようになる:

  1. whereofletdoの次の識別子が{でない場合、オフサイドルールが始まり、その識別子の直前に{が挿入される。
  2. オフサイドルールが始まっている状態で新しい行が始まったとき、最初の識別子の位置が
    1. オフサイドルールを開始した識別子より左なら、}が挿入される。
    2. オフサイドルールを開始した識別子と同じ位置なら、;が挿入される。
    3. オフサイドルールを開始した識別子より右なら、何もされない。
  3. ファイルの最後に、閉じていない波括弧はすべて閉じられる。

これだけ。

例えば、次のようなソースコードを考えてみる:

-- Layout.hs

module Layout where
main = do
  a <- getLine
  putStrLn $ "input: " ++ a

f x = x + 2

これを上記の規則に従って波括弧とセミコロンを挿入すると、次のようになる:

-- Layout.hs

module Layout where
{main = do
  {a <- getLine
  ;putStrLn $ "input: " ++ a

};f x = x + 2
}

このソースコードは問題なくコンパイル出来る。

ところで、次の2つのソースコードは、いずれもコンパイルエラーが起きる。

-- Layout.hs

module Layout where
main = do
  a <- getLine
  putStrLn $ "input: " ++ a

 f x = x + 2
-- Layout.hs

module Layout where
  main = do
    a <- getLine
    putStrLn $ "input: " ++ a

f x = x + 2

これがなぜエラーになるのかは、先程の規則を当てはめて考えると簡単。

まず、1つめは

-- Layout.hs

module Layout where
{main = do
  {a <- getLine
  ;putStrLn $ "input: " ++ a

 }f x = x + 2
}

と変換されるので、関数の定義がセミコロンで区切られなくなってしまう。

また、2つめは

-- Layout.hs

module Layout where
  {main = do
    {a <- getLine
    ;putStrLn $ "input: " ++ a

}}f x = x + 2

と変換されるので、関数fの定義がモジュールの定義の外に出てしまっている。

このように、Haskellソースコードで先頭が揃っているのは、オフサイドルールで波括弧やセミコロンがちゃんと挿入されるようにするため。
規則がちゃんと理解できていれば、悩むことはなくなる。

そして、最初の自分がやった失敗の例に戻ると、これは次のように変換されていたと考えられる:

Prelude> :{
Prelude| let {main = do
Prelude|   }a <- getLine  -- letの直後のmainより左にあるので、波括弧が閉じられる
Prelude|    putStrLn $ "input: " ++ a
Prelude| :}

すると、letと複文の後ろに式らしきものがあるのにinがないので、エラーになってしまう。

一方、大丈夫だったものは、それぞれ次のように変換されていたと考えられる。

Prelude> :{
Prelude| let {main = do
Prelude|       {a <- getLine
Prelude|       ;putStrLn $ "input: " ++ a
Prelude| }}
Prelude| :}
Prelude> :{
Prelude| let
Prelude|   {main = do
Prelude|     {a <- getLine
Prelude|     ;putStrLn $ "input: " ++ a
Prelude| }}
Prelude| :}

これなら何も問題ないので、ちゃんとmain関数が定義される。

今日はここまで!