برنامه نویسی

یک REPL برای تایپ دوستانه با چاق

Summarize this content to 400 words in Persian Lang
مفسر پایتون من، ممفیس، دارای یک REPL (حلقه خواندن-ایوال-چاپ) است!

این یک خبر قدیمی است. تا زمانی که هنگام تعامل با جغد پیر دانا هیچ اشتباهی مرتکب نشده باشید، می‌توانید به دلخواه تفسیر کنید. با فرض اینکه هرگز نخواستید یک عبارت را دو بار ارزیابی کنید، یا اگر این کار را کردید، بدتان نمی آمد که آن را دوباره تایپ کنید. همچنین با صفر اشتباه.

من از این REPL کاملا راضی بودم. حتی هیجان زده من در مورد این REPL به خانه نوشته بودم. اما رئیس رئیسان من از ما خواست که REPL را برای نتیجه نهایی برای مردم بهبود دهیم. آنها مرا به دفترشان صدا زدند و با سر به صندلی مقابل میز چوب ماهونشان اشاره کردند. “برخی از کاربران انتظار دارند که کلید بک اسپیس کار کند.” به جهنم با کاربران! “فلش رو به بالا باید آخرین فرمان آنها را نشان دهد.” پشتیبانی از کلید پیکان بسیار دهه نود است! “آیا می توانید این کار را تا پایان Q5 انجام دهید؟” من استعفا دادم!

بنابراین به پشت میز خود برگشتم و REPL را بهبود دادم.

آنقدر آن را بهبود دادم که همه کلیدها کار کردند. کلید بک اسپیس، فلش رو به بالا، فلش پایین، فلش چپ، و آخرین، اما نه کم اهمیت، فلش بک اسپیس. یک حسابدار می تواند با REPL جدید یک روز کاری داشته باشد. تیک تیک تیک تیک تیک تیک تیک تیک تیک. این همان اعدادی است که حسابدار تایپ می کند، نه بمبی که به آرامی پخش می شود.

من REPL را به آزمایشگاه فرستادم و به ماشین‌کار اصلی‌ام گفتم که این سفارش را با عجله انجام دهد. این همان پاسخی است که گفتم و از نگاه چشمانشان می‌توانستم بفهمم که فهمیدند. 750 میلی‌ثانیه بعد ساخت کامل شد و ما از کلید پیکان پشتیبانی کردیم. من محصول را به سمت کلاه گیس های بزرگ بردم، برای کارم التماس کردم و از آنها نظرشان را پرسیدم. آنها چند دستور را اجرا کردند، چند چاپ را چاپ کردند و تعدادی افزودنی اضافه کردند. اشتباه کردند و کلید بک اسپیس را زدند. چشمامو چرخوندم چون جدی کی اشتباه میکنه ولی راضی به نظر میومد. آنها متوجه شدند که نمی خواهند فرمان طولانی را که قبلاً تایپ کرده بودند اجرا کنند و اینجا جایی بود که زندگی من در یک سبد دستی به جهنم رفت. آنها ضربه بزنید. Ctrl ج- جدی، چه کسی این کار را می کند؟! می دانید که این به روند فعلی پایان می دهد، درست است؟ درسته؟؟؟

“ما تا پایان سال آینده به پشتیبانی Ctrl-C نیاز داریم.” این افراد و مطالباتشان. من پشتیبانی Ctrl-C را اضافه می کنم. اما قطعا در دو سال آینده نخواهد بود.

بنابراین به میز کارم برگشتم و پشتیبانی از Ctrl-C را اضافه کردم.

چه چیزی این REPL را شایسته افرادی با تمایلات انگشتان چاق کرد؟

آیا من یک ابزار خواهم بود؟

من تمام آینده حرفه ای و مالی خود را برای ساختن چیزها “از ابتدا” در نظر گرفته ام، بنابراین در روز اول این پروژه با یک معضل مواجه شدم. من استفاده کردم crossterm برای تشخیص کلید در درجه اول به دلیل پشتیبانی بین پلتفرمی است. هر چند صادقانه بگویم، crossterm خیلی خیلی خوب بود API بصری است و من به خصوص از آن راضی بودم KeyModifiers (که ما نیاز به رسیدگی داشتیم Ctrl-C، که فکر می کردم غیر ضروری است، در بالا ببینید).

