Qual é o problema com Haskell? [fechadas]


109

Conheço alguns programadores que ficam falando sobre Haskell quando estão entre eles, e aqui no SO todo mundo parece adorar essa linguagem. Ser bom em Haskell parece um pouco a marca registrada de um programador genial.

Alguém pode dar alguns exemplos de Haskell que mostram por que ele é tão elegante / superior?

Respostas:


134

A maneira como foi apresentada para mim, e o que eu acho que é verdade depois de ter trabalhado no aprendizado em Haskell por um mês agora, é o fato de que a programação funcional torce seu cérebro de maneiras interessantes: ela força você a pensar sobre problemas familiares de maneiras diferentes : em vez de loops, pense em mapas e dobras e filtros, etc. Em geral, se você tiver mais de uma perspectiva sobre um problema, isso o torna mais apto para raciocinar sobre o problema e alternar os pontos de vista conforme necessário.

A outra coisa realmente legal sobre Haskell é seu sistema de tipos. É estritamente tipado, mas o mecanismo de inferência de tipo faz com que pareça um programa Python que magicamente avisa quando você comete um erro estúpido relacionado ao tipo. As mensagens de erro de Haskell a esse respeito estão um pouco ausentes, mas à medida que você se familiariza mais com a linguagem, dirá a si mesmo: é isso que a digitação deve ser!


47
Deve-se notar que as mensagens de erro de Haskell não faltam, sim as de ghc. O padrão Haskell não especifica como as mensagens de erro são feitas.
PyRulez

Para plebeus como eu, GHC significa Glasgow Haskell Compiler. en.wikipedia.org/wiki/Glasgow_Haskell_Compiler
Lorem Ipsum

137

Este é o exemplo que me convenceu a aprender Haskell (e estou feliz por ter aprendido).

-- program to copy a file --
import System.Environment

main = do
         --read command-line arguments
         [file1, file2] <- getArgs

         --copy file contents
         str <- readFile file1
         writeFile file2 str

OK, é um programa curto e legível. Nesse sentido, é melhor do que um programa C. Mas como isso é tão diferente de (digamos) um programa Python com uma estrutura muito semelhante?

A resposta é avaliação preguiçosa. Na maioria das linguagens (mesmo algumas funcionais), um programa estruturado como o anterior resultaria no arquivo inteiro sendo carregado na memória e, em seguida, escrito novamente com um novo nome.

Haskell é "preguiçoso". Ele não calcula as coisas até que precise e, por extensão , não calcula as coisas de que nunca precisa. Por exemplo, se você remover a writeFilelinha, Haskell não se incomodaria em ler nada do arquivo em primeiro lugar.

Do jeito que está, Haskell percebe que writeFiledepende do readFilee, portanto, é capaz de otimizar esse caminho de dados.

Embora os resultados dependam do compilador, o que normalmente acontecerá quando você executar o programa acima é o seguinte: o programa lê um bloco (digamos, 8 KB) do primeiro arquivo, o grava no segundo arquivo e, em seguida, lê outro bloco do primeiro arquivo e grava no segundo arquivo, e assim por diante. (Tente correr stracenele!)

... que se parece muito com o que faria uma implementação C eficiente de uma cópia de arquivo.

Portanto, Haskell permite que você escreva programas compactos e legíveis - muitas vezes sem sacrificar muito o desempenho.

Outra coisa que devo acrescentar é que Haskell simplesmente torna difícil escrever programas com erros. O incrível sistema de tipos, a falta de efeitos colaterais e, claro, a compactação do código Haskell reduzem os bugs por pelo menos três razões:

  1. Melhor desenho do programa. A complexidade reduzida leva a menos erros lógicos.

  2. Código compacto. Menos linhas para a existência de bugs.

  3. Erros de compilação. Muitos bugs simplesmente não são Haskell válidos .

Haskell não é para todos. Mas todos deveriam tentar.


Como exatamente você mudaria a constante de 8 KB (ou o que quer que seja)? Porque eu aposto que uma implementação de Haskell seria mais lenta do que uma versão C de outra forma, especialmente sem pré-busca ...
user541686

1
@Mehrdad Você pode alterar o tamanho do buffer com hSetBuffering handle (BlockBuffering (Just bufferSize)).
David

