F# Puzzle 2

問題

次のコードは何が出力されるでしょうか?

let toUpper (x: string) -> x.ToUpper()
if true then
  "hoge"
else
  "piyo"
|> toUpper

解答

> if true then
-   "hoge"
- else
-   "piyo"
- |> toUpper;;
val it : string = "hoge"

なんとtoUpperされません。このコードの意図は、if式の結果をtoUpperしたいように感じますよね? 条件をfalseにすると大文字の値が返ります。

val it : string = "hoge"
> if false then
-   "hoge"
- else
-   "piyo"
- |> toUpper;;
val it : string = "PIYO"

この気持ち悪い挙動は、演算子を使った時に、値の先頭を揃えられる仕様が原因です。 F#にはオフサイドルールがあるのですが、演算子を使った場合は演算子の文字数+1文字より前から開始できます。 こう記述できます。

   1
 + 2
 + 3

こんなこともできます。

let (+++++) x y = x + y
      1
+++++ 2

今回の問題では、|> toUpperの前行の"piyo"のインデントが2で、パイプライン演算子の文字数が2のため、"piyo"の行の続きと判断される訳です。

つまり、"piyo"のインデントを4以上にすれば挙動が変わります。

> if true then
-     "hoge"
- else
-     "piyo"
- |> toUpper;;
val it : string = "HOGE"

演算子と改行を組み合わせると、意図しない挙動になる場合があるので、気を付けようという話でした。

また、toUpperの型がstring -> stringなので、この問題を発見しにくいです。 例えば、string -> intな関数を呼ぼうとすると、コンパイルエラーになります。

> if true then
-   "hoge"
- else
-   "piyo"
- |> int;;

  |> int;;
  ---^^^

stdin(36,4): error FS0001: 型が一致しません。
    string -> string
という指定が必要ですが、
    string -> int
が指定されました。型 'string' は型 'int' と一致しません

元ネタ

  1. if-elseでelse側のインデントを2にすると、ifブロック全体にパイプライン演算子が適用されないのはなぜ? - QA@IT
  2. コードのフォーマットに関するガイドライン (F#)