NĂŁo sei vocĂȘ, mas para mim nĂŁo hĂĄ melhor começo do dia do que me preocupar com programação. O sangue ferve ao ver uma crĂtica bem-sucedida de uma das lĂnguas "ousadas" usadas pelos plebeus, atormentada durante todo o dia de trabalho entre visitas tĂmidas ao StackOverflow.
(Enquanto isso, vocĂȘ e eu usamos apenas a linguagem mais esclarecida e as ferramentas sofisticadas projetadas para as mĂŁos hĂĄbeis de mestres como nĂłs).
Obviamente, como autor do sermĂŁo, corro riscos. VocĂȘ pode gostar do idioma que eu zoo! Um panfleto imprudente poderia ter inadvertidamente levado ao meu blog uma multidĂŁo furiosa de celulares com forquilhas e tochas prontas.
Para me proteger do fogo justo e nĂŁo ofender seus sentimentos (provavelmente delicados), falarei sobre a linguagem ...
... quem acabou de aparecer. Sobre uma efĂgie de palha, cujo Ășnico papel Ă© queimar crĂticos em jogo.
Sei que isso parece bobagem, mas acredite, no final, veremos cujo rosto (ou rostos) foi pintado em uma cabeça de palha.
Novo idioma
SerĂĄ um exagero aprender um idioma completamente novo (e chato) apenas para um artigo de blog, entĂŁo digamos que seja muito semelhante ao idioma que jĂĄ conhecemos. Por exemplo Javascript. Chaves e ponto e vĂrgula. if
, while
, etc. - Lingua franca da nossa multidĂŁo.
Eu escolhi JS nĂŁo porque este artigo Ă© sobre ele. Ă apenas uma linguagem na qual o leitor comum provavelmente entrarĂĄ. Voila:
function thisIsAFunction(){ return "!"; }
Como o bicho de pelĂșcia Ă© uma linguagem legal (de leitura ruim), possui funçÔes de primeira classe . EntĂŁo vocĂȘ pode escrever algo como isto:
Este Ă© um dos recursos de primeira classe e, como o nome sugere, eles sĂŁo legais e super Ășteis. VocĂȘ provavelmente estĂĄ acostumado a transformar coleçÔes de dados com a ajuda deles, mas assim que compreende o conceito, começa a usĂĄ-los em qualquer lugar, caramba.
Talvez nos testes:
describe("", function(){ it(" ", function(){ expect("").not.toBe(""); }); };
Ou quando vocĂȘ precisar analisar (analisar) os dados:
tokens.match(Token.LEFT_BRACKET, function(token){
Depois de acelerar, vocĂȘ escreve todos os tipos de bibliotecas e aplicativos reutilizĂĄveis ââque giram em torno de funçÔes, chamadas de funçÔes, retornos de funçÔes - um estande funcional.
tradutor: no original "Functapalooza". O prefixo -a-palooza Ă© tĂŁo legal que vocĂȘ deseja compartilhĂĄ-lo com todos.
Qual a cor da sua função?
E aqui começam as esquisitices. Nossa linguagem tem uma caracterĂstica peculiar:
1. Cada função tem uma cor.
Cada função - um retorno de chamada anÎnimo ou uma função regular com um nome - é vermelha ou azul. Como o destaque do código em nosso blog não destaca as diferentes cores das funçÔes, vamos concordar que a sintaxe é:
blue*function doSomethingAzure(){
Nossa linguagem nĂŁo possui funçÔes incolores. Deseja criar um recurso? - deve escolher uma cor. Estas sĂŁo as regras. E hĂĄ mais algumas regras que vocĂȘ deve seguir:
2. A cor afeta a maneira como a função é chamada
Imagine que existem duas sintaxes para chamar funçÔes - "azul" e "vermelho". Algo como:
doSomethingAzure(...)*blue; doSomethingCarnelian()*red;
Ao chamar uma função, vocĂȘ deve usar uma chamada que corresponda Ă sua cor. Se vocĂȘ nĂŁo adivinhou - eles chamaram a função vermelha com *blue
após os colchetes (ou vice-versa) - algo muito ruim acontecerå. Um pesadelo esquecido na infùncia, como um palhaço com cobras em vez de mãos que se escondia embaixo da sua cama. Ele pularå do monitor e chuparå seus olhos.
Regra estĂșpida, certo? Ah, mas mais uma coisa:
3. Somente a função vermelha pode causar a função vermelha.
VocĂȘ pode chamar a função azul do vermelho. Isto Ă© kosher:
red*function doSomethingCarnelian(){ doSomethingAzure()*blue; }
Mas nĂŁo o contrĂĄrio. Se vocĂȘ tentar:
blue*function doSomethingAzure(){ doSomethingCarnelian()*red; }
- vocĂȘ serĂĄ visitado pelo velho Clown Spider Maw.
Isso torna mais difĂcil escrever funçÔes mais altas, como filter()
do exemplo. Devemos escolher uma cor para cada nova função e isso afeta a cor das funçÔes que podemos passar para ela. A solução óbvia é tornar o filter()
vermelho. Então podemos chamar pelo menos funçÔes vermelhas, pelo menos azuis. Mas então nos machucamos com o próximo espinho na coroa de espinhos, que é o idioma indicado:
4. FunçÔes vermelhas causam dor
NĂŁo identificaremos essa "dor", apenas imagine que o programador deve pular o arco toda vez que ele chamar a função vermelha. A chamada pode ser muito polissilĂĄbica ou vocĂȘ nĂŁo pode executar a função em algumas expressĂ”es. Ou vocĂȘ sĂł pode acessar a função vermelha a partir de linhas Ămpares.
NĂŁo importa o que seja, mas se vocĂȘ decidir tornar a função vermelha, todos que usarem sua API quererĂŁo cuspir no cafĂ© ou fazer algo pior.
A solução Ăłbvia nesse caso Ă© nunca usar funçÔes vermelhas. Apenas deixe tudo azul e vocĂȘ estarĂĄ de volta ao mundo normal, onde todas as funçÔes sĂŁo da mesma cor, o que Ă© igual ao fato de que elas nĂŁo tĂȘm cor e que nossa linguagem nĂŁo Ă© completamente idiota.
Infelizmente, os sĂĄdicos que desenvolveram essa linguagem (todos sabem que os autores de linguagens de programação sĂŁo sĂĄdicos, certo?) Prendem o Ășltimo espinho em nĂłs:
5. Algumas das principais funçÔes do idioma são vermelhas.
Algumas funçÔes integradas Ă plataforma, funçÔes que precisamos usar e que nĂŁo podem ser escritas por nĂłs mesmos, estĂŁo disponĂveis apenas em vermelho. Nesse ponto, uma pessoa inteligente pode começar a suspeitar que esse idioma nos odeia.
Isso Ă© tudo culpa das linguagens funcionais!
VocĂȘ pode pensar que o problema Ă© que estamos tentando usar funçÔes de ordem superior. Se pararmos de brincar com toda essa bobagem funcional e começarmos a escrever funçÔes azuis de primeira ordem normais (funçÔes que nĂŁo operam com outras funçÔes - aprox. Translator), conforme planejado por Deus -, nos livraremos de toda essa dor.
Se chamarmos apenas funçÔes azuis, tornaremos todas as nossas funçÔes azuis. Caso contrårio, tornamos vermelho. Até criarmos funçÔes que aceitem funçÔes, não precisamos nos preocupar com "polimorfismo com a cor da função" (policromåtico?) Ou com outras bobagens.
Mas, infelizmente, funçÔes de ordem superior são apenas um exemplo. O problema surge toda vez que queremos dividir nosso programa em funçÔes para reutilização.
Por exemplo, temos um pequeno pedaço de código que, bem, não sei, implementa o algoritmo de Dijkstra sobre um gråfico que representa o quanto suas conexÔes sociais pressionam umas às outras. (Passei muito tempo tentando decidir o que o resultado significaria. Indesejabilidade transitiva?)
Mais tarde, vocĂȘ precisou usar esse algoritmo em outro lugar. Naturalmente, vocĂȘ quebra o cĂłdigo em uma função separada. Ligue para ela do lugar antigo e do novo. Mas que cor deve ser a função? VocĂȘ provavelmente tentarĂĄ tornĂĄ-lo azul, mas e se ele usar uma dessas funçÔes "apenas vermelhas" desagradĂĄveis ââda biblioteca do kernel?
Digamos que o novo local do qual vocĂȘ deseja chamar a função seja azul? Mas agora vocĂȘ precisa reescrever o cĂłdigo de chamada em vermelho. E, em seguida, refaça a função que chama esse cĂłdigo. Ufa VocĂȘ terĂĄ que se lembrar constantemente da cor de qualquer maneira. Esta serĂĄ a areia do seu calção de banho em uma programação de fĂ©rias na praia.
Alegoria da cor
Na verdade, nĂŁo estou falando de cores. Isso Ă© uma alegoria, um artifĂcio literĂĄrio. Foda-se - isso nĂŁo Ă© sobre as estrelas nas barrigas , Ă© sobre a corrida. VocĂȘ provavelmente jĂĄ suspeita ...
FunçÔes vermelhas - assĂncronas
Se vocĂȘ programar em JavaScript ou Node.js, sempre que definir uma função que chama uma função de retorno de chamada (retorno de chamada) para "retornar" o resultado, vocĂȘ escreverĂĄ uma função vermelha. Veja esta lista de regras e observe como elas se encaixam na minha metĂĄfora:
- FunçÔes sĂncronas retornam um resultado, funçÔes assĂncronas nĂŁo; em troca, chamam um retorno de chamada.
- As funçÔes sĂncronas retornam o resultado como um valor de retorno, as funçÔes assĂncronas o retornam, causando o retorno de chamada que vocĂȘ passou para eles.
- VocĂȘ nĂŁo pode chamar uma função assĂncrona de uma sĂncrona, porque nĂŁo pode saber o resultado atĂ© que a função assĂncrona seja executada posteriormente.
- FunçÔes assĂncronas nĂŁo sĂŁo compiladas em expressĂ”es devido a retornos de chamada, exigem que seus erros sejam tratados de maneira diferente e nĂŁo podem ser usados ââem um bloco de
try/catch
ou em vĂĄrias outras expressĂ”es que controlam o programa. - A coisa toda sobre o Node.js Ă© que a biblioteca do kernel Ă© toda assĂncrona. (Embora eles doem de volta e começaram a adicionar versĂ”es
_Sync()
a vĂĄrias coisas).
Quando as pessoas falam sobre "inferno de retorno de chamada" , falam sobre como Ă© irritante ter funçÔes "vermelhas" em seu idioma. Quando eles criam 4089 bibliotecas para programação assĂncrona (em 2019, jĂĄ em 11217 - aprox. Tradutor), eles tentam lidar com o problema no nĂvel da biblioteca de que estavam presos Ă linguagem.
Eu prometo que o futuro Ă© melhor
na tradução: "Eu prometo que o futuro Ă© melhor" o jogo de palavras do tĂtulo e do conteĂșdo da seção estĂĄ perdido
As pessoas no Node.js hå muito tempo percebem que os retornos de chamada são prejudiciais e estavam procurando soluçÔes. Uma das técnicas que inspirou muitas pessoas são as promises
, que vocĂȘ tambĂ©m pode conhecer pelo apelido de futures
.
na TI russa, em vez de traduzir "promessas" como "promessas", foi estabelecido um papel vegetal do inglĂȘs - "promessas". A palavra "Futuros" Ă© usada como Ă©, provavelmente porque os "futuros" jĂĄ estĂŁo ocupados por gĂrias financeiras.
Promis Ă© um wrapper para retorno de chamada e manipulador de erros. Se vocĂȘ estĂĄ pensando em passar um retorno de chamada para o resultado e outro retorno de chamada para o erro, o future
Ă© a personificação dessa ideia. Este Ă© um objeto bĂĄsico que Ă© uma operação assĂncrona.
Acabei de receber um monte de palavras sofisticadas e pode parecer uma Ăłtima solução, mas principalmente Ă© Ăłleo de cobra . As promessas realmente facilitam a escrita de cĂłdigo assĂncrono. Como sĂŁo mais fĂĄceis de compor em expressĂ”es, a regra 4 Ă© um pouco menos rigorosa.
Mas, para ser sincero, é como a diferença entre um golpe no estÎmago ou na virilha. Sim, não dói tanto, mas ninguém ficarå encantado com essa escolha.
VocĂȘ ainda nĂŁo pode usar promessas com tratamento de exceção ou outros
operadores de gerenciamento. VocĂȘ nĂŁo pode chamar uma função que retorne future
do cĂłdigo sĂncrono. (vocĂȘ pode , mas o prĂłximo mantenedor do seu cĂłdigo inventarĂĄ uma mĂĄquina do tempo, retornarĂĄ no momento em que vocĂȘ o fez e enfiarĂĄ um lĂĄpis na sua cara pelo motivo 2).
As promessas ainda dividem seu mundo em metades assĂncronas e sĂncronas com todo o sofrimento que se segue. Portanto, mesmo que seu idioma suporte promises
ou futures
, ele ainda se parece muito com o meu idioma de pelĂșcia.
(Sim, isso inclui até o Dart que eu uso. Portanto, estou tão feliz que parte da equipe esteja tentando outras abordagens do paralelismo )
link do projeto abandonado oficialmente
Estou aguardando uma solução
Os programadores de C # provavelmente se sentem complacentes (a razĂŁo pela qual estĂŁo se tornando cada vez mais vĂtimas Ă© que Halesberg e a empresa polvilham tudo e polvilham a linguagem com açĂșcar sintĂĄtico). Em C #, vocĂȘ pode usar a palavra-chave await
para chamar uma função assĂncrona.
Isso torna a realização de chamadas assĂncronas tĂŁo fĂĄcil quanto sĂncrona, com a adição de uma pequena palavra-chave atraente. VocĂȘ pode inserir uma chamada em await
nas expressĂ”es, usĂĄ-las no tratamento de exceçÔes, no fluxo de instruçÔes. VocĂȘ pode enlouquecer. Vamos esperar a chuva como dinheiro para o seu novo ĂĄlbum de rapper.
O Async-Waitit Ă© bom, entĂŁo o adicionamos ao Dart. Ă muito mais fĂĄcil escrever cĂłdigo assĂncrono com ele. Mas, como sempre, hĂĄ um "Mas". Aqui estĂĄ. Mas ... vocĂȘ ainda divide o mundo ao meio. As funçÔes assĂncronas agora sĂŁo mais fĂĄceis de escrever, mas ainda sĂŁo funçÔes assĂncronas.
VocĂȘ ainda tem duas cores. A espera assĂncrona resolve o problema irritante nÂș 4 - eles tornam as funçÔes de chamada em vermelho nĂŁo mais difĂceis do que as chamadas em azul. Mas o restante das regras ainda estĂĄ aqui:
- FunçÔes sĂncronas retornam valores, funçÔes assĂncronas retornam um wrapper (
Task<T>
em C # ou Future<T>
em Dart) ao redor do valor. - SĂncrono apenas chamado, necessidade assĂncrona de
await
. - Ao chamar uma função assĂncrona, vocĂȘ obtĂ©m um objeto wrapper quando realmente deseja um valor. VocĂȘ nĂŁo pode expandir o valor atĂ© tornar sua função assĂncrona e chamĂĄ-la com
await
(mas consulte o próximo parågrafo). - Além de aguardar um pouco de decoração, pelo menos resolvemos esse problema.
- A biblioteca principal do C # Ă© mais antiga que a assincronia, entĂŁo eu acho que eles nunca tiveram esse problema.
Async
realmente melhor. Eu preferiria espera assĂncrona a retornos nus em qualquer dia da semana. Mas mentimos para nĂłs mesmos se pensarmos que todos os problemas estĂŁo resolvidos. Assim que vocĂȘ começa a escrever funçÔes de ordem superior ou a reutilizar o cĂłdigo, percebe novamente que a cor ainda estĂĄ lĂĄ, sangrando por todo o seu cĂłdigo-fonte.
Qual idioma nĂŁo Ă© cor?
EntĂŁo, JS, Dart, C # e Python tĂȘm esse problema. CoffeeScript e a maioria das outras linguagens compilando tambĂ©m em JS (e Dart herdado). Acho que mesmo o ClojureScript tem essa pegada, apesar de seus esforços ativos com o core.async
Quer saber qual deles nĂŁo? Java Estou certo Com que frequĂȘncia vocĂȘ diz: "Sim, apenas Java estĂĄ fazendo certo"? E assim aconteceu. Em sua defesa, eles estĂŁo tentando ativamente corrigir sua supervisĂŁo promovendo futures
e E / S assĂncronas. Ă como uma corrida pior que pior.
tudo jĂĄ estĂĄ em Java
C #, de fato, também pode contornar esse problema. Eles escolheram ter cor. Antes de adicionarem async-waitit e todo esse lixo eletrÎnico Task<T>
, era possĂvel usar chamadas de API sĂncronas regulares. TrĂȘs outros idiomas que nĂŁo tĂȘm um problema de "cor": Go, Lua e Ruby.
Adivinha o que eles tĂȘm em comum?
Streams. Ou, mais precisamente: muitas pilhas de chamadas independentes que podem ser trocadas . Esses nĂŁo sĂŁo necessariamente threads do sistema operacional. Corotinas em Go, corotinas em Lua e threads em Ruby sĂŁo adequadas.
(Ă por isso que existe essa pequena advertĂȘncia para C # - vocĂȘ pode evitar a dor assĂncrona em C # usando threads.)
Memória de operaçÔes passadas
O problema fundamental Ă© "como continuar do mesmo local quando a operação (assĂncrona) Ă© concluĂda"? VocĂȘ mergulhou no abismo da pilha de chamadas e depois chamou algum tipo de operação de E / S. Por uma questĂŁo de aceleração, esta operação usa a API assĂncrona subjacente do seu sistema operacional. VocĂȘ nĂŁo pode esperar para concluir. VocĂȘ deve retornar ao loop de eventos do seu idioma e dar tempo ao SO para concluir a operação.
Quando isso acontece, vocĂȘ precisa retomar o que estava fazendo. Geralmente, o idioma "lembra onde estava" na pilha de chamadas . Ele segue todas as funçÔes que foram chamadas no momento e olha para onde o contador de comandos em cada uma delas aparece.
Mas, para executar E / S assĂncrona, vocĂȘ deve relaxar, descartar toda a pilha de chamadas em C. Digite Trick-22. VocĂȘ possui E / S super rĂĄpidas, mas nĂŁo pode usar o resultado! Todos os idiomas com E / S assĂncrona sob o capĂŽ - ou, no caso de JS, o loop de eventos do navegador - sĂŁo forçados a lidar com isso de alguma forma.
Node, com seus retornos de chamada marcantes para sempre, enfia todas essas chamadas em fechamento. Quando vocĂȘ escreve:
function makeSundae(callback) { scoopIceCream(function (iceCream) { warmUpCaramel(function (caramel) { callback(pourOnIceCream(iceCream, caramel)); }); }); }
Cada uma dessas expressÔes funcionais fecha todo o contexto circundante. Isso transfere parùmetros, como iceCream
e caramel
, da pilha de chamadas para a pilha . Quando uma função externa retorna um resultado e a pilha de chamadas Ă© destruĂda, isso Ă© legal. Os dados ainda estĂŁo em algum lugar na pilha.
O problema Ă© que vocĂȘ precisa ressuscitar cada uma dessas chamadas malditas novamente. Existe atĂ© um nome especial para esta conversĂŁo: estilo de passagem de continuação
vincular funcionalidade feroz
Isso foi inventado por hackers de linguagem nos anos 70, como uma representação intermediåria para uso sob o capÎ de compiladores. Essa é uma maneira muito bizarra de introduzir código que facilita a execução de algumas otimizaçÔes do compilador.
Ninguém nunca pensou que um programador pudesse escrever esse código . E então Node apareceu e, de repente, todos fingimos escrever um back-end do compilador. Onde nós viramos o caminho errado?
Observe que promessas e futures
realmente nĂŁo ajudam muito. Se vocĂȘ usĂĄ-los, sabe que ainda estĂĄ acumulando camadas gigantes de expressĂ”es funcionais . VocĂȘ apenas os passa para .then()
vez da prĂłpria função assĂncrona.
Aguardando uma solução gerada
Async-waitit realmente ajuda. Se vocĂȘ olhar embaixo do capĂŽ para o compilador quando ele encontrar, vocĂȘ verĂĄ que ele realmente realiza a conversĂŁo do CPS. Ă por isso que vocĂȘ precisa usar await
em C # - esta é uma dica para o compilador - "interrompa a função aqui no meio". Tudo o que await
depois se torna uma nova função que o compilador sintetiza em seu nome.
à por isso que o async-waitit não precisa de suporte de tempo de execução dentro da estrutura .NET. O compilador compila isso em uma cadeia de fechamentos relacionados, com os quais ele jå sabe como lidar. (Curiosamente, os fechamentos também não precisam de suporte de tempo de execução. Eles são compilados em classes anÎnimas. Em C #, os fechamentos são apenas objetos.)
VocĂȘ provavelmente estĂĄ se perguntando quando menciono os geradores. Existe yield
no seu idioma? EntĂŁo ele pode fazer algo muito semelhante.
(Acredito que os geradores e o async-waitit sĂŁo realmente isomĂłrficos. Em algum lugar nos cantos e recantos empoeirados do meu disco rĂgido existe um pedaço de cĂłdigo que implementa um ciclo de jogo nos geradores usando apenas o async-wait).
EntĂŁo, onde eu estou? Ah sim. Assim, com retornos de chamada, promessas, espera assĂncrona e geradores, vocĂȘ acaba pegando sua função assĂncrona e dividindo-a em vĂĄrios fechamentos que ficam na pilha.
Sua função chama externa em tempo de execução. Quando o loop de eventos ou a operação de E / S Ă© concluĂda, sua função Ă© chamada e continua de onde estava. Mas isso significa que tudo no topo da sua função tambĂ©m deve retornar. VocĂȘ ainda precisa restaurar a pilha inteira.
Ă daĂ que vem a regra: "VocĂȘ sĂł pode chamar a função vermelha a partir da função vermelha" VocĂȘ deve salvar toda a pilha de chamadas nos fechamentos para main()
ou para o manipulador de eventos.
Implementação da pilha de chamadas
Mas, usando threads ( verde ou nĂvel do SO), vocĂȘ nĂŁo precisa fazer isso. VocĂȘ pode simplesmente pausar o segmento inteiro e pular para o SO ou o loop de eventos sem precisar retornar de todas essas funçÔes .
A linguagem Go, no meu entender, faz isso da maneira mais perfeita. Assim que vocĂȘ fizer qualquer operação de E / S, o Go estacionarĂĄ essa corotina e continuarĂĄ qualquer outra que nĂŁo seja bloqueada pela E / S.
Se vocĂȘ observar as operaçÔes de E / S na biblioteca padrĂŁo Golang, elas parecem sĂncronas. Em outras palavras, eles simplesmente funcionam e retornam o resultado quando estiverem prontos. Mas essa sincronização nĂŁo significa o mesmo que no Javascript. Go- , IO . Go .
Go â , . , , .
, API, , . .
, , . , . , 50% .
, , , .
Javascript -, , , JS , JS , . , JS .
, ( ) â , , , async
. import threading
( , AsyncIO, Twisted Tornado, ).
, , , , , , .
, Go, Go .
, , , ( - ) , "async-await ". .
, .
, , .