As linguagens impuras realmente não diferem em princípio das linguagens imperativas mais familiares, especialmente agora que muitos truques funcionais foram copiados. O que é diferente é o estilo - como você resolve problemas.
Se você considera Haskell puro, ou conta a mônada de IO como impureza, o estilo Haskell é uma forma extrema desse estilo e vale a pena aprender.
A mônada Haskell IO é derivada da teoria matemática das mônadas (é claro). No entanto, para programadores imperativos, acho que uma maneira inversa de chegar às mônadas faz mais sentido.
Fase um - uma linguagem funcional pura pode facilmente retornar um grande valor de string como resultado. Essa grande cadeia pode ser o código-fonte de um programa imperativo, derivado de maneira funcional pura de alguns parâmetros de especificação de requisitos. Você pode criar um compilador de "nível superior" que executa seu gerador de código e, em seguida, alimenta automaticamente esse código gerado no compilador de linguagem imperativa.
Fase dois - em vez de gerar código fonte textual, você gera uma árvore de sintaxe abstrata fortemente tipada. Seu compilador de linguagem imperativa é absorvido pelo compilador "de nível superior" e aceita o AST diretamente como código-fonte. Isso é muito mais próximo do que Haskell faz.
Isso ainda é estranho, no entanto. Por exemplo, você tem dois tipos distintos de funções - aquelas avaliadas durante a fase de geração de código e as executadas quando o programa gerado é executado. É um pouco como a distinção entre funções e modelos em C ++.
Portanto, para a fase 3, torne os dois iguais - a mesma função com a mesma sintaxe pode ser parcialmente avaliada durante a "geração de código", ou totalmente avaliada, ou não avaliada. Além disso, descarte todos os nós AST da construção em loop em favor da recursão. De fato, descarte a ideia de nós AST como um tipo especial de dados - não tenha nós AST de "valor literal", apenas valores etc.
Isso é basicamente o que a mônada de E / S faz - o operador de ligação é uma maneira de compor "ações" para formar programas. Não é nada de especial - apenas uma função. Muitas expressões e funções podem ser avaliadas durante a "geração de código", mas aquelas que dependem dos efeitos colaterais de E / S devem ter a avaliação atrasada até o tempo de execução - não por nenhuma regra especial, mas como uma consequência natural das dependências de dados em expressões.
Mônadas em geral são apenas generalizações - elas têm a mesma interface, mas implementam as operações abstratas de maneira diferente; portanto, em vez de avaliar uma descrição do código imperativo, elas avaliam para outra coisa. Ter a mesma interface significa que há algumas coisas que você pode fazer com as mônadas sem se importar com a mônada, o que acaba sendo útil.
Essa descrição sem dúvida fará explodir cabeças de puristas, mas para mim explica algumas das reais razões pelas quais Haskell é interessante. Ele obscurece a fronteira entre programação e metaprogramação e usa as ferramentas de programação funcional para reinventar a programação imperativa sem precisar de sintaxe especial.
Uma crítica que tenho aos modelos C ++ é que eles são uma espécie de sublanguagem funcional pura e quebrada em uma linguagem imperativa - para avaliar a mesma função básica em tempo de compilação, em vez de em tempo de execução, é necessário reimplementá-la usando um estilo completamente diferente de codificação. Em Haskell, embora a impureza deva ser rotulada como tal em seu tipo, a mesma função exata pode ser avaliada no sentido de metaprogramação e no sentido de não metaprogramação em tempo de execução no mesmo programa - não há linha dura entre programação e metaprogramação.
Dito isto, existem algumas coisas de metaprogramação que Haskell padrão não pode fazer, basicamente porque tipos (e talvez algumas outras coisas) não são valores de primeira classe. Existem variantes de idioma que tentam resolver isso, no entanto.
Muitas coisas que eu disse sobre Haskell podem ser aplicadas em linguagens funcionais impuras - e às vezes até em linguagens imperativas. Haskell é diferente porque você não tem escolha a não ser adotar essa abordagem - basicamente obriga você a aprender esse estilo de trabalho. Você pode "escrever C em ML", mas não pode "escrever C em Haskell" - pelo menos não sem aprender o que está acontecendo sob o capô.