Haskellを使おう

 Haskellを使おう

2b027b9ba2b6c1dad842e15f99d17251?s=128

Masahiro Honma

August 04, 2017
Tweet

Transcript

  1. Haskellを使おう 2017.8.4 hiratara@FreakOut!

  2. agenda 今日話すこと。 • Stackとエコシステム • 実行制御 • I/O処理 • 外部パッケージの利用

    今日話さないこと。 • Haskellの文法など、基礎 • 高度な抽象概念を使ったHaskellの美しさ
  3. Stackとエコシステム

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

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

    setup • stack ghci
  6. Stackageでできること • パッケージ一覧 • Hoogle • ドキュメント閲覧

  7. Hoogle • 関数名、型名の検索 ◦ fmap ◦ Maybe • 型による関数の検索 ◦

    :: 型名 ◦ 似た型の関数も探すが、過信は禁物
  8. ドキュメント • モジュール一覧 • 依存、被依存パッケージ

  9. ビルドのポイント • ghc-options: -Wall (*.cabalに) ◦ 全警告の表示 • hlint ◦

    よりよいコードのためのヒント • ghc-options: -threaded (*.cabalに) ◦ 並列処理では必須(忘れがち) • hpack ◦ cabalファイル管理してくれるぽい ◦ https://www.ncaq.net/2017/07/31/ • ghc 8.2.1 出たよー ◦ エラー表示がカラーで見やすい
  10. Haskellのコミュニティ • 日本Haskellユーザグループ ◦ https://haskell.jp/ ◦ Slack : #questions で質問を受け付け

    ◦ reddit : Haskellに関するエントリの共有など ◦ Haskell-jpもくもく会 ▪ 月に一度オフラインで集まって、もくもく開発
  11. Haskellの書籍を出します • タイトル: Haskell入門 • 発売日: 9月27日(予定) • 価格: 未定

    • ページ数: 未定 • 概要(仮) ◦ 開発環境の構築 ◦ 一通りの文法 ◦ モナド変換子 ◦ bytestring, attoparsec, lens, operational, pipes などの主要パッケージ ◦ 並列並行プログラミング ◦ サンプルのアプリケーションをいくつか
  12. Haskellの実行制御

  13. Haskellにおけるプログラムの実行制御 関数型的な記述をするのがHaskellの醍醐味だが、プログラミングに必要な最低限の武 器もしっかりと押さえておく。 • 順次処理 : do記法 • 分岐 :

    case文(パターンマッチ) • 繰り返し : 再帰関数
  14. 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
  15. 分岐 パターンマッチやガード文が充実しており、得意。 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
  16. 分岐(ポリモフィズム) 型クラスを用いると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
  17. 繰り返し(リスト) リストやFreeモナドのような再帰データ型を介した繰り返し処理は得意 • 要素への反復処理: map, foldr, mapM_ など • continue

    は filter で • break は takeWhile で sumEven100 :: Int sumEven100 = foldr (+) 0 $ takeWhile (<= 100) $ filter (\n -> n `mod` 2 == 0) $ [1..]
  18. 繰り返し(手続き的) 手続き的な記述のループは得意ではないが、再帰 で同じものを書くことができる。 # 擬似コード 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
  19. 繰り返し(ストリーミング) 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ブロックに書かれた各操作が第一級であるこ とを利用し、 .| で結合する時にすべての操作を 適切な順番で実行できるよう組み替えている。
  20. HaskellのI/O処理

  21. HaskellとI/O • Haskellでは任意のI/O処理を記述することができる (ただし、得意ではない) • ほとんどのアプリケーションはI/Oが主役 ◦ ファイルの読み書き ◦ KVS、DBへのアクセス

    ◦ APIサーバとのやりとり ◦ サブプロセスの起動と制御 ◦ 時刻の取得 ◦ 設定ファイルの読み込み I/Oを扱うノウハウを会得することが重要。
  22. 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 を持ち上げる • フレームワークなどを使う場合は、これらを探す
  23. IOモナド • PythonやPHPなど、他の言語のプログラムと同等の機能を提供 ◦ 入出力 ◦ ミュータブル変数 ◦ 例外 (の

    catch ) • IO モナドがなければHaskellはこれらの機能を持たない ◦ StateモナドやErrorモナドはあくまで模倣 • 書きやすさは別の話 ◦ モナドの >>= はcallbackを要求するので、callback hellとなる ◦ do 記法を用いると、PythonやPHPとほぼ同様の使い勝手となる • モナドと do 記法に慣れよう
  24. モナド上の「操作」 各モナドには、その上で可能な操作が提供される。 • 操作: 戻り値の型が 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)
  25. 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モナドでは、並べられた操作が順番に実行される
  26. 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 ()
  27. do記法のコツ(2) 操作の結果を直接引数にすることはできない。 冗長でも <- で別の変数に束縛してから使う。 import System.Environment (getProgName) main =

    do -- putStrLn $ "prog: " ++ getProgName とは書けない name <- getProgName putStrLn $ "prog: " ++ name
  28. モナドで使われる演算子 無理し過ぎず、使えそうな部分に使ってみると良い。 main = do -- x <- getProgName; putStrLn

    x と同じ getProgName >>= putStrLn putStrLn =<< getProgName -- Applicative -- x <- getProgName; y <- getProgname; let z = (x, y) と同じ z <- (,) <$> getProgName <*> getProgName print z
  29. I/Oとモナドの関係 • Haskellでは IO はモナド • 複雑な引数の扱いを隠蔽するためのモナド ◦ データを戻り値と引数で明示的に引き回す必要性 ◦

    冗長なので、ReaderかStateのモナドが欲しい よって、 • モナドを使ったプログラミング必須 • 実用上、2つ以上のモナドを同時に使いたい ◦ モナド変換子
  30. アプリケーションと状態 長時間動作するアプリケーションの場合、現在のアプリケーションの状態によって期待さ れる動作が変わることが多い。 • 処理回数が規定回数を超えたか否か • 外部のサーバとの接続が確立されているか否か • 読み込んだファイルが更新されてるか否か •

    etc. しかし、関数間で状態を受け渡すコードは冗長であり、ヒューマンエラーも起きやすい。
  31. 明示的な受け渡し 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 ()
  32. 状態の受け渡しを隠す モナドによって隠蔽することができる。 • State sモナド ◦ s -> (a, s)

    • Reader rモナド ◦ r -> m a ◦ 読み取り専用 しかし、すでにIOモナドを使っているのでモナドは使えない。
  33. モナド変換子 • 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 という型名はコード に現れてないことに注意
  34. 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 状態を取り出し、関数で変換してから返す 関数としてフィールド名を指定すると便利
  35. 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 という操作が追加される。
  36. 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 と使うと読み込みだけじゃなく状態も表現可
  37. 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モナドが配下にいるとミュー タブルに更新できる。
  38. 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)
  39. フレームワークに現れる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 パッケージ)
  40. フレームワークに現れるMonadIO • MonadIO m => MonadIO (HandlerT site m) MonadIO

    m => MonadIO (WidgetT site m) (yesod パッケージ) • MonadIO m => MonadIO (LoggingT m) (monad-loggerパッケージ) • MonadIO Handler (servantパッケージ)
  41. フレームワークに現れるMonadIO • MonadIO X MonadIO Query (xmonadパッケージ) • MonadIO (EventM

    n) (brickパッケージ)
  42. 外部パッケージの利用

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

  44. パッケージの選び方 参考にするサイト • 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版
  45. よく使われるパッケージ • bytestring, text : (普通の)効率的な文字列、必須 • vector : (普通の)効率的な配列

    • containers, unordered-containers : マップなど基本的なコンテナ • transformers : モナド変換子(特にStateTとReaderT) • attoparsec : 高速なパーサコンビネータ • safe-exception : 例外処理のベストプラクティス • conduit : ストリーミング(再利用可能な入出力) • optparse-applicative : コマンドライン引数
  46. パッケージの使い方 動作するコード例を探す。 • haddock の先頭付近 • チュートリアル的なページ ◦ パッケージのトップの Home

    Page欄 • リポジトリ内にexampleディレクトリがないか ◦ パッケージのトップから githubのリンクを探して飛ぶ ◦ もしくは、Downloadsのbrowseリンクから
  47. モナディックなAPIの読み方 • 何を提供するモナドか、メンタルモデルを把握する ◦ I/O (MonadIO) ◦ 引数の引き回し(State, Reader, Writer)

    ◦ 例外処理(Maybe, Either, Except) ◦ 実行順の制御(Cont, ストリーミング系) ◦ ループ(リスト) ◦ 並列、並行性(STM, Eval, Par) ◦ 抽象DSL (Free) • 利用可能な操作を探す • モナドの剥がし方を探す ◦ runSomeT や unSomeT などの命名が多い
  48. 型を読むコツ(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 ()
  49. 型を読むコツ(2) 型クラス(class, instance)の提供するメソッドを見逃さない。 • Monoid ◦ <> で連結できる • IsString

    ◦ 文字列リテラルで書ける • Functor、Applicative、Monad ◦ do 記法で使える
  50. まとめ • stackを使うとHaskellのビルドで困ることはほぼなくなる • 活発化する日本のHaskellコミュニティに乗り遅れない • 逐次実行、分岐、繰り返しの書き方をマスターする • IO モナドの使い方は重要なので、熟知するまで触る

    • ReaderT、StateTのモナド変換子を理解する • ライブラリのドキュメントの見方を知る