8

在 HaskellControl.Arrow文档中,它谈到了 Kleisli 箭头与单子的关系,但对我来说如何使用它并不明显。我有一个我认为适合箭头的功能,除了它涉及 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 箭头中受益,但我不知道如何。任何人都可以提供指导吗?

4

4 回答 4

8

Monad 是Functor从 Hask(Haskell 类型和函数的类别)到 Hask(内函子)的 s。这意味着 Hask 中的某些箭头看起来像a -> m bsome Monad m。对于特定的 monad m,箭头看起来像a -> m b的 Hask 子类别是 Kleisli 类别m

我们知道这是一个类别,因为有一个标识箭头return :: a -> m a和组合(>>>) :: (a -> m b) -> (b -> m c) -> (a -> m c)定义为

(f >>> g) a = join (g <$> f a)

这就是为什么我们需要这是一个Monad--- 我们同时使用returnjoin


在 Haskell 中,我们通常不能只使用子类别,而是使用新类型。

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)

然后我们可以将 type 的函数升级Monad m => a -> m bKleisli m a bs,一个类别中的箭头,并用(.)

arr :: Kleisli IO FilePath [String]
arr = Kleisli (mapM $ fmap show . getModificationTime) . Kleisli getDirectoryContents

不过,通常这在语法上有点嘈杂。newtype 仅在使用Categorytypeclass 重载id和时才有价值(.)。相反,您更有可能看到return并且(>=>)相当于

return a = runKleisli (id a)
f >=> g  = runKleisli $ Kleisli g . Kleisli f
于 2013-11-23T18:29:58.320 回答
6

datedFiles可以使用箭头来实现,因为信息在“固定管道”中流动,如图所示。

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

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 箭头的单子是ListT IO,虽然唯一的不确定性是由getDirectoryContents.

注意最后一行是纯函数;最后(&&&)一行是使用箭头实例的功能。

编辑:包中的Wrapped类型类lens可用于更简洁地添加/删除新类型包装器。将其应用于前面的示例,我们最终得到:

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)
于 2013-11-23T19:02:19.873 回答
3

首先,我建议您将处理单个文件与处理列表分开。在您的示例中,timestamp是有趣的箭头,因为所有其他都是纯函数。不过,我们可以将其中一些做成箭头,以使示例更有趣。使用箭头符号,我们可以将计算一个文件名重写为 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
于 2013-11-24T13:04:05.810 回答
1

让我们记住 Monad 的 main 函数:

(>>=) :: (a -> m b) -> m a  -> m b

现在让我们看看Kleisli

newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }

哪里Kleisli是包装器和runKleisli- 来自新类型的解包器。

有什么共同点?a -> m b部分

让我们看看实例声明:

instance Monad m => Arrow (Kleisli m) where ...

我们看到,如何使Monad一部分Arrow

于 2013-11-24T17:46:14.190 回答