fallibleというパッケージをリリースしました

Posted by Nobutada MATSUBARA(@matsubara0507) on July 18, 2019

タイトルの通り、fallibleというパッケージを紹介します。

ちなみに、fallibleHaskell-jp Slack:

と質問したところ、該当するようなパッケージは無さそうだったので作ったという経緯があります。 その際に助言をくれた fumieval氏のコードをほとんど引用した形になったので、Haskell-jp Blogに紹介記事を載せることにしました(僕は普段、自分のブログに自作したパッケージを書いています)

Link to
here
fallibleパッケージ

Haskellでアプリケーションを記述してると次のようなコードを書くことがありますよね?

import qualified Data.List as L

run :: String -> Token -> Bool -> IO ()
run targetName token verbose = do
  users <- getUsers token
  case users of
    Left err -> logDebug' err
    Right us -> do
      case userId <$> L.find isTarget us of
        Nothing  -> logDebug' emsg
        Just tid -> do
          channels <- getChannels token
          case channels of
            Left err  -> logDebug' err
            Right chs -> do
              let chs' = filter (elem tid . channelMembers) chs
              mapM_ (logDebug' . channelName) chs'
  where
    logDebug' = logDebug verbose
    emsg = "user not found: " ++ targetName
    isTarget user = userName user == targetName

logDebug :: Bool -> String -> IO ()
logDebug verbose msg = if verbose then putStrLn msg else pure ()

Slackのようなチャットツールをイメージしてください。 該当の名前(targetName)を持つユーザーを与えると、そのユーザーが参加しているチャンネルの一覧を表示するというような振る舞いです。 こう段々になってしまうのは気持ち悪いですよね。 fallibleの目的はこの段々を次のように平坦にすることです(where などは割愛):

import Data.Fallible (evalContT, exit, lift, (!?=), (???))

run :: String -> Token -> Bool -> IO ()
run targetName token verbose = evalContT $ do
  users    <- lift (getUsers token) !?= exit . logDebug'
  targetId <- userId <$> L.find isTarget users ??? exit (logDebug' emsg)
  channels <- lift (getChannels token) !?= exit . logDebug'
  lift $ mapM_ (logDebug' . channelName) $
    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 aEither e aFallible 型クラスで一般化されています:

class Applicative f => Fallible f where
  type Failure f :: *
  tryFallible :: f a -> Either (Failure f) a

instance Fallible Maybe where
  type Failure Maybe = ()
  tryFallible = maybe (Left ()) Right

instance Fallible (Either e) where
  type Failure (Either e) = e
  tryFallible = id

(!?=) :: (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 aEither e a)を、モナドトランスフォーマーなしにDo記法で書くことができます!

-- 継続モナドに関する関数
evalConstT :: Monad m => ContT r m r -> m r

exit :: m r -> ContT r m a
exit = ContT . const

Link to
here
サンプルコード

疑似的なIOで良いならfallibleリポジトリのexampleディレクトリにあります(上述の例はそれです)

実際の利用例であれば、最近自作したmatsubara0507/mixlogueというHaskellアプリケーションで多用しています(ココとかココとか)。 ちなみに、mixlogueは特定のSlackの分報チャンネル(times_hoge)の発言を収集するというだけのツールです。

Link to
here
使い方

READMEを参照してください。

現状Hackageにはあげてないので、stackCabalGitHubリポジトリから参照する方法を利用してください。

Link to
here
おしまい

fumieval氏のコードをほとんど引用するだけになったので自分でリリースするか迷ったんですけど、リリースしてくれというのも丸投げがひどいので自分でリリースしました。 まぁこういう結果が生まれるのもOSSコミュニティの醍醐味ということで。 fumieval氏、いつもアドバイスをくれてありがとう!

(もちろん他のHaskell-jpの皆さんも!)