יום 2 - תוכנית rot13 ב Rust

28/12/2022

אחרי שלמדנו איך לכתוב כמה תוכניות ראשונות ב Rust בענן ומקומית, ננסה היום לממש אלגוריתם פשוט שנקרא rot13.

1. מה אנחנו בונים

כתבו תוכנית שמקבלת כקלט משורת הפקודה מחרוזת טקסט ומדפיסה בחזרה את המחרוזת מקודדת ב rot13, כלומר כל אות במחרוזת מוחלפת באות שנמצאת בדיוק 13 מקומות אחריה ב abc, לדוגמה כל a תוחלף ב n, כל b תוחלף ב o וכך הלאה. כשעוברים את z מתחילים מההתחלה לכן כל n תוחלף שוב ב a.

בשביל הפשטות נטפל רק במחרוזות שמכילות אותיות קטנות באנגלית בלבד.

2. קוד התוכנית

את כל הקוד לתוכנית (וגם לכל התוכניות בהמשך המדריך) תוכלו למצוא בריפו rust-8-days.

כיוון טוב להתחלה יהיה לייצר מחרוזת חדשה ריקה, לרוץ בלולאה על כל התווים במחרוזת המקורית וכל פעם שפוגשים תו להוסיף את התו "המוחלף" שלו למחרוזת התוצאה. בשביל זה אני צריך שני דברים:

  1. להבין איך לרוץ בלולאה על כל התווים במחרוזת.

  2. להבין איך לחשב מתוך תו את התו שנמצא 13 מקומות אחריו.

נתחיל במשימה הראשונה ועם הלולאה הבאה:


fn rot13(text: &String) -> String {
    let mut result = String::with_capacity(text.len());
    for ch in text.chars() {
        result.push(ch);
    }

    return result;
}

fn main() {
    let input_word = "about".to_string();
    let output = rot13(&input_word);

    println!("{}", output);
}

זה עבר אבל בינתיים מדפיס בדיוק את אותה מחרוזת איתה התחלנו. השלב הבא יהיה להחליף את ch בתו שמגיע 13 תווים אחריו, ולחזור להתחלה אם עברנו את z. גירסה שניה של הקוד היא:

fn rot13(text: &String) -> String {
    let mut result = String::with_capacity(text.len());
    let first_char_numeric_value = 'a' as u8;
    let shift = 13;
    let number_of_characters = 26;

    for ch in text.chars() {
        let current_char_numeric_value = ch as u8;
        let next_char = (((
            (current_char_numeric_value - first_char_numeric_value) + shift) 
            % number_of_characters)
            + first_char_numeric_value)
        as char;

        result.push(next_char);
    }

    return result;
}

הפקודה as מאפשרת להמיר מ char לערך המספרי שלו, וממספר בחזרה ל char. חוץ מזה אנחנו נשארים עם חיבור ושארית שאנחנו מכירים ממקומות אחרים.

3. קבלת קלט משורת הפקודה

בשביל לקבל מחרוזת טקסט משורת הפקודה במקום להשתמש במילה השמורה hello אני משתמש בפקודה הבאה:

let input_word = std::env::args().nth(1).expect("Usage: rot13 text-string");

אנחנו רוצים לגשת לתא השני במערך env::args ולכן מפעילים את nth. מעניין לשים לב שהגישה לתא לא מחזירה מחרוזת אלא משהו שנקרא Option, או "אופציה למחרוזת". ראסט אומר לנו שאולי יש מחרוזת בתא השני אבל אולי משתמש גם לא העביר ערך. ה Option הוא הדרך של ראסט לטפל בשגיאות בצורה מובנית:

  1. כל פונקציה שעשויה להיכשל מחזירה Option (או Result, אבל עליו עוד נדבר).

  2. בשביל לגשת לדבר שהפונקציה החזירה, צריך להשתמש בפונקציות "שחרור" של ראסט, שמכריחות אותנו להעביר ערך ברירת מחדל או הודעת שגיאה אם אנחנו מעדיפים להיכשל. לדוגמה הפונקציה expect בה השתמשתי מקבלת הודעה, ואם ה Option ריק היא תיכשל עם ההודעה שהעברתי.

עכשיו כשאני מפעיל את התוכנית עם cargo run אני מקבל:

