Este post foi escrito por um desenvolvedor JavaScript que acabou de entrar no mundo do Rust. Não é necessário um histórico de JS para obter valor deste artigo! Mas se você for um colega desenvolvedor da web que virou rustáceo, terá empatia com meus pontos um pouco mais.

Parece que as linguagens construídas na última década estão seguindo uma tendência comum: diminuir modelos orientados a objetos e com programação funcional (FP).

Os desenvolvedores da web podem ter visto o padrão FP emergir em estruturas de front-end modernas como React usando seu modelo de ganchos . Mas mudando para o Rust, você verá como o FP pode ser poderoso ao construir uma linguagem de programação inteira em torno dele-e a abordagem para tentar… catch e null são apenas a ponta do iceberg!

Vamos explorar as falhas de lançar e capturar exceções, o que Rust’s Result enum e a correspondência de padrões podem fazer por você e como isso se estende ao tratamento de valores nulos.

O que é Rust?

Para você novo Rustaceans (yee-claw! ), Rust é construído para ser uma linguagem digitada de nível inferior que é amigável o suficiente para todos os programadores aprenderem. Muito parecido com o C, o Rust compila diretamente para o código de máquina (binário bruto), portanto, os programas do Rust podem ser compilados e executados com uma rapidez incrível. Eles também levam a comunicação e a documentação muito a sério, com uma próspera comunidade de colaboradores e uma abundância de tutoriais excelentes .

Por que você não deve usar blocos try… catch em Rust

Se você é como eu, está acostumado a fazer a dança das pegadas em toda a sua base de código JavaScript. Considere este cenário:

//Cenário 1: captura de uma chamada de banco de dados perigosa app.get (‘/usuário’, função assíncrona (req, res) {try {const user=await electricalDatabaseCall (req.userId) res.send (usuário)} catch (e) {//não foi possível encontrar o usuário! É hora de dizer ao cliente//que foi uma solicitação incorreta res.status (400)}})

Este é um padrão de servidor típico. Vá chamar o banco de dados, envie a resposta ao usuário quando funcionar e envie algum código de erro como 400 quando não funcionar.

Mas como sabíamos usar try… catch aqui? Bem, com um nome como perigosoDatabaseCall e alguma intuição sobre bancos de dados, sabemos que provavelmente lançará uma exceção quando algo der errado.

Agora, vamos pegar este cenário:

//Cenário 2: esquecimento para capturar um arquivo perigoso lendo app.get (‘/applySepiaFilter’, função assíncrona (req, res) {const image=await readFile (“/assets/”+ req.pathToImageAsset) const imageWithSepiaFilter=applySepiaFilter (image) reenviar ( imageWithSepiaFilter)})

Este é um exemplo inventado, é claro. Mas, em resumo, sempre que chamamos applySepiaFilter, queremos ler o arquivo solicitado de nosso servidor/ativos e aplicar esse filtro de cor.

Mas espere, esquecemos de tentar… pegar esta! Portanto, sempre que solicitarmos algum arquivo que não existe, receberemos um erro interno desagradável do servidor. Idealmente, seria um status de 400″solicitação inválida”.

Agora você pode estar pensando: “Tudo bem, mas não Não me esqueci de tentar… pegar… ”Compreensível! Alguns programadores de Node.js podem reconhecer imediatamente que readFile lança exceções.

Mas isso fica mais difícil de prever quando estamos trabalhando com funções de biblioteca sem exceções documentadas ou trabalhando com nossas próprias abstrações (talvez sem documentação em tudo se você for scrappy como eu ).

Resumindo alguns principais problemas com manipulação de exceção JS:

Se uma função alguma vez lançar s, o chamador deve se lembrar de lidar com essa exceção. E não, sua fantasia ESlint não o ajudará aqui! Isso pode levar ao que chamarei de try… catch ansiedade: envolver tudo em um bloco try caso algo dê errado. Ou pior, você se esquecerá de capturar uma exceção completamente, levando a falhas de interrupção, como nossa chamada readFile não capturada O tipo de exceção pode ser imprevisível. Isso pode ser um problema para tentar… captura os invólucros em torno de vários pontos de falha. Por exemplo, e se nossa explosão readFile retornar um código de status e uma falha de applySepiaFilter retornar outro? Temos vários blocos try… catch? E se precisarmos olhar o campo do nome da exceção (que pode não ser confiável lado do navegador)?

Vejamos o enum de Rust’s Result.

Usando o enum de Rust’s Result e a correspondência de padrões

