如何使用带有monads的Kleisli箭头?

时间:2013-11-23 17:30:29

标签: haskell monads arrows kleisli

在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

如果我必须把它画出来,那就是这样的:

enter image description here

我认为它可以从使用Kleisli箭头中受益,但我不知道如何。有人可以提供指导吗?

4 个答案:

答案 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 ---我们同时使用returnjoin


在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可以使用箭头实现,因为信息会在“固定管道”中流动,如图所示。

这是一个可能在列表中不使用mapzip的实现:

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

的一部分