【Functor】すごいHaskellたのしく学ぼう!を読んだ (1)

『すごいhaskellたのしく学ぼう!』 (原著名: Learn You a Haskell for Great Good) を読了したので, メモを残しておきます。
内容も濃く長いので, 今回は開発環境構築の話と9章までの内容です。

haskell_logo

開発環境構築

処理系は GHC (Glasgow Haskell Compilation)が有名です。環境はOSX10.10です。

$ wget https://www.haskell.org/ghc/dist/7.8.2/ghc-7.8.2-x86_64-apple-darwin-mavericks.tar.xz
$ cd ghc-7.8.2
$ ./configure 
$ make install

確認します。

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.8.2

コンソールで動かします。終了する場合は :quit または :q です。

$ ghci
GHCi, version 7.8.2: https://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> 1+1
2
Prelude> :quit
Leaving GHCi.

実行ファイルを生成しないで実行する場合は runghcコマンドを使います。
runhaskellでも runghcに symbolic-linkが張られているのでOKです。

$ runghc --version
runghc 7.8.2
$ ls -l /usr/local/bin/ | grep runhaskell
lrwxr-xr-x  1 Tatsuya  admin   6 10 27 08:55 runhaskell@ -> runghc

Cabalでmodule管理する

Cabalはパッケージシステムです。Cabalには Build機能も備わっています。

$ wget https://www.haskell.org/cabal/release/cabal-install-1.20.0.3/cabal-1.20.0.3-x86_64-apple-darwin-mavericks.tar.gz
$ tar xvzf cabal-1.20.0.3-x86_64-apple-darwin-mavericks.tar.gz 
x dist/build/cabal/cabal
$ cd dist/build/cabal/
$ ./cabal update
$ sudo ./cabal install --global cabal-install

インストール済みのパッケージの一覧表示, パッケージの削除は下記です。

$ ghc-pkg list
$ ghc-pkg unregister [module-name]

Sandbox環境の構築

andbox環境を構築することで, ある程度の開発規模になると発生しやすい依存関係地獄 (Dependency hell) に陥る可能性を下げることができます。

$ mkdir cabal-example
$ cd cabal-example/
$ cabal sandbox init

この状態で cabal install を実行すると, ローカルにインストールされます。sandbox上で使えるpackageの確認は下記。

$ cabal sandbox hc-pkg list

パッケージング

Module開発時には, Cabalで扱えるようにパッケージングしておくと便利です。これを cabalizeと言います。
対話形式で質問に答えていくと, HackageへのModule公開に必要なファイルを生成してくれます。 *.cabalの役割として Makefileに近いものです。

$ cabal init
$ ls
.cabal-sandbox/      Setup.hs             src/
LICENSE              cabal-example.cabal
$ cat cabal-example.cabal 
-- Initial cabal-example.cabal generated by cabal init.  For further 
-- documentation, see https://haskell.org/cabal/users-guide/

name:                cabal-example
version:             0.1.0.0
-- synopsis:            
-- description:         
license:             MIT
license-file:        LICENSE
author:              ttsy
-- maintainer:          
-- copyright:           
-- category:            
build-type:          Simple
-- extra-source-files:  
cabal-version:       >=1.10

library
  -- exposed-modules:     
  -- other-modules:       
  -- other-extensions:    
  build-depends:       base >=4.8 && <4.9
  hs-source-dirs:      src
  default-language:    Haskell2010tatsuya@

この状態で cabal build を行うと, dist/ が生成されます。

$ cabal build
Package has never been configured. Configuring with default flags. If this
fails, please run configure manually.
Resolving dependencies...
Configuring cabal-example-0.1.0.0...
Building cabal-example-0.1.0.0...
Preprocessing library cabal-example-0.1.0.0...
In-place registering cabal-example-0.1.0.0...

実際に, Hackageへ公開するにはユーザ登録と開発者申請が必要のようです。

第1章 はじめの第一歩


1.1 関数呼び出し
1.2 赤ちゃんの最初の関数
1.3 リスト入門
1.4 レンジでチン!
1.5 リスト内包表記
1.6 タプル

