es
Feedback
ToCode

ToCode

Ir al canal en Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Mostrar más
1 420
Suscriptores
Sin datos24 horas
+27 días
-230 días
Archivo de publicaciones
ToCode
1 420
# יום 7 - פיתוח ממשק גרפי למשחק Snake ב Rust זוכרים את משחק סנייק מאתמול? אז היום נכתוב ממשק גרפי כדי שנוכל לשחק בו. ## מה אנחנו בונים המצב של פיתוח ממשקים גרפיים ב Rust לא מזהיר: יש מודולים שיודעים לחבר תוכנית Rust לספריות GUI קיימות משפות אחרות (למשל rust-qt, gtk-rs או fltk-rs), אבל הדברים שנכתבו מאפס בראסט עדיין לא בשלים, וגם לא ברור אם אי פעם Rust הולכת לשמש כשפה לפיתוח יישומים גרפיים. כיוון אחד שכן אנשים בונים ב Rust הוא ממשקי ווב בעזרת קומפילציה ל Web Assembly, אבל זה לא מה שרציתי לבנות היום. כיוון אחר שעובד ב Rust הוא כלי שורת פקודה, ובמיוחד כלים שמציגים ממשקים "גרפיים" מבוססי טקסט, וספריה אחת שראיתי שהיתה פופולרית בהרבה פרויקטים היא ספריית cursive, שמאפשרת לבנות ממשק טקסטואלי מבוסס ncurses לתוכניות. זה אומר שהנחש שלנו יקבל מראה רטרו ויהיה מורכב מטקסט בלבד, אבל זה חלק מהכיף. ## קצת על Cursive ל cursive יש מדריך למתחילים שלי מאוד עזר, אפשר למצוא אותו בשלושה חלקים בקישורים: https://github.com/gyscos/cursive/blob/main/doc/tutorial_1.md https://github.com/gyscos/cursive/blob/main/doc/tutorial_2.md https://github.com/gyscos/cursive/blob/main/doc/tutorial_3.md זאת לדוגמה תוכנית Hello World פשוטה מאותו המדריך:
use cursive::views::TextView;

fn main() {
 let mut siv = cursive::default();

 siv.add_global_callback('q', |s| s.quit());

 siv.add_layer(TextView::new("Hello cursive! Press <q> to quit."));

 siv.run();
}
שימו לב איך כל דבר ב Cursive הוא View - אנחנו יוצרים משהו שנקרא TextView, מעבירים לו מחרוזת טקסט ובסוף מפעילים siv.run כדי "להתחיל" את התוכנית. הפקודה run חוזרת רק כשיוצאים מהממשק הטקסטואלי, והתוכנית כולה מציגה באמצע המסך את הטקסט ומחכה עד שנלחץ q כדי לחזור למסוף. ## ממשק גרפי לנחש בתוכנית שלנו אנחנו גם נבנה View, אבל הוא לא יהיה TextView אלא View מותאם אישית שאני בונה לבד. בשביל לבנות כזה אני צריך לממש פונקציה בשם draw, שמקבלת אוביקט בשם Printer ויכולה להשתמש בו כדי לצייר דברים על המסך. הנה המימוש שלי שמצייר את הנחש, בהתבסס על הסטראקט של המשחק שכתבנו אתמול:
fn draw(&self, printer: &Printer) {
    printer.print(self.board.snake.front().unwrap(), match self.direction {
        Direction::Up => "^",
        Direction::Down => "V",
        Direction::Left => "<",
        Direction::Right => ">",
    });
    for snake_pos in self.board.snake.iter().skip(1) {
        printer.print(snake_pos, "X");
    }

    printer.print(self.board.apple, "O");
}
אני מסתכל על הכיוון אליו אנחנו הולכים ולפי הכיוון מצייר את הסימן עבור ראש הנחש. אחרי זה אני ממשיך ומצייר X-ים לכל הגוף של הנחש, ואת האות O בקואורדינטות של התפוח. נשאר לנו רק לוודא שכל שניה תופעל הפונקציה step של המשחק, ואחרי זה תופעל מחדש draw. אה, וגם לטפל בלחיצות על המקלדת כדי לשנות את כיוון הנחש. ## טיפול באירועים הפוקציה on_event של View אחראית על טיפול באירועים, והאירוע היחיד שמעניין אותנו נקרא Key, כלומר לחיצה על כפתור במקלדת. הנה המימוש שבוחר ערך חדש ל self.direction לפי הכפתור שנלחץ:
fn on_event(&mut self, event: Event) -> EventResult {
    match event {
        Event::Key(key) => {
            self.direction = match key {
                Key::Down => Direction::Down,
                Key::Up => Direction::Up,
                Key::Left => Direction::Left,
                Key::Right => Direction::Right,
                _ => self.direction,
            };
            self.step();
        }
        _ => (),
    }
    EventResult::Ignored
}
## עבודה ברקע ב Cursive

