fugafuga.write

日々のログ



すごいH本 part68

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)

テスト用ファイル

bart.txt

abcdefghijklmnopqrstuvvvvvvv

実行する

> stack ghc -- --make bytestringcopy.hs
[1 of 1] Compiling Main             ( bytestringcopy.hs, bytestringcopy.o )
Linking bytestringcopy ...
> ./bytestringcopy bart.txt bort.txt
> cat bort.txt
abcdefghijklmnopqrstuvvvvvvv
> cat bart.txt
abcdefghijklmnopqrstuvvvvvvv

文字列を使ったプログラムを作成する場合、bytestring を使ったものに 書き換えるには多くの場合、修飾付きインポートして対応する関数の前に モジュール名を付け足すだけでいける。

文字列操作でもしパフォーマンスを上げる必要があるなら、 bytestring を試す価値がある。

所感

copy は独自で定義した関数なのに、型宣言が無かったので一瞬よくわからなかった。 基本的に書いていただきたい。

世間は羽生フィーバーである。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part67

bytestring

まず、遅延評価について

wikipedia より

評価しなければならない値が存在するとき、 実際の計算を値が必要になるまで行わないことをいう。 評価法が指示されているが実際の計算が行われていない中間状態の時 それをプロミス(英: promise)や、 計算の実体をさしてサンク(英: thunk)といい、 プロミスを強制(英: force)することで値が計算される。

リスト [1,2,3,4] を出力する時の評価について考える。

  1. リスト[1,2,3,4]1:2:3:4:[] の構文糖衣
  2. 出力時、先頭の 1 から評価される
  3. 残りの部分 2:3:4:[] はまだ評価されていないものとする
  4. この時、2:3:4:[] はプロミスであるといえる
  5. さらに、2:3:4:[] の計算を指して、サンク(thunk) という

上記のことから、 リストはプロミスであるといえる。 値が必要になった場合に、次の要素と後続のプロミスを渡してくれるようなプロミス。

しかし、単なる数のリストをサンクの列として処理するのは効率が悪い。 サイズの大きなファイルを操作する場合に、オーバーヘッドが問題となる。

そのため、Haskell には bytestring が用意されている。

bytestring はリストに似たデータ構造で、要素は1バイト(あるいは8bit)のサイズ固定。

正格 bytestring と 遅延 bytestring

正格 bytestring は Data.ByteString に定義されていて、遅延性が完全に排除されている。 サンクは一切ない。なので無限の 正格 bytestring のようなものは作ることができない。

遅延 bytestring は Data.ByteString.Lazy で定義されている。遅延評価はされるが、 リストほどではない。リストの場合、要素数と同じ位の数のサンクがあり、これが原因で 場合によっては遅くなる。遅延 bytestring の場合、64Kバイトのチャンク(chunk) という 塊にデータが格納され、遅延 bytestring が評価されたら、最初の 64Kバイトが評価される。 残りはリストと同じようにプロミスとなる。

メモリ使用量も抑えつつ、CPUのL2キャッシュにフィットするサイズになっている。

GHCiで実際に触ってみる。

*Main Lib> import qualified Data.ByteString.Lazy as B

<no location info>: error:
    Could not find module ‘Data.ByteString.Lazy’
    It is a member of the hidden package ‘bytestring-0.10.8.1@bytestring-0.10.8.1’.

また怒られが。

hidden package の対応は以前と同じようにしてみる。

my-project.cabal

executable my-project-exe
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , my-project
                     , random
                     , bytestring <= 追加
  default-language:    Haskell2010

再度

*Main Lib> import qualified Data.ByteString.Lazy as B
*Main Lib B> import qualified Data.ByteString as S
*Main Lib B S>

いけた。

B.pack という関数を見る

*Main Lib B S> :t B.pack
B.pack :: [GHC.Word.Word8] -> B.ByteString

Word8 のリストを受け取って、ByteString を返す。

Word8 型は 8ビット符号無し整数を表す。範囲は 0~255

実際に pack する

