Skip to content

Instantly share code, notes, and snippets.

@cojna

cojna/sugoih8.md Secret

Created April 8, 2013 08:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cojna/617d19bbb56ad9f3b1c8 to your computer and use it in GitHub Desktop.
Save cojna/617d19bbb56ad9f3b1c8 to your computer and use it in GitHub Desktop.

すごいHaskell読書会 in 大阪 #8

本日の内容

  • ToDoリスト
  • 乱数
  • ByteString
  • 演習

9.3 ToDoリスト

  • todo.txt に1行ずつタスクを記録
  • アイテムの追加 (appendtodo.hs)
  • アイテムの削除 (deletetodo.hs)

アイテムの追加 (appendtodo.hs)

import System.IO

main = do
  todoItem <- getLine
  appendFile "todo.txt" (todoItem ++ "\n")
  • getLine :: IO String
  • todoItem :: String
  • appendFile :: FilePath -> String -> IO ()

アイテムの削除

処理の流れ

  • todo.txt の読み込み
  • ToDoリストの表示
  • 番号の入力
  • アイテムを削除
  • 一時ファイルに新しいToDoリストを書き込む
  • todo.txtの削除
  • 一時ファイルのファイル名をtodo.txtに変更

deletetodo.hs

import System.IO
import System.Directory
import Data.List
import Control.Exception

main = do
  contents <- readFile "todo.txt"
  let todoTasks = lines contents
      numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks
  mapM_ putStrLn numberedTasks
  putStrLn "Which one do you want to delete?"
  hFlush stdout
  numberString <- getLine
  let number = read numberString
      newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
  (tempName, tempHandle) <- openTempFile "." "temp"
  hPutStr tempHandle newTodoItems
  hClose tempHandle
  removeFile "todo.txt"
  renameFile tempName "todo.txt"

todo.txtの読み込み

main = do
  contents <- readFile "todo.txt"
  let todoTasks = lines contents
  • readFile "todo.txt" :: IO String
  • contents :: String
  • contents = "Iron the dishes\nDust the dog\nTake salad out of the oven"
  • lines :: String -> [String]
  • todoTasks = [Iron the dishes,Dust the dog,Take salad out of the oven]

ToDoリストの表示

      numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks
  mapM_ putStrLn numberedTasks
  • zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    この場合は (Integer -> String -> String) -> [Integer] -> [String] -> [String]
  • todo.txt が何行あるのか気にしなくていい
  • numberedTasks = [ "0 - Iron the dishes", "1 - Dust the dog", "2 - Take salad out of the oven"]

番号の入力,リストの更新

  putStrLn "Which one do you want to delete?"
  hFlush stdout
  numberString <- getLine
  let number = read numberString
      newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
  • read :: Read a => String -> a
  • numberの型は?
  • readLn :: Read a => IO a という関数もある
  • unlines :: [String] -> String
  • delete :: Eq a => a -> [a] -> [a]
    delete 1 [1,2,3,1,2] = [2,3,1,2]

ファイルへの書き込み

  (tempName, tempHandle) <- openTempFile "." "temp"
  hPutStr tempHandle newTodoItems
  hClose tempHandle
  removeFile "todo.txt"
  renameFile tempName "todo.txt"
  • openTempFile :: FilePath -> String -> IO (FilePath, Handle)
    最後にランダムな文字をいくつか付けて temp4600 みたいなファイルを作る

クリーンアップ

現在の実装だと途中でエラーが起こったときに,一時ファイルが残ってしまう場合がある. 例えば,numberにリストの長さ以上の値を入力すると一時ファイルが残る.

Control.Exception の braketOnError を使う

  bracketOnError (openTempFile "." "temp")
     (\(tempName, tempHandle) -> do
        hClose tempHandle
        removeFile tempName)
     (\(tempName, tempHandle) -> do
        hPutStr tempHandle newTodoItems
        hClose tempHandle
        removeFile "todo.txt"
        renameFile tempName "todo.txt")
  • bracketOnError :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
    • bracket 処理が終わると常にリソースを開放する
    • bracketOnError 例外が発生した時のみリソースを開放

9.4 コマンドライン引数

arg-test.hs

import System.Environment
import Data.List

