简体   繁体   English

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

[英]How to use Kleisli arrows with monads?

In the Haskell Control.Arrow documentation it talks about Kleisli arrows' relationship to monads, but it is not obvious to me how to use this. 在Haskell Control.Arrow文档中,它讨论了Kleisli箭头与monad的关系,但对我来说如何使用它并不明显。 I have a function which I think fits with arrows except for it involving the IO monad, so I think Kleisli arrows may help. 我有一个我认为适合箭头的功能,除了它涉及IO monad,所以我认为Kleisli箭头可能有帮助。

Take the following function which returns pairs of original and modified filenames of a directory. 使用以下函数返回目录的原始和修改文件名对。

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

If I had to draw it out, it would be something like this: 如果我必须把它画出来,它将是这样的:

在此输入图像描述

I think it can benefit from the use of Kleisli arrows, but I don't know how. 我认为它可以从Kleisli箭头的使用中受益,但我不知道如何。 Can anyone provide guidance? 有人可以提供指导吗?

Monads are Functor s from Hask, the category of Haskell types and functions, to Hask---an endofunctor. Monads是Hask中的Functor ,Haskell类型和函数的类别,Hask ---一个endofunctor。 That means that some of the arrows in Hask look like a -> mb for some Monad m . 这意味着Hask中的一些箭头看起来像某个Monad m a -> mb For a particular monad m , the subcategory of Hask where arrows look like a -> mb is the Kleisli category for m . 对于特定的monad m ,Hask的子类别,其中箭头看起来像a -> mbm的Kleisli类别。

We know it's a category because there's an identity arrow return :: a -> ma and composition (>>>) :: (a -> mb) -> (b -> mc) -> (a -> mc) defined like 我们知道它是一个类别,因为有一个标识箭头return :: a -> ma和组合(>>>) :: (a -> mb) -> (b -> mc) -> (a -> mc)定义为

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

which is why we need this to be a Monad ---we use both return and join . 这就是为什么我们需要这个Monad ---我们同时使用returnjoin


In Haskell, we can't just have a subcategory normally, but instead a newtype is used. 在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)

And then we can upgrade functions of type Monad m => a -> mb to Kleisli mab s, arrows in a category, and compose them with (.) 然后我们可以将Monad m => a -> mb类型的函数升级到Kleisli mab s,类别中的箭头,并用(.)组合它们(.)

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

Generally that's a bit syntactically noisy, though. 但一般来说,这有点语法噪音。 The newtype is only valuable in order to use the Category typeclass to overload id and (.) . newtype仅对使用Category类型类重载id(.)有价值。 Instead it's more likely that you'll see return and (>=>) which are equivalent to 相反,你更有可能看到return(>=>)相当于

return a = runKleisli (id a)
f >=> g  = runKleisli $ Kleisli g . Kleisli f

datedFiles can be implemented using arrows because the information flows in a "fixed pipeline", as your diagram shows. datedFiles可以使用箭头实现,因为信息流在“固定管道”中,如图所示。

Here's a possible implementation that does not use map or zip on lists: 这是一个可能的实现,不使用列表上的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)

Arguably, it is not the most intuitive implementation. 可以说,它不是最直观的实现。

The monad for the Kleisli arrows is ListT IO , although the only nondeterminism is caused by getDirectoryContents . Kleisli箭头的monad是ListT IO ,尽管唯一的非确定性是由getDirectoryContents引起的。

Note that the last line is a pure function; 注意最后一行是纯函数; the (&&&) for the last line is using the Arrow instance for functions. 最后一行的(&&&)使用Arrow实例作为函数。

Edit: The Wrapped typeclass from the lens package can be used to add/remove newtype wrappers a bit more succinctly. 编辑: lens包中的Wrapped类型类可以用来更简洁地添加/删除newtype包装器。 Applying it to the previous example, we end up with: 将它应用于前面的示例,我们最终得到:

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)

First I'd suggest you to split handling an individual file from handling the list. 首先,我建议您从处理列表中拆分处理单个文件。 In your example, timestamp is the interesting arrow, because all others are pure functions. 在您的示例中, timestamp是有趣的箭头,因为所有其他都是纯函数。 Nevertheless, we can make some of them into arrows to make the example more interesting. 尽管如此,我们可以将它们中的一些变成箭头,以使示例更有趣。 Using the arrow notation we can rewrite computing one file name as a Kleisli arrow: 使用箭头符号我们可以重写计算一个文件名作为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

Let remember main function from Monad: 让我们记住Monad的主要功能:

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

and now let's look at Kleisli 现在让我们来看看Kleisli

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

where Kleisli is a wrapper and runKleisli - unwrapper from the newtype. 其中Kleisli是一个包装器和runKleisli - 来自runKleisli

What is in common? 有什么共同之处? a -> mb part a -> mb部分

And let's look at instance declaration: 让我们看一下实例声明:

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

we see, how to make Monad part of Arrow 我们看到,如何使Monad成为Arrow一部分

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM