プログラミング勉強ログ

プログラミングの勉強記録

Yet Another Haskell 3章 (8)

3.8 Interactivity
ユーザーインタラクションに関して


キーボードからの入力を受け付ける関数(scanf, cin等)
は厳密な意味では関数ではない。
ユーザーの入力によって結果が変わってしまうからだ。
この問題は数学の圏論におけるモナドによって解決された。
モナドに関しては、5章や9章で詳しく扱う。

インタラクティブなプログラムを書くためには、doを使う。
これを使うことで、プログラムの実行順序を制御できる。
Haskellはlazyな言語なので、通常、プログラムの実行順序は決まっていない)

ユーザーの名前を聞く簡単なプログラムが次だ。

module Main
    where

import System.IO

main = do
  hSetBuffering stdin LineBuffering
  putStrLn "Please enter your name: "
  name <- getLine
  putStrLn ("Hello, " ++ name ++ ", how are you?")

これを実行すると次のようになる。

*Main> main
Please enter your name:
quotientK
Hello, quotientK, how are you?

確かに、インタラクションができている。


次にプログラムの解説。

import System.IOは入出力のための関数を定義しているIOというライブラリを取り込む。

doはコマンドを順番に実行するためのもの。

hSetBuffering stdin LineBufferingはGHCでの実行に必要なもので、特に重要ではないっぽい。(GHCでは通常ある一定の長さの入力を行うまで、処理が次に行かないらしい。このコマンドを実行すると処理が次に行くようになるらしいが、試しにこの行を削除して実行してみても特に問題無かった。環境がWindowsだからかな。)

putStrLnは文字列を画面に出力するコマンド。

name <- getLineは、他の言語では、name = getLine()のようなもの。
ただ、getLineは関数ではない(ユーザーの入力によって結果が代わる)ため、<-を使う。これは「getLineを実行し、その結果をnameに保存する」を意味する。

最後の行は、nameを加工して、画面に文字列を出力する。


こういうものだと思えば何のことは無いが、この後ろで働いてるモナドの理解が大変なんだろうね。まあとりあえず今は気にしないでおこう。


お次は、数字当てゲームの例。
1から100までの数字をランダムに決め、それを当てるプログラムだ。
乱数もまた純粋な関数ではないため、モナドを使う必要がある。

module Main
    where

import System.IO
import System.Random

main = do
  hSetBuffering stdin LineBuffering
  num <- randomRIO (1::Int, 100)
  putStrLn "I’m thinking of a number between 1 and 100"
  doGuessing num

doGuessing num = do
  putStrLn "Enter your guess:"
  guess <- getLine
  let guessNum = read guess
  if guessNum < num
    then do putStrLn "Too low!"
            doGuessing num
    else if guessNum > num
           then do putStrLn "Too high!"
                   doGuessing num
           else do putStrLn "You Win!"


num <- randomRIO (1::Int, 100)は、1から100までの乱数を生成し、numに代入する。getLineと同じく<-を使用する。

Intは生成する乱数が整数であること指定するためのもの。100の方にはいらないのは推論されるからだろう。

その次がdoGuessingを実行する。

doGuessing num = doでdoGuessingの定義が始まる。

guess <- getLineで、guessにユーザーからの入力を保存する。

let guessNum = read guessで、guessを数値に変換する。(getLineの結果はStringだから)
readは純粋な関数のため、read guessでは<-は使わない。
なお、doの中ではlet使用時にinは必要ないとのこと。

そして、guessとnumの値によって分岐し、次に続く処理を決める。

なるほど。


次は、再帰的にコマンドを実行する際の注意。
まず間違った例から。

askForWords = do
  putStrLn "Please enter a word:"
  word <- getLine
  if word == ""
    then return []
    else return (word : askForWords)

間違っているのは最後の行。
askForWordsはリストではなく。アクション(action)だから、:で繋ぐことができない。

で、正しい例は、

askForWords = do
  putStrLn "Please enter a word:"
  word <- getLine
  if word == ""
    then return []
    else do
      rest <- askForWords
      return (word : rest)

<-を使ってアクションの結果をrestに代入し、それを:で繋いで返すのが正しい。

returnがしれっと出てきているが、この<-やreturnがモナドの一部(?)というのは聞いたことがある。

基本的な部分は理解できた。と思う。

Exercise 3.10

module Main
    where

import System.IO

main = do
    hSetBuffering stdin LineBuffering
    nums <- scanNumbers
    putStrLn ("The sum is " ++ show (Main.sum nums))
    putStrLn ("The product is " ++ show (Main.product nums))
    printFactorialList nums

scanNumbers = do
  putStrLn "Give me a number (or 0 to stop):"
  line <- getLine
  let n = read line
  if n == 0
    then return []
    else do
      rest <- scanNumbers
      return (n : rest)

sum list = foldl (+) 0 list

product list = foldl (*) 1 list

factorial 1 = 1
factorial n = n * factorial(n - 1)

printFactorialList [] = do
  return ()

printFactorialList (x:xs) = do
  putStrLn ((show x) ++ " factorial is " ++ (show (factorial x)))
  printFactorialList xs

実行結果

*Main> main
Give me a number (or 0 to stop):
10
Give me a number (or 0 to stop):
2
Give me a number (or 0 to stop):
5
Give me a number (or 0 to stop):
1
Give me a number (or 0 to stop):
0
The sum is 18
The product is 100
10 factorial is 3628800
2 factorial is 2
5 factorial is 120

何もしないことの表し方でハマった…。return ()か。