Как использовать стрелки Клейсли с монадой?

В Хаскеле Control.Arrow В документации говорится об отношении стрел Клейсли к монадам, но для меня не очевидно, как это использовать. У меня есть функция, которая, я думаю, соответствует стрелкам, за исключением того, что она включает монаду ввода-вывода, поэтому я думаю, что стрелки Клейсли могут помочь.

Возьмите следующую функцию, которая возвращает пары оригинальных и измененных имен файлов каталога.

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

Если бы мне пришлось сделать это, это было бы примерно так:

Я думаю, что это может выиграть от использования стрел Клейсли, но я не знаю как. Кто-нибудь может дать руководство?

4 ответа

Решение

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)

Возможно, это не самая интуитивная реализация.

Монада для стрел Клейсли ListT IOхотя единственный недетерминизм вызван getDirectoryContents,

Обратите внимание, что последняя строка является чистой функцией; (&&&) для последней строки используется экземпляр Arrow для функций.

Изменить: Класс типов 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)

Монады Functors от Hask, категории типов и функций Haskell, до Hask--- endofunctor. Это означает, что некоторые стрелки в Hask выглядят как a -> m b для некоторых Monadm, Для конкретной монады m, подкатегория Hask, где стрелки выглядят как a -> m b это категория Клейсли для m,

Мы знаем, что это категория, потому что есть личность стрелка return :: a -> m a и состав (>>>) :: (a -> m b) -> (b -> m c) -> (a -> m c) определяется как

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

вот почему нам нужно, чтобы это было Monad--- мы используем оба return а также join,


В 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)

И тогда мы можем обновить функции типа Monad m => a -> m b в Kleisli m a bs, стрелки в категории, и составьте их с (.)

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

Сначала я бы предложил вам отделить обработку отдельного файла от обработки списка. В вашем примере timestamp это интересная стрелка, потому что все остальные являются чистыми функциями. Тем не менее, мы можем превратить некоторые из них в стрелки, чтобы сделать пример более интересным. Используя обозначение стрелки, мы можем переписать вычисление одного имени файла в виде стрелки Клейсли:

{-# 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

Давайте вспомним основную функцию из Monad:

(>>=) :: (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

Другие вопросы по тегам