Какую общую структуру имеет этот тип?

Взламывая что-то ранее, я создал следующий код:

newtype Callback a = Callback { unCallback :: a -> IO (Callback a) }

liftCallback :: (a -> IO ()) -> Callback a
liftCallback f = let cb = Callback $ \x -> (f x >> return cb) in cb

runCallback :: Callback a -> IO (a -> IO ())
runCallback cb =
    do ref <- newIORef cb
       return $ \x -> readIORef ref >>= ($ x) . unCallback >>= writeIORef ref

Callback a представляет собой функцию, которая обрабатывает некоторые данные и возвращает новый обратный вызов, который следует использовать для следующего уведомления. Обратный вызов, который может, так сказать, заменить сам себя. liftCallback просто поднимает обычную функцию до моего типа, а runCallback использует IORef для преобразования Callback в простую функцию.

Общая структура типа:

data T m a = T (a -> m (T m a))

Похоже, что это может быть изоморфно какой-то хорошо известной математической структуре из теории категорий.

Но что это? Это монада или что? Аппликативный функтор? Преобразованная монада? Даже стрела? Есть ли поисковая система, похожая на Hoogle, которая позволяет мне искать общие шаблоны, подобные этой?


person Niklas B.    schedule 05.02.2013    source источник
comment
Я бы назвал это монадическим косимом, но это несколько своеобразно.   -  person sclv    schedule 06.02.2013
comment
Monadic costream звучит хорошо :-)   -  person Joachim Breitner    schedule 06.02.2013
comment
Это напоминает мне Free< /a> -- Free f a = Either a (f (Either a (f (Either a (f ... -- и Cofree -- Cofree f a = (a, f (a, f (a, f ... -- кроме (->) вместо суммы/произведения. Итак, T f a = a -> f (a -> f (a -> f ..., что делает его контравариантным, в отличие от двух других.   -  person shachaf    schedule 06.02.2013
comment
(Поскольку (->) не является коммутативным, вы также можете получить newtype Bar f a = Bar { unBar :: f (Bar f a) -> a }, который является Functor всякий раз, когда f равен Contravariant, а не наоборот.)   -  person shachaf    schedule 06.02.2013


Ответы (4)


Вы ищете термин бесплатный преобразователь монад. Лучшее место, чтобы узнать, как это работает, — это прочитать статью «Конвейеры сопрограмм» в выпуск 19 журнала The Monad Reader. Марио Блажевич дает очень ясное описание того, как работает этот тип, за исключением того, что он называет его типом «Корутина».

Я записал его тип в пакет transformers-free, а затем он был объединен с пакетом free, который является его новым официальным домом.

Ваш тип Callback изоморфен:

type Callback a = forall r . FreeT ((->) a) IO r

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

Free ((->) a) r

Это будет синтаксическое дерево, которое принимает ноль или более as в качестве входных данных, а затем возвращает значение r.

Однако обычно мы хотим встроить эффекты или сделать следующий шаг синтаксического дерева зависимым от какого-либо эффекта. Для этого мы просто преобразуем нашу свободную монаду в свободный преобразователь монад, который чередует базовую монаду между шагами синтаксического дерева. В случае вашего типа Callback вы чередуете IO между каждым шагом ввода, поэтому ваша базовая монада IO:

FreeT ((->) a) IO r

Преимущество свободных монад в том, что они автоматически являются монадами для любого функтора, поэтому мы можем воспользоваться этим преимуществом, чтобы использовать нотацию do для сборки нашего синтаксического дерева. Например, я могу определить команду await, которая будет связывать ввод внутри монады:

import Control.Monad.Trans.Free

await :: (Monad m) => FreeT ((->) a) m a
await = liftF id

Теперь у меня есть DSL для записи Callbacks:

import Control.Monad
import Control.Monad.Trans.Free

printer :: (Show a) => FreeT ((->) a) IO r
printer = forever $ do
    a <- await
    lift $ print a

Обратите внимание, что мне никогда не приходилось определять необходимый экземпляр Monad. И FreeT f, и Free f автоматически становятся Monads для любого функтора f, и в данном случае ((->) a) — это наш функтор, поэтому он автоматически делает правильные вещи. Это магия теории категорий!

Кроме того, нам никогда не приходилось определять экземпляр MonadTrans, чтобы использовать lift. FreeT f автоматически является монадным преобразователем для любого функтора f, поэтому он позаботился и об этом за нас.

Наш принтер является подходящим Callback, поэтому мы можем передавать ему значения, просто деконструируя свободный монадный преобразователь:

feed :: [a] -> FreeT ((->) a) IO r -> IO ()
feed as callback = do
    x <- runFreeT callback
    case x of
        Pure _ -> return ()
        Free k -> case as of
            []   -> return ()
            b:bs -> feed bs (k b)

Фактическая печать происходит, когда мы связываем runFreeT callback, что затем дает нам следующий шаг в синтаксическом дереве, которому мы передаем следующий элемент списка.

Давай попробуем:

>>> feed [1..5] printer
1
2
3
4
5

Впрочем, вам даже не нужно писать все это самостоятельно. Как заметил Петр, моя библиотека pipes абстрагирует для вас общие шаблоны потоковой передачи, подобные этому. Ваш обратный вызов просто:

forall r . Consumer a IO r

Мы бы определили printer с помощью pipes следующим образом:

printer = forever $ do
    a <- await
    lift $ print a

... и мы можем передать ему список значений следующим образом:

>>> runEffect $ each [1..5] >-> printer
1
2
3
4
5

Я разработал pipes, чтобы охватить очень большой диапазон абстракций потоковой передачи, подобных этим, таким образом, чтобы вы всегда могли использовать нотацию do для создания каждого компонента потоковой передачи. pipes также поставляется с широким спектром элегантных решений для таких вещей, как обработка состояния и ошибок и двунаправленный поток информации, поэтому, если вы сформулируете свою Callback абстракцию в терминах pipes, вы бесплатно получите массу полезных механизмов.

Если вы хотите узнать больше о pipes, я рекомендую вам прочитайте руководство.

person Gabriel Gonzalez    schedule 06.02.2013
comment
Вау, это превосходный ответ! Большое спасибо, наверное, мне понадобится время, чтобы понять все это. - person Niklas B.; 06.02.2013

Общая структура типа выглядит как

data T (~>) a = T (a ~> T (~>) a)

где (~>) = Kleisli m в ваших терминах (стрелка).


Callback сам по себе не выглядит как экземпляр какого-либо стандартного класса типов Haskell, о котором я могу думать, но это контравариантный функтор (также известный как кофунктор, как оказалось, вводящий в заблуждение). Поскольку он не включен ни в одну из библиотек, поставляемых с GHC, существует несколько его определений в Hackage (используйте этот), но все они выглядят примерно так:

class Contravariant f where
    contramap :: (b -> a) -> f a -> f b
 -- c.f. fmap :: (a -> b) -> f a -> f b

Затем

instance Contravariant Callback where
    contramap f (Callback k) = Callback ((fmap . liftM . contramap) f (f . k))

Есть ли какая-то более экзотическая структура из теории категорий, которой обладает Callback? Я не знаю.

person dave4420    schedule 05.02.2013
comment
Правильным местом для получения Cofunctor является контравариантный пакет. И правильнее было бы не называть это кофунктором, так как ко может также означать ковариант. hackage.haskell.org/packages/ архив/contravariant/0.3/doc/html/ - person sclv; 06.02.2013
comment
Я сегодня кое-чему научился, хорошо. Я отредактирую имена в своем ответе. Если какие-либо другие теоретики, не относящиеся к категории, задаются вопросом, что может представлять собой «ковариантный функтор»: обычный функтор. - person dave4420; 06.02.2013
comment
@sclv Я думаю, что лучшая причина не называть это кофунктором заключается в том, что двойственное понятие функтора также является функтором ... Я думаю ... - person jberryman; 06.02.2013
comment
они обе веские причины, а вместе они еще лучшая причина. - person sclv; 06.02.2013

Я думаю, что этот тип очень близок к тому, что я слышал под названием «Контур», который является типом стрелы. На мгновение игнорируя часть ввода-вывода (поскольку мы можем получить это, просто преобразовав стрелку Клисли), трансформатор схемы:

newtype CircuitT a b c = CircuitT { unCircuitT :: a b (c, CircuitT a b c) }

По сути, это стрелка, которая каждый раз возвращает новую стрелку для следующего ввода. Все распространенные классы стрелок (включая цикл) могут быть реализованы для этого преобразователя стрелок, если их поддерживает базовая стрелка. Теперь все, что нам нужно сделать, чтобы сделать его теоретически таким же, как тип, который вы упомянули, - это избавиться от этого дополнительного вывода. Это легко сделать, и поэтому мы находим:

Callback a ~=~ CircuitT (Kleisli IO) a ()

Как будто мы смотрим на правую сторону:

CircuitT (Kleisli IO) a () ~=~
  (Kliesli IO) a ((), CircuitT (Kleisli IO) a ()) ~=~
  a -> IO ((), CircuitT (Kliesli IO) a ())

Отсюда видно, как это похоже на Callback a, за исключением того, что мы также выводим единичное значение. Поскольку единичное значение в любом случае находится в кортеже с чем-то еще, это на самом деле мало что нам говорит, поэтому я бы сказал, что они в основном одинаковы.

Н.Б. Я использовал ~=~ для аналогичного, но не совсем эквивалентного по какой-то причине. Однако они очень похожи, в частности обратите внимание, что мы можем преобразовать Callback a в CircuitT (Kleisli IO) a () и наоборот.

РЕДАКТИРОВАТЬ: я также полностью согласен с идеями о том, что это A) монадический costream (монадная операция, ожидающая бесконечное количество значений, я думаю, что это означает) и B) канал только для потребления (который во многом очень похож на тип схемы без вывода, или, скорее, вывод, установленный на (), поскольку такой канал также мог иметь вывод).

person Community    schedule 05.02.2013
comment
Я думаю, что они точно такие же (или были бы, если бы (a,()) = a было правдой (кортежи не являются истинными произведениями, но в большинстве случаев можно притворяться, что это так). - person Philip JF; 06.02.2013
comment
Что ж, это было и мое ощущение, если бы действительно было так, что единственным значением типа () было бы (), то они были бы точно такими же, но, конечно, undefined также относится к типу (). - person ; 06.02.2013
comment
Мне очень нравится этот ответ. Так получилось, что я столкнулся с типом Circuit в туториале про стрелки всего несколько дней назад, но уже и забыл о нем. Я думаю, что сходство с приемниками/потребителями является наиболее важным, потому что теперь, когда я думаю об этом, то, для чего я использую эту вещь, на самом деле обрабатывает поток as :) - person Niklas B.; 06.02.2013

Просто наблюдение, ваш тип кажется очень связанным с Consumer p a m в библиотеке pipes (и, возможно, другие подобные библиотеки):

type Consumer p a = p () a () C
-- A Pipe that consumes values
-- Consumers never respond.

где C — пустой тип данных, а p — экземпляр Proxy. Он потребляет значения типа a и никогда их не создает (поскольку его выходной тип пуст).

Например, мы можем преобразовать Callback в Consumer:

import Control.Proxy
import Control.Proxy.Synonym

newtype Callback m a = Callback { unCallback :: a -> m (Callback m a) }

-- No values produced, hence the polymorphic return type `r`.
-- We could replace `r` with `C` as well.
consumer :: (Proxy p, Monad m) => Callback m a -> () -> Consumer p a m r
consumer c () = runIdentityP (run c)
  where
    run (Callback c) = request () >>= lift . c >>= run

См. руководство. .

(Это должен был быть скорее комментарий, но он слишком длинный.)

person Petr    schedule 05.02.2013
comment
Да, если подумать, это имеет смысл. Я в основном использую это для обработки потока as - person Niklas B.; 06.02.2013