Qiita記事「Haskell チュートリアル (Haskell Day 2016)」を読んでメモ
以下を読んでメモ。
Haskell チュートリアル (Haskell Day 2016) から学んだこと http://qiita.com/hiratara/items/169b5cb83b0adbfda764
Shell以降は新しいことばっかりだったのと、元記事の完成度が高すぎて途中からは殆ど写経みたいになってしまった。
基本
暗黙的に副作用を起こす式がなく、明示的に副作用を起こす式(IO
型)がある。
言い換えると、IO
型が登場しない関数は副作用がないことを保証できる。
Hindley/Milner型推論アルゴリズムによる推論で、型は1つも書かなくても大丈夫。(read
などは例外)
Shellプログラミング
副作用の塊なので相性が悪いと思われるが(私はそう思ってた)、そうでもない。
1行目にシェバング、2行目にstack runghc
コマンド、を書いておくことで直接実行できる。
#!/usr/bin/env stack -- stack --resolver lts-6.15 --install-ghc runghc --package turtle ...
REPLでは:l
でソースを読み込んで、:main
で実行できる。
その後、:r
、:main
とすると効率的に開発できる。
turtle
Haskellでshell相当の関数が実装されたもの。
以下の2行のおまじないで使える。
#!/usr/bin/env stack -- stack --resolver lts-7.0 --install-ghc runghc --package turtle {-# LANGUAGE OverloadedStrings #-} import Turtle
REPLで試す場合、Stringのオーバーロード設定を忘れずに。
Prelude> :set -XOverloadedStrings
MonadoIO
MonadoIO
は型クラスで、IO
はそのインスタンス。
MonadoIO io => Text -> io ()
は Text -> IO ()
と読みかえて問題ない。
<-
と=
の違い
=
はHaskellの言語仕様で、let
式、where
句で使える。
<-
はバインドの糖衣構文で、do
専用。
main = do let title = "now: " now <- date putStrLn (title <> show now)
<>
は文字列(String
またはText
)の連結。(Monoid)
main関数はmain :: IO ()
という型を持つ。
printfとformat
Turtle.Formatモジュールに定義されている。
*Main> printf ("My name is "%s%". "%d%" years old.\n") "shu1" 0 My name is shu1. 0 years old.
Turtleの関数
-- 引数取得 arguments :: MonadIO io => io [Text] -- ファイルパスに変換 fromText :: Text -> Turtle.FilePath -- 最終更新日付を取得 datefile :: MonadIO io => Turtle.FilePath -> io UTCTime -- Textへ変換 repr :: Show a => a -> Text
mapM IOアクション リスト
という形式で繰り返し処理が出来る。
(「通常の引数をとってモナドに包まれた値を返す」関数をリストにmap
するときに使うものっぽい)
mapM print [1,2,3] mapM echoModified args
IOアクションはモナドから値を取り出した時に、はじめて実行される。
nestedIO = do putStr "Hello, " return (putStrLn "I/O!") main = do r1 <- nestedIO r2 <- r1 -- I/O! はここでの評価によって出力される
ストリーム処理
UNIXのパイプの実現に、IO ...
では役者不足。
Shell ...
というTurtle
が提供する型を利用するのが良い。
IO ...
:すべての結果を一度に返す
`Shell ...``:複数行の結果を1行ずつ返す
入力の関数
入力はShell ...
という型で表現される。
empty :: Shell a stdin :: Shell Text input :: FilePath -> Shell Text select :: [a] -> Shell a "INPUT" :: Shell Text
出力の関数
出力はShell ... -> IO ...
という型で表現される。
最終的にIO ...
になるので、do
ブロックで書ける。
sh :: Shell a -> IO () -- 出力を捨てる view :: Shell a -> IO () -- 出力を表示 stdout, stderr :: Shell Text -> IO output :: FilePath -> Shell Text -> IO () -- ファイルに出力 shell :: Text -> Shell Text -> IO ExitCode -- 外部コマンドに流し込む
パイプの関数
パイプ(|
と|
の間)はShell ... -> Shell ...
という型で表現される。
id :: Shell a -> Shell a limit :: Int -> Shell a -> Shell a inshell :: Text -> Shell Text -> Shell Text -- 外部コマンドを通す
組み合わせ
-- 通常の関数適用 stdout (limit 10 (input "sample.txt")) -- $によるカッコの省略 stdout $ limit 10 $ input "sample.txt" -- 関数合成 (stdout . limit 10 . input) "sample.txt" -- `&`による関数適用(UNIXライク) input "sample.txt" & limit 10 & stdout
fold
Control.Foldl
に定義されたFold
を使って、Stream ...
を回収できる。
import qualified Control.Foldl as Fold fold :: Shell a -> Fold a r -> IO r foldIO :: Shell a -> FoldM IO a r -> IO r
foldl
やfoldr
と違って、初期値は不要。(foldl1
やfoldr1
と同じ)
fmap
すべての行を関数によって変換する。
fmap :: (a -> b) -> Shell a -> Shell b ls "." & fmap (format fp) & stdout
grep
Pattern
とShell ...
をとり、grepされたShell ...
を作る。
grep :: Pattern a -> Shell Text -> Shell Text select ["Haskell", "Turtle", "Shell"] & grep (plus dot <> "ll") & stdout
has
、prefix
、suffix
が便利。
select ["Haskell", "Turtle", "Shell"] & grep (suffix "ll") & stdout
パターンについてはドキュメントを参照。
do記法
IO ...
と同様に、Shell ...
もdo
記法が使える。(モナドだから)
do
ブロックの戻り値はShell ...
となる。
戻り値はループ処理となる。
lsPrintf = do -- Shell ... の do ブロック file <- ls "." -- 全ファイル分、ループ処理される printf (fp%"\n") file main = do -- IO ... の do ブロック lsPrintf & sh
Applicative
[...]
、Maybe ...
、IO ...
、Shell ...
のようなコンテナ型を指す。
通常の関数を使った演算が可能なコンテナのこと。
(<*>) :: f (a -> b) -> f a -> f b (<$>) :: (a -> b) -> f a -> f b (+) <$> [1, 2] <*> [3, 4] -- => [4,5,5,6]
f
がn引数の関数のとき、f <$> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
。
(,)
(タプル)やコンストラクタに適用すると、一度に複数の方法で畳み込める。
Main Fold Turtle> select [1..6] & (`fold` ((,) <$> Fold.minimum <*> Fold.maximum)) & view (Just 1,Just 6)
パーサ
Applicativeはパーサで使われることが多い。
パーサの本質は「文字列の消費」と「結果の生成」からなる。
「結果の生成」をApplicativeで演算。
match :: Pattern a -> Text -> [a] *Main Fold Turtle> match ((,) <$> "a" <> star dot <*> "d" <> star dot) "abcdefg" [("abc","defg")]
戻り値の差し替え
(*>) :: f a -> f b -> f b -- 右のパーサの結果のみを使う (<*) :: f a -> f b -> f a -- 左のパーサの結果のみを使う sed :: Pattern Text -> Shell Text -> Shell Text pure -- パースしない(結果のみを返す) -- 文字列が消費されていって、左か右のどちらの結果を扱うか決めている *Main Fold Turtle> "abcdefg" & sed ("abc" *> pure "xyz") & stdout xyzdefg *Main Fold Turtle> "abcdefg" & sed ("abc" <* "def") & stdout abcg
コマンドライン引数のパーサ
Turtle.Options
でコマンドライン引数をパースできる。
parser :: Parser (Maybe Text, Bool) parser = (,) <$> optional (optText "dir" 'd' "Target directory") <*> switch "show" 's' "Show module names." (mDir, isShow) <- options "Count import." parser
演習問題の答え合わせ
let
は一つの宣言で複数書ける。
let fileTxt = head args file = fromText fileTxt
Haskellでは短い変数名をつけることが多い?(Maybe型の頭にm
をつけるのは分かりやすいと思った)
dt <- datefile file
最後がIOアクションで終わる場合はreturn
は不要。(でも書いておけば、とりあえずコンパイラは黙らせられる)
<-
でバインドしなくても、最終行に記述すればIOアクションは処理される。
nestedIO = do putStr "Hello, " return (putStrLn "I/O!") -- 解答 main = do printIO <- nestedIO printIO -- 私の答え main = do r1 <- nestedIO r2 <- r1 return ()
Turtleの(疑似)パイプ処理は、Shellコマンド実行にしても、関数にしても、&
で繋げられる。
find (suffix ".hs") path & grepImport & (`fold` Fold.length) & view
もうちょっと関数の分離を意識すると良いコードが書けるかも。