ToCode
1 420
הרבה מה לקרוא כאן, אז בואו נדבר על הדברים המרכזיים: 1. אני מגדיר את הנחש בתור רשימה מקושרת, כדי שיהיה קל למחוק את החוליה האחרונה ולהוסיף חוליה לראש (כלומר לזוז). הפונקציה מתחילה בלקיחת הערך הראשון מאותה רשימה, כלומר ראש הנחש. אני יודע שהוא חייב להיות שם ולכן לא מודאג מהשימוש ב unwrap כדי לקבל את הערך. 2. בעזרת match אני בונה את next_head להיות המיקום הבא של הנחש. 3. אני בודק אם הנחש יצא מהלוח, ואם כן מחזיר false. 4. מוסיף את המיקום החדש לראש הנחש. 5. ומסיימים בתפוח - אם הנחש אכל עכשיו תפוח מגרילים מקום חדש לתפוח הבא, אם הנחש לא אכל את התפוח מורידים את החוליה האחרונה מהזנב. ## איך כותבים בדיקות בשביל לדעת שקוד הנחש עובד לפני שיש לנו ממשק גרפי, אפשר להפעיל אותו מתוך פונקציות בדיקה. ב Rust אני מגדיר מודול בדיקה ממש באותו קובץ עם הקוד הרגיל עם הפקודות:
#[cfg(test)]
mod tests {
  use super::*;
כל פונקציית בדיקה בתוך המודול צריכה לקבל את המאפיין test, לדוגמה:
#[test]
fn test_can_move() {
}
ובתוך פונקציית בדיקה אני יכול להשתמש בפונקציות עם התחילית assert כדי לבדוק שדברים מכילים את הערך שאני צריך. לדוגמה:
#[test]
fn test_can_move() {
  let mut game = Game::new();
  game.step(Direction::Up);
  game.step(Direction::Up);
  game.step(Direction::Up);

  assert_eq!(game.snake.front().unwrap().x, 10);
  assert_eq!(game.snake.front().unwrap().y, 7);
  assert_eq!(game.snake.len(), 1);
}
פונקציית הבדיקה יוצרת משחק חדש, מזיזה את הנחש שלושה צעדים למעלה ואז בודקת את המיקום של הנחש ואת הגודל שלו. מאחר ואני יודע איפה הנחש הראשון מתחיל ואיפה התפוח מתחיל אני בטוח שכל פעם שאפעיל את הבדיקה היא תעבור. במאגר בגיטהאב תוכלו למצוא עוד כמה בדיקות שכתבתי כדי לבדוק שהנחש מצליח לאכול את התפוח ושהמשחק מסתיים כשהנחש מתנגש בקיר. בשביל להריץ את הבדיקות משורת הפקודה אני כותב:
cargo test
הפלט כולל אזהרות על משתנים שלא השתמשתי בהם ואת הטקסט:
running 3 tests
test game::tests::test_can_move ... ok
test game::tests::test_can_eat_apple ... ok
test game::tests::test_can_hit_a_wall ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
יש המון אפשרויות להפעלת טסטים, למשל בשביל להפעיל רק טסטים מקובץ מסוים או שהשם שלהם מתאים למחרוזת מסוימת. תוכלו להפעיל משורת הפקודה cargo test --help כדי לקבל רשימה של כל האופציות. ## תרגילים להרחבה מחר נכתוב GUI למשחק סנייק, אבל כבר בינתיים יש הרבה מה לעשות עם הנחש שכבר בנינו. הנה כמה תרגילים שיעזרו לכם להתיידד עם הקוד ועם הרעיון של בדיקות בראסט: 1. הוסיפו בדיקות כדי לוודא שהמשחק יסתיים אם הנחש יתנגש בקירות האחרים (כרגע בריפו בודקים רק את הקיר העליון). 2. עדכנו את הקוד כך שהמשחק יסתיים גם אם הנחש מתנגש בעצמו. הוסיפו בדיקות כדי לוודא שהעדכון שלכם עובד. 3. הוסיפו תמיכה ב"תפוח זהב". תפוח זהב יכול להופיע במקום תפוח רגיל, ואחרי שאוכלים אותו הנחש גדל ב-4 חוליות. אל תשכחו להוסיף בדיקות מתאימות לתפוח הזהב שלכם. מוזמנים להדביק פה את הפיתרונות שלכם להרחבות ומחר נמשיך לכתוב GUI למשחק הנחש.

ToCode
1 420
# יום 6 - כתיבת בדיקות יחידה ב Rust ראסט מציע תמיכה מובנית בכתיבת בדיקות יחידה למודולים. בשיעור זה נתחיל לבנות משחק Snake ונראה איך לבדוק את קוד המשחק שלנו לפני שמתחילים לצייר את ה GUI. ## מה אנחנו בונים המטרה היא לבנות משחק סנייק, אבל לא נספיק לבנות אותו ביום אחד, ולכן אני מחלק את העבודה לשני פוסטים - היום נכתוב את הלוגיקה של המשחק ונשתמש בבדיקות יחידה כדי לראות שאנחנו בכיוון, ומחר נחבר לו ממשק משתמש גרפי כדי שאפשר יהיה גם לשחק. ## קוד המשחק בשביל לבנות משחק סנייק אני צריך שני דברים - אחד הוא קובץ הלוגיקה שאחראי על המיקום של הנחש, התנועה והתאונות שאולי קורות לנחש. השני הוא הממשק הגרפי. בניית הלוגיקה מראש ובדיקה שלה תעזור לנו כשנגיע לכתוב את קוד ה UI, כי אז נוכל להתמקד בממשק ולהיות רגועים לגבי מבנה המשחק. לכן המטרה שלי היום היא לבנות את הפונקציות של הלוגיקה של הנחש, ולהשתמש בבדיקות יחידה כדי לוודא שכל הפונקציות שלי עובדות טוב. ארצה לבנות: 1. סטראקט בשם Game שמייצג את מצב המשחק - איפה הנחש, איפה התפוח ומה גודל המסך. 2. פונקציה בשם step שמקבלת את הכיוון של הנחש ומזיזה את הנחש קדימה. הפונקציה אחראית לבדוק אם הנחש אכל את התפוח או הנתגש בקיר והיא מחזירה משתנה בוליאני, ערך אמת אומר שאפשר להמשיך לשחק וערך שקר אומר שהמשחק הסתיים. 3. פונקציות בדיקה לשתי הפונקציות שתיארתי, כדי שלא נהיה מופתעים כשנחבר ממשק גרפי. נתחיל עם הדברים שאנחנו כבר מכירים. בשביל לכתוב קוד בקובץ נפרד אני פשוט פותח את הקובץ בעורך הטקסט. כל דבר שארצה להשתמש בו צריך לקבל את התחילית pub, לכן מבנה הנתונים של המשחק יהיה:
pub struct Game {
    pub size: Vec2,
    pub snake: LinkedList<XY<usize>>,
    pub apple: XY<usize>,
    pub rng: ThreadRng,
}
ובשביל לדעת מה הכיוון בו מתקדמים אני יכול להגדיר enum:
pub enum Direction {
  Up,
  Down,
  Left,
  Right,
}
בשביל המספרים האקראיים (איפה יהיה התפוח) התקנתי ספריה חיצונית בשם rand. אני טוען אותה וגם מייבא את LinkedList למרחב השמות שלי עם הפקודות:
use std::collections::LinkedList;
use rand::{thread_rng, Rng, rngs::ThreadRng};
וממשיכים לקוד של המשחק. אנחנו יודעים שבדרך כלל אחרי שמגדירים Struct אפשר פשוט לתת ערכים לכל השדות שלו כדי לקבל משתנה מאותו הסוג, כלומר במקרה של Game אפשר היה לכתוב:
let game = Game {
    size: Vec2 { x: 20, y: 20 },
    snake: LinkedList::from([ XY::new(10, 10)]),
    apple: XY::new(5, 5),
    rng: thread_rng(),
};
אבל בשביל שיהיה נוח לקוד שמשתמש ב Struct אני יוצר פונקציה בשם new שמחזירה משחק בדיוק עם הערכים האלה. בצורה כזאת קוד חיצוני שישתמש במשחק יוכל לכתוב רק:
let mut game = Game::new();
הפונקציה אגב נראית כך:
pub fn new() -> Self {
    let game = Game {
        size: Vec2 { x: 20, y: 20 },
        snake: LinkedList::from([ XY::new(10, 10)]),
        apple: XY::new(5, 5),
        rng: thread_rng(),
    };

    return game;
}
והיא מוגדרת בתוך בלוק impl של המשחק:
impl Game {
    // all "Game" functions go here
    // pub fn new() -> Self { ... }
}
הפונקציה השניה והיותר מעניינת שמוגדרת על Game היא step, והיא זאת שמזיזה את המשחק צעד אחד קדימה. ככל שנפעיל אותה לעתים יותר קרובות כך המשחק יהיה יותר קשה. הנה הקוד:
    pub fn step(&mut self, dir: Direction) -> bool {
      let head = self.snake.front().unwrap();

      let next_head = match dir {
        Direction::Up => XY::new(head.x, head.y - 1),
        Direction::Down => XY::new(head.x, head.y + 1),
        Direction::Left => XY::new(head.x - 1, head.y),
        Direction::Right => XY::new(head.x + 1, head.y),
      };

      if (next_head.x >= self.size.x) || (next_head.x == 0) || (next_head.y >= self.size.y) || (next_head.y == 0) {
        return false;
      }

      self.snake.push_front(next_head);

      if (next_head.x != self.apple.x) || (next_head.y != self.apple.y) {
        self.snake.pop_back();
      } else {
        // ate an apple
        self.apple.x = self.rng.gen_range(0..self.size.x);
        self.apple.y = self.rng.gen_range(0..self.size.y);
      }
      
      return true;
    }

ToCode
1 420
4. הפקודה io::copy של ראסט מקבלת Reader ו Writer, ומעתיקה את כל התוכן של ה Reader ל Writer. אפשר לקרוא עליה בתיעוד כאן https://doc.rust-lang.org/std/io/fn.copy.html. החליפו את קוד הקריאה והכתיבה בקריאה ל copy, כדי שהמערכת תעבוד גם על קלט שלא מכיל תווי ירידות שורה. מוזמנים להדביק את הפיתרונות או שאלות אם יש לכם כאן בתגובות, ואנחנו נמשיך מחר לכתוב משחק קצר ב Rust.

ToCode
1 420
# יום 5 - מימוש שרת Echo ב Rust אחרי כל הדברים היפים שמצאתי ב Rust, היום גיליתי את שני הדברים שעדיין חסרים - תמיכה מובנית במודל תכנות אסינכרוני ותמיכה ב Green Threads. בואו נראה את שתי הבעיות דרך בניית שרת Echo. ## מה אנחנו בונים נרצה לבנות שרת שמאזין לבקשות, כל פעם שמשתמש מתחבר (פשוט באמצעות TCP Socket) השרת יקרא שורה מהמשתמש, יכתוב אותה בחזרה לאותו Socket ואז ימתין לשורה הבאה. בשביל לדבר עם כמה לקוחות במקביל השרת ישתמש ב Threads, ונפתח Thread לכל לקוח חדש שמתחבר. ## קוד התוכנית הקוד לא ארוך וילמד אותנו איך לעבוד ברשת ובצורה מקבילית ב Rust. קודם הדבקה אחרי זה הסברים:
use std::io::{Write, BufReader, BufRead, BufWriter};
use std::net::TcpListener;
use std::thread;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:9123").unwrap();
    println!("listening started, ready to accept");
    for stream in listener.incoming() {
        thread::spawn(|| {            
            let stream = match stream {
                Ok(s) => s,
                Err(_) => return,
            };

            let mut reader = BufReader::new(&stream);
            let mut writer = BufWriter::new(&stream);

            loop {
                let mut line = String::new();
                match reader.read_line(&mut line) {
                    Ok(0) => return,
                    Ok(l) => l,
                    Err(_) => return,
                };
    
                let output = format!("You Said: {}", &line);                
                if let Err(_) = writer.write(output.as_bytes()) {
                    return;
                }
                if let Err(_) = writer.flush() {
                    return;
                }
            }
        });
    }
}
## שימו לב לשני הקווים האנכיים הפקודה bind מתחילה להאזין ל Port ואפילו בלי לקרוא עליה אנחנו יודעים שהיא מחזירה Result, בגלל שהקוד מפעיל unwrap עליו כדי לקבל את מה שבפנים. אם לא הצלחנו לתפוס את הפורט זה בסדר לסיים את התוכנית ולא להפעיל את השרת. גם לולאת ה for בשורה הבאה לא צריכה להפתיע. אבל מה שקורה בתוך הלולאה זה סיפור אחר:
thread::spawn(|| {
הפקודה thread::spawn מתחילה Thread חדש. היא מקבלת פרמטר מסוג שעדיין לא נתקלנו בו ונקרא Closure. קלוז'ר הוא בעצם פונקציה שיכולה "לתפוס" את הסביבה שלה, ולגשת למשתנים שהוגדרו מעליה. זה מבנה שאנחנו מכירים משפות דינמיות רבות ובמיוחד JavaScript ו Python. הקלוז'ר של thread::spawn מקבלת זה הקוד שה Thread יריץ, כלומר הקוד שירוץ במקביל לתוכנית הנוכחית. אפשר להגיד שכל איטרציה של הלולאה מתחילה פעולה ברקע, ולכן הלולאה מסתיימת ממש מהר. הפעולות ברקע יכולות להיות איטיות אבל הן לא מפריעות להתקדמות הרגילה של השרת, שמהר מאוד יחזור לחפש עוד חיבורים חדשים. ה Thread ש thread::spawn מייצרת הוא תהליכון של מערכת ההפעלה ולא Green Thread. מצד אחד זה אומר שהקוד שלנו קרוב יותר לברזלים, אבל מצד שני אנחנו מפסידים ביצועים טובים יותר שהיינו יכולים לקבל מספריית ה Thread-ים של ראסט. מה שיותר צורם בקוד הזה הוא הבחירה לעבוד ב Thread-ים במקום במודל פיתוח אסינכרוני, שעדיין לא מובנה בשפת Rust. יש ספריית עזר בשם tokio שכן מאפשרת את המודל האסינכרוני, ואנחנו עוד נחזור לדבר עליה. מבחינת הקוד שרץ בתוך ה Thread, שם לא רציתי לרסק את התהליכון בשום מצב ולכן הקפדתי להשתמש ב match כדי לזהות שגיאות ולא ב unwrap. מבנה כזה יכול לעזור אם חשוב לנו לסגור את החיבור בצורה מסודרת ולא להראות שגיאות בלוג של השרת, אבל ממשחקים שעשיתי גם unwrap עובד די טוב והשרת שורד בעיות תקשורת אצל לקוח אחד בלי לפגוע בשאר הלקוחות. ## תרגילים להרחבה חושבים שהבנתם איך שרת ראסט עובד? בואו נלך לבעוט בו קצת: 1. הפעילו את השרת שיאזין לפורט 9123. השתמשו ב telnet או nc כדי להתחבר אליו מחלון אחר ותראו שאתם מצליחים לשלוח הודעה ולקבל אותה חזרה. 2. הוסיפו מנגנון שסופר כמה לקוחות פעילים יש. כל פעם שלקוח מתחבר יש להדפיס במסך של השרת הודעה שלקוח חדש התחבר וכמה לקוחות כרגע יש. כל פעם שלקוח מתנתק יש להדפיס הודעה דומה במסך של השרת. 3. הפכו את שרת ה Echo לשרת Chat - שמרו וקטור של כל הלקוחות שכרגע מחוברים, וכל פעם שמישהו שולח הודעה שלחו את ההודעה הזאת לכל הלקוחות האחרים.

ToCode
1 420
את הפקודה unwrap אנחנו כבר מכירים - היא מחזירה את התוכן של האופציה Ok מ Result, או מרסקת את התוכנית עם שגיאה אם אותו Result מייצג שגיאה. הפקודה unwrap_or היא גירסה פחות אלימה של אותה unwrap. אם היא מופעלת על Result עם שגיאה, היא מקבלת ערך (עבור ה Or), ואם יש ב Result שגיאה היא תחזיר את אותו ערך. הפקודה expect של Option מחזירה את הערך אם קיים, או מרסקת את התוכנית אם יש שגיאה. סך הכל במשתנה lines נקבל את הערך של השדה lines מ cli, אבל אם לא הוגדר אחד נקבל 10. זיכרו שהשדה lines היה מוגדר בסטראקט כך:
#[arg(short='n')]
lines: Option<String>,
מאפיין אופציונאלי מסוג String והאות שמתאימה לאופציה היא n. המאפיינים reverse ו name לא צריכים טיפול מיוחד, ואחרי הפיענוח אפשר להעביר את כולם לפונקציה tail_file. זה המימוש שלה:
fn tail_file(filename: Option<&str>, lines_count: usize, reverse: bool) {    
    let mut last_lines: Vec<String> = Vec::with_capacity(lines_count);

    let reader: Box<dyn BufRead> = match filename {
        None => Box::new(BufReader::new(stdin())),
        Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
    };

    let mut idx = 0;
    for line in reader.lines() {        
        if idx < last_lines.len() {
            last_lines[idx] = line.unwrap();
        } else {
            last_lines.push(line.unwrap());
        }
        idx = (idx + 1) % lines_count;
    }

    for i in 0..lines_count {
        if reverse {
            let j = lines_count - (i + 1);
            println!("[{}] {}", j, last_lines[(idx + j) % lines_count]);
        } else {
            println!("[{}] {}", i, last_lines[(idx + i) % lines_count]);
        }
        
    }
}
קצת ארוכה אבל אין הרבה חדש. הקוד מגדיר משתנה מסוג Vec<String> שזה וקטור של מחרוזות. וקטור זה ישמור את השורות האחרונות בקובץ, אותן נרצה להדפיס כשהקלט ייגמר. אני יוצר BufReader מהקובץ בדיוק כמו שעשיתי אתמול עם wc, ואז רץ על הקובץ ושומר את השורות לוקטור. בעזרת אופרטור השארית אני יכול למלא את הוקטור וכשהוא ייגמר אוטומטית השורה הבאה תיכתב להתחלה, וכך בסוף הקלט הוקטור יכיל את ה n שורות האחרונות. החלק האחרון של הפונקציה רץ בלולאה על הוקטור ומדפיס את השורות, לפעמים בסדר הנכון ולפעמים בסדר הפוך, לפני ערך המשתנה reverse. ## תרגילים להרחבה אפשר לקרוא די הרבה על clap בדף התיעוד שלה בקישור: https://docs.rs/clap/latest/clap/. התחילו שם ונסו לעדכן את הקוד לפי הסעיפים הבאים: 1. הוסיפו הסבר קצר על התוכנית שיופיע כשמפעילים את מסך העזרה. 2. הוסיפו אפשרות לקבל את מספר השורות גם באמצעות האופציה הארוכה --lines, כלומר שאפשר יהיה להפעיל את התוכנית עם tail --lines 15. 3. הוסיפו תמיכה ב tail על מספר קבצים. אם התוכנית קיבלה מספר שמות של קבצים בשורת הפקודה יש להפעיל tail על כל אחד מהם. 4. פקודת tail האמיתית תומכת באפשרות שנקראת Follow, בה מציגים את השורות האחרונות ואז מחכים שייכתב מידע נוסף לקובץ ואז ממשיכים להציג גם אותו. קראו על הספריה notify של ראסט והוסיפו תמיכה ב Follow לפקודת ה tail שלנו. עד לכאן טיפול בקלט משורת הפקודה ב Rust. מחר ניקח הפסקה מתוכניות שורת פקודה ונלך לראות איך Rust מתמודד עם ביצוע משימות במקביל ועבודה ברשת. כמו תמיד אם פתרתם חלק מתרגילי ההרחבה מוזמנים להדביק את הקוד כאן בתגובות.

ToCode
1 420
# יום 4 - מימוש tail ב Rust אם יש משהו שלמדתי מהתרגיל של אתמול זה החשיבות (והקושי) בפיענוח ארגומנטים שהגיעו משורת הפקודה. היום נדבר על הספריה clap שהופכת את הכתיבה של יישומי שורת פקודה למשימה הרבה יותר נעימה. ## מה אנחנו בונים אחרי שכתבנו את wc נוכל להמשיך לכלי יוניקס מפורסם נוסף - זהו tail. הפקודה tail קצת יותר מאתגרת מ wc, כי בשבילה נצטרך ללמוד איך לשמור מספר שורות יחד בוקטור, ובנוסף נרצה להתקין ספריה חיצונית בשם clap שתעזור לנו בפיענוח ארגומנטים שקיבלנו משורת הפקודה. כמו תמיד הקוד לפוסט נמצא בגיטהאב בקישור https://github.com/ynonp/rust-8-days/tree/main/day4%20-%20tail/tail. בואו נראה את עיקרי הדברים ואז נדבר עליהם. ## קוד התוכנית אני מתחיל את סקירת הקוד מהקובץ Cargo.toml, קובץ שהופיע בכל הפרויקטים שכתבנו עד עכשיו אבל לא זכה לתשומת לב. קובץ זה מגדיר את מבנה הפרויקט ובין השאר מגדיר את התלויות - ספריות חיצוניות ממאגר חבילות רשמי של rust שהפרויקט שלנו צריך. הכלי Cargo באופן אוטומטי קורא את הקובץ ומתקין את התלויות כשאנחנו מפעילים cargo run. הקובץ מכיל את התוכן הבא:
[package]
name = "tail"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.0.19", features = ["derive"] }
הפורמט נקרא toml והוא מהווה אלטרנטיבה נוחה ל yml עבור קבצי קונפיגורציה. הקובץ מחולק לבלוקים כאשר כל בלוק מקבל כותרת בתוך סוגריים מרובעים, במקרה שלנו הבלוקים הם package ו dependencies. הבלוק package מספר על הפרויקט, והבלוק dependencies מכיל רשימה של הספריות החיצוניות בהן הפרויקט משתמש. חבילה חיצונית בראסט נקראת Crate וכולן מאוחסנות במאגר https://crates.io/. את הדף של clap נוכל למצוא בקישור https://crates.io/crates/clap. בחזרה ל main.rs שמתחיל עם הבלוק:
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    name: Option<String>,

    #[arg(short)]
    reverse: bool,

    #[arg(short='n')]
    lines: Option<String>,
}
אנחנו כבר יודעים להגדיר struct, אבל מה זה התחביר שמעל ה Struct? ב Rust אפשר להוסיף למבנים מסוימים Metadata בעזרת סימן הסולמית. זה נקרא בראסטית Attributes ואפשר לקרוא עליהם בקישור https://doc.rust-lang.org/rust-by-example/attribute.html. המאפיין derive למשל מגדיר מימוש בסיסי ל Trait אותו הקומפיילר ישתול בצורה אוטומטית. המאפיין command ייקרא על ידי הספריה clap ויגיד לקלאפ שזה הסטראקט שמגדיר את הארגומנטים שאנחנו מצפים לקבל משורת הפקודה. מה שנמצא בסוגריים זה פרמטרים שקלאפ ישתמש בהם, במקרה שלנו כדי לדעת איזה מידע להציג כשמישהו מפעיל את התוכנית. מה שיותר מעניין זה מה שקורה בתוך אותו הסטראקט - כל אחד מהשדות בו יהפוך לארגומנט שהתוכנית יכולה לקבל משורת הפקודה, ובעזרת המאפיין arg אפשר להגדיר עוד פרטים לגבי אותו ארגומנט, למשל להגדיר אותו בתור short (שזה אומר שהוא יופעל עם אות אחת) ולציין מה האות. שימו לב מה קורה כשאני מפעיל את התוכנית עם המתג --help:
$ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/tail --help`
Usage: tail [OPTIONS] [NAME]

Arguments:
  [NAME]

Options:
  -r
  -n <LINES>
  -h, --help      Print help information
  -V, --version   Print version information
מסך העזרה מיוצר על ידי ספריית clap, ומציג את כל הארגומנטים והאופציות שהתוכנית מצפה לקבל. הרשימה הזאת נבנית ישירות מהסטראקט CLI. בואו נמשיך ונראה איך main מפענח את הארגומנטים אחרי שהגדרנו את הסטראקט:
fn main() {
    let cli = Cli::parse();

    // You can check the value provided by positional arguments, or option arguments
    let lines: usize = cli.lines.unwrap_or(String::from("10")).parse().expect("n is not a number");

    println!("Value for reverse: {}", cli.reverse);
    println!("Value for n = {}", lines);
    println!("name: {:?}", cli.name.as_deref());

    tail_file(cli.name.as_deref(), lines, cli.reverse);
}
מעט שורות עם הרבה תוכן:

ToCode
1 420
קוד הטיפול ב None וב Some צריך להחזיר את אותו טיפוס למשתנה reader, אבל הבעיה ש BufReader::new מחזיר טיפוס מידע שונה לפי הדבר שהוא קיבל (זה נקרא Generics). בשביל לתקן את זה אני עוטף את שני ה reader-ים ב Box ומבקש מהקומפיילר לא להקצות מראש מקום בזיכרון ל Box, אלא לחשב את הגודל דינמית בזמן ריצה, בעזרת המילה השמורה dyn. סך הכל יהיה לי ב reader קופסה שבתוכה יהיה BufReader. אני לא יודע בזמן קומפילציה איזה BufReader בדיוק זה יהיה, אבל זה בסדר - כי עם שניהם אני עובד באותה צורה. ## לולאת הקריאה עצמה הפונקציה reader.lines מחזירה אוסף של כל השורות בדבר שה Reader קורא ממנו, בין אם זה קובץ או stdin. לכן אפשר לרוץ בלולאה:
for line in reader.lines() {
    let the_line = line.unwrap();
    line_count += 1;
    words_count += the_line.split_whitespace().count();
    char_count += the_line.chars().count() + 1; // +1 because of the newline
}
ושימו לב לפקודה line.unwrap - דיברנו על Option וראינו שפונקציות מסוימות שמחזירות ערך אופציונאלי מחזירות Option, וכך "מכריחות" את מי שקורא להן לבדוק אם היה שם ערך או לא (כלומר מונעות מצב של עבודה עם Null Values). החלופה ל Option היא Result. גם זה Enum שהערכים שהוא יכול להחזיק הם Ok ו Err. ל Ok יש פרמטר שזו התוצאה (במקרה שלנו השורה שנקראה), ול Err יש פרמטר שזו הודעת השגיאה. הפקודה unwrap של Result בודקת את מה שכתוב בפנים, אם זה Ok היא תחזיר את התוצאה ואם זה Err היא תרסק את התוכנית עם הודעת השגיאה. בתוכניות גדולות כדאי להיזהר מפקודה זו ולהשתמש בבדיקה מפורשת עם match, אבל בלולאה שלנו זה עדיין בסדר לסיים את התוכנית אם לא הצלחנו לקרוא שורה. ## תרגילים להרחבה אחרי שהפעלתם את התוכנית אצלכם וראיתם שאתם מצליחים להריץ אותה עם שם קובץ ובלי, נסו את התרגילים הבאים להרחבה: 1. חלקו את ה main לשתי פונקציות, אחת מפענחת את הקלט שהתקבל משורת הפקודה והשניה מכילה את לולאת הקריאה מהקובץ. 2. הוציאו את הפונקציות לקובץ נפרד, ושלבו אותו ב main עם פקודה mod. 3. הוסיפו תמיכה במתגים האופציונאליים w, c ו-l כדי שאפשר יהיה להדפיס רק את מספר השורות, מספר המילים או מספר התווים בקלט. נסו לתמוך גם בכתיב נפרד למשל wc -l -w וגם בכתיב המשולב wc -cl. 4. הוסיפו אפשרות לקרוא מספר קבצים, אם משתמש מעביר שמות של מספר קבצים בשורת הפקודה למשל wc file1.txt file2.txt. מחר נדבר על ספריית ההרחבה clap שמאפשרת לטפל בצורה מהירה בכל האפשרויות לפיענוח קלט משורת הפקודה. עד אז מוזמנים להדביק את הפיתרונות שלכם כאן בתגובות.

ToCode
1 420
# יום 3 - מימוש wc ב Rust היום נבנה גירסת Rust לתוכנית שורת פקודה פופולרית ביוניקס שנקראת wc. התוכנית סופרת כמה תווים, שורות ומילים היא קיבלה בקלט. ## מה אנחנו בונים לספור כמה שורות, מילים ותווים יש בקלט זה לא החלק הקשה בבניית wc, אלא האינטגרציה שלה עם העולם- 1. התוכנית יכולה לקבל שם קובץ משורת הפקודה, ואז היא תקרא את כל הקלט מאותו קובץ. 2. אם לא קיבלה שם קובץ התוכנית תקרא את הקלט מ Standard Input. לכן האתגרים שלנו היום יהיו ללמוד איך קוראים מידע מקבצים ואיך קוראים מ Standard Input, ואיך לכתוב פונקציה שיודעת לקרוא משני המקורות באותה צורה. ## קוד התוכנית הקוד הפעם קצר. תחילה הדבקה ואחריה הסברים:
use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader};

fn main() {
    let input = env::args().nth(1);
    let reader: Box<dyn BufRead> = match input {
        None => Box::new(BufReader::new(io::stdin())),
        Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
    };

    let mut line_count: usize = 0;
    let mut char_count: usize = 0;
    let mut words_count: usize = 0;

    for line in reader.lines() {
        let the_line = line.unwrap();
        line_count += 1;
        words_count += the_line.split_whitespace().count();
        char_count += the_line.chars().count() + 1; // +1 because of the newline
    }

    println!("{} {} {}", line_count, words_count, char_count);
}
## קריאת שם הקובץ משורת הפקודה בחלק הראשון של התוכנית אני קורא את שם הקובץ מהארגומנט השני משורת הפקודה, בדיוק כמו שראינו בדוגמה מאתמול. מה ששונה הפעם הוא שאני נשאר עם Option<String> בגלל שהפרמטר אופציונאלי - אולי יהיה שם קובץ, ואולי לא ואז נצטרך לקרוא מ stdin. בהינתן Option (ב Rust זה נקרא enum), אני יכול לטפל בכל האפשרויות שלו עם פקודת match. בואו ננסה את זה בדוגמה פשוטה ב Playground לפני שנמשיך לקרוא את הקוד של wc. נתחיל עם ה enum הראשון שמגדיר 3 צבעים:
enum Color {
    Red,
    Blue,
    Green
}

fn main() {
    let my_color = Color::Red;
    match my_color {
        Color::Red => println!("It's red!"),
        Color::Blue => println!("It's blue!"),
        Color::Green => println!("It's green!"),
    }
}
הפקודה match מקבלת משתנה ובודקת אם הוא מתאים לכל אחת מהאפשרויות. בעיקרון אפשר להשתמש ב match לכל סוג משתנה, אבל ל enum הוא מתאים כמו כפפה ליד. enum-ים בראסט יכולים גם להחזיק ערך (או כמה), ואז אפשר לכתוב דברים מדליקים הדוגמה הבאה מהספר Rust By Example:
enum WebEvent {
    // An `enum` may either be `unit-like`,
    PageLoad,
    PageUnload,
    // like tuple structs,
    KeyPress(char),
    Paste(String),
    // or c-like structures.
    Click { x: i64, y: i64 },
}

// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("page loaded"),
        WebEvent::PageUnload => println!("page unloaded"),
        // Destructure `c` from inside the `enum`.
        WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
        WebEvent::Paste(s) => println!("pasted \"{}\".", s),
        // Destructure `Click` into `x` and `y`.
        WebEvent::Click { x, y } => {
            println!("clicked at x={}, y={}.", x, y);
        },
    }
}
וזה יפה כי ה match מתאים למבנה של ה Enum, וגם מאפשר להגדיר משתנים שיקחו את הערכים מתוך ה enum. בקוד של wc רשמתי את הביטוי הבא:
let reader: Box<dyn BufRead> = match input {
    None => Box::new(BufReader::new(io::stdin())),
    Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
};
הקוד בודק את הערך של input מול שתי האפשרויות של Option - או שיש שם Some, ואז הפרמטר הוא שם הקובץ, או שיש שם None ואז אין לנו שם קובץ ועלינו לקרוא את הקלט מ stdin. ## יצירת ה reader

ToCode
1 420
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);
}
## בניה ל Release הרצה עם cargo run מתאימה למצב פיתוח אבל אם תנסו למדוד זמנים תגלו שהתוכנית רצה יחסית לאט. בשביל לבנות את התוכנית במצב Release כדי שתוכלו גם לשלוח אותה לחברים נפעיל:
cargo build -r
אחרי זה אפשר להפעיל את התוכנית בגירסה המהירה שלה:
./target/release/rot13 hello
## תרגילי הרחבה רוצים לוודא שהבנתם וללמוד עוד קצת על ראסט? נסו את התרגילים הבאים: 1. העתיקו אליכם את המאגר עם קוד התוכנית. 2. הריצו את התוכנית כדי לראות שהכל עובד לכם. 3. עדכנו את הקוד כך שיקבל פרמטר שורת פקודה (אופציונאלי) נוסף שיגדיר בכמה להזיז כל תו. ברירת המחדל היא 13. 4. עדכנו את הקוד כדי לתמוך גם באותיות גדולות באנגלית ובתווים נוספים כמו סימני ניקוד או רווח. את סימני הניקוד והרווח לא צריך לשנות. 5. בעזרת משתנה סביבה הוסיפו לתוכנית מצב Debug. אם משתנה הסביבה DEBUG מוגדר, התוכנית תדפיס שורת Debug לכל תו במחרוזת בו יודפס התו המקורי והתו אחרי החלפה. אפשר לקרוא על עבודה עם משתני סביבה בראסט בספר. מוזמנים להדביק פה בתגובות את הפיתרונות שלכם לתרגילי ההרחבה, ואנחנו נמשיך מחר עם תוכנית ראסט נוספת.

ToCode
1 420
# יום 2 - תוכנית rot13 ב Rust אחרי שלמדנו איך לכתוב כמה תוכניות ראשונות ב Rust בענן ומקומית, ננסה היום לממש אלגוריתם פשוט שנקרא rot13. ## מה אנחנו בונים כתבו תוכנית שמקבלת כקלט משורת הפקודה מחרוזת טקסט ומדפיסה בחזרה את המחרוזת מקודדת ב rot13, כלומר כל אות במחרוזת מוחלפת באות שנמצאת בדיוק 13 מקומות אחריה ב abc, לדוגמה כל a תוחלף ב n, כל b תוחלף ב o וכך הלאה. כשעוברים את z מתחילים מההתחלה לכן כל n תוחלף שוב ב a. בשביל הפשטות נטפל רק במחרוזות שמכילות אותיות קטנות באנגלית בלבד. ## קוד התוכנית את כל הקוד לתוכנית (וגם לכל התוכניות בהמשך המדריך) תוכלו למצוא בריפו 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. חוץ מזה אנחנו נשארים עם חיבור ושארית שאנחנו מכירים ממקומות אחרים. ## קבלת קלט משורת הפקודה בשביל לקבל מחרוזת טקסט משורת הפקודה במקום להשתמש במילה השמורה hello אני משתמש בפקודה הבאה:
let input_word = std::env::args().nth(1).expect("Usage: rot13 text-string");
אנחנו רוצים לגשת לתא השני במערך env::args ולכן מפעילים את nth. מעניין לשים לב שהגישה לתא לא מחזירה מחרוזת אלא משהו שנקרא Option<String>, או "אופציה למחרוזת". ראסט אומר לנו שאולי יש מחרוזת בתא השני אבל אולי משתמש גם לא העביר ערך. ה 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
## הוצאת הפונקציה לקובץ נפרד לאורך זמן אולי נרצה להוסיף עוד פונקציות המרה ועוד יכולות לתוכנית שלנו, ולכן יהיה נחמד אם כבר עכשיו נלמד איך לפצל את התוכנית למספר קבצים. בשביל לקחת את הפונקציה rot13 לקובץ אחר אני יוצר קובץ חדש בשם utils.rs באותה תיקיה של main.rs, מעביר אליו את קוד הפונקציה ומוסיף את התחילית pub. תוכן הקובץ הוא לכן:
pub fn rot13(text: &String) -> String {

ToCode
1 420
cost: i32,
}

impl Book {
    fn describe(&self) {
        println!("{} was written by {}. It costs {}$", self.title, self.author_name, self.cost);
    }
}


fn main() {
    let b1 = Book { title: "The Picture of Dorian Gray".to_string(), author_name: "Oscar Wilde".to_string(), cost: 0 };
    let b2 = Book { title: "Dracula".to_string(), author_name: "Mary Wollstonecraft Shelley".to_string(), cost: 0 };
    let b3 = Book { title: "Jane Eyre".to_string(), author_name: "Charlotte Brontë".to_string(), cost: 0 };
    let car = ToyCar { color: "green".to_string(), cost: 15 };

    b1.describe();
    b2.describe();
    b3.describe();

    b1.sell();
    car.sell();
}
שיטת ניהול הזיכרון והעבודה עם Struct-ים הם לדעתי שני החלקים הכי מורכבים ב Rust. אני מקווה שאחרי קריאת הפוסט דברים נראים קצת הגיוניים, ובטוח שבהמשך הסידרה וככל שנראה עוד דוגמאות דברים יראו אפילו יותר הגיוניים. ## תרגילים להרחבה רוצים לראות שהבנתם את כל מה שהלך פה, ולהתאמן על ראסט עד הפוסט של מחר? הנה כמה רעיונות: 1. התקינו ראסט אצלכם על המכונה. 2. כתבו תוכנית ראסט שמוצאת את כל המספרים הראשוניים בין 1 ל 100. 3. כתבו תוכנית ראסט שמגרילה מחרוזת בת 6 תווים - התוכנית תרוץ בלולאה וכל איטרציה תוסיף עוד אות למחרוזת. 4. כתבו Struct ב Rust שמייצג אוסף סיסמאות (כן תצטרכו למצוא איך מגדירים מערך ב Rust). הגדירו פונקציה שמקבלת שם של אתר, מגרילה עבורו סיסמה ושומרת אותו במערך הסיסמאות, ופונקציה נוספת שמקבלת שם של אתר ומחזירה את הסיסמה שהוגרלה עבור אותו אתר. 5. ב Struct של ספר כתבתי:
impl Book {
    fn describe(&self) {
        println!("{} was written by {}. It costs {}$", self.title, self.author_name, self.cost);
    }
}
בשביל מה הייתי צריך את סימן ה & לפני ה self? מה יקרה אם נוותר עליו?

ToCode
1 420
let mut name = String::from("hello");
    
    change_name(&mut name);
    change_name(&mut name);
    change_name(&mut name);

    print_name(&name);
    print_name(&name);
    print_name(&name);

    println!("My name is still {}", name);
}
הפונקציה change_name רוצה לקחת את name ולשנות אותו, ולכן היא צריכה לציין את זה בחתימה שלה, ובקריאה אליה אנחנו ממש צריכים להגיד שאנחנו מעבירים גירסה ניתנת לשינוי של המשתנה. ככל שנכתוב יותר קוד Rust נראה יותר דרכים יצירתיות להעביר משתנים בין פונקציות ונצטרך לזכור לבדוק מי ה Owner של כל מידע במהלך ריצת התוכנית. ## סטראקטים ב Rust רכיב מרכזי נוסף בשפת Rust הוא ה Struct. אפשר לחשוב על סטראקט כמו אוביקט בשפת JavaScript, רק בלי הפונקציות. סטראקט מחזיק מידע ואנחנו יכולים לכתוב פונקציות שיהיו "קשורות" לאותו סטראקט. בנוסף אפשר לכתוב אוספים של פונקציות שאפשר "להדביק" על כל Struct, וכל אוסף כזה נקרא Trait. אז הנה דוגמה Struct ראשון מייצג ספר:
struct Book {
    title: String,
    author_name: String,
    cost: i32,
}
אני יכול ליצור כמה ספרים ולהדפיס מהם מידע:
struct Book {
    title: String,
    author_name: String,
    cost: i32,
}

fn main() {   
    let b1 = Book { title: "The Picture of Dorian Gray".to_string(), author_name: "Oscar Wilde".to_string(), cost: 0 };
    let b2 = Book { title: "Dracula".to_string(), author_name: "Mary Wollstonecraft Shelley".to_string(), cost: 0 };
    let b3 = Book { title: "Jane Eyre".to_string(), author_name: "Charlotte Brontë".to_string(), cost: 0 };

    println!("{} was written by {}", b1.title, b1.author_name);
}
את פקודת ההדפסה אני יכול להעביר לפונקציה של הסטראקט, ואז התחביר ירגיש ממש כמו תכנות מונחה עצמים:
struct Book {
    title: String,
    author_name: String,
    cost: i32,
}

impl Book {
    fn describe(&self) {
        println!("{} was written by {}. It costs {}$", self.title, self.author_name, self.cost);
    }
}

fn main() {
    let b1 = Book { title: "The Picture of Dorian Gray".to_string(), author_name: "Oscar Wilde".to_string(), cost: 0 };
    let b2 = Book { title: "Dracula".to_string(), author_name: "Mary Wollstonecraft Shelley".to_string(), cost: 0 };
    let b3 = Book { title: "Jane Eyre".to_string(), author_name: "Charlotte Brontë".to_string(), cost: 0 };

    b1.describe();
    b2.describe();
    b3.describe();
}
עכשיו בואו נבנה עוד Struct, נניח עבור מכונית צעצוע:
struct ToyCar {
    color: String,
    cost: i32,
}
ונשים לב שלשני הסטראקטים שלנו יש מאפיין cost. אז אפשר לכתוב פונקציה ש"תתאים" לשניהם, למשל הפונקציה sell שמוכרת את הדבר. ראסט מאפשר כתיב מקוצר עבור אוסף של פונקציות שאפשר להפעיל על סטראקטים וזה נקרא Trait. נכתוב Trait בשם Sellable שמגדיר סטראקטים שאפשר למכור אותם. הוא יוסיף פונקציה בשם sell שפשוט תדפיס את המחיר:
trait Sellable {
    fn get_price(&self) -> i32;

    fn sell(&self) {
        println!("Got {}$", self.get_price());
    }
}
נגדיר ש Book ו ToyCar מממשים את ה Trait, ונבנה לכל אחד מהם את הפונקציה get_price שלו:
impl Sellable for Book {
    fn get_price(&self) -> i32 {
        return self.cost;
    }
}

impl Sellable for ToyCar {
    fn get_price(&self) -> i32 {
        return self.cost;
    }
}
שימו לב שב Trait אי אפשר לגשת לשדות המידע של Struct, רק להפעיל פונקציות שלו, ולכן הייתי צריך להגדיר את הפונקציה get_price שרק מחזירה את ערך שדה המידע cost. אחרי כל הבלאגן אני יכול להשתמש בפונקציות החדשות של Sellable על ספרים או על מכוניות צעצוע:
book.sell();
car.sell();
והתוכנית המלאה:
trait Sellable {
    fn get_price(&self) -> i32;

    fn sell(&self) {
        println!("Got {}$", self.get_price());
    }
}

impl Sellable for Book {
    fn get_price(&self) -> i32 {
        return self.cost;
    }
}

impl Sellable for ToyCar {
    fn get_price(&self) -> i32 {
        return self.cost;
    }    
}

struct ToyCar {
    color: String,
    cost: i32,
}

struct Book {
    title: String,
    author_name: String,

ToCode
1 420
בנוסף אני ממליץ להשתמש ב VS Code כדי לערוך קבצי rust מקומית. יש תוסף בשם rust-analyzer שמראה לכם מה הטיפוס של כל משתנה כשאתם עורכים את הקוד, ומציג שגיאות קומפילציה אם יש ממש מתוך העורך. אחרי התקנה בשביל לפתוח פרויקט חדש בתיקייה חדשה נכתוב משורת הפקודה:
cargo new myapp
וזה ייצור תיקיה בשם myapp ובתוכה את הקבצים:
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
בימים הבאים נלמד איך להתקין ספריות עזר ואז נראה למה הקובץ Cargo.toml חשוב. בינתיים תוכלו לעיין בקובץ main.rs שנוצר ולהריץ עם:
cargo run
## איך ראסט מתייחס לזיכרון (כולל מה זה Box) אני רוצה להמשיך לעוד כמה ביטים על השפה שיעזרו לכם להבין את התוכניות שיתפרסמו במהלך השבוע ולהרחיב אותן בעצמכם. נקודה ראשונה ומיוחדת בראסט היא היחס לזיכרון. הגישה של ראסט לחיים היא די ייחודית בנוף של שפות תכנות - מצד אחד ראסט לא כוללת Garbage Collector, אבל מצד שני הזיכרון לא מנוהל לגמרי ידנית. כל המשתנים שראינו עד עכשיו החזיקו מספרים פשוטים, ולכן ראסט שמר אותם על המחסנית. מידע שנשמר על המחסנית מנוקה אוטומטית כשיוצאים מה Stack Frame, כלומר ביציאה מהפונקציה או הבלוק שבו הוא הוגדר. ראסט תומך גם בהקצאת מידע על ה"ערימה". מידע שמוקצה על הערימה הוא כזה שאפשר להעביר בין פונקציות, כי הוא לא נמחק אוטומטית ביציאה מהבלוק. במקום זה, מידע שיוקצה על הערימה יקבל Owner, שזה משתנה מיוחד שאחראי על המידע הזה. אם ה Owner יוצא מה Scope המידע יימחק, אבל הרבה פעולות בראסט מחליפות owner וכך אפשר לשמור את המידע לאורך זמן. אני חושב שדוגמה תעזור כאן. מחרוזת היא דוגמה למשתנה שמקבל מקום על הערימה, ולכן אפשר לעדכן אותה ולהעביר אותה בין פונקציות. ננסה לכתוב קוד שעובד עם מחרוזת כדי ללמוד עוד קצת על זיכרון וניהולו ב Rust. תוכנית ראשונה פשוט יוצרת מחרוזת ומדפיסה אותה:
fn main() {
    let name = String::from("hello");
    
    println!("got: {}", name);
}
עד כאן לא קרה הרבה. עכשיו ננסה להעביר את ההדפסה לפונקציה אחרת:
fn print_name(name: String) {
    println!("{}", name);
}

fn main() {
    let name = String::from("hello");
    print_name(name);
}
אנחנו עדיין בסדר. התוכנית מתקמפלת ורצה. עכשיו נוסיף עוד כמה קריאות לפונקציית ההדפסה כדי לשבור את הקוד:
fn print_name(name: String) {
    println!("{}", name);
}

fn main() {   
    let name = String::from("hello");
    
    print_name(name);
    print_name(name);
    print_name(name);
}
ו-התוכנית כבר לא מתקמפלת. השגיאה מופיעה בקריאה השניה לפונקציית ההדפסה:
use of moved value: `name`
זאת אחת השגיאות הראשונות שקיבלתי ב Rust ואחת הנפוצות. מה שראסט אומר לי כאן זה שלכל String בתוכנית יש Owner, שזה המשתנה ש"שומר" עליה. כשהמשתנה הזה יוצא מ Scope המחרוזת תימחק. כשהתוכנית מתחילה ה Owner הוא המשתנה name, אבל כשאני מעביר את name לפונקציה print_name שיניתי גם בעלות, ועכשיו הפונקציה "השתלטה" על המידע. ניסיון לעבוד עם המשתנה אחרי שנתתי אותו לפונקציה ייכשל, כי המשתנה הוא כבר לא ה"בעלים" של המידע, ואולי המידע כבר לא בתוקף. ראסט מנסה לשמור עליי מטעויות של גישה למידע אחרי שמחקתי אותו. בשביל להצליח להעביר משתנה לפונקציה בלי שהפונקציה תיקח "בעלות" על המידע, אנחנו יכולים להעביר Reference למשתנה. זה נראה ככה:
fn print_name(name: &String) {
    println!("{}", name);
}

fn main() {   
    let name = String::from("hello");
    
    print_name(&name);
    print_name(&name);
    print_name(&name);

    println!("My name is still {}", name);
}
ההבדל הוא האפשרות של ראסט לבצע בדיקת טיפוסים - שימו לב מה קורה אם אני מנסה לשנות את המחרוזת מתוך הפונקציה:
fn print_name(name: &String) {
    name.push_str("yay");
}
הקוד הזה מנסה להוסיף את הסיומת yay למחרוזת, אבל הוא לא מתקמפל. המחרוזת נכנסה לפונקציה בתור Reference ולכן אין לפונקציה הרשאה לשנות אותה. רגע, ומה אם אנחנו כן רוצים להעביר גירסה ניתנת לשינוי של המחרוזת לפונקציה, ועדיין בלי להעביר בעלות? גם את זה אפשר, אבל זה דורש קצת יותר תווים. הנה הקוד:
fn print_name(name: &String) {
    println!("{}", name)
}

fn change_name(name: &mut String) {
    name.push_str("--- the end; ");
}

fn main() {