|
| 1 | +--- |
| 2 | +title: fallibleというパッケージをリリースしました |
| 3 | +headingBackgroundImage: ../../img/background.png |
| 4 | +headingDivClass: post-heading |
| 5 | +author: Nobutada MATSUBARA |
| 6 | +postedBy: <a href="https://matsubara0507.github.io/whoami">Nobutada MATSUBARA(@matsubara0507)</a> |
| 7 | +date: July 18, 2019 |
| 8 | +tags: |
| 9 | +... |
| 10 | +--- |
| 11 | + |
| 12 | +タイトルの通り、fallibleというパッケージを紹介します。 |
| 13 | + |
| 14 | +- [matsubara0507/fallible: interface for fallible data type like Maybe and Either. - GitHub](https://github.com/matsubara0507/fallible) |
| 15 | + |
| 16 | +ちなみに、fallibleはHaskell-jp Slackで: |
| 17 | + |
| 18 | +<img src="../../img/2019/fallible/slack.jpg" style="width: 100%;"> |
| 19 | + |
| 20 | +と質問したところ、該当するようなパッケージは無さそうだったので作ったという経緯があります。 |
| 21 | +その際に助言をくれた [fumieval](https://github.com/fumieval)氏のコードをほとんど引用した形になったので、Haskell-jp Blogに紹介記事を載せることにしました(僕は普段、自分のブログに自作したパッケージを書いています)。 |
| 22 | + |
| 23 | +## fallibleパッケージ |
| 24 | + |
| 25 | +Haskellでアプリケーションを記述してると次のようなコードを書くことがありますよね? |
| 26 | + |
| 27 | +```Haskell |
| 28 | +import qualified Data.List as L |
| 29 | + |
| 30 | +run :: String -> Token -> Bool -> IO () |
| 31 | +run targetName token verbose = do |
| 32 | + users <- getUsers token |
| 33 | + case users of |
| 34 | + Left err -> logDebug' err |
| 35 | + Right us -> do |
| 36 | + case userId <$> L.find isTarget us of |
| 37 | + Nothing -> logDebug' emsg |
| 38 | + Just tid -> do |
| 39 | + channels <- getChannels token |
| 40 | + case channels of |
| 41 | + Left err -> logDebug' err |
| 42 | + Right chs -> do |
| 43 | + let chs' = filter (elem tid . channelMembers) chs |
| 44 | + mapM_ (logDebug' . channelName) chs' |
| 45 | + where |
| 46 | + logDebug' = logDebug verbose |
| 47 | + emsg = "user not found: " ++ targetName |
| 48 | + isTarget user = userName user == targetName |
| 49 | + |
| 50 | +logDebug :: Bool -> String -> IO () |
| 51 | +logDebug verbose msg = if verbose then putStrLn msg else pure () |
| 52 | +``` |
| 53 | + |
| 54 | +Slackのようなチャットツールをイメージしてください。 |
| 55 | +該当の名前(`targetName`)を持つユーザーを与えると、そのユーザーが参加しているチャンネルの一覧を表示するというような振る舞いです。 |
| 56 | +こう段々になってしまうのは気持ち悪いですよね。 |
| 57 | +fallibleの目的はこの段々を次のように平坦にすることです(`where` などは割愛): |
| 58 | + |
| 59 | +```Haskell |
| 60 | +import Data.Fallible (evalContT, exit, lift, (!?=), (???)) |
| 61 | + |
| 62 | +run :: String -> Token -> Bool -> IO () |
| 63 | +run targetName token verbose = evalContT $ do |
| 64 | + users <- lift (getUsers token) !?= exit . logDebug' |
| 65 | + targetId <- userId <$> L.find isTarget users ??? exit (logDebug' emsg) |
| 66 | + channels <- lift (getChannels token) !?= exit . logDebug' |
| 67 | + lift $ mapM_ (logDebug' . channelName) $ |
| 68 | + filter (elem targetId . channelMembers) channels |
| 69 | +``` |
| 70 | + |
| 71 | +### やってること |
| 72 | + |
| 73 | +というか、もともとのアイデアは下記のブログです: |
| 74 | + |
| 75 | +- [ContT を使ってコードを綺麗にしよう! - BIGMOON Haskeller's BLOG](https://haskell.e-bigmoon.com/posts/2018/06-26-cont-param.html) |
| 76 | + |
| 77 | +これを一般化(`Maybe a` 固有ではなく `Either e a` でも使う)できないかなぁというのがもともとの発想です。 |
| 78 | + |
| 79 | +### 基本演算子 |
| 80 | + |
| 81 | +次の4つの演算子を利用します: |
| 82 | + |
| 83 | +```Haskell |
| 84 | +(!?=) :: Monad m => m (Either e a) -> (e -> m a) -> m a |
| 85 | +(!??) :: Monad m => m (Maybe a) -> m a -> m a |
| 86 | +(??=) :: Applicative f => Either e a -> (e -> f a) -> f a |
| 87 | +(???) :: Applicative f => Maybe a -> f a -> f a |
| 88 | +``` |
| 89 | + |
| 90 | +ただし、内部実装的には `Maybe a` や `Either e a` は `Fallible` 型クラスで一般化されています: |
| 91 | + |
| 92 | +```Haskell |
| 93 | +class Applicative f => Fallible f where |
| 94 | + type Failure f :: * |
| 95 | + tryFallible :: f a -> Either (Failure f) a |
| 96 | + |
| 97 | +instance Fallible Maybe where |
| 98 | + type Failure Maybe = () |
| 99 | + tryFallible = maybe (Left ()) Right |
| 100 | + |
| 101 | +instance Fallible (Either e) where |
| 102 | + type Failure (Either e) = e |
| 103 | + tryFallible = id |
| 104 | + |
| 105 | +(!?=) :: (Monad m, Fallible t) => m (t a) -> (Failure t -> m a) -> m a |
| 106 | +(???) :: (Applicative f, Fallible t) => t a -> f a -> f a |
| 107 | +``` |
| 108 | + |
| 109 | +これらを継続モナドと組み合わせることでIOと失敗系モナド(`Maybe a` や `Either e a`)を、モナドトランスフォーマーなしにDo記法で書くことができます! |
| 110 | + |
| 111 | +```Haskell |
| 112 | +-- 継続モナドに関する関数 |
| 113 | +evalConstT :: Monad m => ContT r m r -> m r |
| 114 | + |
| 115 | +exit :: m r -> ContT r m a |
| 116 | +exit = ContT . const |
| 117 | +``` |
| 118 | + |
| 119 | +## サンプルコード |
| 120 | + |
| 121 | +疑似的なIOで良いなら[fallibleリポジトリのexampleディレクトリ](https://github.com/matsubara0507/fallible/tree/master/example)にあります(上述の例はそれです)。 |
| 122 | + |
| 123 | +実際の利用例であれば、最近自作した[matsubara0507/mixlogue](https://github.com/matsubara0507/mixlogue)というHaskellアプリケーションで多用しています([ココ](https://github.com/matsubara0507/mixlogue/blob/8afd16ab4048ff62976b8e38347078fdaa7417dd/src/Mixlogue/Cmd.hs#L81-L93)とか[ココ](https://github.com/matsubara0507/mixlogue/blob/8afd16ab4048ff62976b8e38347078fdaa7417dd/src/Mixlogue/Message.hs#L15-L25)とか)。 |
| 124 | +ちなみに、mixlogueは特定のSlackの分報チャンネル(`times_hoge`)の発言を収集するというだけのツールです。 |
| 125 | + |
| 126 | +## 使い方 |
| 127 | + |
| 128 | +READMEを参照してください。 |
| 129 | + |
| 130 | +現状Hackageにはあげてないので、stackやCabalでGitHubリポジトリから参照する方法を利用してください。 |
| 131 | + |
| 132 | +## おしまい |
| 133 | + |
| 134 | +fumieval氏のコードをほとんど引用するだけになったので自分でリリースするか迷ったんですけど、リリースしてくれというのも丸投げがひどいので自分でリリースしました。 |
| 135 | +まぁこういう結果が生まれるのもOSSコミュニティの醍醐味ということで。 |
| 136 | +fumieval氏、いつもアドバイスをくれてありがとう! |
| 137 | + |
| 138 | +(もちろん他のHaskell-jpの皆さんも!) |
0 commit comments