maximumBy的惯用单子式实现是什么?如何处理带效应容器的最大值选取?
嘿,我来帮你搞定这两个Haskell相关的问题~
1. 如何实现maximumBy的单子式(monadic)版本?
标准库的maximumBy是纯函数式的:它接受一个纯的比较函数a -> a -> Ordering和一个列表,返回列表中的最大值。要实现它的monadic版本,我们需要让比较过程支持效应(比如IO、Reader等),函数签名和实现可以这样写:
安全版本(处理空列表)
maximumByM :: Monad m => (a -> a -> m Ordering) -> [a] -> m (Maybe a) maximumByM _ [] = return Nothing maximumByM cmp (x:xs) = foldM go (Just x) xs where go Nothing y = return (Just y) go (Just current) y = do ord <- cmp current y return $ case ord of GT -> Just current -- 当前元素更大,保留 _ -> Just y -- 新元素更大或相等,替换
非空列表简化版
如果能保证输入列表一定非空(比如从目录读取的文件列表),可以写一个更简洁的版本,直接返回m a:
maximumByM' :: Monad m => (a -> a -> m Ordering) -> [a] -> m a maximumByM' _ [] = error "maximumByM': 不能传入空列表" maximumByM' cmp (x:xs) = foldM go x xs where go current y = do ord <- cmp current y return $ if ord == GT then current else y
核心思路是用foldM遍历列表,维护当前的最大值,每一步都通过monadic的比较函数判断是否需要更新最大值。
2. 从带效应的容器中取最大值(比较属性带效应)的更可读实现
看了你给出的代码,你现在是先把每个FilePath转换成(UTCTime, FilePath)元组(通过z函数触发datefile的IO效应),再用Fold.maximum取最大值——这种方式虽然可行,但确实不够直观,因为你需要先把所有元素转换一遍,而不是直接表达“比较两个文件的修改时间,取最新的那个”的逻辑。
结合你提到的maximumBy的需求,我们可以用上面的单子式maximumByM,或者针对Turtle的Shell容器写一个自定义的Fold,让逻辑更清晰:
方案一:用单子式maximumByM处理文件列表
先把Turtle的Shell FilePath转换成普通列表,再用maximumByM比较每个文件的修改时间:
import Turtle import Data.Time.Clock (UTCTime) import Control.Monad (foldM) -- 复用刚才的maximumByM maximumByM :: Monad m => (a -> a -> m Ordering) -> [a] -> m (Maybe a) maximumByM _ [] = return Nothing maximumByM cmp (x:xs) = foldM go (Just x) xs where go Nothing y = return (Just y) go (Just current) y = do ord <- cmp current y return $ case ord of GT -> Just current _ -> Just y latest :: FilePath -> IO (Maybe FilePath) latest dir = do files <- toList (ls dir) -- 把Shell转换成IO [FilePath] maximumByM compareFileTime files where -- 带IO效应的比较函数:比较两个文件的修改时间 compareFileTime a b = do timeA <- datefile a timeB <- datefile b return $ compare timeA timeB -- 时间越晚,Ordering越大
方案二:自定义Turtle Fold(贴合流式处理)
如果不想把Shell转换成列表(毕竟Shell是流式的,大目录下更高效),可以写一个支持monadic比较的Fold:
import Turtle import Data.Time.Clock (UTCTime) import Control.Foldl (Fold(..)) maximumByFold :: Monad m => (a -> a -> m Ordering) -> Fold a (m (Maybe a)) maximumByFold cmp = Fold step initial extract where initial = return Nothing step Nothing x = return (Just x) step (Just current) x = do ord <- cmp current x return $ if ord == GT then Just current else Just x extract = id latest :: FilePath -> IO (Maybe FilePath) latest dir = fold (ls dir) (maximumByFold compareFileTime) where compareFileTime a b = do timeA <- datefile a timeB <- datefile b return $ compare timeA timeB
这个方案直接在Shell的流式处理中计算最大值,不需要一次性加载所有文件到内存,逻辑上也更直接:我们告诉程序“遍历目录下的每个文件,用带IO的时间比较规则,找出最新的那个”。
两种方案都比你原来的写法更贴合“临时属性选择”的需求——你不需要提前把所有文件和时间绑定成元组,而是直接在比较逻辑中处理属性的效应,代码可读性会高很多。
内容的提问来源于stack exchange,提问作者sevo