Compiling rot13 v0.1.0 (/Users/ynonp/tmp/rust/rust-in-seven-days/day1/rot13)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/rot13`
thread 'main' panicked at 'Usage: rot13 text-string', src/main.rs:25:41
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

אבל אם אני מעביר מילה בתור ארגומנט אקבל אותה בתור ה input_word והקוד ירוץ:

cargo run hello
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rot13 hello`
uryyb

4. הוצאת הפונקציה לקובץ נפרד

לאורך זמן אולי נרצה להוסיף עוד פונקציות המרה ועוד יכולות לתוכנית שלנו, ולכן יהיה נחמד אם כבר עכשיו נלמד איך לפצל את התוכנית למספר קבצים. בשביל לקחת את הפונקציה rot13 לקובץ אחר אני יוצר קובץ חדש בשם utils.rs באותה תיקיה של main.rs, מעביר אליו את קוד הפונקציה ומוסיף את התחילית pub. תוכן הקובץ הוא לכן:

pub fn rot13(text: &String) -> String {
  let mut result = String::with_capacity(text.len());
  let first_char_numeric_value = 'a' as u8;
  let shift = 13;
  let number_of_characters = 26;

  for ch in text.chars() {
      let current_char_numeric_value = ch as u8;
      let next_char = (((
          (current_char_numeric_value - first_char_numeric_value) + shift) 
          % number_of_characters)
          + first_char_numeric_value)
      as char;

      result.push(next_char);
  }

  return result;
}

בקובץ main אני צריך לטעון את הקובץ החיצוני שיצרתי. בגלל שהקובץ utils.rs הוא חלק מהפרויקט הפקודה לטעון אותו היא mod, ולכן אני מוסיף שורה ראשונה חדשה לקובץ main.rs:

mod utils;

ומשנה את הקריאה מ rot13 ל utils::rot13. סך הכל הקובץ נראה כך:

mod utils;

fn main() {
    let input_word = std::env::args().nth(1).expect("Usage: rot13 text-string");
    let output = utils::rot13(&input_word);

    println!("{}", output);
}

בעבודה עם קבצים חיצוניים הרבה פעמים נרצה לקצר את השמות ולא לכתוב את כל ה"נתיב" לפונקציה, כלומר לחזור לכתוב rot13 במקום utils::rot13. הפקודה use מאפשרת לחבר פונקציה ממודול אחר אלינו, כך שלא נצטרך להוסיף לה את שם המודול בתור תחילית. זה נראה ככה:

mod utils;
use std::env;
use utils::rot13;

fn main() {
    let input_word = env::args().nth(1).expect("Usage: rot13 text-string");
    let output = rot13(&input_word);

    println!("{}", output);
}

5. בניה ל Release

הרצה עם cargo run מתאימה למצב פיתוח אבל אם תנסו למדוד זמנים תגלו שהתוכנית רצה יחסית לאט. בשביל לבנות את התוכנית במצב Release כדי שתוכלו גם לשלוח אותה לחברים נפעיל:

cargo build -r

אחרי זה אפשר להפעיל את התוכנית בגירסה המהירה שלה:

./target/release/rot13 hello

6. תרגילי הרחבה

רוצים לוודא שהבנתם וללמוד עוד קצת על ראסט? נסו את התרגילים הבאים:

  1. העתיקו אליכם את המאגר עם קוד התוכנית.

  2. הריצו את התוכנית כדי לראות שהכל עובד לכם.

  3. עדכנו את הקוד כך שיקבל פרמטר שורת פקודה (אופציונאלי) נוסף שיגדיר בכמה להזיז כל תו. ברירת המחדל היא 13.

  4. עדכנו את הקוד כדי לתמוך גם באותיות גדולות באנגלית ובתווים נוספים כמו סימני ניקוד או רווח. את סימני הניקוד והרווח לא צריך לשנות.

  5. בעזרת משתנה סביבה הוסיפו לתוכנית מצב Debug. אם משתנה הסביבה DEBUG מוגדר, התוכנית תדפיס שורת Debug לכל תו במחרוזת בו יודפס התו המקורי והתו אחרי החלפה. אפשר לקרוא על עבודה עם משתני סביבה בראסט בספר.

מוזמנים להדביק פה בתגובות את הפיתרונות שלכם לתרגילי ההרחבה, ואנחנו נמשיך מחר עם תוכנית ראסט נוספת.