Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Haskellを使おう

 Haskellを使おう

Masahiro Honma

August 04, 2017
Tweet

More Decks by Masahiro Honma

Other Decks in Technology

Transcript

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

    今日話さないこと。 • Haskellの文法など、基礎 • 高度な抽象概念を使ったHaskellの美しさ
  2. Hoogle • 関数名、型名の検索 ◦ fmap ◦ Maybe • 型による関数の検索 ◦

    :: 型名 ◦ 似た型の関数も探すが、過信は禁物
  3. ビルドのポイント • ghc-options: -Wall (*.cabalに) ◦ 全警告の表示 • hlint ◦

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

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

    • ページ数: 未定 • 概要(仮) ◦ 開発環境の構築 ◦ 一通りの文法 ◦ モナド変換子 ◦ bytestring, attoparsec, lens, operational, pipes などの主要パッケージ ◦ 並列並行プログラミング ◦ サンプルのアプリケーションをいくつか
  6. 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
  7. 分岐 パターンマッチやガード文が充実しており、得意。 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
  8. 分岐(ポリモフィズム) 型クラスを用いると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
  9. 繰り返し(リスト) リストやFreeモナドのような再帰データ型を介した繰り返し処理は得意 • 要素への反復処理: map, foldr, mapM_ など • continue

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

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

    catch ) • IO モナドがなければHaskellはこれらの機能を持たない ◦ StateモナドやErrorモナドはあくまで模倣 • 書きやすさは別の話 ◦ モナドの >>= はcallbackを要求するので、callback hellとなる ◦ do 記法を用いると、PythonやPHPとほぼ同様の使い勝手となる • モナドと do 記法に慣れよう
  15. モナド上の「操作」 各モナドには、その上で可能な操作が提供される。 • 操作: 戻り値の型が 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)
  16. 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モナドでは、並べられた操作が順番に実行される
  17. 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 ()
  18. モナドで使われる演算子 無理し過ぎず、使えそうな部分に使ってみると良い。 main = do -- x <- getProgName; putStrLn

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

    冗長なので、ReaderかStateのモナドが欲しい よって、 • モナドを使ったプログラミング必須 • 実用上、2つ以上のモナドを同時に使いたい ◦ モナド変換子
  20. 明示的な受け渡し 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 ()
  21. 状態の受け渡しを隠す モナドによって隠蔽することができる。 • State sモナド ◦ s -> (a, s)

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

    m => MonadIO (WidgetT site m) (yesod パッケージ) • MonadIO m => MonadIO (LoggingT m) (monad-loggerパッケージ) • MonadIO Handler (servantパッケージ)
  30. パッケージの選び方 参考にするサイト • 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版
  31. よく使われるパッケージ • bytestring, text : (普通の)効率的な文字列、必須 • vector : (普通の)効率的な配列

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

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

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

    ◦ 文字列リテラルで書ける • Functor、Applicative、Monad ◦ do 記法で使える