حالت خام دردناک است

ما به آن نیاز داشتیم تا ترمینال کلیدهای خاصی را برای ما جابجا نکند. اما لعنتی، نمی‌دانستم که این باعث می‌شود صفحه ما به یک ماشین تحریر خراب تبدیل شود. به هر حال، من مجبور شدم همه رشته ها را عادی کنم تا قبل از هر کاراکتر خط جدید، یک بازگشت کالسکه اضافه کنم. که خوب کار کرد و من از آن هیجان زده هستم.

/// When the terminal is in raw mode, we must emit a carriage return in addition to a newline,
/// because that does not happen automatically.
fn normalize<T: Display>(err: T) -> String {
let formatted = format!(“{}”, err);
if terminal::is_raw_mode_enabled().expect(“Failed to query terminal raw mode”) {
formatted.replace(“\n”, “\n\r”)
} else {
formatted.to_string()
}
}

/// Print command which will normalize newlines + carriage returns before printing.
fn print_raw<T: Display>(val: T) {
print!(“{}”, normalize(val));
io::stdout().flush().expect(“Failed to flush stdout”);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست ادغام جالب بود

تحت REPL قدیمی خود (که ترجیح دادم، در بالا ببینید)، می‌توانم آن را با اجرای باینری و ارسال کدهای پایتون به stdin آزمایش کنم. من فکر می کنم به دلیل اختلاف در قرارداد، هنگام استفاده از کراس ترم از کار افتاد. من صادقانه نمی‌توانم آن را به طور کامل توضیح دهم، اما event::read() در تست یکپارچه‌سازی ارائه شده با ورودی stdin به پایان می‌رسد و شکست می‌خورد. پس مسخره اش کردم

pub trait TerminalIO {
fn read_event(&mut self) -> Result<Event, io::Error>;
fn write<T: Display>(&mut self, output: T) -> io::Result<()>;
fn writeln<T: Display>(&mut self, output: T) -> io::Result<()>;
}

/// A mock for testing that doesn’t use `crossterm`.
struct MockTerminalIO {
/// Predefined events for testing
events: Vec<Event>,

/// Captured output for assertions
output: Vec<String>,
}

impl TerminalIO for MockTerminalIO {
fn read_event(&mut self) -> Result<Event, io::Error> {
if self.events.is_empty() {
Err(io::Error::new(io::ErrorKind::Other, “No more events”))
} else {
// remove from the front (semantically similar to VecDequeue::pop_front).
Ok(self.events.remove(0))
}
}

fn write<T: Display>(&mut self, output: T) -> io::Result<()> {
self.output.push(format!(“{}”, output));
Ok(())
}

fn writeln<T: Display>(&mut self, output: T) -> io::Result<()> {
self.write(output)?;
self.write(“\n”)?;
Ok(())
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که منجر به تبدیل شدن کل موضوع به یک آزمون واحد شد؟ راستش من نمی دانم. در این مرحله، اگر الف) یک باینری را در داخل باینری دیگر فراخوانی کنم، یا 2) یک سرور راه اندازی کنم / یک پورت را باز کنم / در یک سوکت در داخل یک تست گوش دهم، آن را تست یکپارچه سازی می نامم. اگر تعریف دیگری دارید که می‌خواهید در نظرات بنویسید، لطفاً تعریف نکنید زیرا TBH آزاردهنده به نظر می‌رسد.

/// Run the complete flow, from input code string to return value string. If you need any Ctrl
/// modifiers, do not use this!
fn run_and_return(input: &str) -> String {
let mut terminal = MockTerminalIO::from_str(input);
Repl::new().run(&mut terminal);
terminal.return_val()
}

fn string_to_events(input: &str) -> Vec<Event> {
input
.chars()
.map(|c| {
let key_code = match c {
‘\n’ => KeyCode::Enter,
_ => KeyCode::Char(c),
};
Event::Key(KeyEvent::new(key_code, KeyModifiers::NONE))
})
.collect()
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم این سناریوهای رایج را با دیگ بخار نسبتاً کمی آزمایش کنیم.

#[test] fn test_repl_name_error() {
let return_val = run_and_return(“e\n”);
assert!(return_val.contains(“NameError: name ‘e’ is not defined”));
}

#[test] fn test_repl_expr() {
let third_from_last = run_and_return(“12345\n”);
assert_eq!(third_from_last, “12345”);
}

