Поднимите функцию и ее аргумент в другой монадический контекст

Я не знаю, как с научной точки зрения сформулировать этот вопрос, поэтому я просто покажу вам пример.

Я использую состояние в преобразователе StateT. Базовым является IO. Внутри операции StateT IO мне нужно использовать alloca. Однако я не могу поднять alloca до StateT IO, потому что он ожидает аргумент типа (Ptr a -> IO a), а мне нужно, чтобы он работал с аргументом (Ptr a -> StateT IO MyState a).

(Однако это общий вопрос о преобразователях монад, а не о конкретных IO, StateT или alloca.)

Я придумал следующее рабочее решение:

-- for reference
-- alloca :: (Storable a) => (Ptr a -> IO b) -> IO b

allocaS :: (Storable a) => (Ptr a -> StateT s IO b) -> StateT s IO b
allocaS f = do
  state <- get
  (res, st) <- liftIO $ alloca $ \ptr -> (runStateT (f ptr) state)
  put st
  return res

Однако мне кажется неправильным, что я должен де- и реконструировать действие StateT, чтобы использовать его с alloca. Также я не раз встречал этот паттерн в некоторых вариациях и не всегда он так прост и безопасен, как здесь с StateT.

Есть лучший способ сделать это?


person firefrorefiddle    schedule 03.08.2014    source источник
comment
Я думаю, что тип alloca недостаточно общий, чтобы разрешить то, что вы хотите сделать. Он принимает функцию, возвращающую IO, вместо функции, возвращающей любую монаду, в которой может быть поднят IO (т. е. MonadIO). В этом конкретном случае вы можете избежать распаковки, если реализуете allocaS, используя malloc/free вместо alloca (если вы это сделаете, следите за исключениями). В общем случае я не вижу способа сделать это без более общей формы alloca. Возможно, вы сможете сначала написать такой общий alloca, перейдя на довольно низкий уровень и используя примитивы GHC.   -  person chi    schedule 03.08.2014


Ответы (2)


Это можно сделать с помощью MonadBaseControl в monad-control, который был разработан точно для этой цели:

{-# LANGUAGE FlexibleContexts #-}
import Control.Monad
import Control.Monad.Trans.Control
import qualified Foreign.Ptr as F
import qualified Foreign.Marshal.Alloc as F
import qualified Foreign.Storable as F

alloca :: (MonadBaseControl IO m, F.Storable a) => (F.Ptr a -> m b) -> m b 
alloca f = control $ \runInIO -> F.alloca (runInIO . f)

Эта расширенная версия alloca может использоваться с любым стеком монад, основанным на IO, который реализует MonadBaseControl, включая StateT s IO.

Экземпляры MonadBaseControl позволяют закодировать свои монадические значения в базовой монаде (здесь IO), передать функции в базовой монаде (например, F.alloca) и затем реконструировать их обратно.

См. также Для чего нужен MonadBaseControl?

Пакет lifted-base содержит множество стандартных IO функций, повышенных до MonadBaseControl IO, но alloca не нет (пока) среди них.

person Petr    schedule 03.08.2014

Добрый день,

Насколько я знаю, нет общего способа превратить функцию типа (a -> m b) -> m b в (a -> t m b) -> t m b, потому что это подразумевало бы существование функции типа MonadTrans t => (a -> t m b) -> (a -> m b).

Такая функция не может существовать, так как большинство преобразователей не так просто отделить от сигнатуры типа (как превратить MaybeT m a в m a для всех a ?). Следовательно, наиболее общий способ превратить (a -> m b) -> m b в (a -> t m b) -> t m b — это undefined.

В случае с StateT s m есть лазейка, позволяющая определить его в любом случае. Начиная с StateT s m a === s -> m (s,a), мы можем переписать уравнение типа:

(a -> StateT s m b) -> StateT s m b
=== (a -> s -> m (s,b)) -> s -> m (s,b)
=== s -> (s -> (a -> m (s,b)) -> m (s,b) -- we reorder curried arguments
=== s -> (s -> (A -> m B)) -> m B -- where A = a, B = (s,b)

Решение этой новой подписи типа теперь тривиально:

liftedState f s run = f (run s)
allocaS :: Storable a => (Ptr a -> StateT IO b) -> StateT IO b
allocaS = isomorphic (liftedState alloca)

Это лучшее, что мы можем сделать с точки зрения повторного использования кода, если не считать определения нового подкласса MonadTrans для всех монад, демонстрирующих одинаковое поведение.

Надеюсь, я достаточно ясно выразился (не хотел вдаваться в подробности, чтобы не запутаться)

Хорошего дня :-)

person user3340662    schedule 03.08.2014
comment
Тоже интересный ответ, +1 и спасибо за вашу работу. Что такое isomorphic? - person firefrorefiddle; 03.08.2014
comment
isomorphic — достаточно умная (и вымышленная) функция, которая знает, как конвертировать между всеми изоморфными типами. На практике мы можем использовать линзы для достижения желаемого эффекта (в этом случае isomorphic будет view (flipped.bimapping (flipped.wrapping StateT) (wrapping StateT))). - person user3340662; 04.08.2014
comment
Поскольку мы уже доказали изоморфизм между типами liftedState alloca и allocaS, все, что делает isomorphic, — это характеризует этот изоморфизм и применяет его к view. ОШИБКИ: flipped.wrapping StateT должно быть flipped.mapping (wrapping StateT) - person user3340662; 04.08.2014