在Haskell Control.Arrow
文档中,它讨论了Kleisli箭头与monad的关系,但对我来说如何使用它并不明显。我有一个我认为适合箭头的功能,除了它涉及IO monad,所以我认为Kleisli箭头可能有帮助。
使用以下函数返回目录的原始和修改文件名对。
import System.Directory
import System.FilePath
datedFiles target = do
fns <- getDirectoryContents target
tms <- mapM (fmap show . getModificationTime) fns
return $
zip fns $
zipWith replaceBaseName fns $
zipWith (++) (map takeBaseName fns) tms
如果我必须把它画出来,那就是这样的:
我认为它可以从使用Kleisli箭头中受益,但我不知道如何。有人可以提供指导吗?
答案 0 :(得分:8)
Monads是来自Hask(Haskell类型和函数的类别)的Functor
到Hask ---一个endofunctor。这意味着对于某些a -> m b
Monad
,Hask中的某些箭头看起来像m
。对于特定的monad m
,Hask的子类别(箭头看起来像a -> m b
)是m
的Kleisli类别。
我们知道这是一个类别,因为有一个身份箭头return :: a -> m a
和构成(>>>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
定义为
(f >>> g) a = join (g <$> f a)
这就是为什么我们需要这是Monad
---我们同时使用return
和join
。
在Haskell中,我们不能正常使用子类别,而是使用newtype。
import Prelude hiding ((.), id)
import Control.Category
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }
instance Monad m => Category (Kleisli m) where
id = Kleisli return
Kleisli g . Kleisli f = Kleisli (join . fmap g . f)
然后我们可以将Monad m => a -> m b
类型的函数升级为Kleisli m a b
s,类别中的箭头,并使用(.)
arr :: Kleisli IO FilePath [String]
arr = Kleisli (mapM $ fmap show . getModificationTime) . Kleisli getDirectoryContents
一般来说,这在语法上有点吵。新类型只有在使用Category
类型类重载id
和(.)
时才有价值。相反,您更有可能看到return
和(>=>)
等同于
return a = runKleisli (id a)
f >=> g = runKleisli $ Kleisli g . Kleisli f
答案 1 :(得分:6)
datedFiles
可以使用箭头实现,因为信息会在“固定管道”中流动,如图所示。
这是一个可能在列表中不使用map
或zip
的实现:
import System.Directory
import System.FilePath
import Control.Monad.List
import Control.Arrow
datedFiles :: FilePath -> IO [(FilePath,FilePath)]
datedFiles = fmap runListT . runKleisli $
(Kleisli $ ListT . getDirectoryContents)
>>>
returnA &&& ((Kleisli $ liftIO . getModificationTime) >>^ show)
>>^
fst &&& (\(path,time) -> replaceBaseName path $ takeBaseName path ++ time)
可以说,它不是最直观的实现。
Kleisli箭的monad是ListT IO
,尽管唯一的非确定性是由getDirectoryContents
引起的。
注意最后一行是纯函数;最后一行的(&&&)
使用箭头实例进行功能。
编辑: lens
包中的Wrapped类型类可以用来更简洁地添加/删除newtype包装器。将它应用于前面的示例,我们最终得到:
import Control.Lens
datedFiles :: FilePath -> IO [(FilePath,FilePath)]
datedFiles = fmap runListT . runKleisli $
ListT . getDirectoryContents ^. wrapped
>>>
returnA &&& (liftIO . getModificationTime ^. wrapped >>^ show)
>>^
fst &&& (\(path,time) -> replaceBaseName path $ takeBaseName path ++ time)
答案 2 :(得分:2)
首先,我建议您从处理列表中拆分处理单个文件。在您的示例中,timestamp
是有趣的箭头,因为所有其他都是纯函数。尽管如此,我们可以将它们中的一些变成箭头,以使示例更有趣。使用arrow notation我们可以重写计算一个文件名作为Kleisli箭头:
{-# LANGUAGE Arrows #-}
import Control.Arrow
import System.Directory
import System.FilePath
import System.Time
-- Get a timestamp of a file as an arrow:
timestamp :: Kleisli IO FilePath ClockTime
timestamp = Kleisli getModificationTime
-- Insert a given string in front of the extension of a file.
-- Just as an example - we'd rather use a simple `let` instead of making it
-- an arrow.
append :: (Monad m) => Kleisli m (FilePath, String) FilePath
append = arr $ \(fn, suffix) ->
let (base, ext) = splitExtension fn
in base ++ suffix ++ ext
-- Given a directory, receive the name of a file as an arrow input
-- and produce the new file name. (We could also receive `dir`
-- as an input, if we wanted.)
datedArrow :: FilePath -> Kleisli IO FilePath (FilePath, FilePath)
datedArrow dir = proc fn -> do
ts <- timestamp -< replaceDirectory fn dir
fn' <- append -< (fn, show ts)
returnA -< (fn, fn')
datedFiles' :: FilePath -> IO [(FilePath, FilePath)]
datedFiles' target = do
fns <- getDirectoryContents target
mapM (runKleisli $ datedArrow target) fns
答案 3 :(得分:1)
让我们记住Monad的主要功能:
(>>=) :: (a -> m b) -> m a -> m b
现在让我们看一下Kleisli
newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }
其中Kleisli
是一个包装器,runKleisli
- 来自newtype的解包器。
有什么共同之处? a -> m b
部分
让我们看一下实例声明:
instance Monad m => Arrow (Kleisli m) where ...
我们看到,如何Monad
成为Arrow