
Olá Habr!
Já faz algum tempo que estou escrevendo minha estrutura de jogo - um projeto tão bom para a alma. E já que para a alma você precisa escolher algo que você gosta (e, neste caso, o que você gosta de escrever), minha escolha caiu nim. Neste artigo, quero falar especificamente sobre nim, sobre seus recursos, prós e contras, e o tema gamedev apenas define o contexto da minha experiência - quais tarefas eu resolvi, que dificuldades surgiram.
Era uma vez, quando a grama era mais verde e o céu mais limpo, eu o encontrei. Não, não é assim. Era uma vez, eu queria desenvolver jogos para escrever o meu jogo mais legal - acho que muitos passaram por isso. Naqueles dias, Unity e Unreal Engine estavam apenas começando a aparecer na audiência e, como, ainda não estavam livres. Eu não os usei, não tanto por causa da ganância, mas por causa do desejo de escrever tudo sozinho, de criar um mundo de jogo completamente do zero, desde o início. o primeiro zero byte. Sim, por um longo tempo, sim, é difícil, mas o próprio processo traz prazer - mas o que mais é necessário para a felicidade?
Armado com Straustrup e Qt, eu bebi merda para o bem, porque, em primeiro lugar, eu não era uma das 10 pessoas no mundo que conhecia bem o C ++ e , em segundo lugar, as vantagens colocavam paus nas minhas rodas. Não vejo razão para repetir o que o platoff já escreveu extraordinariamente para mim:
Como eu encontrei a melhor linguagem de programação do mundo. Parte 1
Como eu encontrei a melhor linguagem de programação do mundo. Parte 2
Como eu encontrei a melhor linguagem de programação do mundo. Yo Part (2,72)
É um zumbido louco quando você escreve código livremente, quase sem pensar, sem esperar pelo core despejado antes de cada lançamento, quando os recursos são adicionados bem diante de nossos olhos, agora podemos fazê-lo, e agora é assim, diga-me, por favor, que diferença isso faz para mim Eu não tenho modelos se eu nem senti falta deles? A produtividade é o principal objetivo do programador que faz as coisas e a única tarefa da ferramenta que ele usa.
Ao trabalhar com C ++, eu estava constantemente pensando em como escrever o que quero, e não o que escrever para mim. Então eu mudei para nim. A história acabou, deixe-me compartilhar minha experiência com você depois de vários anos no nim.
Informações gerais para quem não conhece
- Open Source Compiler (MIT), desenvolvido por entusiastas. O criador do idioma é Andreas Rumpf (Araq). O segundo desenvolvedor é Dominik Picheta (dom96), que escreveu o livro Nim em ação . Além disso, há algum tempo, a Status começou a patrocinar o desenvolvimento da linguagem, então nim conseguiu mais 2 desenvolvedores em período integral. Além deles, é claro, outras pessoas contribuirão.
- A versão 1.0 foi lançada recentemente, o que significa que o idioma é estável e não são mais esperadas "alterações recentes". Se antes você não queria usar a versão instável, porque as atualizações poderiam interromper o aplicativo, agora é a hora de tentar nim em seus projetos.
- Nim compila (ou transpõe) em C, C ++ (que são compilados posteriormente em código nativo) ou JS (com algumas limitações). Assim, com a ajuda da FFI, todas as bibliotecas existentes para C e C ++ estão disponíveis para você. Se não houver um pacote necessário no nim - procure por s ou vantagens.
- Os idiomas mais próximos são python (por sintaxe, à primeira vista) e D (por funcionalidade) - IMHO
A documentação
Isso é realmente ruim. Os problemas:
- A documentação está espalhada em diferentes fontes
- A documentação
merda não descreve totalmente todos os recursos do idioma - A documentação às vezes é muito concisa.
Exemplo: se você deseja escrever aplicativos multithread, há muitos núcleos, mas não há para onde ir.
Aqui está a seção de documentação oficial sobre fluxos . Não, veja você, os threads são uma parte importante separada do idioma, seu recurso que você precisa incluir com o sinalizador --threads:on
ao compilar. Lá, heap compartilhado ou local de encadeamento, dependendo do coletor de lixo, todos os tipos de memória e bloqueios compartilhados, segurança de encadeamento, módulos compartilhados especiais e muito mais. Como eu sabia tudo sobre isso? É isso mesmo, desde o livro em ação, o fórum, o excesso de pilha, a TV e o vizinho, em geral, de qualquer lugar, mas não da documentação oficial.
Ou há um chamado. "notação" - funciona muito bem ao usar modelos etc., geralmente sempre que você precisar passar um retorno de chamada ou apenas um bloco de código. Onde posso ler sobre isso? Sim, no manual de recursos experimentais .
Concordo, coletar informações sobre várias fontes não informativas ainda é um prazer. Se você escreve em nim, precisa fazê-lo.
No fórum e nas questões do github, havia sugestões para melhorar a documentação, mas as coisas não avançaram. Parece-me que está faltando algum tipo de mão dura, que dirá "tudo, a comunidade, pegue as pás e vá vasculhar esse monte de g ... engenhosos pedaços de texto espalhados".
Felizmente, eu bati no meu próprio, então eu apresento a você a lista de campeões
A documentação
- Tutorial 1 , Tutorial 2 - comece com eles
- Nim em ação é um livro explicativo que realmente explica bem muitos aspectos da linguagem, às vezes muito melhores do que. documentação
- Nim manual - na verdade, um manual - quase tudo é descrito, mas não
- Nim manual experimental - por que não continuar a documentação em uma página separada?
- O Índice - links para tudo são coletados aqui, ou seja, geralmente tudo o que pode ser encontrado em nim. Se você não encontrou o que precisa nos tutoriais e no manual, definitivamente o encontrará no índice.
Lições e Tutoriais
- Nim basics - o básico para iniciantes, tópicos complexos não abordados
- Nim Days - pequenos projetos (exemplos ao vivo)
- Código Rosetta - é muito legal comparar a solução das mesmas tarefas em diferentes PL, incluindo nim
- Exercism.io - aqui você pode ir "nim path", completando tarefas
- Nim por exemplo
Ajuda
- O IRC é o habitat principal ... dos nimmers?, Que é transmitido em Discord e Gitter . Eu nunca usei o IRC (e ainda não o uso). Em geral, essa é uma escolha muito estranha. Ainda há correio de pombo para ele ... tudo bem, brincando.
- Fórum Nim Os recursos do fórum são mínimos, mas 1) aqui você pode encontrar a resposta 2) aqui você pode fazer uma pergunta se o item 1 não funcionou 3) a probabilidade de uma resposta é superior a 50% 4) os desenvolvedores de idiomas estão sentados no fórum e estão respondendo ativamente. A propósito, o fórum está escrito em nim e, portanto, não há funcionalidade
- Nim grupo de telegrama - é possível fazer uma pergunta e [não] obter uma resposta.
- Há também um grupo de telegrama russo, se você está cansado de nim e não quer ouvir nada sobre isso, você deve ir lá :) (em parte uma piada)
Parque infantil
- Nim playground - aqui você pode executar o programa no nim diretamente no navegador
- Nim docker cross-compiling - aqui você pode ler como iniciar uma imagem do docker e compilar o programa para diferentes plataformas.
Pacotes
Mudar para nim de outros idiomas
Do que você gosta
Não faz sentido listar todos os recursos do idioma, mas aqui estão alguns recursos:
Complexidade fractal
Nim fornece um "fractal de complexidade". Você pode escrever código de alto nível. Você pode usar ponteiros brutos e aproveitar todas as attempt to read from nil
. Você pode incorporar o código C. Você pode escrever inserções no assembler . Você pode escrever procedimentos (expedição estática). Não é suficiente - existem "métodos" (envio dinâmico). Mais? Existem genéricos e existem genéricos que imitam funções. Existem modelos - um mecanismo de substituição, mas não tão vômito quanto em C ++ (existem macros aqui - é apenas uma substituição de texto ou é algo mais inteligente?). No final, existem macros - é como o IDDQD, elas ativam o modo god e permitem que você trabalhe diretamente com o AST e literalmente substitua partes da árvore de sintaxe, ou expanda o idioma você mesmo.
Ou seja, em um nível "alto", você pode escrever palavras do inferno e pesar para não saber, mas ninguém o proíbe de realizar fraudes de qualquer complexidade.
Velocidade de desenvolvimento
A curva de aprendizado não é uma curva. Isso é direto. Ao instalar o nim, você iniciará seu primeiro olá mundo no primeiro minuto e, no primeiro dia, escreverá um utilitário simples. Mas em alguns meses você terá algo a aprender. Por exemplo, comecei com procedimentos, depois precisei de métodos, depois de um tempo os genéricos foram muito úteis para mim, recentemente descobri modelos em toda a sua glória e, ao mesmo tempo, não toquei em macros. Comparando com a mesma ferrugem ou c ++, a fusão com o nim é muito mais fácil.
Gerenciamento de pacotes
Existe um gerenciador de pacotes chamado nimble que pode instalar, desinstalar, criar pacotes e carregar dependências. Ao criar seu pacote (= projeto), é possível gravar tarefas diferentes no nimble (usando nimscript, que é um subconjunto de nim executável na VM), por exemplo, gerando documentação, executando testes, copiando ativos etc. Nimble não apenas coloca as dependências necessárias, mas também permite que você configure o ambiente de trabalho para o seu projeto. Ou seja, ágil é, grosso modo, o CMake, que foi escrito não por pervertidos, mas por pessoas normais.
Legibilidade e expressividade
Externamente, nim é muito semelhante ao python com anotações de tipo, embora nim não seja python. Os pitonistas terão que esquecer a digitação dinâmica, a herança, os decoradores e outras alegrias, e geralmente reestruturar seu pensamento. Não tente transferir sua experiência python para o nim, porque a diferença é muito grande. No começo, eu realmente quero coleções heterogêneas e mixins com decoradores. mas então de alguma forma você se acostuma a viver dificuldades :)
Aqui está um exemplo de programa nim:
type NumberGenerator = object of Service # this service just generates some numbers NumberMessage = object of Message number: int proc run(self: NumberGenerator) = if not waitAvailable("calculator"): echo "Calculator is unavailable, shutting down" return for number in 0..<10: echo &"Sending number {number}" (ref NumberMessage)(number: number).send("calculator")
Modularidade
Tudo é dividido em módulos que você pode importar como quiser - para importar apenas determinados caracteres, ou todos, exceto certos, ou todos, ou nenhum, e forçar o usuário a especificar o caminho completo para la module.function()
e também importar com um nome diferente. É claro que toda essa variedade é muito útil como outro argumento no debate "qual linguagem de programação é melhor"; bem, em seu projeto, você escreverá discretamente o import mymodule
e não lembrará outras opções.
Sintaxe de Chamada de Método
Uma chamada de função pode ser gravada de diferentes maneiras:
double(2) double 2 2.double() 2.double
Por um lado, agora todo mundo ... escreve como ele gosta (e todo mundo gosta de maneiras diferentes, é claro, e de maneiras diferentes, mesmo dentro da estrutura de um projeto). Mas todas as funções podem ser escritas como uma chamada de método, o que melhora muito a legibilidade. Em python, pode ser:
list(set(some_list))
O mesmo código no nim pode ser reescrito mais logicamente:
some_list.set.list #
OOP
OOP, embora esteja presente, difere dele em vantagens e python: objetos e métodos são entidades diferentes e podem muito bem existir em módulos diferentes. Além disso, você pode escrever seus métodos para tipos básicos como int
proc double(number: int): int = number * 2 echo $2.double() # prints "4"
Por outro lado, há encapsulamento no nim (a primeira regra do módulo no nim é não contar a ninguém sobre identificadores sem um asterisco). Aqui está um exemplo de um módulo padrão:
# sharedtables.nim type SharedTable*[A, B] = object ## generic hash SharedTable data: KeyValuePairSeq[A, B] counter, dataLen: int lock: Lock
O tipo SharedTable*
está marcado com um asterisco, o que significa que é "visível" em outros módulos e pode ser importado. Mas aqui data
, counter
e lock
são membros privados e sharedtables.nim
não são acessíveis de fora. Isso me deixou muito feliz quando decidi escrever algumas funções adicionais para o tipo SharedTable
, como len
ou hasKey
, e hasKey
que não tinha acesso ao counter
ou aos data
, e a única maneira de "expandir" o SharedTable
era escrever o seu próprio com bl
Em geral, a herança é usada com muito menos frequência do que no mesmo python (por experiência pessoal), porque há sintaxe de chamada de método (veja acima) e variantes de objeto (veja abaixo). O caminho nim é composição, e não herança. O mesmo ocorre com o polimorfismo: em nim existem métodos que podem ser substituídos em classes sucessoras, mas isso deve ser especificado explicitamente durante a compilação usando o --multimethods:on
. Ou seja, por padrão, os métodos não funcionam, o que incentiva um pouco o trabalho sem eles.
Execução em tempo de compilação
Const - a capacidade de calcular algo no estágio de compilação e "costurá-lo" no binário resultante. É legal e confortável. Em geral, nim tem uma relação especial com "tempo de compilação", existe até uma palavra-chave when
- é como if
, mas a comparação está no estágio de compilação. Você pode escrever algo como
when defined(SDL_VIDEO_DRIVER_WINDOWS): import windows ## oldwinapi lib elif defined(SDL_VIDEO_DRIVER_X11): import x11/x, x11/xlib ## x11 lib
Isso é muito conveniente, embora haja restrições sobre o que você pode fazer no estágio de compilação (por exemplo, você não pode fazer chamadas FFI).
Tipo de referência
Tipo de referência - um análogo do shared_ptr em C ++, do qual o coletor de lixo cuidará. Mas você também pode ligar para o coletor de lixo nos momentos em que for conveniente para você. Ou você pode tentar opções diferentes para coletores de lixo. Ou você pode desativar o coletor de lixo e usar ponteiros regulares.
Idealmente, se você não usar ponteiros brutos e FFI, é improvável que obtenha erros de segmentação. Na prática, até agora sem a FFI em qualquer lugar.
Lambdas
Existem procedimentos anônimos (também conhecidos como lambdas no python), mas, diferentemente do python no procedimento anônimo, você pode usar várias instruções:
someProc(callback=proc(a: int) -> int = var b = 5*a; result = a)
Exceções
Existem exceções, elas são muito inconvenientes para lançar: python raise ValueError('bad value')
, nim raise newException(ValueError, "bad value")
. Nada mais incomum - tente, exceto, finalmente, tudo é como todo mundo. Eu, como defensor de exceções, não de códigos de erro, me alegro. A propósito, você pode indicar para funções quais exceções elas podem lançar, e o compilador verificará isso:
proc p(what: bool) {.raises: [IOError, OSError].} = if what: raise newException(IOError, "IO") else: raise newException(OSError, "OS")
Genéricos
Os genéricos são muito expressivos, por exemplo, você pode limitar os tipos possíveis
proc onlyIntOrString[T: int|string](x, y: T) = discard # int string
E você pode passar um tipo em geral como parâmetro - parece uma função comum, mas de fato um genérico:
proc p(a: typedesc; b: a) = discard # is roughly the same as: proc p[T](a: typedesc[T]; b: T) = discard # hence this is a valid call: p(int, 4) # as parameter 'a' requires a type, but 'b' requires a value.
Templates
Modelos são algo como macros em C ++, feitas corretamente corretamente :) - você pode transferir com segurança blocos inteiros de código para modelos, e não pensar que a substituição arruine algo no código externo (mas você pode novamente , para fazer bagunça, se você realmente precisar).
Aqui está um exemplo de modelo de app
, que, dependendo do valor da variável, chama um dos blocos de código:
template app*(serverCode: untyped, clientCode: untyped) = # ... case mode of client: clientCode of server: serverCode else: discard
Com do
posso passar blocos inteiros para o modelo, por exemplo:
app do: # serverCode echo "I'm server" serverProc() do: # clientCode echo "I'm client" clientProc()
Shell interativo
Se você precisar testar rapidamente algo, ou seja, a capacidade de chamar um "intérprete" ou "nim shell" (como se você executasse python
sem parâmetros). Para fazer isso, use o comando nim secret
ou faça o download do pacote inim .
Ffi
FFI - a capacidade de interagir com bibliotecas de terceiros em C / C ++. Infelizmente, para usar uma biblioteca externa, você precisa escrever um wrapper explicando de onde e de onde importar. Por exemplo:
{.link: "/usr/lib/libOgreMain.so".} type ManualObjectSection* {.importcpp: "Ogre::ManualObject::ManualObjectSection", bycopy.} = object
Existem ferramentas que tornam esse processo semi-automático:
O que não gosta
Dificuldade
Muitas coisas. A linguagem foi concebida como minimalista, mas agora está muito longe da verdade. Por exemplo, por que conseguimos reordenar o código ?!
Redundância
Muita merda: system.addInt - "Converte inteiro para sua representação de string e anexa ao resultado". Parece-me que esta é uma função muito conveniente, eu a uso em todos os projetos. Aqui está outra questão interessante: fileExists and existirFile ( https://forum.nim-lang.org/t/3636 )
Sem unificação
"Há apenas uma maneira de fazer algo" - nenhuma:
- Sintaxe de chamada de método - escreva uma chamada de função como desejar
fmt
vs &
- camelCase e underscore_notation
- isso e isso (spoiler: é a mesma coisa)
- função vs procedimento vs modelo
Erros (sem sacos!)
Existem bugs, cerca de 1400 . Ou basta ir ao fórum - eles constantemente encontram alguns erros.
Estabilidade
Além do parágrafo anterior, a v1 implica estabilidade, certo? E aqui o criador da linguagem Araq voa para o fórum e diz: "caras, eu tenho outro (sexto) coletor de lixo aqui, é mais legal, mais rápido, mais novo, dá a você uma memória compartilhada de threads (ha ha, e antes disso você sofreu e muletas usadas), baixe o ramo de desenvolvimento e tente ". E tudo isso "Uau, que legal! E o que isso significa para meros mortais? Agora precisamos mudar todo o código novamente?" Parece que não, então, atualizo o nim, corro um novo coletor de lixo --gc:arc
e meu programa trava em algum lugar no estágio de compilação do código c ++ (ou seja, não no nim, mas no gcc):
/usr/lib/nim/system.nim:274:77: error: 'union pthread_cond_t' has no member named 'abi' 274 | result = x
Ótimo! Agora, em vez de escrever um novo código, tenho que reparar o antigo. Não era disso que eu estava fugindo quando escolhi nim?
É bom saber que não estou sozinha
Métodos e multithreading
Por padrão, os sinalizadores multimétodos e threads estão desativados - você não vai 2019 2020 escreva um aplicativo multiencadeado com métodos de substituição ?! E como é ótimo se sua biblioteca foi criada sem considerar os fluxos e o usuário os ativou ... Ah, sim, existem pragmas maravilhosos {.inheritable.} E {.base.} Para herança, para que seu código não seja muito conciso.
Variantes de objeto
Você pode evitar a herança usando o chamado variantes de objeto:
type CoordinateSystem = enum csCar, # Cartesian csCyl, # Cylindrical Coordinates = object case cs: CoordinateSystem: # cs is the coordinate discriminator of csCar: x: float y: float z: float of csCyl: r: float phi: float k: float
Dependendo do valor de cs
, os campos x, y, z ou r, phi e k estarão disponíveis para você.
Quais são as desvantagens?
Em primeiro lugar, a memória é reservada para a "maior opção" - para garantir que ela caiba na memória alocada para o objeto.
Em segundo lugar, a herança ainda é mais flexível - você sempre pode criar um descendente e adicionar mais campos, e na variante de objeto todos os campos são rigidamente definidos em uma seção.
Em terceiro lugar, o que mais enfurece é que você não pode "reutilizar" campos em diferentes tipos:
type # The 3 notations refer to the same 3-D entity, and some coordinates are shared CoordinateSystem = enum csCar, # Cartesian (x,y,z) csCyl, # Cylindrical (r,φ,z) Coordinates = object case cs: CoordinateSystem: # cs is the coordinate discriminator of csCar: x: float y: float z: float # z already defined here of csCyl: r: float phi: float z: float # fails to compile due to redefinition of z
Faça notação
Apenas para citar :
- fazer com parênteses é um processo anônimo
- fazer sem parênteses é apenas um bloco de código
Uma expressão significa coisas diferentes ¯_ (ツ) _ / ¯
Quando usar
Portanto, temos funções, procedimentos, genéricos, multimétodos, modelos e macros. Quando é melhor usar um modelo e quando é um procedimento? Modelo ou genérico? Função ou procedimento? Então, e as macros? Eu acho que você entendeu.
Pragma personalizado
Existem decoradores em python que podem ser aplicados mesmo a classes, até a funções.
Existem pragmas para isso. E aqui está o que:
Nimble
O que está morto não pode morrer. No ágil, vários projetos que não são atualizados há muito tempo (e, no caso, são como a morte) - e não são removidos. Ninguém está seguindo isso. É claro, compatibilidade com versões anteriores ", você não pode simplesmente pegar e remover o pacote do nabo", mas ainda assim ... Ok, obrigado, pelo menos não como o npm.
Abstração com vazamento
Existe uma lei dessas abstrações vazias - você usa algum tipo de abstração, mas mais cedo ou mais tarde encontrará um "buraco" nela que o levará a um nível mais baixo. Nim é uma abstração de C e C ++ e, mais cedo ou mais tarde, você falhará lá. Aposto que você não gosta daqui?
Error: execution of an external compiler program 'g++ -c -w -w -fpermissive -pthread -I/usr/lib/nim -I/home/user/c4/systems/network -o /home/user/.cache/nim/enet_d/@m..@s..@s..@s..@s..@s..@s.nimble@spkgs@smsgpack4nim-0.3.0@smsgpack4nim.nim.cpp:6987:136: note: initializing argument 2 of 'void unpack_type__k2dhaoojunqoSwgmQ9bNNug(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA*, NU&)' 6987 | N_LIB_PRIVATE N_NIMCALL(void, unpack_type__k2dhaoojunqoSwgmQ9bNNug)(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA* s, NU& val) { nimfr_("unpack_type", "/home/user/.nimble/pkgs/msgpack4nim-0.3.0/msgpack4nim.nim"); |
tyObject_MsgStreamcolonObjectType ___ kto5qgghQl207nm2KQZEDA * s, NU & val) {nimfr _ ( "unpack_type", "/home/user/.nimble/pkgs/msgpack4nim-0.3.0/msgpack4nim.nim"); Error: execution of an external compiler program 'g++ -c -w -w -fpermissive -pthread -I/usr/lib/nim -I/home/user/c4/systems/network -o /home/user/.cache/nim/enet_d/@m..@s..@s..@s..@s..@s..@s.nimble@spkgs@smsgpack4nim-0.3.0@smsgpack4nim.nim.cpp:6987:136: note: initializing argument 2 of 'void unpack_type__k2dhaoojunqoSwgmQ9bNNug(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA*, NU&)' 6987 | N_LIB_PRIVATE N_NIMCALL(void, unpack_type__k2dhaoojunqoSwgmQ9bNNug)(tyObject_MsgStreamcolonObjectType___kto5qgghQl207nm2KQZEDA* s, NU& val) { nimfr_("unpack_type", "/home/user/.nimble/pkgs/msgpack4nim-0.3.0/msgpack4nim.nim"); |
/usr/bin/ld: /home/user/.cache/nim/enet_d/stdlib_dollars.nim.cpp.o: in function `dollar___uR9bMx2FZlD8AoPom9cVY9ctA(tyObject_ConnectMessage__e5GUVMJGtJeVjEZUTYbwnA*)': stdlib_dollars.nim.cpp:(.text+0x229): undefined reference to `resizeString(NimStringDesc*, long)' /usr/bin/ld: stdlib_dollars.nim.cpp:(.text+0x267): undefined reference to `resizeString(NimStringDesc*, long)' /usr/bin/ld: stdlib_dollars.nim.cpp:(.text+0x2a2): undefined reference to `resizeString(NimStringDesc*, long)'
Então
Eu sou um programador idiota. Não quero saber como o GC funciona, o que existe e como está vinculado, onde está armazenado em cache e como o lixo é removido. É como um carro - em princípio, eu sei como funciona, um pouco sobre o alinhamento das rodas, um pouco sobre a caixa de velocidades, preciso preencher o óleo e outras coisas, mas, em geral, só quero me sentar e ir (e rápido) para a festa. Uma máquina não é uma meta, mas um meio para atingir um fim. Se quebrar, não quero entrar no assunto, mas leve-o ao serviço (no sentido, vou abrir o problema no github), e seria ótimo se eles o corrigissem rapidamente.
Nim deveria ser uma máquina dessas. Em parte, ele se tornou, mas ao mesmo tempo, quando corro pela estrada neste carro, meu volante cai e o espelho traseiro aponta para a frente. Os engenheiros correm atrás de mim e prendem algo em tempo real ("agora seu carro é ainda mais rápido com esse novo spoiler"), mas a partir disso o porta-malas cai. E você sabe o que? Ainda gosto muito desse carro, porque este é o melhor de todos os carros que vi.