『すごいhaskellたのしく学ぼう!』 (原著名: Learn You a Haskell for Great Good) を読了したので, メモを残しておきます。
内容も濃く長いので, 今回は開発環境構築の話と9章までの内容です。
開発環境構築
処理系は 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のバグについて