#[test] fn test_repl_statement() {
let return_val = run_and_return(“a = 5.5\n”);

// empty string because a statement does not have a return value
assert_eq!(return_val, “”);
}

#[test] fn test_repl_function() {
let code = r#”
def foo():
a = 10
return 2 * a

foo()
“#;
let return_val = run_and_return(code);
assert_eq!(return_val, “20”);
}

#[test] fn test_repl_ctrl_c() {
let mut events = string_to_events(“123456789\n”);
let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char(‘c’), KeyModifiers::CONTROL));
events.insert(4, ctrl_c);
let mut terminal = MockTerminalIO::new(events);

Repl::new().run(&mut terminal);
assert_eq!(terminal.return_val(), “56789”);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نقاط ورودی کد مرا صبح از رختخواب بلند می کند

یکی از انگیزه‌های من در افزودن یک REPL به این دلیل بود که معتقدم وقتی یک ورودی دوم را اضافه می‌کنید، کد خود را بهتر می‌کنید. شما اساساً دومین کاربر کتابخانه خود هستید، که به شما کمک می‌کند به درک یک انتزاع کامل که همه در جستجوی آن به صفحه‌کلیدهای خود می‌پردازیم، نزدیک‌تر شوید. منظورم این نکته جدی است.

“صفر وابستگی” 😉

REPL اکنون پشت پرچم ویژگی به عنوان راهی برای بازگشت به مدیریت است. من توانایی تفسیر کد پایتون را با کمک جعبه‌های شخص ثالث زنده نگه می‌دارم، به این معنی که crossterm یا باید یک استثنا باشد یا یک پرچم ویژگی معرفی می‌کنم. حالا، اگر بدون فعال بودن REPL کامپایل کنید و «ممفیس» را اجرا کنید، مؤدبانه به شما می گوید «ساخت اشتباه، احمقانه».

خداحافظ

REPL اینجاست. شما می توانید آن را به این صورت اجرا کنید. اگر می خواهید آن را بخرید، به نظر یک کلاهبرداری است. خوب باش و زود حرف بزن

مفسر پایتون من، ممفیس، دارای یک REPL (حلقه خواندن-ایوال-چاپ) است!

این یک خبر قدیمی است. تا زمانی که هنگام تعامل با جغد پیر دانا هیچ اشتباهی مرتکب نشده باشید، می‌توانید به دلخواه تفسیر کنید. با فرض اینکه هرگز نخواستید یک عبارت را دو بار ارزیابی کنید، یا اگر این کار را کردید، بدتان نمی آمد که آن را دوباره تایپ کنید. همچنین با صفر اشتباه.

من از این REPL کاملا راضی بودم. حتی هیجان زده من در مورد این REPL به خانه نوشته بودم. اما رئیس رئیسان من از ما خواست که REPL را برای نتیجه نهایی برای مردم بهبود دهیم. آنها مرا به دفترشان صدا زدند و با سر به صندلی مقابل میز چوب ماهونشان اشاره کردند. “برخی از کاربران انتظار دارند که کلید بک اسپیس کار کند.” به جهنم با کاربران! “فلش رو به بالا باید آخرین فرمان آنها را نشان دهد.” پشتیبانی از کلید پیکان بسیار دهه نود است! “آیا می توانید این کار را تا پایان Q5 انجام دهید؟” من استعفا دادم!

بنابراین به پشت میز خود برگشتم و REPL را بهبود دادم.

آنقدر آن را بهبود دادم که همه کلیدها کار کردند. کلید بک اسپیس، فلش رو به بالا، فلش پایین، فلش چپ، و آخرین، اما نه کم اهمیت، فلش بک اسپیس. یک حسابدار می تواند با REPL جدید یک روز کاری داشته باشد. تیک تیک تیک تیک تیک تیک تیک تیک تیک. این همان اعدادی است که حسابدار تایپ می کند، نه بمبی که به آرامی پخش می شود.

