Vamos primeiro distinguir entre aprender os conceitos abstratos e aprender exemplos específicos deles.
Você não vai muito longe ignorando todos os exemplos específicos, pela simples razão de que eles são totalmente onipresentes. De fato, as abstrações existem em grande parte porque unificam as coisas que você faria de qualquer maneira com os exemplos específicos.
As abstrações em si, por outro lado, são certamente úteis , mas não são imediatamente necessárias. Você pode ir longe ignorando completamente as abstrações e apenas usando os vários tipos diretamente. Você vai querer entendê-los eventualmente, mas sempre poderá voltar depois. Na verdade, eu posso quase garantir que, se você fizer isso, quando voltar a fazê-lo, dará um tapa na testa e se perguntará por que passou todo esse tempo fazendo as coisas da maneira mais difícil, em vez de usar as convenientes ferramentas de uso geral.
Tome Maybe acomo exemplo. É apenas um tipo de dados:
data Maybe a = Just a | Nothing
É tudo menos auto-documentado; é um valor opcional. Ou você tem "apenas" algo do tipo aou não tem nada. Digamos que você tenha uma função de pesquisa de algum tipo, que retorne Maybe Stringpara representar a pesquisa de um Stringvalor que pode não estar presente. Então, você combina com o valor para ver qual é:
case lookupFunc key of
Just val -> ...
Nothing -> ...
Isso é tudo!
Realmente, não há mais nada que você precise. Sem Functors ou Monads ou qualquer outra coisa. Eles expressam maneiras comuns de usar Maybe avalores ... mas são apenas expressões idiomáticas, "padrões de design", como você quiser chamar.
O único lugar em que você realmente não pode evitá-lo completamente é IO, mas de qualquer maneira é uma caixa preta misteriosa, então não vale a pena tentar entender o que isso significa como algo assim Monad.
De fato, aqui está uma folha de dicas para tudo o que você realmente precisa saber IOpor enquanto:
Se algo tem um tipo IO a, isso significa que é um procedimento que faz algo e cospe um avalor.
Quando você tem um bloco de código usando a donotação, escreva algo como isto:
do -- ...
inp <- getLine
-- etc...
... significa executar o procedimento à direita do <-e atribuir o resultado ao nome à esquerda.
Considerando que se você tiver algo parecido com isto:
do -- ...
let x = [foo, bar]
-- etc...
... significa atribuir o valor da expressão simples (não um procedimento) à direita da =e ao nome à esquerda.
Se você colocar algo lá sem atribuir um valor, assim:
do putStrLn "blah blah, fishcakes"
... significa executar um procedimento e ignorar qualquer coisa que ele retorne. Alguns procedimentos têm o tipo IO ()- o ()tipo é um tipo de espaço reservado que não diz nada, de modo que apenas significa que o procedimento faz algo e não retorna um valor. Como uma voidfunção em outros idiomas.
Executar o mesmo procedimento mais de uma vez pode gerar resultados diferentes; esse é o tipo de ideia. É por isso que não há como "remover" o IOvalor de um valor, porque algo IOnão é um valor, é um procedimento para obter um valor.
A última linha de um dobloco deve ser um procedimento simples, sem atribuição, onde o valor de retorno desse procedimento se torna o valor de retorno para todo o bloco. Se você deseja que o valor de retorno use algum valor já atribuído, a returnfunção usa um valor simples e fornece um procedimento não operacional que retorna esse valor.
Fora isso, não há nada de especial IO; esses procedimentos são realmente valores simples, e você pode transmiti-los e combiná-los de maneiras diferentes. É somente quando eles são executados em um dobloco chamado em algum lugar mainque eles fazem qualquer coisa.
Então, em algo como este programa de exemplo totalmente chato e estereotipado:
hello = do putStrLn "What's your name?"
name <- getLine
let msg = "Hi, " ++ name ++ "!"
putStrLn msg
return name
... você pode lê-lo como um programa imperativo. Estamos definindo um procedimento chamado hello. Quando executado, primeiro ele executa um procedimento para imprimir uma mensagem perguntando seu nome; Em seguida, ele executa um procedimento que lê uma linha de entrada e atribui o resultado a name; depois atribui uma expressão ao nome msg; depois imprime a mensagem; em seguida, ele retorna o nome do usuário como resultado de todo o bloco. Como nameé a String, isso significa que helloé o procedimento que retorna a String, então ele tem o tipo IO String. E agora você pode executar esse procedimento em outro lugar, assim como ele é executado getLine.
Pfff, mônadas. Quem precisa deles?