*Main Lib B S> B.pack [99,97,110]
"can"
*Main Lib B S> B.pack [90..120]
"Z[\\]^_`abcdefghijklmnopqrstuvwx"

unpack する

*Main Lib B S> let by = B.pack [98,111,114,116]
*Main Lib B S> by
"bort"
*Main Lib B S> B.unpack by
[98,111,114,116]

正格 bytestring と 遅延 bytestring を相互変換することも可能

toChunksは、遅延 bytestring を受け取って、正格 bytestring のリストに変換する

*Main Lib B S> B.toChunks $ B.pack [1..255]
["\SOH\STX\ETX\EOT\ENQ\ACK\a\b\t\n\v\f\r\SO\SI\DLE\DC1\DC2\DC3\DC4\NAK\SYN\ETB\CAN\EM\SUB\ESC\FS\GS\RS\US ","!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`","abcdefghijklmnopqrstuvwxyz{|}~\DEL\128\129\130\131\132\133\134\135\136\137\138\139\140\141\142\143\144\145\146\147\148\149\150\151\152\153\154\155\156\157\158\159\160\161\162\163\164\165\166\167\168\169\170\171\172\173\174\175\176\177\178\179\180\181\182\183\184\185\186\187\188\189\190\191\192\193\194\195\196\197\198\199\200\201\202\203\204\205\206\207\208\209\210\211\212\213\214\215\216\217\218\219\220\221\222\223\224","\225\226\227\228\229\230\231\232\233\234\235\236\237\238\239\240\241\242\243\244\245\246\247\248\249\250\251\252\253\254\255"]

fromChunksは、正格 bytestring のリストを受け取って、遅延 bytestring に変換する

*Main Lib B S> B.fromChunks [S.pack [40,41,42], S.pack[43,44,45], S.pack[46,47,48]]
"()*+,-./0"

bytestring 版の :cons

*Main Lib B S> B.cons 88 $ B.pack [1,2,3,4,5]
"X\SOH\STX\ETX\EOT\ENQ"

bytestring モジュールの関数は以下にに全部載っている

http://hackage.haskell.org/package/bytestring

bytestring モジュールには System.IO モジュールが提供する関数と同様の動作を する関数もある。String の代わりに ByteString を受け取る。

*Main Lib B S> :t readFile
readFile :: FilePath -> IO String
*Main Lib B S> :t B.readFile
B.readFile :: FilePath -> IO B.ByteString
*Main Lib B S> :t S.readFile
S.readFile :: FilePath -> IO S.ByteString

所感

遅延評価がネックになってくる部分を補うための方法が用意されていてよい。

promise と thunk はまだイメージがふんわりしているので、 進めながら理解していきたいところ。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part66

ランダム性とI/O

getStdGen という I/O アクションは、何らかの初期データを使って システムのグローバル乱数ジェネレータを初期化する。

import System.Random

main = do
    gen <- getStdGen
    putStrLn $ take 20 (randomRs ('a', 'z') gen)

実行

> stack ghc -- --make random_string.hs
[1 of 1] Compiling Main             ( random_string.hs, random_string.o )
Linking random_string ...
> ./random_string
pwmtcyvvuiqrlcxxugkg
> ./random_string
xgtscdkplkltwsviragk
> ./random_string
knqwbdakxwwngtwhfznj
> ./random_string
gaeihsegnztzrhktends

毎回違う値がとれている。

一度に2つのランダムな値が欲しいとする。

import System.Random

main = do
    gen <- getStdGen
    putStrLn $ take 20 (randomRs ('a', 'z') gen)
    gen2 <- getStdGen
    putStrLn $ take 20 (randomRs ('a', 'z') gen2)

実行

> stack ghc -- --make random_string.hs                                        
[1 of 1] Compiling Main             ( random_string.hs, random_string.o )
Linking random_string ...
> ./random_string                                                             
ogifizssaztchwbwaavl
ogifizssaztchwbwaavl
> ./random_string                                                             
zyytwpyywogfjmtolvjm
zyytwpyywogfjmtolvjm
> ./random_string                                                             
lopqapxwdkakjuipcgfv
lopqapxwdkakjuipcgfv

