LINUX.ORG.RU

Чистый код и монад трансформеры

 


0

2

Хаскель рекламируется как язык в котором чистые функции отделены от IO actions. При этом, как я это понимаю, функции работающие в остальных монадах все равно остаются частью чистого кода. И если мы возьмем монад трансформер Q, вычисления типа Q a m b где m монада которая не инстанс MonadIO, то мы тоже легко можем запускать на этом коде QuickCheck и доказывать о нем разнообразные факты.

Но если для монады определен liftIO, то что же получается, опять все в кучу? Дискасс.


Ответ на: комментарий от dave

Так никто не говорит чтобы без IO — но нынешняя имплементация IO имеет «особенности», из-за которых хотелось держать ее преимущественно внизу. И пример с liftIO выглядит как беспомощный workaround — хотелось бы собрать конструкцию из чистого кода, поставить на IO и запустить, а приходится и в середине стэка какие-то реальные действия выполнять. То ли реальность неидеальна, то ли programming model.

nokachi
() автор топика
Ответ на: комментарий от nokachi

В моем случае liftIO - жизненная необходимость, ибо позволяет моделировать стохастические системы - именно то, что является основной целью разработанного мною кода.

И меня вполне устраивает существующий хаскель. Вот, стрелкам недавно нашел применение, только, черт возьми, ArrowLoop никак не выходит, поскольку не удается instance MonadFix Cont, что вполне понятно. Это даже хорошо, что не выходит, иначе бы были нарушены физический и математический смыслы одной вещи :)

В общем, я нахожу, что liftIO не является проблемой, а это вполне рабочий инструмент, хорошо зарекомендовавший себя.

хотелось бы собрать конструкцию из чистого кода, поставить на IO и запустить, а приходится и в середине стэка какие-то реальные действия выполнять.

Кстати, я тут опять замечаю одну вещь в твоих рассуждениях. Я воспринимаю liftIO именно как «поставить на IO», поскольку ты лично сам не запускаешь побочный эффект. Ты именно говоришь, что здесь будет побочный эффект.

dave ★★★★★
()
Ответ на: комментарий от dave

ладно, видимо я пока еще не понял как удовлетворительно для себя решить мои задачи. Может быть о каких-то специальных абстракциях не знаю.

nokachi
() автор топика
Ответ на: комментарий от nokachi

Внезапно мы для m определяем liftIO — и наша хорошая монада становится ничуть не лучше IO

Не становится. Инатсансы тайпклассов вообще никак не меняют тип, для которого определены.

s9gf4ult ★★
()
Ответ на: комментарий от nokachi

Ты для начала определи для чистого кода liftIO и посмотри как он выглядит, а потом расскажи нам, как новый ИНСТАНС нарушил чистоту кода, и какого конкретно.

s9gf4ult ★★
()
Ответ на: комментарий от nokachi

еще один про кашу.

может пора задуматься о том, что это не случайно?

Проблемы бы были в случае если бы assert ломался, но он будет выполняться всегда.

foo :: (MonadState m) => m ()
foo = do
  put x
  -- do io
  x' <- get 
  assert x == x'
  return ()

qnikst ★★★★★
()
Ответ на: комментарий от dave

Ну, пусть она там что-то нарушает в их стройной выдуманной системе.

ты понял его юзкей, где IO что-то нарушает?

qnikst ★★★★★
()
Ответ на: комментарий от nokachi

ну.. да. Хотя я бы предпочел заметить, что мои ответы это попытка заставить тебя сформулировать проблему или нормально или на работающем примере, в этом случае вопрос или отпадёт или будет поставлен в такой форме, что можно начинать обсуждение. (С вкраплением небольшого объяснения, что такое монадический стек и какие гарантии он дает, т.к. мне показалось, что именно в этом месте ты требуешь от него невозможного (на мой взгляд и ненужного), соответсвенно ты приходишь к проблеме, что «чистый код» стал не чистым.)

qnikst ★★★★★
()
Ответ на: комментарий от nokachi

если же ты про:

import Control.Monad.IO.Class
import System.IO.Unsafe

instance MonadIO Maybe where
  liftIO f = let r = unsafePerformIO f 
             in r `seq` Just r

test :: a -> Maybe a
test a = do
  liftIO (putStrLn "hacked")
  return a

