Как использовать стрелки Клейсли с монадой?
В Хаскеле 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)
Монады Functor
s от Hask, категории типов и функций Haskell, до Hask--- endofunctor. Это означает, что некоторые стрелки в Hask выглядят как a -> m b
для некоторых Monad
m
, Для конкретной монады 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 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
Сначала я бы предложил вам отделить обработку отдельного файла от обработки списка. В вашем примере 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