8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Haskellでletとcaseを一緒に使おうとしてインデントルールにハマった

Last updated at Posted at 2017-05-20

TL; DR

こういうコードを書いて、

f y =
    let y = case x of
        0 -> 0
        _ -> 1
    in y

parse error on input ‘->’ というエラーメッセージが出たら、

f x =
    let y = case x of
            0 -> 0
            _ -> 1
    in y

こう書き換えればコンパイルが通ります。

再現コード

このコードが最小再現コードになります。調査環境は IDE One(ghc 8.0.1)です。

f x =
    let y = case x of
        0 -> 0
        _ -> 1
    in y

注: 言うまでもないかもしれませんが、このコードは

f 0 = 0
f _ = 1

と書くべきです。これはあくまでコンパイルエラーを起こすためのコードであり、実際にはここまで単純なコードではない(このような書き換えができない)ものと考えてください。

なぜエラーになるのか

まず前提として、Haskell の let 式の書式はこうです。

let x = 0
    y = 1
in x + y

{-
let 変数名1 = 式1
    変数名2 = 式2
in 式0
-}

したがって、エラーになるコードをコンパイラが見ると

let y = case x of
    0 -> 0
    _ -> 1
in y

let の2行目では 変数名 = 式 の形が来ると予想しているために、エラーになります。0 は変数名として不正なのでそこでエラーになって欲しいところですが、実際にエラーメッセージが表示されるのは -> の部分になります。

つまり、コンパイラにとっては 0 -> 0_ -> 1 は case ではなく let に属しているように見えているということです。人間にとっては 0 -> 0 が case の一部なのは自明ですが、以下のようなコードであればどうでしょうか。

let y = case x of
    z -> 0
    w = 2
in y + w

case がどこまで続いているのか、let で束縛されるのはどれか、すぐに判別できたでしょうか。コンパイラとしては、zw を見ただけではこれが let の一部なのか case の一部なのかわかりません。決定しようとすれば次のトークンである =-> を見る必要があります。しかしそれではパースが大変になるので、Haskell は文法を簡単にする道を選んだのでしょう。

解決方法

0 -> 0 が let と case のどちらに属するかわからないのですから、インデントにより明確化すれば OK です。

let y = case x of
        0 -> 0
        _ -> 1
in y
--  ^ let
--      ^ case

let と case で合わせて 2段階インデントを深くすればいい訳です。

なお、case の範囲さえわかればいいので、次のような書き方でも問題ありません。

let y = case x of {
    0 -> 0;
    _ -> 1;
    }
in y

インデントルールの闇そんなことなかった

追記: 以下の記述は誤りです。詳細はコメント欄を参照してください。

以下のコードで、f はコンパイルでき g はコンパイルできません(in IDE One Haskell ghc 8.0.1)。なぜだかわかりますか?

-- OK
f x =
  let y = case x of  -- 2 spaces
       0 -> 0        -- 2 + 4 + 1 spaces
       _ -> 1
  in y
-- OK
f x =
    let y = case x of  -- 4 spaces
         0 -> 0        -- 4 + 4 + 1 spaces
         _ -> 1
    in y
-- OK
f x =
        let y = case x of  -- 1 tab (8 spaces)
                0 -> 0     -- 2 tabs (8 + 4 + 4 spaces)
                _ -> 1
        in y
-- Error
g x =
    let y = case x of  -- 4 spaces
        0 -> 0         -- 4 + 4 spaces
        _ -> 1
    in y
-- Error
g x =
  let y = case x of  -- 2 spaces
      0 -> 0         -- 2 + 2 + 2 spaces
      _ -> 1
  in y

どうやら Haskell は、インデントは 4スペースでするもの、という前提を持っているようです。let の前のスペースの数に関わらず、letの桁位置から4+1スペース離れれば let とは別のものとして扱っています。とにかく、余計な混乱を防ぐには、

Haskell のインデントはスペース 4 つを使え

ということのようです。ところで、その割に2スペースでインデントされたコードをよく見るのはなぜなんでしょうか。この挙動を制御するようなコンパイルオプションがあるとか?

8
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?