то приходи, тогда, когда ты перепишешь это без UnsafePerformIO, а для защиты от подобных хаков есть SafeHaskell.

Заметь, приведенный код очень сильно сужает объект рассмотрения и проблематики. Правда данная проблема относится к трансформерам чуть менее, чем никак. А к quickcheck ещё меньше.

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 3)
Ответ на: комментарий от qnikst

Тут была проблема в терминологии: возможное двоякое толкование термина «чистая функция», чем, кстати, хаскелисты очень сильно грешат. Вон, даже сейчас на haskell cafe есть подобная дискуссия вокруг названия pure. Мне кажется, что здесь мы с этим уже разобрались :)

dave ★★★★★
()
Ответ на: комментарий от qnikst

Типичный лор — вопроса не понял, но зачем-то про монадический стек рассказывает. Вопрос я пояснил здесь много раз (но не тебе). Unsafeperformio тут не при чем. Если ты считаешь себя экспертом, то пойми вопрос и попробуй объяснить без посланий в википидию. По крайней мере Брайан О'Салливан спокойно отвечал и на тупые вопросы, а по поводу твоей компетентности у меня нет данных. Ладно, затянули тред, спать пора.

nokachi
() автор топика
Ответ на: комментарий от nokachi

И если мы возьмем монад трансформер Q, вычисления типа Q a m b

действительно и при чем тут стек...

P.S. я тебя в википедию не посылал, а ни на один из моих вопросов ты не ответил..

qnikst ★★★★★
()
Ответ на: комментарий от nokachi

И пример с liftIO выглядит как беспомощный workaround

Кстати, вот вспомнилось из моей же практики. LiftIO отлично подошел для обработки ошибок IOException в монаде, основанной на продолжениях (стандартный ContT этого не умеет - там банально не хватает еще одного дополнительного продолжения для перенаправления вычисления по другой ветке, где уже расставлены нужные обработчики). Если бы не было liftIO, то я даже не представляю, как можно было бы обработать IOException в ленивом языке. А сейчас все получилось у меня очень стройно и красиво. Ошибки IOException нужно перехватывать только внутри liftIO, потому что в чистом коде их возникнуть не должно в принципе (да, есть еще предательский throw, который это дело может обойти, но это уже будет логическая ошибка программиста-пользователя).

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от dave

Погоди-ка, даже monad-control не умеет ловить эксепшены в стеках с ContT. Чего это ты там навертел такого?

anonymous
()
Ответ на: комментарий от dave

Мне кажется, что здесь мы с этим уже разобрались :)

тогда откуда столько шелухи вокруг в общем-то терминологического вопроса?

qnikst ★★★★★
()
Ответ на: комментарий от KblCb

А ErrorT чем тебе не угодил?

Посмотрел на ErrorT. Ничего такого не увидел, что нужно мне.

Вообще, Cont - крайне сложная штука, несмотря на свою кажущуюся простоту. С ним люди часто ошибаются. По крайней мере, в одной библиотеке я недавно видел неправильно определенную обработку ошибок для Cont. Только там автор честно признался, что работает не так, как надо. Названия уже не помню.

dave ★★★★★
()
Ответ на: комментарий от qnikst

тогда откуда столько шелухи вокруг в общем-то терминологического вопроса?

Именно так оно и бывает, особенно здесь на ЛОРе. Поэтому я сразу почувствовал неладное :)

dave ★★★★★
()
Ответ на: комментарий от dave

на всякий случай тут сводку своих мыслей на тему запишу:

1). есть чистый код, есть «не-чистый» код который в BaseMonad (IO/ST/Извращения руками с World).

2). есть трансформеры, которые работают только со своим уровнем в стеке, и предоставляют все соот. гарантии на этом уровне и они полиморфны по нижним уровням, т.е. там может быть как «чистая» монада, так и base.

3). есть возможность поднимать значения с нижних уровней выше (lift/liftIO), при этом вычисления проходят в нижележащей монаде, а поднимается только результат.

4). разница между тем какой уровень внизу чистый или нет, есть только при выполнении монады в top-level и т.к. результат «вывернут» нижний уровень первый, то тип полностью описывает выражение.

5). вставить IO в-центр стрека нельзя (насколько я понял nokashi неявно утвержадал, что можно)