main = do
  args <- getArgs
  progName <- getProgName
  putStrLn "The arguments are:"
  mapM_ putStrLn args
  putStrLn "The program name is:"
  putStrLn progName
  • getArgs :: IO [String]
  • getProgName :: IO String

9.5 ToDoリストをもっと楽しむ

  • todo.txt だけでなく別のファイル名にも対応させる
  • 以下の3つの操作に対応させる
    • タスクの閲覧
    • タスクの追加
    • タスクの削除

$ ./todo view todo.txt
$ ./todo add todo.txt "Find the magic sword of power"
$ ./todo remove todo.txt 2

todo.hs

import System.Environment
import System.Directory
import System.IO
import Data.List

dispatch :: String -> [String] -> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove

main = do
  (command:argList) <- getArgs
  dispatch command argList

add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")

view :: [String] -> IO ()
view [fileName] = do
  contents <- readFile fileName
  let todoTasks = lines contents
      numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
                      [0..] todoTasks
  putStr $ unlines numberedTasks

remove :: [String] -> IO ()
remove [fileName, numberString] = do
  contents <- readFile fileName
  let todoTasks = lines contents
      numberedTasks = zipWith (\n line -> show n ++ " - " ++ line)
                      [0..] todoTasks
  putStrLn "These are your TO-DO items:"
  mapM_ putStrLn numberedTasks
  let number = read numberString
      newTodoItems = unlines $ delete (todoTasks !! number) todoTasks
  bracketOnError (openTempFile "." "temp")
     (\(tempName, tempHandle) -> do
        hClose tempHandle
        removeFile tempName)
     (\(tempName, tempHandle) -> do
        hPutStr tempHandle newTodoItems
        hClose tempHandle
        removeFile fileName
        renameFile tempName fileName)

練習問題

ファイルとタスク番号を受け取り, そのタスクをToDoリストの先頭に持ってくるbump関数を実装してみましょう.

bump :: [String] -> IO ()
bump [fileName, numberString] = do
  contents <- readFile fileName
  let todoTasks = lines contents
      number = read numberString
      (xs, y:ys) = splitAt number todoTasks
      newTodoItems = unlines $ y:xs ++ ys
  bracketOnError (openTempFile "." "temp")
     (\(tempName, tempHandle) -> do
        hClose tempHandle
        removeFile tempName)
     (\(tempName, tempHandle) -> do
        hPutStr tempHandle newTodoItems
        hClose tempHandle
        removeFile "todo.txt"
        renameFile tempName "todo.txt")

不正な入力への対応

存在しないコマンドに対して

dispatch :: String -> [String] -> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
dispatch command = doesntExist command

doesntExist :: String -> [String] -> IO ()
doesntExist command _ =
  putStrLn $ unwords ["The", command, "command doesn't exist"]

引数がおかしい

add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")
add _ = putStrLn "The add command takes exactly two arguments"

view,removeに対しても同様

その他の不正な入力

  • $ ./todo
    (command:argList) <- getArgs でエラーが起きる
  • 存在しないファイル名を指定

9.6 ランダム性

  • Haskellは 参照透明
    • 何回呼んでも同じ結果を返す
randomNumber :: Int
randomNumber = 4
  • 乱数はどうするのか?
random :: RandomGen g => g -> (a, g)
  • g 乱数のジェネレータ
  • a 乱数

System.Random の使い方

  • 乱数のジェネレータの作り方
    • StdGen型を使う(RandomGenのインスタンスになってる)
    • data StdGen = StdGen Int32 Int32
    • mkStdGen :: Int -> StdGen
ghci> random (mkStdGen 100)