getStdGen を2回実行しても同じグローバル乱数ジェネレータをシステムが 返してくるのでこれではダメ。

これを実現するためには、newStdGen アクションを使う。 これはグローバル乱数ジェネレータを2つのジェネレータに分割する。

import System.Random

main = do
    gen <- getStdGen
    putStrLn $ take 20 (randomRs ('a', 'z') gen)
    gen' <- newStdGen
    putStrLn $ take 20 (randomRs ('a', 'z') gen')

実行

> stack ghc -- --make random_string.hs                                        
[1 of 1] Compiling Main             ( random_string.hs, random_string.o )
Linking random_string ...
> ./random_string                                                             
wxyevwiruiwsoqwyabdr
uwcvveeltuejritxceih
> ./random_string                                                             
zjqgfnuekjdaelehlxvj
goefmqrrebmzmqsiomao
> ./random_string                                                             
enxgkrluzhbyqoxhqssr
klekqhhqszulpjsntjsl

できた。

newStdGen を束縛すると、新しい乱数ジェネレータが 得られるだけでなく、グローバルジェネレータも更新される。

ユーザーにプログラムが考えた数を当てさせるプログラムを書く

import System.Random
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? "
    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
        askForNumber newGen

実行

> stack ghc -- --make guess_the_number.hs                                     
[1 of 1] Compiling Main             ( guess_the_number.hs, guess_the_number.o )
Linking guess_the_number ...
> ./guess_the_number                                                          
Which number in the range from 1 to 10 am I thinking of?
1
Sorry, it was 7
Which number in the range from 1 to 10 am I thinking of?
2
Sorry, it was 5
Which number in the range from 1 to 10 am I thinking of?
3
Sorry, it was 1
Which number in the range from 1 to 10 am I thinking of?
4
Sorry, it was 1
Which number in the range from 1 to 10 am I thinking of?
5
Sorry, it was 6
Which number in the range from 1 to 10 am I thinking of?
6
Sorry, it was 9
Which number in the range from 1 to 10 am I thinking of?
7
Sorry, it was 6
Which number in the range from 1 to 10 am I thinking of?
5
Sorry, it was 10
Which number in the range from 1 to 10 am I thinking of?
3
Sorry, it was 9
Which number in the range from 1 to 10 am I thinking of?
2
Sorry, it was 1
Which number in the range from 1 to 10 am I thinking of?
1
Sorry, it was 5
Which number in the range from 1 to 10 am I thinking of?
2
Sorry, it was 10
Which number in the range from 1 to 10 am I thinking of?
3
Sorry, it was 7
Which number in the range from 1 to 10 am I thinking of?
4
Sorry, it was 5
Which number in the range from 1 to 10 am I thinking of?
5
Sorry, it was 10
Which number in the range from 1 to 10 am I thinking of?
6
You are correct.

なかなか当たらん。

所感

let number = read numberString でなんで数字に変換されるのか調べる。 デフォルトで文字列 -> 数値 の変換になるのかドキュメントのどこに書いてあるかわからん。

追記

型推論っぽい

Haskell の read 関数で、文字列から代数的データ型へ変換 - 導出インスタンスを使って | すぐに忘れる脳みそのためのメモ

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part65

もっともっとランダム関数

コインを投げる話の続き。

もっとたくさんのランダム値が欲しい場合どうするか

randoms を使う

*Main System.Random> :t randoms
randoms :: (RandomGen g, Random a) => g -> [a]

乱数ジェネレータを受け取って無限長のランダム値のリストを返す。

*Main System.Random> take 5 $ randoms (mkStdGen 100) :: [Int]
[-3633736515773289454,-1610541887407225575,4434840125058622350,1116419036860971948,1434273519690261584]
*Main System.Random> take 5 $ randoms (mkStdGen 100) :: [Bool]
[True,False,False,False,False]

randoms の実装はこんな感じでできる

randoms' :: (RandomGen g, Random a) => g -> [a]
randoms' gen = let (value, newGen) = random gen in value:randoms' newGen

let の中で value と newGen を受け取って、value をリストの先頭につけて再帰。 これで無限リストが作れる。

有限のリストと新しいジェネレータを生成する関数はこう実装する

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)

実行

Prelude System.Random> :l three-coins.hs
[1 of 1] Compiling Main             ( three-coins.hs, interpreted )

three-coins.hs:14:15: error:
    • Could not deduce (Eq n) arising from the literal ‘0’
      from the context: (RandomGen g, Random a, Num n)
        bound by the type signature for:
                   finiteRandoms :: (RandomGen g, Random a, Num n) =>
                                    n -> g -> ([a], g)
        at three-coins.hs:13:1-69
      Possible fix:
        add (Eq n) to the context of
          the type signature for:
            finiteRandoms :: (RandomGen g, Random a, Num n) =>
                             n -> g -> ([a], g)
    • In the pattern: 0
      In an equation for ‘finiteRandoms’: finiteRandoms 0 gen = ([], gen)
Failed, modules loaded: none.

エラーが発生した。 finiteRandoms関数のパターンマッチで 0 と Num a を比較しようとしている部分でエラーが発生している模様。 メッセージにあるように、Eq n の制約を追加する。

finiteRandoms :: (RandomGen g, Random a, Num n, Eq 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)

再度実行

Prelude System.Random> :l three-coins.hs
[1 of 1] Compiling Main             ( three-coins.hs, interpreted )
Ok, modules loaded: Main.
*Main System.Random> take 5 $ randoms' (mkStdGen 100)
[-3633736515773289454,-1610541887407225575,4434840125058622350,1116419036860971948,1434273519690261584]
*Main System.Random> take 5 $ randoms' (mkStdGen 100) :: [Int]
[-3633736515773289454,-1610541887407225575,4434840125058622350,1116419036860971948,1434273519690261584]
*Main System.Random> take 5 $ randoms' (mkStdGen 100) :: [Bool]
[True,False,False,False,False]
*Main System.Random> finiteRandoms 5 (mkStdGen 100) :: ([Bool], StdGen)
([True,False,False,False,False],942794516 652912057)
*Main System.Random> finiteRandoms 5 (mkStdGen 100) :: ([Int], StdGen)
([-3633736515773289454,-1610541887407225575,4434840125058622350,1116419036860971948,1434273519690261584],1772499918 2118231989)

できた。

ある範囲の乱数を生成したい場合は randomR を使う

*Main System.Random> :t randomR
randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g)

実行

*Main System.Random> randomR (1, 10) (mkStdGen 400) :: (Int, StdGen)
(2,16045614 40692)
*Main System.Random> randomR (1, 10) (mkStdGen 121212) :: (Int, StdGen)
(4,555249856 40692)
*Main System.Random> randomR (1, 10) (mkStdGen 24242424) :: (Int, StdGen)
(5,1521307037 40692)
*Main System.Random> randomR (1, 10) (mkStdGen 595959) :: (Int, StdGen)
(5,224424247 40692)
*Main System.Random> randomR (1, 10) (mkStdGen 59595966) :: (Int, StdGen)
(6,966268608 40692)

randomRs もある。指定された範囲の乱数を無限に生成する。

*Main System.Random> :t randomRs
randomRs :: (RandomGen g, Random a) => (a, a) -> g -> [a]
*Main System.Random> take 5 $ randomRs (1, 10) (mkStdGen 121212)
[4,5,9,1,6]
*Main System.Random> take 5 $ randomRs (1, 10) (mkStdGen 56754399)
[7,1,8,7,6]
*Main System.Random> take 5 $ randomRs ('a', 'z') (mkStdGen 56754399) :: [Char]
"uwhsf"

所感

let を使った例を出してくれるので let の使い方の参考になる。 理解はできるが書けないと思うので多様な例を出してくれるのはありがたい。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part64

