Есть ли способ эмулировать поведение Java при вызове статического метода родительского класса для простой обработки ошибок global-ish? - PullRequest
0 голосов
/ 26 апреля 2018

Я пытаюсь реализовать простой интерпретатор в Rust для выдуманного языка программирования под названием rlox, следуя книге Боба Нистрома Создание интерпретаторов .

Я хочу, чтобы ошибки могли возникать в любом дочернем модуле, и чтобы о них "сообщалось" в модуле main (это сделано в книге с Java, просто вызывая статический метод для содержащего класс, который печатает оскорбительный токен и строку). Однако, если происходит ошибка, я не могу просто вернуться рано с Result::Err (что, я полагаю, идиоматический способ обработки ошибок в Rust), потому что интерпретатор должен продолжать работать - постоянно искать ошибки.

Есть ли (идиоматический) способ для меня эмулировать поведение Java при вызове статического метода родительского класса из дочернего класса в Rust с модулями? Должен ли я полностью отказаться от чего-то подобного?

Я подумал о стратегии, в которой я вставляю ссылку на некоторую структуру ErrorReporter в качестве зависимости в структуры Scanner и Token, но мне это кажется громоздкой (я не считаю, что репортер ошибок должен быть частью подписи структуры, я не прав?):

struct Token {
   error_reporter: Rc<ErrorReporter>, // Should I avoid this?
   token_type: token::Type,
   lexeme: String,
   line: u32   
}

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

rlox [package]
└───src
    ├───main.rs (uses scanner + token mods, should contain logic for handling errors)
    ├───lib.rs (just exports scanner and token mods)
    ├───scanner.rs (uses token mod, declares scanner struct and impl)
    └───token.rs (declares token struct and impl)

1 Ответ

0 голосов
/ 27 апреля 2018

Дословный перевод

Важно отметить, что статический метод Java не имеет доступа ни к какому состоянию экземпляра. Это означает, что он может быть реплицирован в Rust с помощью функции или связанной функции , ни один из которых не имеет какого-либо состояния. Разница лишь в том, как вы их называете:

fn example() {}

impl Something {
    fn example() {}
}

fn main() {
    example();
    Something::example();
}

Глядя на источник , который вы копируете , он не просто сообщает об ошибке, а имеет такой код:

public class Lox {
  static boolean hadError = false;

  static void error(int line, String message) {
    report(line, "", message);
  }

  private static void report(int line, String where, String message) {
    System.err.println(
        "[line " + line + "] Error" + where + ": " + message);
    hadError = true;
  }
}

Я не эксперт по JVM, но я уверен, что использование такой статической переменной означает, что ваш код больше не является поточно-ориентированным. Вы просто не можете сделать это в безопасном Rust; вы не можете «случайно» сделать небезопасный код памяти.

Самый буквальный перевод этого, который безопасен, использовал бы связанные функции и атомарные переменные:

use std::sync::atomic::{AtomicBool, Ordering, ATOMIC_BOOL_INIT};

static HAD_ERROR: AtomicBool = ATOMIC_BOOL_INIT;

struct Lox;

impl Lox {
    fn error(line: usize, message: &str) {
        Lox::report(line, "", message);
    }

    fn report(line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {}] Error{}: {}", line, where_it_was, message);
        HAD_ERROR.store(true, Ordering::SeqCst);
    }
}

Вы также можете выбрать более богатые структуры данных для хранения в вашем глобальном состоянии, используя lazy_static и Mutex или RwLock, если они вам нужны.

Идиоматический перевод

Хотя это может быть удобно, я не думаю, что такой дизайн хорош . Глобальное состояние просто ужасно. Я бы предпочел использовать внедрение зависимости.

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

struct LoggingErrorSink {
    had_error: bool,
}

impl LoggingErrorSink {
    fn error(&mut self, line: usize, message: &str) {
        self.report(line, "", message);
    }

    fn report(&mut self, line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {} ] Error {}: {}", line, where_it_was, message);
        self.had_error = true;
    }
}

fn some_parsing_thing(errors: &mut LoggingErrorSink) {
    errors.error(0, "It's broken");
}

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

trait ErrorSink {
    fn error(&mut self, line: usize, message: &str) {
        self.report(line, "", message);
    }

    fn report(&mut self, line: usize, where_it_was: &str, message: &str);
}

struct LoggingErrorSink {
    had_error: bool,
}

impl LoggingErrorSink {
    fn report(&mut self, line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {} ] Error {}: {}", line, where_it_was, message);
        self.had_error = true;
    }
}

fn some_parsing_thing<L>(errors: &mut L)
where
    L: ErrorSink,
{
    errors.error(0, "It's broken");
}

Есть много вариантов реализации этого, все в зависимости от ваших компромиссов.

  • Вы можете выбрать для логгера взять &self вместо &mut, что заставит этот случай использовать что-то вроде Cell, чтобы получить внутреннюю изменчивость had_error.
  • Вы можете использовать что-то вроде Rc, чтобы избежать добавления дополнительных времен жизни в цепочку вызовов.
  • Вы можете сохранить регистратор как член структуры вместо параметра функции.

Для дополнительной работы с клавиатурой вы можете проверить свои ошибки . Просто создайте фиктивную реализацию черты, которая сохраняет информацию во внутренние переменные, и передайте ее во время теста.

Мнения, ахой!

стратегия, в которой я вставляю ссылку на некоторую ErrorReporter структуру как зависимость в Scanner

Да, внедрение зависимостей - удивительное решение для большого числа проблем кодирования.

и Token структуры

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

но это кажется мне громоздким. Я не чувствую, что репортер ошибок должен быть частью подписи структуры, я не прав?

Я бы сказал, да, вы не правы; Вы назвали это абсолютной истиной, которой очень мало в программировании.

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

Еще несколько человек заботятся о фактической подписи вашего типа. Это обоюдоострый клинок. Чтобы добиться максимальной производительности, Rust заставит вас указывать ваши родовые типы и время жизни в сигнатурах ваших типов. Иногда это отстой, но либо выигрыш в производительности того стоит, либо вы можете как-то это скрыть и принять крошечный удар. Это преимущество языка, который дает вам выбор.

Смотри также

...