<interactive>:3:1:
    Ambiguous type variable `a0' in the constraint:
      (Random a0) arising from a use of `random'
    Probable fix: add a type signature that fixes these type variable(s)
    In the expression: random (mkStdGen 100)
    In an equation for `it': it = random (mkStdGen 100)

型を書かないと怒られます

ghci> random (mkStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
ghci> random (mkStdGen 949494) :: (Int, StdGen)
(539963926,466647808 1655838864)
ghci> random (mkStdGen 949488) :: (Float, StdGen)
(0.8241101,1597344447 1655838864)
ghci> random (mkStdGen 949488) :: (Bool, StdGen)
(False,1485632275 40692)
ghci> random (mkStdGen 949488) :: (Integer, StdGen)
(1691547873,1597344447 1655838864)

コイントス

  • 3回コイントスする関数
threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
  let (firstCoin, newGen) = random gen
      (secondCoin, newGen') = random newGen
      (thirdCoin, newGen'') = random newGen'
  in (firstCoin, secondCoin, thirdCoin)
  • ジェネレータを変えないと (True, True, True) か (False, False, False) のどちらかの結果にしかならない
ghci> threeCoins (mkStdGen 21)
(True,True,True)
ghci> threeCoins (mkStdGen 22)
(True,False,True)
ghci> threeCoins (mkStdGen 943)
(True,False,True)
ghci> threeCoins (mkStdGen 944)
(True,True,True)
  • 今回は型注釈がなくても大丈夫

コインの数が増えたときはどうするのか?

randoms :: RandomGen g => g -> [a]
randoms gen = (\(value, newGen) -> value : randoms newGen) (random gen)
  • 新しいジェネレータは返せない
    • randoms :: RandomGen g => g -> ([a], g) ではない
    • 無限リストを返すとき,gとして何を返すのか?
  • 有限リストならジェネレータを返せる
finiteRandoms :: (RandomGen g, Random a, Num n) => n -> g -> ([a], g)
finiteRandoms 0 gen = ([], gen)
finiteRandoms n gen =
  let (value, newGen) = random gen
      (restOfList, finalGen) = finiteRandoms (n-1) newGen
  in (value:restOfList, finalGen)

ある範囲の乱数を生成するときはどうするのか

randomR :: RandomGen g => (a, a) -> g -> (a, g)
randomRs :: RandomGen g => (a, a) -> g -> [a]
ghci> randomR (1,6) (mkStdGen 359353)
(6,1494289578 40692)
ghci> randomR (1,6) (mkStdGen 35935335)
(3,1250031057 40692)
ghci> take 10 $ randomRs ('a','z') (mkStdGen 3)
"ndkxbvmomg"

ランダム性とI/O

  • 今までのプログラムだと毎回同じ乱数を生成してしまう
import System.Random

main = do
  gen <- getStdGen
  putStrLn $ take 20 (randomRs ('a','z') gen)
$ ./random_string
guqlrxsyismobhollrmp
$ ./random_string
qznahaxkoilnnympgtmn
$ ./random_string
fzihvweqmegurclnefsn

getStdGen と newStdGen

import System.Random

main = do
  res <- sequence [getStdGen, getStdGen, newStdGen, getStdGen, getStdGen]
  mapM_ print res
$ runghc random_string2
323060062 1
323060062 1
1221755171 2147483398
323060063 40692
323060063 40692
  • getStdGen
    グローバル乱数ジェネレータの値を読む
  • newStdGen
    • グローバル乱数ジェネレータの値をリセットする
    • 新しいジェネレータの値を返しているわけではない

数当てゲーム

import System.Random
import System.IO
import Control.Monad (when)

main = do
  gen <- getStdGen
  askForNumber gen

askForNumber :: StdGen -> IO ()
askForNumber gen = do
  let (randNumber, newGen) = randomR (1,10) gen :: (Int, StdGen)
  putStrLn "Which number in the range from 1 to 10 am I thinking of? "
  hFlush stdout
  numberString <- getLine
  when (not $ null numberString) $ do
    let number = read numberString
    if randNumber == number
       then putStrLn "You are correct!"
       else putStrLn $ "Sorry, it was " ++ show randNumber
    hFlush stdout
    askForNumber newGen
./guess_the_number
Which number in the range from 1 to 10 am I thinking of?
1
Sorry, it was 10
Which number in the range from 1 to 10 am I thinking of?
1
Sorry, it was 2
Which number in the range from 1 to 10 am I thinking of?
1
You are correct!
Which number in the range from 1 to 10 am I thinking of?

note

  • "haha"のような入力した場合,readが失敗してエラーになる
  • reads :: Read a => ReadS a
    • type ReadS a = String -> [(a, String)]

別実装

import System.Random
import System.IO
import Control.Monad (when)

main = do
  gen <- getStdGen
  askForNumber gen
  let (randNumber, _) = randomR (1,10) gen :: (Int, StdGen)
  putStrLn "Which number in the range from 1 to 10 am I thinking of? "
  hFlush stdout
  numberString <- getLine
  when (not $ null numberString) $ do
    let number = read numberString
    if randNumber == number
       then putStrLn "You are correct!"
       else putStrLn $ "Sorry, it was " ++ show randNumber
    hFlush stdout
    newStdGen
    main
  • 最初の実装のほうがよい
    • main関数のやることが少ない
    • 関数の再利用も簡単

9.7 ByteString

  • HaskellのStringはCharのリスト
  • ByteString
    • 効率のよい文字列処理

正格ByteStringと遅延ByteString

-- 正格ByteString
data ByteString = PS {-# UNPACK #-} !(ForeignPtr Word8) -- payload
                     {-# UNPACK #-} !Int                -- offset
                     {-# UNPACK #-} !Int                -- length
-- 遅延ByteString
data ByteString = Empty | Chunk {-# UNPACK #-} !S.ByteString ByteString
  • 正格ByteString
    • 配列になっている
    • 無限の正格ByteStringは作れない
  • 遅延ByteString
    • 64Kバイトの正格ByteStringのリスト
    • 64KバイトはCPUのL2キャッシュにフィットする

遅延ByteStringの使い方

  • インポートは修飾子付きで
    • import qualified Data.ByteString.Lazy as B
    • import qualified Data.ByteString as S
  • ByteStringへの変換
    • pack :: [Word8] -> ByteString
    • unpack :: ByteString -> [Word8]
    • Word8`は8ビットの符号なし整数
