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 a
como 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 a
ou não tem nada. Digamos que você tenha uma função de pesquisa de algum tipo, que retorne Maybe String
para representar a pesquisa de um String
valor 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 Functor
s ou Monad
s ou qualquer outra coisa. Eles expressam maneiras comuns de usar Maybe a
valores ... 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 IO
por enquanto:
Se algo tem um tipo IO a
, isso significa que é um procedimento que faz algo e cospe um a
valor.
Quando você tem um bloco de código usando a do
notaçã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 void
funçã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 IO
valor de um valor, porque algo IO
não é um valor, é um procedimento para obter um valor.
A última linha de um do
bloco 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 return
funçã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 do
bloco chamado em algum lugar main
que 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?