haskell-jp / beginners #25 at 2024-08-09 17:12:22 +0900

Haskell プログラムらしい書き方について質問させてください

import Control.Exception
import Control.Monad
import System.Directory
import System.Posix.Files
import System.Posix.Types

lessThan :: FileOffset -> FilePath -> IO Bool
lessThan threshold path = do
    s <- getFileStatus path
    return $ and [isRegularFile s, fileSize s < threshold]

smallFiles :: FileOffset -> FilePath -> IO (Either String [FilePath])
smallFiles threshold dir = do
    ei <- try (listDirectory dir)

    case ei of
        Left (e::SomeException) -> return $ Left (displayException e)

        Right contents -> do
            paths <- filterM (lessThan threshold) contents
            return $ Right paths

引数で指定したディレクトリから、閾値以下のサイズのファイル・パスを取得する関数を書きました。

ghci> smallFiles 100 "."
Right ["retry.hs",".gitignore","oop.hs"]
it :: Either String [FilePath]

想定通りに動作することを確認しました。

上記のコードでは、`try (listDirectory dir)` が Either a b を戻すため、それを case で判定して
また Left や Right でくるむような形になっています。

これをスマートに記述する方法はあるのでしょうか ?

(怒られないので、調子に乗って質問ばかりさせていただいていますが、これがマナー違反であればご指摘ください)
アプリケーション本体の具体的な書き換えは面倒なんでやりませんが、 ExceptT IO Exception a に都度変換するといいかも知れません。
except :: Exception e => IO a -> ExceptT IO e a
except = ExceptT . try

みたいなユーティリティー関数があるとやりやすいと思います(:bow:申し訳なくもこの関数実装が合っているか自信ないですが)。
ExceptT モナド変換子はこういう使い方をするものなんですね。
難しそうで後回しにしていましたが、調べて使えるようにしてみます。

ご回答ありがとうございました。
ExceptT IO は、「 IO 自身にも例外を投げる機能があるんだから意味ないし、誤解を招くだろ!」と批判されがちですが、今回のように局所的に使う分には便利なのでお試しください。
もし自分がやるとするならば、

> IO 自身にも例外を投げる機能があるんだから意味ないし、誤解を招くだろ!
という話の通り try を使わずに実行してしまって、例外をキャッチするのはなるべく上部にして、ログ処理とかはひとまとめにしてしまいますね。

極端な例ですが、 getFileStatus すら例外を投げる可能性があります。

ghci> getFileStatus "/abc"
*** Exception: /abc: getFileStatus: does not exist (No such file or directory)

これを考えると lessThanEither などに包む必要があるでしょう、でもそれはめんどくさいので、 IO は失敗可能性あるんだから仕方ないと呼び出しトップレベルで try することが多いです。
特に私がよく書くwebアプリケーションとかだと実行最後に確認して例外なら場合によってはログ出して別HTTPステータスで返して…とかしますね。
getFileStatus すら例外を投げる可能性があります。
たしかに、言われてみれば、、

呼び出しトップレベルで try することが多いです。
そういうやり方もあるんですね。
それなら、一括で処理できて楽そうです

細かく確認してご回答いただき、ありがとうございました。