6). можно «поломать» стек, сделав нечестный MonadIO как я сделал в примере выше, но это ССЗБ и не рассматривается.

где тут неконсистентность мне совершенно не понятно, так же непонятно, где какое-либо перемешивание, и где мы не можем сделать предположения о нашем коде. Собственно пример кода я потому и просил..

qnikst ★★★★★
()
Ответ на: комментарий от dave

Так ты так и не объяснил что именно тебе нужно. Мне просто любопытно. Можешь привести пример или что-то вроде того.

KblCb ★★★★★
()
Ответ на: комментарий от KblCb

Со стороны выглядит все очень просто. Монадический тип Cont со своими аналогами throw, catch и finally, плюс liftIO, где собственно ошибка IOException и перехватывается.

Идея та же самая, что в F# async workflow, только у Дона Сайма ошибка перехватывается внутри самого комбинатора >>=, который там зовется Bind, ибо язык F# - энергичный. В хаскеле большого смысла перехватывать IOException в >>= нет, да и по хорошему не получится из-за ленивости (и чистоты), а вот в liftIO - вполне легко.

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от dave

это бесполезно, он мне не будет отвечать, я же не Брайн :).

Поэтому я выложил здесь этот список, чтобы более адекватные участники дискуссии могли подтвердить этот список, опровергнуть или отметить, что я упустил важные детали.

qnikst ★★★★★
()
Ответ на: комментарий от dave

Либо там не ContT, либо ловля эксепшенов не работает, извини. Желания вникать в мл-говнину, чтобы выяснить, что из этих двух вариантов, у меня правда нет :-(

anonymous
()
Ответ на: комментарий от dave

Я всё равно не понял, почему ты не можешь запихнуть под свой ContT любой не связанный с MonadError. Если напишешь какой именно ты хочешь получить workflow я смогу объяснить точнее что я имею в виду.

KblCb ★★★★★
()
Ответ на: комментарий от KblCb

Я не понял, что ты хочешь от меня :)

В MonadError не вижу finallyError, а без него пропадает весь смысл в обработке ошибок для моей задачи (нужно моделировать ошибкоустойчивый и гарантированный возврат ресурса). А так вполне можно было бы сделать instance - не вижу проблем (написать три строчки), только нафиг такой хромой instance?

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от dave

Я хочу чтобы ты привёл мне код или псевдокод поясняющий что ты хочешь получить на выходе. Я вот сейчас с ходу даже не соображу что есть finallyError.

KblCb ★★★★★
()
Ответ на: комментарий от KblCb

Да эта обработка ошибок у меня почти год уже как работает на 100%. Так что, ничего не хочу :)

Что касается finallyError, то я бы дополнили MonadError e m следующей функцией:

finallyError :: m a -> m b -> m a

Здесь блок m b должен гарантированно сработать даже в случае возникновения ошибки e (это обобщение функции finally). Без этого нахожу текущее определение MonadError неполным (и ненужным мне).

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от dave

То есть так? Кстати, ты уверен что сигнатура именно m a -> m b -> m a, а не как у меня?

finallyError :: (Monad m, MonadError e m) => m a -> m b -> m b
finallyError m1 m2 = catchError (void m1) (void . return) >> m2

Я и не спорю с тем, что у тебя уже всё работает. Просто с подобными проблемами я не сталкивался, потому мне и интересны причины их возникновения.

KblCb ★★★★★
()
Последнее исправление: KblCb (всего исправлений: 2)
Ответ на: комментарий от KblCb

Да, уверен с точностью до условия MonadError. В твоей версии теряется результат вычисления m a, который важен для меня. В блоке m b обычно сидит что-нибудь типа команды «отдай ресурс такой-то, чтобы там ни случилось прежде в m a». Но вернуть нужно именно результат m a.

Что-то мне подсказывает, что finallyError не выражается в терминах catchError. Да, и вообще, для Cont функция finallyError сложнее в реализации, чем catchError - нужно подменить все продолжения сразу, которых у меня аж три штуки. То же самое у Дона Сайма.

Дополнение. Тем более, что ни catchError, ни (>>=) не подменяют третьего продолжения, а finallyError подменяет, и это очень важно. То есть, явно одно не выражается через другое.

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от KblCb