من REPL را به آزمایشگاه فرستادم و به ماشین‌کار اصلی‌ام گفتم که این سفارش را با عجله انجام دهد. این همان پاسخی است که گفتم و از نگاه چشمانشان می‌توانستم بفهمم که فهمیدند. 750 میلی‌ثانیه بعد ساخت کامل شد و ما از کلید پیکان پشتیبانی کردیم. من محصول را به سمت کلاه گیس های بزرگ بردم، برای کارم التماس کردم و از آنها نظرشان را پرسیدم. آنها چند دستور را اجرا کردند، چند چاپ را چاپ کردند و تعدادی افزودنی اضافه کردند. اشتباه کردند و کلید بک اسپیس را زدند. چشمامو چرخوندم چون جدی کی اشتباه میکنه ولی راضی به نظر میومد. آنها متوجه شدند که نمی خواهند فرمان طولانی را که قبلاً تایپ کرده بودند اجرا کنند و اینجا جایی بود که زندگی من در یک سبد دستی به جهنم رفت. آنها ضربه بزنید. Ctrl ج- جدی، چه کسی این کار را می کند؟! می دانید که این به روند فعلی پایان می دهد، درست است؟ درسته؟؟؟

“ما تا پایان سال آینده به پشتیبانی Ctrl-C نیاز داریم.” این افراد و مطالباتشان. من پشتیبانی Ctrl-C را اضافه می کنم. اما قطعا در دو سال آینده نخواهد بود.

بنابراین به میز کارم برگشتم و پشتیبانی از Ctrl-C را اضافه کردم.

چه چیزی این REPL را شایسته افرادی با تمایلات انگشتان چاق کرد؟

آیا من یک ابزار خواهم بود؟

من تمام آینده حرفه ای و مالی خود را برای ساختن چیزها “از ابتدا” در نظر گرفته ام، بنابراین در روز اول این پروژه با یک معضل مواجه شدم. من استفاده کردم crossterm برای تشخیص کلید در درجه اول به دلیل پشتیبانی بین پلتفرمی است. هر چند صادقانه بگویم، crossterm خیلی خیلی خوب بود API بصری است و من به خصوص از آن راضی بودم KeyModifiers (که ما نیاز به رسیدگی داشتیم Ctrl-C، که فکر می کردم غیر ضروری است، در بالا ببینید).

حالت خام دردناک است

ما به آن نیاز داشتیم تا ترمینال کلیدهای خاصی را برای ما جابجا نکند. اما لعنتی، نمی‌دانستم که این باعث می‌شود صفحه ما به یک ماشین تحریر خراب تبدیل شود. به هر حال، من مجبور شدم همه رشته ها را عادی کنم تا قبل از هر کاراکتر خط جدید، یک بازگشت کالسکه اضافه کنم. که خوب کار کرد و من از آن هیجان زده هستم.

/// When the terminal is in raw mode, we must emit a carriage return in addition to a newline,
/// because that does not happen automatically.
fn normalize<T: Display>(err: T) -> String {
    let formatted = format!("{}", err);
    if terminal::is_raw_mode_enabled().expect("Failed to query terminal raw mode") {
        formatted.replace("\n", "\n\r")
    } else {
        formatted.to_string()
    }
}