3
É incrível que essa resposta tenha 116 votos positivos, mas o que está lá está simplesmente errado. Este programa irá ler o arquivo inteiro, a menos que você use Bytestrings preguiçosos (com os quais você pode fazer Data.Bytestring.Lazy.readFile), que não têm nada a ver com o Haskell ser uma linguagem preguiçosa (não estrita). Mônadas são sequenciadas - isso significa aproximadamente "todos os efeitos colaterais acabam quando você tira o resultado". Quanto à mágica do "bytestring preguiçoso": isso é perigoso e você pode fazer isso com uma sintaxe semelhante ou mais simples na maioria das outras linguagens.
Jo So

14
O velho padrão chato readFiletambém faz IO preguiçoso da mesma maneira Data.ByteString.Lazy.readFile. Portanto, a resposta não está errada e não é apenas uma otimização do compilador. Na verdade, isso é parte da especificação de Haskell : "A readFilefunção lê um arquivo e retorna o conteúdo do arquivo como uma string. O arquivo é lido lentamente, sob demanda, como com getContents."
Daniel Wagner

1
Acho que as outras respostas apontam para coisas que são mais especiais sobre Haskell. Muitas linguagens / ambientes têm córregos, você pode fazer algo semelhante no Nó const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2)). O Bash também tem algo semelhante:cat $1 > $2
Max Heiber

64

Você está fazendo a pergunta errada.

Haskell não é uma linguagem em que você olha alguns exemplos legais e diz "aha, entendo agora, é isso que o torna bom!"

É mais como, nós temos todas essas outras linguagens de programação, e elas são mais ou menos semelhantes, e então há Haskell que é totalmente diferente e maluco de uma forma que é totalmente incrível quando você se acostuma com a maluquice. Mas o problema é que leva um bom tempo para se aclimatar à loucura. Coisas que diferenciam Haskell de quase qualquer outra linguagem, mesmo semi-mainstream:

  • Avaliação preguiçosa
  • Sem efeitos colaterais (tudo é puro, IO / etc acontece via mônadas)
  • Sistema de tipo estático incrivelmente expressivo

bem como alguns outros aspectos que são diferentes de muitas línguas convencionais (mas compartilhados por alguns):

  • funcional
  • espaço em branco significativo
  • tipo inferido

Como alguns outros participantes responderam, a combinação de todos esses recursos significa que você pensa sobre a programação de uma maneira totalmente diferente. Portanto, é difícil encontrar um exemplo (ou conjunto de exemplos) que comunique isso adequadamente ao Joe-mainstream-programmer. É uma coisa experiencial. (Para fazer uma analogia, posso mostrar as fotos da minha viagem à China em 1970, mas depois de ver as fotos, você ainda não saberá como era ter vivido lá naquela época. Da mesma forma, posso mostrar um Haskell 'quicksort', mas você ainda não sabe o que significa ser um Haskeller.)


17
Eu discordo da sua primeira frase. Fiquei realmente impressionado com alguns exemplos de código Haskell inicialmente e o que realmente me convenceu de que valia a pena aprender foi este artigo: cs.dartmouth.edu/~doug/powser.html Mas é claro, isso é interessante para um matemático / físico. Um programador que examinasse coisas do mundo real acharia este exemplo ridículo.
Rafael S. Calsaverini

2
@Rafael: Isso levanta a questão "com o que um programador olhando para as coisas do mundo real ficaria impressionado"?
JD,

Boa pergunta! Não sou um programador do "mundo real", então não sei do que eles gostam. hahaha ... Eu sei o que os físicos e matemáticos gostam. : P
Rafael S. Calsaverini

27

O que realmente diferencia Haskell é o esforço que ele faz em seu design para aplicar a programação funcional. Você pode programar em um estilo funcional em praticamente qualquer linguagem, mas é muito fácil abandoná-lo na primeira conveniência. Haskell não permite que você abandone a programação funcional, então você deve levá-lo à sua conclusão lógica, que é um programa final mais fácil de raciocinar e que contorna toda uma classe dos mais espinhosos tipos de bugs.

Quando se trata de escrever um programa para uso no mundo real, você pode achar que Haskell carece de alguma forma prática, mas sua solução final será melhor por ter conhecido Haskell para começar. Definitivamente, ainda não cheguei lá, mas até agora aprender Haskell foi muito mais esclarecedor do que, digamos, Lisp estava na faculdade.