1章では List内包表記や Tupleを扱っています。

main = do
 print (1,3)
 print [(1,4), (8,2)]
 print $ fst ("wow", "hey") -- "wow"
 print $ zip [1,2,3,4,5] [9,8,7,6,5] -- [(1,9),(2,8),(3,7),(4,6),(5,5)]

第2章 型を信じろ!


2.1 明示的な型宣言
2.2 一般的なHaskellの型
2.3 型変数
2.4 型クラス 初級講座

2章では型クラスの概念を扱います。型は何らかの性質を持ち, 型クラスを定義するということは型が満たす性質に共通するインターフェイスを定義することになります。

第3章 関数の構文


3.1 パターンマッチ
3.2 場合分けして、きっちりガード!
3.3 where?!
3.4 let It Be
3.5 case 式

3章では関数型言語の特徴のひとつであるパターンマッチを扱います。
例題としてBMI計算プログラムを作ります。

f 1 = "good"
f 2 = "normal"
f a = "bad"

-- firstletter
firstletter :: String -> String
firstletter "" = "Empty String"
firstletter all@(x:xs) = "The First Letter Of" ++ all ++ " is " ++ [x]

-- calculate BMI
bmiCalc :: Double -> Double -> String
bmiCalc weight height
 | bmi <= 18.5 = "slim"
 | bmi <= 25.0 = "normal"
 | otherwise = "fat"
 where bmi = weight / height ^2

main = do
 -- Pattern match
 print $ f 0
 print $ f 1
 print $ f 2

 -- list comprehension
 let xs = [(1,3), (4,5), (3,2), (11,3)]
 print [a + b | (a,b) <- xs]

 -- as Pattern
 print $ firstletter "FiS Project"

 -- where keyword
 print $ bmiCalc 60.5 1.68

第4章 Hello 再帰!


4.1 最高に最高!
4.2 さらにいくつかの再帰関数
4.3 クイック、ソート!
4.4 再帰的に考える

4章では再帰でエレガントな書き方を学びます。

fact 1 = 1
fact n = n * fact (n - 1)

maxinum' :: (Ord a) => [a] -> a
maxinum' [] = error "empty list"
maxinum' [x] = x
maxinum' (x:xs) = max x (maxinum' xs)

main = do
 print $ fact 7 -- 5040
 print $ maxinum'[1,2,3,4,5,10] -- 10

クイックソートが簡潔に書けるのも関数型の利点。

-- quick sort
qsort :: (Ord a) => [a] -> [a]
qsort [] = []
qsort (x:xs) = let
 smallOrEqual = [a | a <- xs, a <= x]
 larger = [a | a <- xs, a > x]
 in qsort smallOrEqual++ [x] ++  qsort larger

main = print $ qsort [6,3,4,9,1,12,0] -- [0,1,3,4,6,9,12]

第5章 高階関数


5.1 カリー化関数
5.2 高階実演
5.3 関数プログラマの道具箱
5.4 ラムダ式
5.5 畳み込み、見込みアリ!
5.6 \$ を使った関数適用
5.7 関数合成

5章は高階関数について。カリー化関数, ラムダ式, 畳関数を扱います。

第6章 モジュール


6.1 モジュールをインポートする
6.2 標準モジュールの関数で問題を解く
6.3 キーから値へのマッピング
6.4 モジュールを作ってみよう

6章ではモジュールを扱います。例題はシーザー暗号 (Caesar Cipher)。単一換字式暗号であるシーザー暗号は古代ローマ時代に発明されました。
単純さ故に頻度分析で容易に解読できてしまいます。

import Data.Char

encode :: Int -> String -> String
encode offset = map (chr . (+offset) . ord)

decode :: Int -> String -> String
decode shift = encode (negate shift)

main = do
 print $ encode 1 "hello world" -- "ifmmp!xpsme"
 print $ decode 1 "ifmmp!xpsme" -- "hello world"
 print $ encode 3 "hello world" -- "khoor#zruog"
 print $ decode 3 "khoor#zruog" -- "hello world"

第7章 型や型クラスを自分で作ろう


