My Favorite Things - Coding or die.

とある技術者の経験記録、的な。

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

foldlfoldrと違って、初期値は不要。(foldl1foldr1と同じ)

fmap

すべての行を関数によって変換する。

fmap :: (a -> b) -> Shell a -> Shell b

ls "." & fmap (format fp) & stdout

grep

PatternShell ...をとり、grepされたShell ...を作る。

grep :: Pattern a -> Shell Text -> Shell Text
select ["Haskell", "Turtle", "Shell"] & grep (plus dot <> "ll") & stdout

hasprefixsuffixが便利。

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

もうちょっと関数の分離を意識すると良いコードが書けるかも。