Slide 1

Slide 1 text

Haskellを使おう 2017.8.4 hiratara@FreakOut!

Slide 2

Slide 2 text

agenda 今日話すこと。 ● Stackとエコシステム ● 実行制御 ● I/O処理 ● 外部パッケージの利用 今日話さないこと。 ● Haskellの文法など、基礎 ● 高度な抽象概念を使ったHaskellの美しさ

Slide 3

Slide 3 text

Stackとエコシステム

Slide 4

Slide 4 text

Stack ● cabal-install に変わるHaskellのビルドツール ● 扱うパッケージの形式は旧cabal-installと互換 ● 複数プロジェクト間の依存パッケージのバージョン違いを適切に扱う

Slide 5

Slide 5 text

最速でREPL グローバルプロジェクトを利用してREPLを立ち上げる。 ● curl -sSL https://get.haskellstack.org/ | sh ● stack setup ● stack ghci

Slide 6

Slide 6 text

Stackageでできること ● パッケージ一覧 ● Hoogle ● ドキュメント閲覧

Slide 7

Slide 7 text

Hoogle ● 関数名、型名の検索 ○ fmap ○ Maybe ● 型による関数の検索 ○ :: 型名 ○ 似た型の関数も探すが、過信は禁物

Slide 8

Slide 8 text

ドキュメント ● モジュール一覧 ● 依存、被依存パッケージ

Slide 9

Slide 9 text

ビルドのポイント ● ghc-options: -Wall (*.cabalに) ○ 全警告の表示 ● hlint ○ よりよいコードのためのヒント ● ghc-options: -threaded (*.cabalに) ○ 並列処理では必須(忘れがち) ● hpack ○ cabalファイル管理してくれるぽい ○ https://www.ncaq.net/2017/07/31/ ● ghc 8.2.1 出たよー ○ エラー表示がカラーで見やすい

Slide 10

Slide 10 text

Haskellのコミュニティ ● 日本Haskellユーザグループ ○ https://haskell.jp/ ○ Slack : #questions で質問を受け付け ○ reddit : Haskellに関するエントリの共有など ○ Haskell-jpもくもく会 ■ 月に一度オフラインで集まって、もくもく開発

Slide 11

Slide 11 text

Haskellの書籍を出します ● タイトル: Haskell入門 ● 発売日: 9月27日(予定) ● 価格: 未定 ● ページ数: 未定 ● 概要(仮) ○ 開発環境の構築 ○ 一通りの文法 ○ モナド変換子 ○ bytestring, attoparsec, lens, operational, pipes などの主要パッケージ ○ 並列並行プログラミング ○ サンプルのアプリケーションをいくつか

Slide 12

Slide 12 text

Haskellの実行制御

Slide 13

Slide 13 text

Haskellにおけるプログラムの実行制御 関数型的な記述をするのがHaskellの醍醐味だが、プログラミングに必要な最低限の武 器もしっかりと押さえておく。 ● 順次処理 : do記法 ● 分岐 : case文(パターンマッチ) ● 繰り返し : 再帰関数

Slide 14

Slide 14 text

do記法 操作を並べて書くことができる。 import System.Environment (getArgs, getProgName) import Data.List (intercalate) main = do name <- getProgName putStrLn $ "prog: " ++ name args <- getArgs let argstr = intercalate "," args putStrLn $ "input: " ++ argstr

Slide 15

Slide 15 text

分岐 パターンマッチやガード文が充実しており、得意。 legalDrink :: Maybe Int -> Bool legalDrink (Just n) | n >= 20 = True | otherwise = False legalDrink Nothing = False -- または legalDrink' mn = case mn of Just n | n >= 20 -> True | otherwise -> False Nothing -> False

Slide 16

Slide 16 text

分岐(ポリモフィズム) 型クラスを用いるとOOPのようなアドホック多相を実現できる。 class Drinkable a where legalDrink :: a -> Bool data Adult = Adult instance Drinkable Adult where legalDrink _ = True data Young = Young instance Drinkable Young where legalDrink _ = False

