[Перевод] Ошибки, которые не ловит Rust

tfu5vinauktrvp3jxgqwbso1lfe.jpeg


Мне по-прежнему интересны языки программирования. Но сегодня уже не так сильно, и не из-за того, что они позволяют мне делать, а, скорее, из-за того, что они мне делать не позволяют.

В конечном итоге, возможности того, что можно сделать при помощи языка программирования, редко ограничены самим языком: нет ничего, что можно сделать на C++, но нельзя повторить на C, при наличии бесконечного количества времени.

Если язык полон по Тьюрингу и компилируется в ассемблерный код, каким бы ни был интерфейс, вы общаетесь с одной и той же машиной. Вы ограничены возможностями оборудования, количеством его памяти (и её скоростью), подключенной к нему периферией, и так далее.

На самом деле, достаточно лишь команды mov.


Разумеется, существуют различия в выразительности: для выполнения определённых задач в разных языках может потребоваться больше или меньше кода. Язык Java печально известен своей многословностью:, но благодаря другим его преимуществам он и сегодня является привлекательным выбором для многих компаний.

Кроме того, есть такие аспекты, как производительность, отладкопригодность (если такого слова нет, то его стоит придумать) и дюжина других факторов, которые стоит рассмотреть при «выборе языка».

Размеры леса


Но задумайтесь: из полного множества комбинаций всех возможных команд лишь крошечная доля является действительно полезными программами. И ещё меньшая доля решает задачу, которую вам нужно решить.

Поэтому можно считать «программирование» поиском подходящей программы в этом множестве. И можно считать достоинством «более строгих» языков то, что они уменьшают размер множества, в котором мы выполняем поиск, потому что в них есть меньше «допустимых» комбинаций.

Учитывая это, возникает соблазн составить рейтинг языков по «количеству допустимых программ». Не стоит ожидать, что все достигнут консенсуса по единственному рейтингу, но некоторые разногласия вполне приемлемы.

Рассмотрим следующую программу на JavaScript:

function foo(i) {
  console.log("foo", i);
}

function bar() {
  console.log("bar!");
}

function main() {
  for (i = 0; i < 3; i++) {
    foo(i);
  }
  return;
  bar();
}

main();


В этом коде bar() никогда не вызывается — main выполняет возврат до её вызова.

При запуске программы в node.js мы не получим никаких предупреждений:

$ node sample.js
foo 0
foo 1
foo 2


Тот же пример на языке Go тоже не вызовет предупреждений:

package main

import "log"

func foo(i int) {
  log.Printf("foo %d", i)
}

func bar() {
  log.Printf("bar!")
}

func main() {
  for i := 0; i < 3; i++ {
    foo(i)
  }
  return
  bar()
}
$ go build ./sample.main
$ ./sample
2022/02/06 17:35:55 foo 0
2022/02/06 17:35:55 foo 1
2022/02/06 17:35:55 foo 2


Однако инструмент go vet (поставляемый в стандартном дистрибутиве Go) отреагирует на этот код:

$ go vet ./sample.go
# command-line-arguments
./sample.go:18:2: unreachable code


Потому что несмотря на то, что, строго говоря, наш код не является неверным, он… подозрительный. Он похож на неверный код. Поэтому linter вежливо спрашивает: «вы на самом деле это имели в виду? если да, то всё в порядке, можете заставить lint замолчать., а если нет, то у вас есть шанс исправить код».

Тот же код, но написанный на Rust, приведёт к гораздо большему шуму:

fn foo(i: usize) {
    println!("foo {}", i);
}

fn bar() {
    println!("bar!");
}

fn main() {
    for i in 0..=2 {
        foo(i)
    }
    return;
    bar()
}
$ cargo run
   Compiling lox v0.1.0 (/home/amos/bearcove/lox)
warning: unreachable expression
  --> src/main.rs:14:5
   |
13 |     return;
   |     ------ any code following this expression is unreachable
14 |     bar()
   |     ^^^^^ unreachable expression
   |
   = note: `#[warn(unreachable_code)]` on by default

warning: `lox` (bin "lox") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/lox`
foo 0
foo 1
foo 2


Мне нравится, что он не просто сообщает, что код недостижим, но и почему этот код недостижим.

Обратите внимание, что это по-прежнему предупреждение, то есть разработчик может просто просмотреть его в нужное время, но выполнению кода оно не помешает. (Если только мы не поместим #![deny(unreachable_code)] в начало main.rs — это эквивалент передачи -Werror=something в gcc/clang).

Ошибёмся сейчас, узнаем об этом… когда?


Давайте немного изменим пример. Допустим, полностью уберём определение bar.

В конце концов, она ведь никогда не вызывается, разве это на что-то повлияет?

function foo(i) {
  console.log("foo", i);
}

function main() {
  for (i = 0; i < 3; i++) {
    foo(i);
  }
  return;
  bar();
}

main();
$ node sample.js
foo 0
foo 1
foo 2


Реализация node.js считает, что никого не волнует bar, потому что её никогда не вызывают.

Однако Go сильно против исчезновения bar:

package main

import "log"

func foo(i int) {
  log.Printf("foo %d", i)
}

func main() {
  for i := 0; i < 3; i++ {
    foo(i)
  }
  return
  bar()
}
$ go run ./sample.go 
# command-line-arguments
./sample.go:14:2: undefined: bar


… и как всегда краток.

Компилятор Rust тоже расстроен:

fn foo(i: usize) {
    println!("foo {}", i);
}

fn main() {
    for i in 0..=2 {
        foo(i)
    }
    return;
    bar()
}
$ cargo run
   Compiling lox v0.1.0 (/home/amos/bearcove/lox)
error[E0425]: cannot find function `bar` in this scope
  --> src/main.rs:10:5
   |
10 |     bar()
   |     ^^^ not found in this scope

warning: unreachable expression
  --> src/main.rs:10:5
   |
9  |     return;
   |     ------ any code following this expression is unreachable
10 |     bar()
   |     ^^^^^ unreachable expression
   |
   = note: `#[warn(unreachable_code)]` on by default

For more information about this error, try `rustc --explain E0425`.
warning: `lox` (bin "lox") generated 1 warning
error: could not compile `lox` due to previous error; 1 warning emitted


… и продолжает настаивать, что если бы bar существовала (хотя на самом деле сейчас нет), её всё равно никогда не вызывали бы и мы всё равно должны… пересмотреть своё мнение.

Итак, и Go, и Rust отвергают эти программы как недопустимые (они выдают ошибку и отказываются создавать скомпилированную форму программы), даже несмотря на то, что, откровенно говоря, это совершенно правильная программа.

Но этому есть совершенно разумное и практичное объяснение.

По сути, node.js является интерпретатором. В его составе есть компилятор just-in-time (на самом деле, несколько), но это уже подробность реализации. Мы можем представить, что программы исполняются «на лету», в процессе нахождения в коде новых выражений и операторов, и быть достаточно близкими к правде.

Поэтому node.js не нужно озадачиваться существованием символа bar до того самого момента, пока его не вызовут (или получат к нему доступ, или присвоят ему значение, и т. д.).

После чего он выдаст ошибку. В процессе выполнения нашей программы.

function foo(i) {
  console.log("foo", i);
}

function main() {
  for (i = 0; i < 3; i++) {
    foo(i);
  }
  // 
    
            

© Habrahabr.ru