ランダム性

ランダム値を扱う。 Haskell は参照透明性を持つので関数が同じ引数で2回呼ばれた場合、 必ず同じ値を返すようになっていなければならない。 その中でランダム値をどのように作るのかを見る。

System.Random モジュールに random という関数があるので見ゆ

*Main Lib> :t random

<interactive>:1:1: error: Variable not in scope: random
*Main Lib> import System.Random

<no location info>: error:
    Could not find module ‘System.Random’
    It is a member of the hidden package ‘random-1.1@random-1.1-9tceXaeYIMZ4JrKq20Egog’.

めっちゃ怒られる

my-project.cabalを編集

executable my-project-exe
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , my-project
                     , random <= 追加
  default-language:    Haskell2010

stack に対して依存パッケージを指定してあげないとダメな模様

再度チャレンジ

*Main Lib> import System.Random
*Main Lib System.Random> :t random
random :: (RandomGen g, Random a) => g -> (a, g)

いけた。

  • RandomGen 型クラスはランダム性の源として扱える型を表現
  • Random 型クラスはランダムな値になることができる型を表現

乱数ジェネレータを受け取り、ランダムな値と新しい乱数ジェネレータを返す

乱数ジェネレータが欲しい。 System.Random に StdGen という型がある。 これは RandomGen のインスタンスになっている。

*Main Lib System.Random> :i RandomGen
class RandomGen g where
  next :: g -> (Int, g)
  genRange :: g -> (Int, Int)
  split :: g -> (g, g)
  {-# MINIMAL next, split #-}
        -- Defined in ‘System.Random’
instance RandomGen StdGen -- Defined in ‘System.Random’

乱数ジェネレータを自分で作るには mkStdGen 関数を使う。

*Main Lib System.Random> :t mkStdGen
mkStdGen :: Int -> StdGen

整数を引数にとってその値を元に乱数ジェネレータを返す。

ランダム値を作ってみる

*Main Lib System.Random> random (mkStdGen 100) :: (Int, StdGen)
(-3633736515773289454,693699796 2103410263)
*Main Lib System.Random> random (mkStdGen 100) :: (Int, StdGen)
(-3633736515773289454,693699796 2103410263)
*Main Lib System.Random> random (mkStdGen 223344) :: (Int, StdGen)
(-262505389206341605,1147014161 2103410263)

タプルの1番目の値がランダム値、2番目が新しい乱数ジェネレータの数値としての表現。 同じ乱数ジェネレータを渡すと同じ値が返ってくる。

型注釈をつけることで、Int 以外のランダム値を得ることができる。

*Main Lib System.Random> random (mkStdGen 100) :: (Bool, StdGen)
(True,4041414 40692)
*Main Lib System.Random> random (mkStdGen 100) :: (Float, StdGen)
(0.6512469,651872571 1655838864)
*Main Lib System.Random> random (mkStdGen 100) :: (Integer, StdGen)
(-3633736515773289454,693699796 2103410263)

コイントス

3回のコイントスをシミュレートする関数を書く

import System.Random

threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
    let (firstCoin, newGen) = random gen
        (secondCoin, newGen') = random newGen
        (thirdCoin, newGen'') = random newGen'
    in (firstCoin, secondCoin, thirdCoin)

実行

*Main System.Random> threeCoins (mkStdGen 100)
(True,False,False)
*Main System.Random> threeCoins (mkStdGen 200)
(True,False,True)
*Main System.Random> threeCoins (mkStdGen 300)
(True,True,True)

random gen で *Coin の値が 型注釈無しで Bool 値になるのは、 関数の型宣言に書いてあるため、Haskellが型推論できるから。

所感

他の言語でもランダム値の生成については今まであまり触れてこなかったので、 Haskell でどのように乱数が生成されるのかを知れて良かった。

あと、let めっちゃかっこよく使っててすごい。

(参考)

https://qiita.com/katsuyan/items/a132d7bf6817f19af2d6

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る