smellman's Broken Diary

クソみたいなもんです

すごいHaskellたのしく学ぼう!(第1版) 第3章のbmiの計算でハマった件について

タイトルどおりの感じですが、なかなか面白かったのでメモ。ちなみにうちの環境は以下のとおり。

$ ghci --version
The Glorious Glasgow Haskell Compilation System, version 7.6.3

今回は最初に作ったbaby.hsにひたすら出てきた関数を書いていくようにして試していたのだけど、P.37 の訳注でこんな記述がありました。

.hs ファイルの先頭に {-# OPTIONS -Wall -Werror #-} を付けてコーディングすれば、意図せぬ動作やクラッシュの原因になりがちな箇所を GHC が指摘してくれるので、良い Haskell コードを書く訓練になりますよ!

とあったので追加してみました。

当然、今まで書いたものは型を定義してないので warning が出るのでひと通り型の定義を追加してしてから先に進めたのですが、 P.42 のガードで計算しているところでドはまりしました。

まず、このプログラム自体本に書いてある内容は古いようで、原著の記述では Double ではなく RealFloat を使うようになっています。

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                 = "You're a whale, congratulations!"  

Syntax in Functions - Learn You a Haskell for Great Good!

これを読み込むと前述の OPTIONS を付けた状態だと warning が出て通らなくなります(もちろん、 OPTIONS を付けて無ければ通って動作します)。

Prelude> :l baby
[1 of 1] Compiling Main             ( baby.hs, interpreted )

baby.hs:80:23: Warning:
    Defaulting the following constraint(s) to type `Integer'
      (Integral b0) arising from a use of `^' at baby.hs:80:23
      (Num b0) arising from the literal `2' at baby.hs:80:25
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'
    In the expression: weight / height ^ 2 <= 18.5

baby.hs:81:23: Warning:
    Defaulting the following constraint(s) to type `Integer'
      (Integral b0) arising from a use of `^' at baby.hs:81:23
      (Num b0) arising from the literal `2' at baby.hs:81:25
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'
    In the expression: weight / height ^ 2 <= 25.0

baby.hs:82:23: Warning:
    Defaulting the following constraint(s) to type `Integer'
      (Integral b0) arising from a use of `^' at baby.hs:82:23
      (Num b0) arising from the literal `2' at baby.hs:82:25
    In the second argument of `(/)', namely `height ^ 2'
    In the first argument of `(<=)', namely `weight / height ^ 2'
    In the expression: weight / height ^ 2 <= 30.0

<no location info>:
Failing due to -Werror.
Failed, modules loaded: none.

ここで注目するのは以下の記述です。

    Defaulting the following constraint(s) to type `Integer'
      (Integral b0) arising from a use of `^' at baby.hs:80:23
      (Num b0) arising from the literal `2' at baby.hs:80:25

この記述では通常 Integer となるところが '^' を使ってるので Integral と推測されて、 2 から Num だと推測されてわからんっていう状態になるというものっぽいです。
ということは Integer 型として処理をしてくれと明示的に指定すればよいというものです。
以下のように修正すると無事通るようになります。

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ (2 :: Integer) <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ (2 :: Integer) <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ (2 :: Integer) <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                 = "You're a whale, congratulations!"  

最後に今朝の僕の情報を入れて試してみましょう。

Prelude> :l baby
[1 of 1] Compiling Main             ( baby.hs, interpreted )
Ok, modules loaded: Main.
*Main> bmiTell 70.4 1.65
"You're fat! Lose some weight, fatty!"

うるせぇ、これでも今年だけで約10キロ減ってるんだよ!!!!