7.1 新しいデータ型を定義する
7.2 形づくる
7.3 レコード構文
7.4 型引数
7.5 インスタンスの自動導出
7.6 型シノニム
7.7 再帰的なデータ構造
7.8 型クラス 中級講座
7.9 YesとNoの型クラス
7.10 Functor型クラス
7.11 型を司るもの、種類

7章では独自型を扱います。Functor (関手)が登場します。Functorは圏から圏への対応付けます。
Functorは型クラスで, fmapが定義されています。

Prelude> :i Functor
class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  	-- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’

関数合成(function composition)は fmapと同様です。

二分木構築/探索プログラム。

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show)

singleton :: a -> Tree a
singleton x = Node x EmptyTree EmptyTree

treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x
treeInsert x (Node a left right)
 | x == a = Node x left right
 | x < a = Node a (treeInsert x left) right
 | x > a = Node a left (treeInsert x right)

treeElem :: (Ord a) => a -> Tree a ->  Bool
treeElem x EmptyTree = False
treeElem x (Node a left right)
 | x == a = True
 | x < a = treeElem x left
 | x > a = treeElem x right


main = do
 let nums = [8,1,9,3,2,5,6,0,7]
 let numsTree = foldr treeInsert EmptyTree nums
 print numsTree
 print $ 8 `treeElem` numsTree
 print $ 10 `treeElem` numsTree

第8章 入出力


8.1 不純なものと純粋なものを分離する
8.2 Hello, World!
8.3 I/O アクションどうしをまとめる
8.4 いくつかの便利なI/O関数
8.5 I/O アクションおさらい

8章はI/O周りについて。副作用のない綺麗な世界と, 副作用の可能性のある汚い世界を分離する方法について。
ようやくHelloWorldが登場しました。

main = putStrLn "helloworld"

doはI/Oアクションになります。

import System.IO

main = do
 handle <- openFile "linux.txt" ReadMode
 contents <- hGetContents handle
 putStr contents
 hClose handle

Handling command-line arguments

コマンドの引数を取得する例。

import System.Environment
import Data.List

main = do
 args <- getArgs
 progName <- getProgName
 putStrLn "args are : "
 mapM_ putStrLn args
 putStrLn "program is : "
 putStrLn progName

第9章 もっと入力、もっと出力


9.1 ファイルとストリーム
9.2 ファイルの読み書き
9.3 ToDoリスト
9.4 コマンドライン引数
9.5 ToDoリストをもっと楽しむ
9.6 ランダム性
9.7 bytestring

9章ではファイルI/Oやストリーム等を扱います。
そして, ラムダ式や高階関数など前半の知識を活用してTODO管理プログラムを作ります。
9章のtodoプログラムにはバグがあるので注意。日本語版だけでなく原著のpdfにもバグは存在します。

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

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

main = do
  (command:args) <- getArgs
  let (Just action) = lookup command dispatch
  action args

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

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

--  remove task
remove :: [String] -> IO ()
remove [fileName, numberString] = do
  (tempName, tempHandle) <- openTempFile "." "temp"
  contents <- readFile fileName
  let number = read numberString
   tasks = lines contents
   newTasks = delete (tasks !! number) tasks
  hPutStr tempHandle $ unlines newTasks
  hClose tempHandle
  removeFile fileName
  renameFile tempName fileName

-- replace bracketOnError
bump :: [String] -> IO ()
bump [fileName, numberString] = do
  (tempName, tempHandle) <- openTempFile "." "temp"
  contents <- readFile fileName
  let number = read numberString
   tasks = lines contents
   bumpee = tasks !! number
   newTasks = bumpee : delete bumpee tasks
  hPutStr tempHandle $ unlines newTasks
  hClose tempHandle
  removeFile fileName
  renameFile tempName fileName

使い方。

$ ./TODO add todo.txt "get book"
$ cat todo.txt 
get book
$ ./TODO view todo.txt 
0 - get book
$ ./TODO remove todo.txt 0

続きは, part2で書きます。今回のCodeはGitHubにあります。


[1] Functor(関手)ってなんですか?
[2] todo.hsのバグについて