/// Print command which will normalize newlines + carriage returns before printing.
fn print_raw<T: Display>(val: T) {
    print!("{}", normalize(val));
    io::stdout().flush().expect("Failed to flush stdout");
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست ادغام جالب بود

تحت REPL قدیمی خود (که ترجیح دادم، در بالا ببینید)، می‌توانم آن را با اجرای باینری و ارسال کدهای پایتون به stdin آزمایش کنم. من فکر می کنم به دلیل اختلاف در قرارداد، هنگام استفاده از کراس ترم از کار افتاد. من صادقانه نمی‌توانم آن را به طور کامل توضیح دهم، اما event::read() در تست یکپارچه‌سازی ارائه شده با ورودی stdin به پایان می‌رسد و شکست می‌خورد. پس مسخره اش کردم

pub trait TerminalIO {
    fn read_event(&mut self) -> Result<Event, io::Error>;
    fn write<T: Display>(&mut self, output: T) -> io::Result<()>;
    fn writeln<T: Display>(&mut self, output: T) -> io::Result<()>;
}

/// A mock for testing that doesn't use `crossterm`.
struct MockTerminalIO {                                                        
    /// Predefined events for testing
    events: Vec<Event>,

    /// Captured output for assertions
    output: Vec<String>,
}

impl TerminalIO for MockTerminalIO {
    fn read_event(&mut self) -> Result<Event, io::Error> {
        if self.events.is_empty() {
            Err(io::Error::new(io::ErrorKind::Other, "No more events"))
        } else {
            // remove from the front (semantically similar to VecDequeue::pop_front).
            Ok(self.events.remove(0))
        }
    }

    fn write<T: Display>(&mut self, output: T) -> io::Result<()> {
        self.output.push(format!("{}", output));
        Ok(())
    }

    fn writeln<T: Display>(&mut self, output: T) -> io::Result<()> {
        self.write(output)?;
        self.write("\n")?;
        Ok(())
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که منجر به تبدیل شدن کل موضوع به یک آزمون واحد شد؟ راستش من نمی دانم. در این مرحله، اگر الف) یک باینری را در داخل باینری دیگر فراخوانی کنم، یا 2) یک سرور راه اندازی کنم / یک پورت را باز کنم / در یک سوکت در داخل یک تست گوش دهم، آن را تست یکپارچه سازی می نامم. اگر تعریف دیگری دارید که می‌خواهید در نظرات بنویسید، لطفاً تعریف نکنید زیرا TBH آزاردهنده به نظر می‌رسد.

/// Run the complete flow, from input code string to return value string. If you need any Ctrl
/// modifiers, do not use this!
fn run_and_return(input: &str) -> String {
    let mut terminal = MockTerminalIO::from_str(input);
    Repl::new().run(&mut terminal);
    terminal.return_val()
}

fn string_to_events(input: &str) -> Vec<Event> {
    input
        .chars()
        .map(|c| {
            let key_code = match c {
                '\n' => KeyCode::Enter,
                _ => KeyCode::Char(c),
            };
            Event::Key(KeyEvent::new(key_code, KeyModifiers::NONE))
        })
        .collect()
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم این سناریوهای رایج را با دیگ بخار نسبتاً کمی آزمایش کنیم.

#[test]
fn test_repl_name_error() {
    let return_val = run_and_return("e\n");
    assert!(return_val.contains("NameError: name 'e' is not defined"));
}

#[test]
fn test_repl_expr() {
    let third_from_last = run_and_return("12345\n");
    assert_eq!(third_from_last, "12345");
}

#[test]
fn test_repl_statement() {
    let return_val = run_and_return("a = 5.5\n");

    // empty string because a statement does not have a return value
    assert_eq!(return_val, "");
}

#[test]
fn test_repl_function() {
    let code = r#"
def foo():
    a = 10
    return 2 * a

foo()
"#;
    let return_val = run_and_return(code);
    assert_eq!(return_val, "20");
}

#[test]
fn test_repl_ctrl_c() {
    let mut events = string_to_events("123456789\n");
    let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
    events.insert(4, ctrl_c);
    let mut terminal = MockTerminalIO::new(events);

    Repl::new().run(&mut terminal);
    assert_eq!(terminal.return_val(), "56789");
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نقاط ورودی کد مرا صبح از رختخواب بلند می کند

یکی از انگیزه‌های من در افزودن یک REPL به این دلیل بود که معتقدم وقتی یک ورودی دوم را اضافه می‌کنید، کد خود را بهتر می‌کنید. شما اساساً دومین کاربر کتابخانه خود هستید، که به شما کمک می‌کند به درک یک انتزاع کامل که همه در جستجوی آن به صفحه‌کلیدهای خود می‌پردازیم، نزدیک‌تر شوید. منظورم این نکته جدی است.

“صفر وابستگی” 😉

REPL اکنون پشت پرچم ویژگی به عنوان راهی برای بازگشت به مدیریت است. من توانایی تفسیر کد پایتون را با کمک جعبه‌های شخص ثالث زنده نگه می‌دارم، به این معنی که crossterm یا باید یک استثنا باشد یا یک پرچم ویژگی معرفی می‌کنم. حالا، اگر بدون فعال بودن REPL کامپایل کنید و «ممفیس» را اجرا کنید، مؤدبانه به شما می گوید «ساخت اشتباه، احمقانه».

خداحافظ

REPL اینجاست. شما می توانید آن را به این صورت اجرا کنید. اگر می خواهید آن را بخرید، به نظر یک کلاهبرداری است. خوب باش و زود حرف بزن

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا