タイトルの通り、fallibleというパッケージを紹介します。
ちなみに、fallibleはHaskell-jp Slackで:
と質問したところ、該当するようなパッケージは無さそうだったので作ったという経緯があります。 その際に助言をくれた fumieval氏のコードをほとんど引用した形になったので、Haskell-jp Blogに紹介記事を載せることにしました(僕は普段、自分のブログに自作したパッケージを書いています)。
Link to
herefallibleパッケージ
Haskellでアプリケーションを記述してると次のようなコードを書くことがありますよね?
import qualified Data.List as L
run :: String -> Token -> Bool -> IO ()
= do
run targetName token verbose <- getUsers token
users case users of
Left err -> logDebug' err
Right us -> do
case userId <$> L.find isTarget us of
Nothing -> logDebug' emsg
Just tid -> do
<- getChannels token
channels case channels of
Left err -> logDebug' err
Right chs -> do
let chs' = filter (elem tid . channelMembers) chs
mapM_ (logDebug' . channelName) chs'
where
= logDebug verbose
logDebug' = "user not found: " ++ targetName
emsg = userName user == targetName
isTarget user
logDebug :: Bool -> String -> IO ()
= if verbose then putStrLn msg else pure () logDebug verbose msg
Slackのようなチャットツールをイメージしてください。
該当の名前(targetName
)を持つユーザーを与えると、そのユーザーが参加しているチャンネルの一覧を表示するというような振る舞いです。
こう段々になってしまうのは気持ち悪いですよね。
fallibleの目的はこの段々を次のように平坦にすることです(where
などは割愛):
import Data.Fallible (evalContT, exit, lift, (!?=), (???))
run :: String -> Token -> Bool -> IO ()
= evalContT $ do
run targetName token verbose <- lift (getUsers token) !?= exit . logDebug'
users <- userId <$> L.find isTarget users ??? exit (logDebug' emsg)
targetId <- lift (getChannels token) !?= exit . logDebug'
channels $ mapM_ (logDebug' . channelName) $
lift filter (elem targetId . channelMembers) channels
Link to
hereやってること
というか、もともとのアイデアは下記のブログです:
これを一般化(Maybe a
固有ではなく Either e a
でも使う)できないかなぁというのがもともとの発想です。
Link to
here基本演算子
次の4つの演算子を利用します:
(!?=) :: Monad m => m (Either e a) -> (e -> m a) -> m a
(!??) :: Monad m => m (Maybe a) -> m a -> m a
(??=) :: Applicative f => Either e a -> (e -> f a) -> f a
(???) :: Applicative f => Maybe a -> f a -> f a
ただし、内部実装的には Maybe a
や Either e a
は Fallible
型クラスで一般化されています:
class Applicative f => Fallible f where
type Failure f :: *
tryFallible :: f a -> Either (Failure f) a
instance Fallible Maybe where
type Failure Maybe = ()
= maybe (Left ()) Right
tryFallible
instance Fallible (Either e) where
type Failure (Either e) = e
= id
tryFallible
(!?=) :: (Monad m, Fallible t) => m (t a) -> (Failure t -> m a) -> m a
(???) :: (Applicative f, Fallible t) => t a -> f a -> f a
これらを継続モナドと組み合わせることでIOと失敗系モナド(Maybe a
や Either e a
)を、モナドトランスフォーマーなしにDo記法で書くことができます!
-- 継続モナドに関する関数
evalConstT :: Monad m => ContT r m r -> m r
exit :: m r -> ContT r m a
= ContT . const exit
Link to
hereサンプルコード
疑似的なIOで良いならfallibleリポジトリのexampleディレクトリにあります(上述の例はそれです)。
実際の利用例であれば、最近自作したmatsubara0507/mixlogueというHaskellアプリケーションで多用しています(ココとかココとか)。
ちなみに、mixlogueは特定のSlackの分報チャンネル(times_hoge
)の発言を収集するというだけのツールです。
Link to
here使い方
READMEを参照してください。
現状Hackageにはあげてないので、stackやCabalでGitHubリポジトリから参照する方法を利用してください。
Link to
hereおしまい
fumieval氏のコードをほとんど引用するだけになったので自分でリリースするか迷ったんですけど、リリースしてくれというのも丸投げがひどいので自分でリリースしました。 まぁこういう結果が生まれるのもOSSコミュニティの醍醐味ということで。 fumieval氏、いつもアドバイスをくれてありがとう!
(もちろん他のHaskell-jpの皆さんも!)