1
Bem, sempre existe a possibilidade de usar sempre e apenas a mônada ST e / ou unsafePerformIOpara pessoas que só querem ver o mundo queimar;)
sara

22

Parte da confusão é que a pureza e a tipagem estática permitem o paralelismo combinado com otimizações agressivas. As linguagens paralelas estão em alta agora, com o multicore sendo um pouco perturbador.

Haskell oferece mais opções de paralelismo do que qualquer linguagem de uso geral, junto com um compilador de código nativo rápido. Não há competição com este tipo de suporte para estilos paralelos:

Portanto, se você se preocupa em fazer seu multicore funcionar, Haskell tem algo a dizer. Um ótimo lugar para começar é com o tutorial de Simon Peyton Jones sobre programação paralela e concorrente em Haskell .


"junto com um compilador de código nativo rápido"?
JD de

Eu acredito que dons está se referindo ao GHCI.
Gregory Higley

3
@Jon: shootout.alioth.debian.org/u32/… Haskell se sai muito bem no shootout, por exemplo.
Peaker de

4
@Jon: O código do tiroteio é muito antigo, e vem de um passado distante onde o GHC era menos um compilador otimizador. Ainda assim, prova que o código Haskell pode ir para baixo nível para produzir desempenho, se necessário. As soluções mais recentes no shootout são mais idiomáticas e ainda rápidas.
Peaker

1
@GregoryHigley Existe uma diferença entre GHCI e GHC.
Jeremy List


18

Passei o último ano aprendendo Haskell e escrevendo um projeto razoavelmente grande e complexo nele. (O projeto é um sistema de negociação de opções automatizado e tudo, desde os algoritmos de negociação até a análise e manipulação de feeds de dados de mercado de baixo nível e alta velocidade, é feito em Haskell.) É consideravelmente mais conciso e fácil de entender (para aqueles com apropriado) do que uma versão Java seria, além de extremamente robusta.

Possivelmente, a maior vitória para mim foi a capacidade de modularizar o fluxo de controle por meio de coisas como monóides, mônadas e assim por diante. Um exemplo muito simples seria o monóide de ordenação; em uma expressão como

c1 `mappend` c2 `mappend` c3

para onde c1e assim por diante LT,EQ ouGT , c1retornando EQcausas a expressão para continuar, avaliando c2; se c2retorna LTou GTesse é o valor do todo, e c3não é avaliado. Esse tipo de coisa fica consideravelmente mais sofisticado e complexo em coisas como geradores de mensagens monádicas e analisadores, onde posso estar carregando diferentes tipos de estado, ter condições de aborto variadas ou posso querer ser capaz de decidir por qualquer chamada em particular se abortar realmente significa "nenhum processamento adicional" ou significa "retornar um erro no final, mas continuar o processamento para coletar outras mensagens de erro".

Tudo isso leva algum tempo e provavelmente algum esforço para aprender e, portanto, pode ser difícil apresentar um argumento convincente para aqueles que ainda não conhecem essas técnicas. Acho que o tutorial Tudo Sobre Mônadas dá uma demonstração bastante impressionante de uma faceta disso, mas não esperaria que alguém não familiarizado com o material já "entendesse" na primeira, ou mesmo na terceira, leitura cuidadosa.

De qualquer forma, também há muitas outras coisas boas em Haskell, mas esta é uma das principais que não vejo mencionado com tanta frequência, provavelmente porque é bastante complexo.


2
Muito interessante! Quantas linhas de código Haskell foram para o seu sistema de negociação automatizado no total? Como você lidou com a tolerância a falhas e quais tipos de resultados de desempenho você obteve? Tenho pensado recentemente que Haskell tem potencial para ser bom para programação de baixa latência ...
JD

12

Para obter um exemplo interessante, você pode observar: http://en.literateprograms.org/Quicksort_(Haskell)

O que é interessante é observar a implementação em várias linguagens.

O que torna o Haskell tão interessante, junto com outras linguagens funcionais, é o fato de que você tem que pensar diferente sobre como programar. Por exemplo, você geralmente não usará loops for ou while, mas usará recursão.

Como mencionado acima, Haskell e outras linguagens funcionais se destacam com processamento paralelo e aplicativos de escrita para trabalhar em vários núcleos.