ghci> B.pack [99,97,110]
Chunk "can" Empty
ghci> B.pack [98..120]
Chunk "bcdefghijklmnopqrstuvwx" Empty
ghci> let by = B.pack [98,111,114,116]
ghci> by
Chunk "bort" Empty
ghci> B.unpack by
[98,111,114,116]

遅延ByteStringと正格ByteStringの変換

  • toChunks :: ByteString -> [ByteString]
    遅延BytesStringを正格ByteStringのリストに
  • fromChunks :: [ByteString] -> ByteString
    正格ByteStringのリストを遅延ByteStringに

ByteStringのcons

  • cons :: Word8 -> ByteString -> ByteString
    • B.cons は o(1)
    • S.cons は o(n)

ByteStringでファイル入力

  • readFile :: FilePath -> IO ByteString
    • 正格ByteStringだとファイル内容のすべてがメモリ上に1度に読み込まれる

ByteStringを使ったファイルのコピー

import System.Environment
import System.Directory
import System.IO
import Control.Exception
import qualified Data.ByteString.Lazy as B

main = do
  (fileName1:fileName2:_) <- getArgs
  copy fileName1 fileName2

copy source dest = do
  contents <- B.readFile source
  bracketOnError (openTempFile "." "temp")
    (\(tempName, tempHandle) -> do
         hClose tempHandle
         removeFile tempName)
    (\(tempName, tempHandle) -> do
         B.hPutStr tempHandle contents
         hClose tempHandle
         renameFile tempName dest)

OverloadedString拡張

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy as B

main = B.putStrLn "Hello, World!"

パターンガード,ViewPatterns拡張

{-# LANGUAGE OverloadedStrings, ViewPatterns #-}
import qualified Data.ByteString.Lazy as B

main = do
   B.putStrLn $ f "Hello, World!"
   B.putStrLn $ g "Hello, World!"

-- パターンガード
f bs
  | Just (c, bs') <- B.uncons bs = bs'
  | otherwise = bs

-- ViewPatterns
g (B.uncons -> Just (c, bs')) = bs'
g bs = bs

文字列のまとめ

  • String
    • 使いやすい(パターンマッチとか)
    • 大きいデータには向かない
  • ByteString
    • 効率がいい
    • 正格ByteStringと遅延ByteStringを使い分ける
    • 言語拡張を使えば楽に扱える

まとめ

  • ToDoリスト
    • ファイルの入出力
    • コマンドライン引数
  • 乱数
  • ByteString
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment