LINUX.ORG.RU

Что делают хаскеллисты если нужно внезапно выполнить I/O?

 


1

4

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

def func(args):
    a = subfunc1(args)
    b = subfunc2(a)
    return subfunc3(b)
func args = subfunc3 b where
     b = subfunc2(a)
     a = subfunc1(args)

Функции работают одинаково, плюс относительно хаскелловской функции я знаю что она не зависит от глобального состояния и прочий хаскелловский блёрб.

Но тут ВНЕЗАПНО я понял, что хотел бы логгировать все случаи, когда subfunc1 выдал какой-то определенный результат. В случае с императивным кодом все просто

def func(args):
    a = subfunc(args)
    if some_condition(a):
        logger.warning('Some shit happened')
    ....

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

Вопрос, как правильно писать на хаскеле чтоб в такой ситуации отделываться минимальными изменениями кода?

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

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

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

Есть unsafePerformIO, но используй на свой страх и риск.

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

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

Ворнинг - это не факап, во-первых. А во-вторых, заранее не предугадаешь где он понадобится, особенно когда с предметной областью не слишком хорошо знаком.

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

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

а покажи мне результаты вычисления на каждой итерации

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

yoghurt ★★★★★
()

Если чтобы добавить логирование в одну функцию тебе приходится «все с нуля переписывать», то ты делаешь что-то не так. Должны поменяться 1-2 сигнатуры максимум.

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

Ну и да, если ты уже засел в стеке, то единственным изменением в сигнатуре будет MonadWriter w m =>

anonymous
()

Можно возвращать ворнинги и агрегировать их вместе с результатом. Т.е. если раньше ф-я отдавала скаляр, то теперь будет кортеж из скаляра и, например, списка.

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

гонять потом эти итерации вручную из IO, попутно логируя.

Это ж ЧМ, число итераций может быть достаточно велико, да и отчет надо все равно перепечатывать. Проще переписать через монаду. В общем, факт в том, что в императивном языке такая правка занимает 30 секунд, а в хаскеле минут где-то 5, или даже 10 если не помнишь наизусть все сигнатуры монадрайтера.

Но я допускаю, что это просто мои мозги безнадежно испорчены императивщиной и чаю вразумления от просвещенных адептов ФП.

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

приходится «все с нуля переписывать»

Я имел в виду переписать всю функцию с нуля, не весь проект, разумеется.

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

Это ж ЧМ, число итераций может быть достаточно велико

И что? (псевдокод)

while (not $ goal reached)
    thisState <- readIOref state
    let newState = doSingleAlgoIteration thisState
    printState newState
    writeIOref state newState
yoghurt ★★★★★
()
Ответ на: комментарий от yoghurt

И что? (псевдокод)

Та ниче. Кол-во добавленного и переписанного кода все равно больше чем в случае с императивщиной. Не забывай, что при этом красивую чисто-рекурсивную функцию придется выкинуть в /dev/null.

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

при этом красивую чисто-рекурсивную функцию придется выкинуть в /dev/null.

Ээ, если из этой красивой чисто-рекурсивной ф-ии сложно вытащить код одной итерации, то видимо не такая уж она и красивая :)

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

Ладно, последняя попытка.

Императивный язык:

  • Найти цикл вычисления
  • Добавить оператор вывода

ФП:

  • Найти функцию вычисления
  • Скопипастить из нее функцию итерации
  • Напомнить себе сигнатуры монадрайтера (опционально)
  • Написать итерацию через монаду.
  • Исправить ошибки (опционально)

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

provaton ★★★★★
() автор топика

Но тут ВНЕЗАПНО я понял, что хотел бы логгировать все случаи, когда subfunc1 выдал какой-то определенный результат... В хаскелле для такого надо фактически все с нуля переписывать через какую-то монаду.

Ты не понял, хаскель работает по-другому. Тебе это не нужно.

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

Это лучший коммент ветки, спасибо.

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

ВНЕЗАПНО
Если это использовать в том _стиле_

Стиль внезапного кодирования? Если для отладки - то совет приемлим (внутри Debug.Trace и Hood то же самое). А если нет - башкой сначала подумать над проектированием, все равно в чистой фунции порядок вычислений обычно неопределен.

anonymous
()

Тонко! Хаскелисты не могут в решение практических задач на подкорочном уровне. ЧТД.

anonymous
()

Вопрос, как правильно писать на хаскеле чтоб в такой ситуации отделываться минимальными изменениями кода?

Внезапно, никак! Monad — это ведь достаточно простая штука, основанная на том что любое хаскельное выражение может управлять вычислимостью своих параметров. И Logger — это отдельная ветка вычисления, выбираемая соответствующим комбинатором.