Aqui está uma surpresa: Rust não tem um bloco try… catch. Caramba, eles nem mesmo têm”exceções”, como os conhecemos.

Compreendendo a correspondência no Rust

Sinta-se à vontade para pular para a próxima seção se você já entende a correspondência de padrões.

Antes de explorar como isso é possível, vamos entender Rust’s ideia de correspondência de padrões. Este é um cenário:

Um cliente faminto pede uma refeição do nosso menu de comida de rua coreana e queremos servir-lhe uma refeição diferente, dependendo do orderNumber que escolheram.

Em JavaScript , você pode alcançar uma série de condicionais como esta:

let meal=null switch (orderNumber) {case 1: meal=”Bulgogi”break case 2: meal=”Bibimbap”break default: meal=”Kimchi Jjigae”break} return meal

Isso é legível o suficiente, mas tem uma falha perceptível (além de usar uma instrução switch feia): Nossa refeição precisa começar como nula e precisa usar let para reatribuição em nossos casos de switch. Se ao menos switch pudesse realmente retornar um valor como este…

//Observação: isso não é JavaScript real! const meal=switch (orderNumber) {case 1:”Bulgogi”case 2:”Bibimbap”default:”Kimchi Jjigae”}

Adivinha? Rust permite que você faça exatamente isso!

let meal=match order_number {1=>”Bulgogi”2=>”Bibimbap”_=>”Kimchi Jjigae”}

Sintaxe sagrada, Batman!

Essa é a beleza do design baseado em expressão de Rust. Nesse caso, correspondência é considerada uma expressão que pode:

Executar alguma lógica em tempo real (combinando nosso número de pedido com uma string de refeição) Retornar esse valor no final (atribuível a refeição)

Os condicionais podem ser expressões , também. Onde os desenvolvedores de JavaScript podem alcançar um ternário:

const meal=orderNumber===1?”Bulgogi”:”Something else”

Rust apenas permite que você escreva uma declaração if:

let meal=if order_number==1 {“Bulgogi”} else {“Something else”}

E sim, você pode pular a palavra return. A última linha de uma expressão Rust é sempre o valor de retorno.

Aplicando correspondência a exceções

Tudo bem, então, como isso se aplica às exceções?

Vamos pular para o exemplo primeiro desta vez. Digamos que estejamos escrevendo o mesmo endpoint applySepiaFilter de antes. Vou usar os mesmos helpers req e res para maior clareza:

use std:: fs:: read_to_string;//primeiro, leia o arquivo solicitado para uma correspondência de string read_to_string (“/assets/”+ req.path_to_image_asset) {//se a imagem voltou ay-OK… Ok (raw_image)=> {//aplique o filtro para aquela imagem_prima… deixe sepia_image=apply_sepia_filter (raw_image)//e envie o resultado. res.send (sepia_image)}//caso contrário, retorna um status de 400 Err (_)=> res.status (400)}

Hm, o que está acontecendo com aqueles wrappers Ok e Err? Vamos comparar o tipo de retorno de read_to_string de Rust com readFile de Node:

Em Node land, readFile retorna uma string com a qual você pode trabalhar imediatamente em Rust, read_to_string não retorna uma string, mas, em vez disso, retorna um tipo de resultado envolvendo uma string. O tipo de retorno completo se parece com isto: Result . Em outras palavras, esta função retorna um resultado que é uma string ou um erro de E/S (o tipo de erro que você obtém ao ler e gravar arquivos)

Isso significa que não podemos trabalhar com o resultado de read_to_string até que “ desembrulhe-o (ou seja, descubra se é uma string ou um erro). Aqui está o que acontece se tentarmos tratar um Resultado como se já fosse uma string:

let image=read_to_string (“/assets/”+ req.path_to_image_asset)//ex. tente obter o comprimento de nossa string de imagem let length=image.len ()//& # x1f6a8; Erro: nenhum método chamado `len` encontrado para enum//` std:: result:: Result `

A primeira e mais perigosa maneira de desembrulhar é chamando você mesmo a função unbrap ():

let raw_image=read_to_string (“/assets/”+ req.path_to_image_asset).unwrap ()

Mas isso não é muito seguro! Se você tentar chamar o desdobramento e read_to_string retornar algum tipo de erro, todo o programa irá travar do que é chamado de pânico . E lembre-se, Rust não tem um try… catch, então isso pode ser um problema bem desagradável.

A segunda maneira mais segura de desembrulhar nosso resultado é através da correspondência de padrões. Vamos revisitar aquele bloco anterior com alguns comentários esclarecedores:

