Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Haskellを使おう

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

 Haskellを使おう

Avatar for Masahiro Honma

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 記法で使える