В хаскеле ведь нет никакой магии. Хочешь чистоты и порядку, изволь на *каждом* шаге проверять, а не захотелось ли вдруг нашему вычислению чего-то написать в лог. Типа ставим на тумбочку два стакана: один с водой, если вдруг ночью захочется пить, другой пустой — если не захочется.

Понятно дело, для вещей типа ЧМ это гроб. Поэтому, приходится думать как же сделать так чтобы проверка происходила не на каждом шаге. Или в таком месте, где это не сильно угробит производительность.

Macil ★★★★★
()

Если для отладки то, Используют Debug.Trace.trace, который в свою очередь использует unsafePerformIO, или используют unsafePerformIO напрямую. Более модные хацкелисты используют unsafeDupablePerformIO, там где надо. Но если это не отладка, а часть логики, то будь добр выносит эффекты в типы.

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

Не обозначает. Просто добавляешь эффект лога (или мтлный уровень) и соотв запускаешь КПК ruNoopLogger (будет без IO) или runIOLogger.

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

Если бы у тебя код возвращал список итераций, то проблем бы не было, а так надо было писать с самого начала в 90% случаев. При этом если они не нужны (результаты) то все прекрасно фьюзится, а учитывая, что обычно нужен предпоследний результат (последний используется для погрешности) то сам Карри велел так делать.

qnikst ★★★★★
()
Ответ на: комментарий от yoghurt
iterations = iterate method initial.
solution = takeWhile (not.resultReached) iterations
result x = (l0, f l l0)
  where 
    l = last x
    l0 =  last $ init x
logging = mapM print solution
qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Монадные стеки никак не мешают в реализации сложных ЧМ, только помогают структурировать сложный код (как и везде). Там как раз всё как везде — монада для опций, монада для логов, монада для планирования параллельных операций.

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

Но ведь чистый код поднимается в монаду элементарно, на уровне «сделать C-2 C-x tab, вписать в начале do и let». Вместо logger.warning ты будешь точно так же вызывать функцию с типом из своей монады. Только код будет чище и композабельнее за счёт того, что стеки монад способствуют постепенному наращиванию функциональности.

Куда интереснее то, что ты не обратил внимание на время вычисления a и some_condition(a) и то, как оно может повлиять на юзабельность программы и логов.

anonymous
()

Но тут ВНЕЗАПНО я понял, что хотел бы логгировать все случаи, когда subfunc1 выдал какой-то определенный результат.

Монада writer и монадические трансформеры.

theNamelessOne ★★★★★
()

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

Не надо, хаскель достаточно гибок. В данном случае можно, например, так:

func args = subfunc3 <$> b where
    b = subfunc2 <$> a
    a =
        do let a' = subfunc1 args
             when (someCondition a') $ Logger.warning "Some shit happened"
             return a'
Выглядит почти так же. А вообще, советую по возможности использовать let, а не where, и писать в нормальном порядке:
func args =
    let a = subfunc1 args
        b = subfunc2 a
    in subfunc3 b
vs
func args =
    do let a = subfunc1 args
       when (someCondition a) $ Logger.warning "Some shit happened"
       let b = subfunc2 a
       return $ subfunc3 b

Miguel ★★★★★
()
Последнее исправление: Miguel (всего исправлений: 1)

Как уже сказали, если для отладки, то Trace (да, это узаконенный хак на уровне стандартной библиотеки). Если логирование - часть логики, то нужно вносить явный IO и создавать свое вычисление (ту же монаду).

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

Про упомянутый Trace говорят, что он нарушает «ссылочную прозрачность». Это более конкретно, хотя злые языки на stack overflow утверждают, что этот термин неоднознычный, и им лучше не пользоваться)

Кстати, unsafePerformIO может сыграть злую шутку. При прочих равных компилятор GHC предполагает, что код является ссылочно прозрачным, а потому компилятор может сгенерировать код совсем не так, как предполагал программист - и у меня такое было) Тогда я полностью переписал без unsafePerformIO. В итоге получилось намного лучше и красивее.

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

Монадные стеки никак не мешают в реализации сложных ЧМ, только помогают структурировать сложный код (как и везде). Там как раз всё как везде — монада для опций, монада для логов, монада для планирования параллельных операций.

Каждый уровень стека вносит оверхед что в ЧМ может сильно мешать, да и я не могу себе представить где там вообще нужен стек монад.

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

А вообще, советую по возможности использовать let, а не where, и писать в нормальном порядке:

Очень неоднозначный совет, по мне так код с where выглядит приятнее в большинстве случаев, т.к. сразу видна суть и гораздо более приятные отступы и сложнее ошибиться со скоупом значений (чтобы значение не перевычислялось).

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

Но ведь чистый код поднимается в монаду элементарно

