произошла.
Итак, коллега решал для себя простенькую задачу по реализации интерпретатора бреинфак-подобного языка на js и я решил, что чего бы мне тоже не написать, на хаскеле естественно.
Сразу была написана фримонада для AST и интерпретатор. Команды всего четыре, инкремент значения в памяти, декремент, установка в нуль и вывод. Поэтому для интерпретатора я заюзал StateT для памяти и WriterT для вывода. Думаю, код понятен, если работали с free (если нет, пишите, я все с радостью объясню в любое время суток).
runBrainscrambler :: Brainscrambler a -> String
runBrainscrambler
= flip execWriterT ""
. flip evalStateT 0
. foldFree go
where
go (Increment next) = modify succ >> pure next
go (Decrement next) = modify pred >> pure next
go (PushZero next) = put 0 >> pure next
go (Output next) = get >>= tell . show >> pure next
ВАЖНО ЗАМЕТИТЬ: код полностью рабочий, он не просто компилируется, он выдаст правильный результата на любых примерах.
Но тут есть несколько ошибок, которые я не заметил, так как не написал тайпинги, мол хаскель сам выведет. Кто их уже заметил, молодец, остальным тоже советую посидеть и поискать.
В проекте было подключено OverloadedString-расширение, так что любой строковой литерал полиморфен и компилятор будет кидать ворнинг о том, что "используется дефолтный тип, так как тип не указан явно", если тип не указать явно. Собственно в коде выше "" не аннотирован явно, но тип четко выводится из результата функции (String). Ворнинг тем не менее был:
• Defaulting the following constraint to type ‘[Char]’
IsString b0 arising from the literal ‘""’
• In the second argument of ‘flip’, namely ‘""’
In the first argument of ‘(.)’, namely ‘flip execWriterT ""’
In the expression:
flip execWriterT "" . flip evalStateT (0 :: Int) . foldFree go
|
16 | = flip execWriterT ""
| ^^
Для меня это стала загадкой, с просьбой решить ее я обратился в @haskellru, но там никто не смог помочь (говорили про то, что ворнинг из-за того, что нет аннотации, но не увидили, что тип четко известен, полагаю никто, как я, в чужой код не вчитывается).
И вот я решил добавить тайпинги и весь код сломался:
go :: BrainscramblerF a -> StateT Int (Writer String) a
И вот тут-то я наконец понял, в чем дело.
1. У меня Writer, а не WriterT, поэтому нужно execWriter, а не execWriterT
2. (exec|eval|run)WriterT? не принимают начального "состояния", так как они работают с моноидом и берут в качестве начального вывода mempty.
Эти две ошибки вместе дают ОЧЕНЬ интересный результат. Раз execWriterT не принимает начального состояния, то почему выше код работал? Что именно flip флипал? И тут до меня дошло озарение я в спешке побежал проверять:
:t flip execWriterT
flip execWriterT :: b -> WriterT c ((->) b) a -> c
Именно, из-за flip хаскель вывел инстанс монады для (->) b, то есть Reader. И использовал этот (->) b как монаду для дыры монадного трансформера WriterT. Тот "" был не начальным состоянием WriterT, а энвароментом Reader, который в коде нигде явно не используется (нет ask), поэтому и тип вывести не получается, поэтому и был ворнинг.
Это эпичнейшая ошибка в моей жизни, скажу честно. Итоговый правильный код выглядит так:
runBrainscrambler :: Brainscrambler a -> String
runBrainscrambler
= execWriter
. flip evalStateT (0 :: Int)
. foldFree go
where
go :: BrainscramblerF a -> StateT Int (Writer String) a
go (Increment next) = modify succ >> pure next
go (Decrement next) = modify pred >> pure next
go (PushZero next) = put 0 >> pure next
go (Output next) = get >>= tell . show >> pure next
Вот вам и статически типизированный хаскель с выводом типов - выведет даже неверный код в верный, как тут хаскель не любить-то.
Пиши на ruHaskell мини-статью
Обсуждают сегодня