標準入出力を使って、大域変数をループで回しながら状態を更新して最後に計算結果を出すという手続き型でよくあるタイプの問題をHaskell でどう書くか?というの考えています。
例題としては例題 3_5 「成績が80点以上は優,70点から79点までは良,60点から69点までは可,59点以下は不可,を判定せよ。」を取り上げてみました。
(ボスの母国語がFortran だったのと手続き型の言語も勉強するべということでのチョイスです。)
パターンとしては最近界隈でちょっと取り上げられていた状態モナドを使えるのかな?と勘繰ったのですがそこまでは息が続かず、結局大変ナイーブながら仮引数を引き回すコードに天下り的に初期状態を食わせるコードになりました。
とりあえず想定の動作をするコードは書けたのですが、不恰好です、皆さまお知恵をお貸しください。
Fortran での実装
模範解答とは少々異なりますがご愛嬌。
! ex3_5.f90
! grading
PROGRAM Grade
Integer :: i, n1=0, n2=0, n3=0, n4=0
Print *, "put a score (to quit, -1)"
! If the read is outside, goto 9
1 Read *, i; If(i<0 .OR. i>100) Goto 9
Select Case(i)
Case(80:)
Print*, "A"; n1 = n1+1
Case(70:79)
Print*, "B"; n2 = n2+1
Case(60:69)
Print*, "C"; n3 = n3+1
Case Default
Print*, "F"; n4 = n4+1
End Select
Goto 1
9 Print *, "A= ", n1, ", B= ", n2, ", C= ", n3, ", F= ", n4
END PROGRAM Grade
! $ ./ex3_5.out
! 40
! F
! 50
! F
! 60
! C
! 70
! B
! 80
! A
! 90
! A
! 100
! A
! -1
! A= 3 , B= 1 , C= 1 , F= 2
流れとしてはまず整数型の宣言とゼロで初期化。
標準入力から整数を受け取りもし負ならgoto 文で累計結果を表示。
もし正しい採点範囲の数ならケース文で分岐、それぞれの場合で人数n を更新します(1足す)。
ということになります。
Haskell での実装
> -- import Control.Monad.State
> -- State s t = s -> (t,s)
>
> data Grade = A | B | C | F | O
> deriving (Show, Eq)
>
> isIn100 :: Int -> Grade
> isIn100 n
> | 0 <= n && n < 60 = F
> | 60 <= n && n < 70 = C
> | 70 <= n && n < 80 = B
> | 80 <= n && n <= 100 = A
> | otherwise = O
>
> data ABCF = ABCF { numA :: Int, numB :: Int, numC :: Int, numF :: Int }
> deriving (Show)
>
> zero :: ABCF
> zero = ABCF 0 0 0 0
>
> add :: Grade -> ABCF -> ABCF
> add A (ABCF a b c f) = ABCF (a+1) b c f
> add B (ABCF a b c f) = ABCF a (b+1) c f
> add C (ABCF a b c f) = ABCF a b (c+1) f
> add F (ABCF a b c f) = ABCF a b c (f+1)
> add O k = k
>
> grade :: ABCF -> IO ()
> grade abcf = do
> putStrLn "Put a grade here:"
> x <- getLine
> let o = isIn100 (read x)
> if o == O then
> print abcf
> else do
> print o
> grade (add o abcf)
>
> main :: IO ()
> main = grade zero
$ runghc ex3_5.lhs
Put a grade here:
59
F
Put a grade here:
69
C
Put a grade here:
79
B
Put a grade here:
89
A
Put a grade here:
99
A
Put a grade here:
101
ABCF {numA = 2, numB = 1, numC = 1, numF = 1}
せっかくなので細かい代数データ型Grade を用意、それに伴って引数と将来的に引き回されるそれぞれの人数を集計するABCF も用意。
あとはそれぞれのGrade の人数に1を足す関数を書いておきます。
grade 関数の中身はFortran のフローとほとんど変わりありません、goto ではなくて自己再帰呼び出しなのが違いといえば違いですが。
grade 関数は現在の人数を持つABCF を引き回しながら標準入力から整数を受け取って状態の更新を行います。
最後にmain としてこのgrade 関数に初期値としてゼロを食わせて一応の完成、というわけです。
問題
以下がしばらく考えても分からなかった問題点たちです
grade 関数が仕事しすぎ
IO と条件分岐と状態の更新をなんとか分離できないもんか?
素朴にFortran のやや不恰好なHaskell 翻訳に過ぎないので、エレガントとはいえないでももっとHaskell らしい書き方ができる気がする。
仮引数で引き回される、というパターンは、、、
このパターンはまさに状態系のモナドを使えるはずな気がするのだけど、、、
Haskell でどう書くのが"良い"のかお気づきの方、コメントお待ちしてます!