2
a recursão é a bomba. isso e correspondência de padrões.
Ellery Newcomer,

1
Livrar-se de loops for e while é a parte mais difícil para mim ao escrever em uma linguagem funcional. :)
James Black,

4
Aprender a pensar em recursão em vez de loops foi a parte mais difícil para mim também. Quando finalmente entendi, foi uma das maiores epifanias de programação que já tive.
Chris Connett,

8
Exceto que o programador Haskell ativo raramente usa recursão primitiva; principalmente você usa funções de biblioteca como map e foldr.
Paul Johnson

18
Acho mais interessante que o algoritmo quicksort original de Hoare foi transformado em bastardizado nesta forma baseada em lista fora do lugar, aparentemente para que implementações ineficientes inutilmente pudessem ser escritas "elegantemente" em Haskell. Se você tentar escrever um quicksort real (no local) em Haskell, você descobrirá que é feio como o inferno. Se você tentar escrever um quicksort genérico de desempenho competitivo em Haskell, você descobrirá que é realmente impossível devido a bugs de longa data no coletor de lixo do GHC. Saudando o quicksort como um bom exemplo para a crença dos mendigos de Haskell, IMHO.
JD

8

Eu não poderia te dar um exemplo, sou um cara do OCaml, mas quando estou em uma situação como você, a curiosidade simplesmente toma conta e eu tenho que baixar um compilador / interpretador e tentar. Você provavelmente aprenderá muito mais dessa forma sobre os pontos fortes e fracos de uma determinada linguagem funcional.


1
Não se esqueça de ler o código-fonte do compilador. Isso também lhe dará muitas informações valiosas.
JD

7

Uma coisa que acho muito legal ao lidar com algoritmos ou problemas matemáticos é a avaliação preguiçosa inerente de Haskell de cálculos, o que só é possível devido à sua natureza estritamente funcional.

Por exemplo, se você deseja calcular todos os primos, pode usar

primes = sieve [2..]
    where sieve (p:xs) = p : sieve [x | x<-xs, x `mod` p /= 0]

e o resultado é na verdade uma lista infinita. Mas Haskell irá avaliá-lo da esquerda para a direita, então contanto que você não tente fazer algo que exija a lista inteira, você ainda pode usá-la sem que o programa fique preso no infinito, como:

foo = sum $ takeWhile (<100) primes

que soma todos os números primos menores que 100. Isso é bom por vários motivos. Em primeiro lugar, só preciso escrever uma função primo que gere todos os primos e então estou praticamente pronto para trabalhar com primos. Em uma linguagem de programação orientada a objetos, eu precisaria de alguma maneira de dizer à função quantos primos ela deveria calcular antes de retornar ou emular o comportamento de lista infinita com um objeto. Outra coisa é que, em geral, você acaba escrevendo um código que expressa o que deseja calcular e não em que ordem avaliar as coisas - em vez disso, o compilador faz isso por você.

Isso não é útil apenas para listas infinitas, na verdade, ele é usado sem que você saiba o tempo todo, quando não há necessidade de avaliar mais do que o necessário.


2
Isso não é totalmente verdade; com o comportamento de retorno de rendimento do C # (uma linguagem orientada a objetos), você também pode declarar listas infinitas que são avaliadas sob demanda.
Jeff Yates,

2
Bom ponto. Você está correto e devo evitar afirmar o que pode e não pode ser feito em outras línguas de forma tão categórica. Acho que meu exemplo falhou, mas ainda acho que você ganhou algo com a forma de avaliação preguiçosa de Haskell: está realmente lá por padrão e sem nenhum esforço do programador. E isso, acredito, se deve à sua natureza funcional e à ausência de efeitos colaterais.
Waxwing de

8
Você pode estar interessado em ler por que "peneira" não é a peneira de Eratóstenes: lambda-the-ultimate.org/node/3127
Chris Conway

@Chris: Obrigado, esse foi realmente um artigo bastante interessante! A função primos acima não é a que venho usando para meus próprios cálculos, uma vez que é dolorosamente lenta. No entanto, o artigo traz um bom ponto de que verificar todos os números para o mod é realmente um algoritmo diferente.
waxwing

6

Concordo com outros que ver alguns pequenos exemplos não é a melhor maneira de exibir Haskell. Mas vou dar um pouco de qualquer maneira. Aqui está uma solução extremamente rápida para os problemas 18 e 67 do Projeto Euler , que pedem que você encontre o caminho de soma máxima da base ao ápice de um triângulo:

bottomUp :: (Ord a, Num a) => [[a]] -> a
bottomUp = head . bu
  where bu [bottom]     = bottom
        bu (row : base) = merge row $ bu base
        merge [] [_] = []
        merge (x:xs) (y1:y2:ys) = x + max y1 y2 : merge xs (y2:ys)

Aqui está uma implementação completa e reutilizável do algoritmo BubbleSearch de Lesh e Mitzenmacher. Usei-o para empacotar grandes arquivos de mídia para armazenamento de arquivo em DVD sem desperdício:

data BubbleResult i o = BubbleResult { bestResult :: o
                                     , result :: o
                                     , leftoverRandoms :: [Double]
                                     }
bubbleSearch :: (Ord result) =>
                ([a] -> result) ->       -- greedy search algorithm
                Double ->                -- probability
                [a] ->                   -- list of items to be searched
                [Double] ->              -- list of random numbers
                [BubbleResult a result]  -- monotone list of results
bubbleSearch search p startOrder rs = bubble startOrder rs
    where bubble order rs = BubbleResult answer answer rs : walk tries
            where answer = search order
                  tries  = perturbations p order rs
                  walk ((order, rs) : rest) =
                      if result > answer then bubble order rs
                      else BubbleResult answer result rs : walk rest
                    where result = search order

perturbations :: Double -> [a] -> [Double] -> [([a], [Double])]
perturbations p xs rs = xr' : perturbations p xs (snd xr')
    where xr' = perturb xs rs
          perturb :: [a] -> [Double] -> ([a], [Double])
          perturb xs rs = shift_all p [] xs rs

shift_all p new' [] rs = (reverse new', rs)
shift_all p new' old rs = shift_one new' old rs (shift_all p)
  where shift_one :: [a] -> [a] -> [Double] -> ([a]->[a]->[Double]->b) -> b
        shift_one new' xs rs k = shift new' [] xs rs
          where shift new' prev' [x] rs = k (x:new') (reverse prev') rs
                shift new' prev' (x:xs) (r:rs) 
                    | r <= p    = k (x:new') (prev' `revApp` xs) rs
                    | otherwise = shift new' (x:prev') xs rs
                revApp xs ys = foldl (flip (:)) ys xs

Tenho certeza de que este código parece um jargão aleatório. Mas se você ler a entrada do blog de Mitzenmacher e entender o algoritmo, ficará surpreso ao saber que é possível empacotar o algoritmo em código sem dizer nada sobre o que está procurando.

Tendo dado alguns exemplos conforme você pediu, direi que a melhor maneira de começar a apreciar Haskell é ler o artigo que me deu as idéias de que eu precisava para escrever o empacotador de DVD: Why Functional Programming Matters, de John Hughes. O artigo na verdade é anterior a Haskell, mas explica de maneira brilhante algumas das ideias que fazem as pessoas gostarem de Haskell.


5

Para mim, a atração de Haskell é a promessa de correção garantida do compilador . Mesmo que seja para partes puras do código.

Eu escrevi muitos códigos de simulação científica e me perguntei por isso muitas vezes se havia um erro em meus códigos anteriores, o que poderia invalidar um monte de trabalho atual.


6
Como isso garante a correção?
Jonathan Fischoff

As partes puras do código são muito mais seguras do que as impuras. O nível de confiança / esforço investido é muito mais alto.
rpg

1
O que te deu essa impressão?
JD

5

Acho que para certas tarefas sou incrivelmente produtivo com Haskell.

A razão é por causa da sintaxe sucinta e da facilidade de teste.

Esta é a sintaxe da declaração da função:

foo a = a + 5

Essa é a maneira mais simples que consigo pensar em definir uma função.

Se eu escrever o inverso

inverseFoo a = a - 5

Posso verificar se é o inverso de qualquer entrada aleatória escrevendo

prop_IsInverse :: Double -> Bool
prop_IsInverse a = a == (inverseFoo $ foo a)

E chamando da linha de comando

jonny @ ubuntu: runhaskell quickCheck + nomes fooFileName.hs

O que irá verificar se todas as propriedades em meu arquivo são mantidas, testando as entradas aleatoriamente centenas de vezes.

Não acho que Haskell seja a linguagem perfeita para tudo, mas quando se trata de escrever pequenas funções e testar, não vi nada melhor. Se sua programação possui um componente matemático, isso é muito importante.


Que problemas você está resolvendo e quais outras linguagens já tentou?
JD

1
Gráficos 3D em tempo real para o celular e o iPad.
Jonathan Fischoff,

3

Se você conseguir entender o sistema de tipos em Haskell, acho que em si já é uma grande realização.


1
O que há para conseguir? Se for necessário, pense em "dados" == "classe" e "typeclass" = "interface" / "papel" / "traço". Não poderia ser mais simples. (Não há nem mesmo "nulo" para bagunçar você. Nulo é um conceito que você pode incorporar ao seu tipo.)
jrockway

8
Há muito o que fazer, Jrockway. Embora você e eu achemos isso relativamente simples, muitas pessoas - até mesmo muitos desenvolvedores - acham certos tipos de abstrações muito difíceis de entender. Conheço muitos desenvolvedores que ainda não entendem bem a ideia de ponteiros e referências em linguagens mais convencionais, embora os usem todos os dias.
Gregory Higley

2

ele não tem construções de loop. poucas línguas têm esse traço.


17
ghci>: m + Control.Monad ghci> forM_ [1..3] print 1 2 3
sastanin

1

Eu concordo com aqueles que disseram que a programação funcional torce seu cérebro para ver a programação de um ângulo diferente. Usei-o apenas como um hobby, mas acho que mudou fundamentalmente a maneira como abordo um problema. Não acho que teria sido tão eficaz com LINQ sem ter sido exposto a Haskell (e usando geradores e compreensões de lista em Python).


-1

Para apresentar uma visão contrária: Steve Yegge escreve que as linguagens Hindely-Milner carecem da flexibilidade necessária para escrever bons sistemas :

HM é muito bonito, em um sentido matemático formal totalmente inútil. Ele lida muito bem com algumas construções de computação; o despacho de correspondência de padrões encontrado em Haskell, SML e OCaml é particularmente útil. Sem surpresa, ele lida com algumas outras construções comuns e altamente desejáveis ​​de forma estranha, na melhor das hipóteses, mas eles explicam esses cenários dizendo que você está enganado, você realmente não os quer. Você sabe, coisas como, oh, definir variáveis.

Vale a pena aprender Haskell, mas tem suas próprias fraquezas.


5
Embora seja certamente verdade que os sistemas de tipo forte geralmente exigem que você os siga (isso é o que torna sua força útil), também é o caso que muitos (a maioria?) Sistemas de tipo baseados em HM existentes, de fato, têm algum tipo de ' escape hatch 'conforme descrito no link (tome Obj.magic em O'Caml como um exemplo, embora eu nunca tenha usado, exceto como um hack); na prática, entretanto, para muitos tipos de programas, nunca se precisa de tal dispositivo.
Zach Snow,

3
A questão de saber se definir variáveis ​​é "desejável" depende de quanta dor é usar os construtos alternativos versus quanta dor é causada pelo uso de variáveis. Isso não é para descartar todo o argumento, mas sim para apontar que tomar a afirmação "variáveis ​​são uma construção altamente desejável" como um axioma não é a base de um argumento convincente. Acontece que é a maneira como a maioria das pessoas aprende a programar.
gtd

5
-1: As declarações de Steve estão parcialmente desatualizadas, mas na maioria das vezes completamente erradas. A restrição de valor relaxada do OCaml e o sistema de tipos do .NET são alguns exemplos contrários óbvios para suas declarações.
JD de

4
Steve Yegge tem uma abelha irracional em seu chapéu sobre digitação estática, e não apenas a maior parte do que ele diz sobre isso está errado, ele também continua trazendo isso à tona em todas as oportunidades disponíveis (e até mesmo em algumas indisponíveis). Você faria bem em confiar apenas em sua própria experiência a esse respeito.
ShreevatsaR

3
Embora eu discorde de Yegge quanto à tipagem estática vs. dinâmica, Haskell tem o tipo Data.Dynamic. Se quiser digitação dinâmica, você pode ter!
jrockway
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.