Slide 17

Slide 17 text

繰り返し(リスト) リストやFreeモナドのような再帰データ型を介した繰り返し処理は得意 ● 要素への反復処理: map, foldr, mapM_ など ● continue は filter で ● break は takeWhile で sumEven100 :: Int sumEven100 = foldr (+) 0 $ takeWhile (<= 100) $ filter (\n -> n `mod` 2 == 0) $ [1..]

Slide 18

Slide 18 text

繰り返し(手続き的) 手続き的な記述のループは得意ではないが、再帰 で同じものを書くことができる。 # 擬似コード def main(): while (isEOF()): line = getLine() print(line) import System.IO (isEOF) main = loop where loop = do done <- isEOF if done then return () else do line <- getLine putStrLn line loop

Slide 19

Slide 19 text

繰り返し(ストリーミング) conduitパッケージを使うと、手続き処理もリストっぽく扱える。 {-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString as BS import Data.Conduit ((.|)) import qualified Data.Conduit as C import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.List as CL import Data.Monoid ((<>)) import System.IO (stdin, stdout) main :: IO () main = C.runConduit $ CB.sourceHandle stdin .| CB.lines .| CL.map (<> "\n") .| CB.sinkHandle stdout 愚直に書いた再帰とほぼ同じ意味となる。 doブロックに書かれた各操作が第一級であるこ とを利用し、 .| で結合する時にすべての操作を 適切な順番で実行できるよう組み替えている。

Slide 20

Slide 20 text

HaskellのI/O処理

Slide 21

Slide 21 text

HaskellとI/O ● Haskellでは任意のI/O処理を記述することができる (ただし、得意ではない) ● ほとんどのアプリケーションはI/Oが主役 ○ ファイルの読み書き ○ KVS、DBへのアクセス ○ APIサーバとのやりとり ○ サブプロセスの起動と制御 ○ 時刻の取得 ○ 設定ファイルの読み込み I/Oを扱うノウハウを会得することが重要。

Slide 22

Slide 22 text

I/Oが可能な場所 ● IO モナドの中 ○ a -> b -> … -> IO z という形式の型を持つ関数 ○ 特に、 main 関数 ● IO をモナド変換子でラップしたモナドの中 ○ a -> b -> … -> OtherMonadT Hoge (SomeMonadT Foo Bar IO) z などの形式 ○ lift 関数をラップされた回数だけ使って IO を持ち上げる ● MonadIO 型クラス ○ MonadIO m => a -> b -> … -> m z という形式 ○ liftIO 関数で IO を持ち上げる ● フレームワークなどを使う場合は、これらを探す

Slide 23

Slide 23 text

IOモナド ● PythonやPHPなど、他の言語のプログラムと同等の機能を提供 ○ 入出力 ○ ミュータブル変数 ○ 例外 (の catch ) ● IO モナドがなければHaskellはこれらの機能を持たない ○ StateモナドやErrorモナドはあくまで模倣 ● 書きやすさは別の話 ○ モナドの >>= はcallbackを要求するので、callback hellとなる ○ do 記法を用いると、PythonやPHPとほぼ同様の使い勝手となる ● モナドと do 記法に慣れよう

Slide 24

Slide 24 text

モナド上の「操作」 各モナドには、その上で可能な操作が提供される。 ● 操作: 戻り値の型が Monad m => m a の形式のもの ○ putStrLn :: String -> IO () ○ catchIOError :: IO a -> (IOError -> IO a) -> IO a ○ return :: Monad m => a -> m a ○ mapM :: Monad m => (a -> m b) -> [a] -> m [b] ○ put :: Monad m => s -> StateT s m () ● 操作ではないもの ○ unsafePerformIO :: IO a -> a ○ runState :: State s a -> s -> (a, s)

Slide 25

Slide 25 text

do記法に書けるもの ● 操作に引数を適用した、 Monad m => m a 型を持つ式 ○ putStrLn “Hi” :: IO () ○ catchIOError getContents (return . show) :: IO String ○ return True :: Monad m => m Bool ○ mapM print [1..10] :: IO [()] ○ put (1 :: Int) :: Monad m => StateT Int m () ● 1つのdoブロックには1つのモナドの操作のみ ○ IOモナドのブロックには StateT Int mモナドの操作は書けない ● 並べた操作がどう実行されるかはモナドによる ○ IOモナドでは、並べられた操作が順番に実行される

Slide 26

Slide 26 text

do記法のコツ(1) let x = … と x <- … を使い分ける。 ● <- は操作を実行した結果を取り出して束縛 ● let は右辺をそのまま束縛 ○ 操作ではないが do ブロックに書ける main = do let x = 1 + 2 :: Int y <- return (3 + 4) :: IO Int print (x + y :: Int) :: IO ()

Slide 27

Slide 27 text

do記法のコツ(2) 操作の結果を直接引数にすることはできない。 冗長でも <- で別の変数に束縛してから使う。 import System.Environment (getProgName) main = do -- putStrLn $ "prog: " ++ getProgName とは書けない name <- getProgName putStrLn $ "prog: " ++ name

Slide 28

Slide 28 text

モナドで使われる演算子 無理し過ぎず、使えそうな部分に使ってみると良い。 main = do -- x <- getProgName; putStrLn x と同じ getProgName >>= putStrLn putStrLn =<< getProgName -- Applicative -- x <- getProgName; y <- getProgname; let z = (x, y) と同じ z <- (,) <$> getProgName <*> getProgName print z

Slide 29

Slide 29 text

I/Oとモナドの関係 ● Haskellでは IO はモナド ● 複雑な引数の扱いを隠蔽するためのモナド ○ データを戻り値と引数で明示的に引き回す必要性 ○ 冗長なので、ReaderかStateのモナドが欲しい よって、 ● モナドを使ったプログラミング必須 ● 実用上、2つ以上のモナドを同時に使いたい ○ モナド変換子

Slide 30

Slide 30 text

アプリケーションと状態 長時間動作するアプリケーションの場合、現在のアプリケーションの状態によって期待さ れる動作が変わることが多い。 ● 処理回数が規定回数を超えたか否か ● 外部のサーバとの接続が確立されているか否か ● 読み込んだファイルが更新されてるか否か ● etc. しかし、関数間で状態を受け渡すコードは冗長であり、ヒューマンエラーも起きやすい。

Slide 31

Slide 31 text

明示的な受け渡し newtype AppStat = AppStat Int deriving (Eq, Show) appGetLine (AppStat n) = do x <- getLine return (x, AppStat $ n + 1) appPutStrLn (AppStat n) l = do putStrLn $ show n ++ ": " ++ l return $ AppStat n main = loop $ AppStat 0 where loop s = do (line, s') <- appGetLine s s'' <- appPutStrLn s' line let AppStat n = s'' if n < 3 then loop s'' else return ()

Slide 32

Slide 32 text

状態の受け渡しを隠す モナドによって隠蔽することができる。 ● State sモナド ○ s -> (a, s) ● Reader rモナド ○ r -> m a ○ 読み取り専用 しかし、すでにIOモナドを使っているのでモナドは使えない。

Slide 33

Slide 33 text

モナド変換子 ● MonadTrans 型クラスのインスタンスのこと ● モナド m に操作を追加する ● モナド m の操作は lift を経由して使う ○ lift :: (MonadTrans t, Monad m) => m a -> t m a ● 一般的に、 runHogehogeT という命名でモナド m に戻す関数が存在 main = runHogehogeT $ do opHogehoge1 lift $ print “This is I/O action” opHogehoge2 ここには HogehogeT IO の操作を書ける 全体としては IO 型 HogehogeT IO という型名はコード に現れてないことに注意

Slide 34

Slide 34 text

StateTモナド変換子 ● newtype StateT s m a = StateT (s -> m (a, s)) ● runStateT に状態の初期値(s型)と共に与えて剥がす ● 追加される操作 ○ get :: Monad m => StateT s m s ○ put :: Monad m => s -> StateT s m () ● 他によく使う操作 ○ modify' :: Monad m => (s -> s) -> StateT s m () 関数を適用して状態を直接変更する ’ がついていると正格 ○ gets :: Monad m => (s -> a) -> StateT s m a 状態を取り出し、関数で変換してから返す 関数としてフィールド名を指定すると便利

Slide 35

Slide 35 text

StateTモナド変換子 import Control.Monad.Trans import Control.Monad.Trans.State newtype AppStat = AppStat { runAppStat :: Int } deriving (Eq, Show) appGetLine = do x <- lift getLine modify $ AppStat . (+1) . runAppStat return x appPutStrLn l = do n <- gets runAppStat lift $ putStrLn $ show n ++ ": " ++ l main = (`runStateT` AppStat 0) $ loop where loop = do line <- appGetLine appPutStrLn line n <- gets runAppStat if n < 3 then loop else return () 状態を受け渡すコードを隠蔽してくれる。 状態を取り出す gets や、状態を更新する modify という操作が追加される。

Slide 36

Slide 36 text

ReaderTモナド変換子 ● newtype ReaderT r m a = ReaderT (r -> m a) ● runReaderTに環境の値(r型)と共に渡して剥がす ● 追加される操作 ○ ask :: Monad m => ReaderT r m r ○ local :: (r -> r) -> ReaderT r m a -> ReaderT r m a ● よく使う操作 ○ asks :: Monad m => (r -> a) -> ReaderT r m a 状態を取り出し、関数で変換してから返す 関数としてフィールド名を指定すると便利 ● IO, IORef と使うと読み込みだけじゃなく状態も表現可

Slide 37

Slide 37 text

ReaderT モナド変換子 import Control.Monad.Trans import Control.Monad.Trans.Reader import Data.IORef newtype AppStat = AppStat { runAppStat :: IORef Int } deriving (Eq) appGetLine = do x <- lift getLine modify' (+ 1) return x appPutStrLn l = do n <- get' lift $ putStrLn $ show n ++ ": " ++ l get' = do r <- asks runAppStat lift $ readIORef r modify' f = do r <- asks runAppStat lift $ atomicModifyIORef' r (\x -> (f x, ())) main = do r <- newIORef 0 (`runReaderT` AppStat r) $ loop where loop = do line <- appGetLine appPutStrLn line n <- get' if n < 3 then loop else return () 設定を受け渡すコードを隠蔽してくれる。 設定を取り出す asks が追加される。 更新操作はないが、 IOモナドが配下にいるとミュー タブルに更新できる。

Slide 38

Slide 38 text

MonadIO 型クラス ● モナド変換子の問題 ○ IO を持ち上げるために lift が何回必要かわかりにくい ● base パッケージの Control.Monad.IO.Class モジュール ○ liftIO :: IO a -> m a いつでも1回の利用で IO を持ち上げられる ○ MonadIO をモナド変換子で持ち上げたものも MonadIO となるよう実装 ● 例1: instance (MonadIO m) => MonadIO (StateT s m) ● 例2: instance (MonadIO m) => MonadIO (ReaderT r m)

Slide 39

Slide 39 text

フレームワークに現れるMonadIO 純粋な処理を提供するフレームワーク以外では登場する。 ● MonadIO m => MonadIO (ConduitM i o m) MonadIO m => MonadIO (Pipe l i o u m) (conduitパッケージ) ● MonadIO m => MonadIO (Proxy a' a b' b m) (pipes パッケージ) ● MonadIO m => MonadIO (WebStateT conn sess st m) (spock パッケージ)

Slide 40

Slide 40 text

フレームワークに現れるMonadIO ● MonadIO m => MonadIO (HandlerT site m) MonadIO m => MonadIO (WidgetT site m) (yesod パッケージ) ● MonadIO m => MonadIO (LoggingT m) (monad-loggerパッケージ) ● MonadIO Handler (servantパッケージ)

Slide 41

Slide 41 text

フレームワークに現れるMonadIO ● MonadIO X MonadIO Query (xmonadパッケージ) ● MonadIO (EventM n) (brickパッケージ)

Slide 42

Slide 42 text

外部パッケージの利用

Slide 43

Slide 43 text

パッケージの選び方 パッケージとはHaskellのライブラリのこと。 Stackageの自分が使うLTSから選ぶ。 ● 少なくとも、ライブラリの依存でハマることがない ● Cのライブラリへの依存でハマることは多い(特にWindows) ● 慌てず、ドキュメントを見つつaptなどで必要なものを導入

Slide 44

Slide 44 text

パッケージの選び方 参考にするサイト ● https://haskell-lang.org/libraries ○ FP completeが立ち上げたHaskellのサイト ○ 本家は haskell.org ● https://haskelliseasy.readthedocs.io/en/latest/ ○ Haskell Programming from First Principles の著者 ● http://dev.stephendiehl.com/hask/ ○ What I Wish I Knew When Learning Haskell ○ ライブラリに限らず、情報量が多い ● http://lotz84.github.io/haskellbyexample/ ○ Go by Example の Haskell版

Slide 45

Slide 45 text

よく使われるパッケージ ● bytestring, text : (普通の)効率的な文字列、必須 ● vector : (普通の)効率的な配列 ● containers, unordered-containers : マップなど基本的なコンテナ ● transformers : モナド変換子(特にStateTとReaderT) ● attoparsec : 高速なパーサコンビネータ ● safe-exception : 例外処理のベストプラクティス ● conduit : ストリーミング(再利用可能な入出力) ● optparse-applicative : コマンドライン引数

Slide 46

Slide 46 text

パッケージの使い方 動作するコード例を探す。 ● haddock の先頭付近 ● チュートリアル的なページ ○ パッケージのトップの Home Page欄 ● リポジトリ内にexampleディレクトリがないか ○ パッケージのトップから githubのリンクを探して飛ぶ ○ もしくは、Downloadsのbrowseリンクから

Slide 47

Slide 47 text

モナディックなAPIの読み方 ● 何を提供するモナドか、メンタルモデルを把握する ○ I/O (MonadIO) ○ 引数の引き回し(State, Reader, Writer) ○ 例外処理(Maybe, Either, Except) ○ 実行順の制御(Cont, ストリーミング系) ○ ループ(リスト) ○ 並列、並行性(STM, Eval, Par) ○ 抽象DSL (Free) ● 利用可能な操作を探す ● モナドの剥がし方を探す ○ runSomeT や unSomeT などの命名が多い

Slide 48

Slide 48 text

型を読むコツ(1) データコンストラクタに惑わされないようにする。 type SpockM conn sess st = SpockCtxM () conn sess st newtype SpockCtxT ctx m a = SpockCtxT { runSpockT :: W.SpockAllT m (ReaderT (LiftHooked ctx m) m) a } deriving (Monad, Functor, Applicative, MonadIO) data PoolOrConn a where PCPool :: Pool a -> PoolOrConn a PCConn :: ConnBuilder a -> PoolOrConn a PCNoDatabase :: PoolOrConn ()

Slide 49

Slide 49 text

型を読むコツ(2) 型クラス(class, instance)の提供するメソッドを見逃さない。 ● Monoid ○ <> で連結できる ● IsString ○ 文字列リテラルで書ける ● Functor、Applicative、Monad ○ do 記法で使える

Slide 50

Slide 50 text

まとめ ● stackを使うとHaskellのビルドで困ることはほぼなくなる ● 活発化する日本のHaskellコミュニティに乗り遅れない ● 逐次実行、分岐、繰り返しの書き方をマスターする ● IO モナドの使い方は重要なので、熟知するまで触る ● ReaderT、StateTのモナド変換子を理解する ● ライブラリのドキュメントの見方を知る