Тут дело не в исключении, а в том третьем продолжении. Оно отвечает за экстренную отмену вычисления. В таком случае должны сработать только блоки finally, где обычно освобождаются ресурсы. Блоки catch игнорируются.

А обычный сценарий я уже упомянул. Вычисляется m a. Если все нормально, то срабатывает m b, который обычно освобождает ресурс, а затем возвращается результат m a.

В случае возникновения ошибки действительно результат не возвращается, но m b, тем не менее, срабатывает. Это уже третий сценарий.

dave ★★★★★
()
Ответ на: комментарий от KblCb

Пожалуй, стоит написать письмо турецком султану.. то есть, в haskell cafe на счет MonadError и finallyError, но надо собраться с мыслями.

dave ★★★★★
()
Ответ на: комментарий от qnikst

Опять настрочил целую простыню, но речь вообще не об этом. Никто не говорит о том чтобы стэки ломать и делать что-то незаконное с type system. Мой тезис был в том что вызывать связывать IO и чистый код плохо.

Дальше идет некомпилируемый код

-- we can call foo from anywhere where type allows
foo :: m ()
foo = do let x = return ()
         x

-- now we require m to be an instance of MonadIO
-- and only can call it from OS threads
foo :: (MonadIO m) => m ()
foo = do let x = return ()
         liftIO $ ffi_call
         x

теперь нам надо ходить и выяснять, используем мы этот код только в правильных тредах или случайно запустили это в чем-то запущенном через forkIO. Компилятор это понятное дело не энфорсит, type system тут вообще не при чем (). Но так писать, на мой взгляд, плохой стиль, по вышеописанной причине.

nokachi
() автор топика
Ответ на: комментарий от nokachi

Уф… Ты хотел чтобы в небе возникли огненные буквы говорящие о том, что кусок кода нужно тестировать в особом порядке? Ну так вот же они: MonadIO m. Я уже не говорю о том, что мне не вполне понятно почему связывать IO и «чистый» код плохо при условии что все функции в программе вызываются из IO.

KblCb ★★★★★
()
Ответ на: комментарий от nokachi

Мой тезис был в том что вызывать связывать IO и чистый код плохо.

«вызывать связывать» это что значит? Разверни мысль пожалуйста.

теперь нам надо ходить и выяснять, используем мы этот код только в правильных тредах или случайно запустили это в чем-то запущенном через forkIO.

Во-первых, далеко не все ffi вызовы требуют bounded threads, а только те, которые используют thread storage, например всякий opengl (больше я и примеров-то и не знаю).

Если ты находишься в такой ситуации и хочешь повысить безопасность: сделай свой враппер над IO, BoundedIO, в runBoundedIO напиши большой комментарий, что его можно пускать только из IO, не делай инстанса MonadIO, а сделай какой-нить unsafeIO :: IO a -> BoundedIO a, если уж припрёт иметь случайные IO вызовы.

qnikst ★★★★★
()
Ответ на: комментарий от KblCb

Ну так вот же они: MonadIO m.

формально это не говорит о том, что там особый ffi вызов, который можно сделать только из bounded thread. В основном т.к. это никому не надо и случай редкий, в принципе система типов позволяет извратиться и суметь это записать, но как-то не сильно нужно.

Я уже не говорю о том, что мне не вполне понятно почему связывать IO и «чистый» код плохо при условии что все функции в программе вызываются из IO.

может агду посоветовать, или haskell до появления IO Monad?

qnikst ★★★★★
()
Ответ на: комментарий от nokachi

если кратко, эксперты, посчитав очередную фабоначчу, говорят что и так все зашибись

Ну конечно зашибись, у фибоначчивых экспертов-то.

anonymous
()
Ответ на: комментарий от dave

у меня фильтр, всё что в кафе это не спам. Я попробую по header-ам понять, что не так, а ты напрямую через gmail посылал?

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Нет, через почтовый клиент Thunderbird. Один раз я уже так делал, и все было нормально.

dave ★★★★★
()
Ответ на: комментарий от qnikst

Там, кстати, в теме нет подстроки «[Haskell-cafe]». Может, в этом дело. Я так и понял, вроде бы система рассылки сама обычно добавляет. Или я должен был?

dave ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.