Зачем ты врешь?

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

сразу видна суть

Ну, это у кого как.

гораздо более приятные отступы

Не понял.

и сложнее ошибиться со скоупом значений

Тоже не понял. Примерчики можно?

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

2. В варианте с where отсупы ровнее, мне лично меньше глаза ломает, (естественно не в случае в do нотации, где where не является аналогом)

3. был не прав, там был обратный пример

qnikst ★★★★★
()

Monad Writer, DeepSeq, и unsafePerformIO тебе в помощь. В Хаскеле можно впендюрить логирование в чистую функцию. Но не нужно. Потому что чистую функцию можно дебажить в интерпретаторе.

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

Если логирование - часть логики, то нужно вносить явный IO

Нет, если нужен только чёрный ящик с логами.

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

Каждый уровень стека вносит оверхед что в ЧМ может сильно мешать,

Нет.

да и я не могу себе представить где там вообще нужен стек монад.

Ну наверное потому что надо на хаскелле что-то реальное писать, а не фибы с факами?

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

Нет.

аргументация зашкаливает.

Ну наверное потому что надо на хаскелле что-то реальное писать, а не фибы с факами?

ну давай примеры не фибов с факами для ЧМ-ов, где активно используются монадные стеки.

qnikst ★★★★★
()

Написать монаду, которая будет выполнять логгирование и поднять требуемый код в нее?

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

монада для опций, монада для логов, монада для планирования параллельных операций.

ЧМ ничем в этом плане от остального не отличаются. Чем отличаются, выше указывалось, но ОПушке на это пока насрать.

Ещё неясно, зачем было говорить про IO в корне стека. Всё перекладывание байтов прекрасно живёт в ST на вершине стека, для логирования итерационных методов IO вообще не нужно.

аргументация зашкаливает.

Ну не я же первый кукарекал про оверхед монад. RWS/чистые short-circuit-монады никакого оверхеда не вносят по сравнению с немонадической реализацией того же вычислительного процесса.

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

монада для планирования параллельных операций.

я все ещё жду кода не факториалов и использованим монадных стеков, уровень в слой будь то Par/ST/IO не в счет.

Ну не я же первый кукарекал про оверхед монад. RWS/чистые short-circuit-монады никакого оверхеда не вносят по сравнению с немонадической реализацией того же вычислительного процесса.

Ок, про оверхед монад читаем в [1], как будет опровержение, будем дальше говорить.

[1] http://okmij.org/ftp/Haskell/extensible/exteff.pdf

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

Очень бегло просмотрел. Забавно. Используют продолжения. Впрочем, совсем неудивительно.

dave ★★★★★
()

У тебя получается код, одновременно что-то возвращающий, и логирующий «some shit happened». В хаскеле для этого есть тип Either SomeShit Res, его и используй. Переделывать всего-ничего все места, где результат используется, либо пробрасывать как-то дальше SomeShit, либо что-то решать и что-то с этим делать (логировать etc.).

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

по сравнению с немонадической реализацией того же вычислительного процесса.

Так там в пейпере монадический код и раскрыт обратно в чистый.

Это не говоря о том, что числодробление в ST будет занимать большую часть времени по сравнению с логированием или чтением опций.

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

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

Это не говоря о том, что числодробление в ST будет занимать большую часть времени по сравнению с логированием или чтением опций.

возможно, но при этом каждый return в коде будет иметь сложность пропорциональную глубине стека об этом и говорится. Если ты делаешь (return $ runST ..) то внутри runST естественно оверхеда нет, т.к. там нету стека, так вполне делают, но при этом таскать весь стек может быть дорого.

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

Но самое главное, там обоснование почему монадический стек вносит оверхед.

Да, и это обоснование заключается в разворачивании монад обратно в чисто функциональный стиль. Там всего один абзац, прочитай его наконец.

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

я все ещё жду подробного и обоснованного объяснение почему обоснование не верно, а так же ЧМ кода с использованием монадических стеков.

qnikst ★★★★★
()

Задача: осуществить логгирование.

Решение на нормальном языке:

printf(«foo\n»);

Решение на Haskell:

сложное вычисление, чистая функция, разбиение на подфункции, эквивалентый полупсевдокод, глобальное состояние, unsafePerformIO, Either, монады, монады, монады, монад ридер, итерации вручную из IO, MonadWriter, переписать через монаду, сигнатуры монадрайтера, unsafeDupablePerformIO, doSingleAlgoIteration, сам Карри велел так делать, монадные стеки, монада для опций, монада для логов, монада для планирования параллельных операций, подъем в монаду, монадические трансформеры, DeepSeq, чёрный ящик, фибы с факами, RWS/чистые short-circuit-монады, Par/ST/IO, монады, монады, монады...

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