match read_to_string (“/assets/”+ req.path_to_image_asset) {//verificar se nosso resultado é”Ok”, um subtipo de Resultado que//contém um valor do tipo”string”Result:: Ok (raw_image)=> {//aqui, podemos acessar a string dentro desse invólucro!//isso significa que estamos seguros para passar aquela raw_image para nosso filtro fn… let sepia_image=apply_sepia_filter (raw_image)//e enviar o resultado res.send (sepia_image)}//caso contrário, verifique se nosso resultado é um”Err,”outro subtipo//que envolve um erro de E/S. Result:: Err (_)=> res.status (400)}

Observe que estamos usando um sublinhado _ dentro desse Err no final. Essa é a maneira Rust-y de dizer:”Não nos importamos com este valor”, porque sempre retornamos um status de 400. Se nos importássemos com esse objeto de erro, poderíamos capturá-lo de forma semelhante à nossa imagem_prima e até mesmo faça outra camada de correspondência de padrões por tipo de exceção.

Por que a correspondência de padrões é a maneira mais segura de lidar com exceções

Então, por que lidar com todos esses “invólucros” inconvenientes como Resultado? Pode parecer irritante à primeira vista, mas eles são realmente irritantes por design, porque:

você é forçado a lidar com os erros sempre que eles aparecem, definindo o comportamento para os casos de sucesso e falha com correspondência de padrões. E, nos momentos em que você realmente deseja obter seu resultado e seguir em frente, pode optar por um comportamento inseguro usando wrap (). Você sempre sabe quando uma função pode apresentar um erro com base em seu tipo de retorno, o que significa que não tente mais… captura a ansiedade e chega de verificação de tipo janky

How to use null in Rust

Este é outro canto cabeludo do JS que o Rust pode resolver. Para valores de retorno de função, alcançamos nulo (ou indefinido) quando temos algum tipo de caso especial ou padrão a considerar. Podemos lançar um nulo quando alguma conversão falha, um objeto ou elemento de array não existe, etc.

Mas, nesses contextos, nulo é apenas uma exceção sem nome! Podemos alcançar valores de retorno nulos em JS porque lançar uma exceção parece inseguro ou extremo. O que queremos é uma maneira de gerar uma exceção, mas sem o incômodo de um tipo de erro ou mensagem de erro, e esperando que o chamador use um try… catch.

Rust reconheceu isso também. Então, Rust baniu null da linguagem e introduziu o invólucro Option .

Digamos que temos uma função get_waiter_comment que dá ao cliente um elogio, dependendo na ponta eles saem. Podemos usar algo assim:

fn get_waiter_comment (tip_percentage: u32)-> Opção {if tip_percentage <=20 {None} else {Some ("Essa é uma dica generosa!". To_string ())} }

Poderíamos ter retornado uma string vazia””quando não queremos um elogio. Mas ao usar Option (da mesma forma que usar um nulo), é mais fácil descobrir se temos um elogio para mostrar ou não. Veja como essa declaração de correspondência pode ser legível:

match get_waiter_comment (tip) {Some (comment)=> tell_customer (comment) None=> walk_away_from_table ()}

Quando usar Opção vs. Resultado

A linha entre Resultado e Opção está borrada. Poderíamos facilmente refatorar o exemplo anterior:

fn get_waiter_comment (tip_percentage: u32)-> Result {if tip_percentage <=20 {Err (SOME_ERROR_TYPE)} else {Result ("Essa é uma dica generosa!".to_string ())}}... match get_waiter_comment (tip) {Ok (comment)=> tell_customer (comment) Err (_)=> walk_away_from_table ()}

A única diferença é que precisamos fornecer algum erro objete ao nosso caso Err, o que pode ser um incômodo porque o receptor precisa apresentar um tipo/mensagem de erro para usar e o chamador precisa verificar se vale a pena ler e comparar a mensagem de erro.

Mas aqui, está bem claro que uma mensagem de erro não agregará muito valor à nossa função get_waiter_comment. É por isso que geralmente procuro uma opção até ter um bom motivo para mudar para o tipo de resultado. Ainda assim, a decisão depende de você!

Concluindo (sem trocadilhos)

A abordagem de Rust para exceção e tratamento de nulos é uma grande vitória para a segurança de tipo. Armado com os conceitos de expressões, correspondência de padrões e tipos de wrapper, espero que você esteja pronto para lidar com erros com segurança em todo o seu aplicativo!