迈出正确的一步
令我困惑的是getNextFile
。让我们进入一个简化的世界,暂时不考虑 IO。这个类型是Maybe DataFile -> Maybe DataFile
。我认为,它应该简化为DataFile -> Maybe DataFile
,并且我将假设可以进行此调整。而这个函数看起来像是 unfoldr
的一个很好的候选。首先,我将制作一个简化版本的 unfoldr,它不太通用,但使用起来更加简单。
import Data.List
myUnfoldr :: (a -> Maybe a) -> a -> [a]
myUnfoldr f v = v : unfoldr (fmap tuplefy . f) v
where tuplefy x = (x,x)
现在类型 f :: a -> Maybe a
与 getNextFile :: DataFile -> Maybe DataFile
相匹配
getFiles :: String -> [DataFile]
getFiles = myUnfoldr getNextFile . getFirstFile
很美,对吧?unfoldr
与iterate
非常相似,只是一旦遇到Nothing
,它就会终止列表。
现在有一个问题,那就是IO
。我们如何在其中加入IO
并完成同样的事情呢?不要想着使用不能被命名的函数,我们需要一个强大的unfoldr来处理它。幸运的是,我们可以访问unfoldr的源代码。
unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b =
case f b of
Just (a,new_b) -> a : unfoldr f new_b
Nothing -> []
现在我们需要什么?大量的IO
。 liftM2 unfoldr
几乎可以得到正确的类型,但这次还不够。
一个实际的解决方案
unfoldrM :: Monad m => (b -> m (Maybe (a, b))) -> b -> m [a]
unfoldrM f b = do
res <- f b
case res of
Just (a, b') -> do
bs <- unfoldrM f b'
return $ a : bs
Nothing -> return []
这是一个相当简单的转换;我想知道是否有一些组合子可以实现同样的功能。
有趣的事实是:我们现在可以定义 unfoldr f b = runIdentity $ unfoldrM (return . f) b
让我们再次定义一个简化版的 myUnfoldrM
,我们只需要在其中添加一个 liftM
:
myUnfoldrM :: Monad m => (a -> m (Maybe a)) -> a -> m [a]
myUnfoldrM f v = (v:) `liftM` unfoldrM (liftM (fmap tuplefy) . f) v
where tuplefy x = (x,x)
现在我们已经准备好了,就像之前一样。
getFirstFile :: String -> IO DataFile
getNextFile :: DataFile -> IO (Maybe DataFile)
getFiles :: String -> IO [DataFile]
getFiles str = do
firstFile <- getFirstFile str
myUnfoldrM getNextFile firstFile
getFiles' :: String -> IO [DataFile]
getFiles' = myUnfoldrM getNextFile <=< getFirstFile
顺便提一下,我使用 data DataFile = NoClueWhatGoesHere
对它们进行了类型检查,并且设置 getFirstFile
和 getNextFile
的类型签名为 undefined
。
[编辑] 更改了myUnfoldr
和myUnfoldrM
的行为,使其更像iterate
,包括在结果列表中包含初始值。
[编辑] 解开展开的另一种方式:
如果您很难理解展开,科拉兹猜想可能是最简单的例子之一。
collatz :: Integral a => a -> Maybe a
collatz 1 = Nothing
collatz n | even n = Just $ n `div` 2
| otherwise = Just $ 3 * n + 1
collatzSequence :: Integral a => a -> [a]
collatzSequence = myUnfoldr collatz
记住,myUnfoldr
是一种简化的展开函数,适用于“下一个种子”和“当前输出值”相同的情况,这就是collatz的情况。通过在unfoldr
中定义myUnfoldr
并使用tuplefy x = (x,x)
,可以很容易地看出这种行为。
ghci> collatzSequence 9
[9,28,14,7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1]
更多,大部分无关的想法
其余内容与问题完全无关,但我就是忍不住要思考。我们可以通过使用myUnfoldrM
来定义myUnfoldr
:
myUnfoldr f v = runIdentity $ myUnfoldrM (return . f) v
看起来很熟悉吧?我们甚至可以将这个模式抽象出来:
sinkM :: ((a -> Identity b) -> a -> Identity c) -> (a -> b) -> a -> c
sinkM hof f = runIdentity . hof (return . f)
unfoldr = sinkM unfoldrM
myUnfoldr = sinkM myUnfoldrM
sinkM
应该能够处理任何形如
Monad m => (a -> m b) -> a -> m c
的函数,实现函数的“下沉”(相反于“提升”)。
因为这些函数中的 Monad m
可以与 sinkM
中的 Identity
monad 约束统一。然而,我没有看到任何 实际上可以使用 sinkM
的场景。
myUnfoldr
和myUnfoldrM
不会保留列表中的第一个文件;它只被视为生成其余列表的种子。如果需要,这很容易得到解决。 - Dan BurtonunfoldM
的组合器,但我设法将其转换为这个完全无法理解的一行代码:unfoldrM f = (>>= maybe (return []) (uncurry $ \a -> liftM (a:) . unfoldrM f)) . f
。 - pat(Identity.)
和(runIdentity.)
可以在a -> Identity b
和a -> b
之间进行转换,因此您可以编写sinkM f = (runIdentity.) . f . (Identity.)
。还请注意,sinkM mapM = map
,sinkM filterM = filter
。 - sdcvvcsinkM
编写的类型签名过于严格,无法用于这些情况。它应该是((a -> Identity b) -> a1 -> Identity c) -> (a -> b) -> a1 -> c
。事实上,使用return
而不是Identity
,它被推断为更加通用。 - Dan Burton