ru
Feedback
ToCode

ToCode

Открыть в Telegram

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

Больше
1 420
Подписчики
Нет данных24 часа
+27 дней
-230 день
Архив постов
ToCode
1 420
# קוד לשימוש חוזר אחת הבעיות שאני מוצא יותר מדי במערכות היא האתגר של שימוש חוזר בקוד, וזה הולך ככה - מצד אחד אנחנו רוצים לכתוב קוד שאפשר יהיה להשתמש בו שוב ושוב, אבל מצד שני אנחנו (כרגע) עסוקים מדי בשביל לעשות את העבודה הקשה של לכתוב קוד לשימוש חוזר. התוצאה היא קוד שנראה כמו קוד שמתאים לשימוש חוזר, אבל כשאתה בא להשתמש בו בעוד מקום אתה מגלה שיש יותר מדי תלויות. במקרה הטוב תפסת את זה בזמן וכתבת הכל מחדש, במקרה הרע תפסת את זה מאוחר מדי ואתה תקוע בבוץ מנסה לארגן מחדש את הקוד "לשימוש חוזר" וצריך לפתור עכשיו באגים בשני פיצ'רים במקום רק בפיצ'ר החדש שאתה בונה. דוגמה? בשמחה. קחו קוד פייתון שעושה יותר מדי עבודה על משפט:
import re

text = "one two three I want to see three four five I am alive"

# delete all spaces in sentences
text = re.sub(r'\s+', '', text)

# replace each letter by its ordinal value
text = [ord(c) for c in text]

# leave only values that are larger than what came before
new_text = [text[0]]
for value in text:
    if value > new_text[-1]:
        new_text.append(value)

text = new_text

print(sum(text))
לקוד יש שלושה חלקים שבנויים לעבוד אחד אחרי השני עם הערה שמתארת מה עושה כל חלק. לא בטוח שכולם נכונים אבל לפחות הקוד סגור כולו באותו מקום, ואף אחד לא יחלום להשתמש בו בעוד מקום במערכת. במקרה הגרוע יצטרכו לקרוא את הקוד וממש להעתיק את השורות שעושות עבודה דומה. גירסה מסוכנת יותר של הקוד הזה עשויה להיראות כך:
import re

def main():
    text = "one two three I want to see three four five I am alive"
    text = delete_all_spaces(text)
    text = change_to_ord_values(text)
    text = change_to_increasing(text)
    print_result(text)


def delete_all_spaces(text):
    # delete all spaces in sentences
    text = re.sub(r'\s+', '', text)
    return text

def change_to_ord_values(text):
    # replace each letter by its ordinal value
    text = [ord(c) for c in text]
    return text

def change_to_increasing(text):
    # leave only values that are larger than what came before
    new_text = [text[0]]
    for value in text:
        if value > new_text[-1]:
            new_text.append(value)

    text = new_text
    return text

def print_result(text):
    print(sum(text))


main()
איזה יופי נכון? הכל מסודר עכשיו בפונקציות, כל פונקציה מקבלת את הקלט שלה ומחזירה פלט והתוכנית מחזירה את אותה תוצאה. עכשיו אם נרצה להשתמש רק בחלק מהקוד למקום אחר נוכל - נכון? רוב הסיכויים שלא. הפונקציות לא מספיק גנריות בשביל שיהיו יעילות במקום אחר, כתבתי אותן פשוט בגלל שזה מה שהיה לי מול העיניים והחלוקה שרירותית לגמרי. הרבה יותר סביר שמישהו ינסה להשתמש באחת מהן, יבין שאין לו בדיוק קלט מהסוג שהפונקציה צריכה ואז ייתקע בארגון מחדש של הקוד. אז כן אם יש לכם זמן לכתוב קוד גנרי, בארכיטקטורה נכונה שקל להשתמש בו בעוד מקומות ואתם יודעים לתאר מקומות נוספים במערכת שיצטרכו אותו - לכו על זה וכתבו את הקוד הגנרי. אבל שווה להיזהר מקוד שרק נראה גנרי, ושיישבר בשניה שננסה לשלב אותו בעוד מקום.

ToCode
1 420
# חדש באתר: תעודות סיום קורס שתי הודעות משמחות להיום - קודם כל למדתי איך עובדים עם PDF-ים ב Ruby, ודבר שני הוספתי לאתר מנגנון שמאפשר לכם להוריד תעודה אחרי שסיימתם קורס. ועכשיו הפרטים. ## איך עובדים עם PDF ב Ruby בשנים שהאתר הזה באוויר הרבה אנשים מכל מיני סיבות ביקשו לקבל איזשהו אישור על קורסים שסיימו פה. תקופה מאוד ארוכה לא אהבתי את הרעיון. תעודות נראו לי (ועדיין נראות) כמו שריד מיושן מהעבר, כמו משהו שצריך היה לעבור מזמן מהעולם. לימוד הוא הגמול של עצמו ואם מישהו רוצה להוכיח מיומנות אז תיק עבודות איכותי שווה יותר מכל תעודה. ועם כל זה מסתבר שעדיין אישור מסודר שסיימתם קורס עוזר לחלק מכם להתמיד בקורס ולראות את כל השיעורים, ועוזר לחלק מהמעסיקים שלכם לכסות בשמחה את עלות הקורס. בשביל שתי הקבוצות האלה הוספתי היום לאתר קצת קוד שעיקרו בקטע הבא:
class CertificatesController < ApplicationController
  def show
    @bundle = Bundle.find_by(slug: params[:b])
    authorize! :graduate, @bundle

    if current_user.name_on_certificate.blank? || current_user.certificate_gender.blank?
      return redirect_to(edit_user_registration, flash: t('notifications.missing_certificate_info'))
    end

    pdf = Prawn::Document.new
    pdf.font Rails.root.join('certificates', './Alef-Regular.ttf')
    pdf.bounding_box([140, 554], width: 300, height: 100) do
      pdf.text current_user.name_on_certificate, direction: :rtl, size: 24
    end
    pdf.bounding_box([80, 370], width: 500, height: 100) do
      pdf.text @bundle.name, size: 24
    end
    pdf.bounding_box([374, 242], width: 50, height: 100) do
      pdf.text @bundle.recommended_duration_in_hours, size: 16
    end

    # get the existing pdf
    template = CombinePDF.load Rails.root.join('certificates', "certificate_template_#{current_user.certificate_gender}.pdf")
    user = CombinePDF.parse pdf.render

    template.pages[0] << user.pages[0]

    send_data template.to_pdf, filename: "tocode_#{@bundle.slug}.pdf", type: "application/pdf"
  end
end
וכמו שאני אוהב בואו נעשה קצת Code Review כדי ללמוד קצת על רובי ועל ה Trade Offs בפיתוח פיצ'רים: 1. דבר ראשון שאני לא אוהב בקוד זה שכל הקוד כתוב בפונקציה אחת ארוכה. בעולם מושלם הייתי לוקח את כל החלק המרכזי ומוציא אותו לפונקציה אחרת בקלאס אחר. יום אחד זה כנראה יקרה בסבב Refactoring כלשהו, אבל אני חושב שבקוד שאינו תשתיתי במערכת אין בעיה להתחיל עם משהו שעובד ולהתקדם משם. 2. החלק הראשון של הפונקציה מורכב מ-5 השורות הראשונות. הוא בודק שאתם יכולים לייצר תעודה, כלומר שסיימתם את הקורס ושאני יודע איך קוראים לכם. הפונקציה t של ריילס היא הדרך לטעון הודעה מקובץ תרגום, ובאופן כללי כל הטקסטים באתר שמורים בקובץ תרגום בצד בפורמט yml. 3. החלק השני של הפונקציה יוצר את התעודה בפורמט PDF בעזרת שתי ספריות: prawn ו combine_pdf. שתיהן לא הכי מתוחזקות אבל עובדות. בשביל שהתעודה תצא יפה יצרתי שני קבצי PDF בתור תבנית (אחד לבנים עם המילה "השתתף" והשניה לבנות עם המילה "השתתפה") ושמרתי אותם בתיקיית הפרויקט. בקוד אני נותן ל prawn לייצר PDF חדש רק עם החלקים הדינמיים בתעודה, כלומר שם המשתמש, שם הקורס וכמה שעות הקורס. אחרי זה אני משתמש ב combine_pdf כדי לשלב את החלקים הדינמיים עם התבנית ולקבל קובץ תעודה עם הנתונים. שווה לשים לב שכל העבודה מבוצעת בזיכרון בלי קבצים זמניים, וכך לא צריך להמציא שמות ולהתמודד עם התנגשויות. 4. השורה האחרונה שולחת את התעודה חזרה לדפדפן. ## איך להוריד תעודה בקורס שסיימתי אם אתם מנויים לאתר ורוצים לראות איך נראית תעודה, כל מה שאתם צריכים לעשות הוא לסיים לצפות באיזשהו קורס (מקווה שכבר עשיתם את זה), ואז במסך תוכן העניינים של הקורס תופיע לכם תיבה חדשה משמאל לרשימת השיעורים עם אפשרות להוריד את התעודה. ## תקלות? שאלות? דברו איתי והכי חשוב אם הורדתם את התעודה או תעודות שלכם ועדיין יש לכם שאלות, הערות, תקלות או מחשבות על הפיצ'ר שלחו אימייל או השאירו הודעה באתר. אשמח לשמוע ולהמשיך לשפר את התשתית.

ToCode
1 420
# מחקר ופיתוח בכל החברות אוהבים לקרוא למחלקת תוכנה R&D, קיצור של מחקר ופיתוח. הם רק שכחו להגיד לנו ששני הדברים האלה שונים ודורשים התנהלות שונה: פיתוח זה החלק שאנחנו כותבים פיצ'רים ומתקנים באגים. הצלחה בפיתוח מגיעה כשכותבים קוד יפה, יציב ומתועד. כשהבאגים באמת מסתדרים וכשהמערכת בסוף יום עבודה נראית יותר טוב ממה שהיתה כשהתחלת. מחקר זה החלק שאנחנו לומדים ומגלים שיטות עבודה חדשות. מחקר יכול להיכשל. מחקר הוא לא לינארי ולא מדיד. מחקר זה מה שקורה כשיושבים שלושה ימים להרים POC רק בשביל לגלות שמשהו מאוד בסיסי בהתחלה היה שגוי וצריך למחוק את הכל. בסוף המחקר המערכת תהיה באותו מצב או אפילו גרוע יותר. מה שהשתפר זה הידע שלך. פיתוח נותן הרגשה טובה יותר. מחקר הוא הדרך היחידה קדימה. אל תוותרו על אף אחד מהם.

ToCode
1 420
# דו לשוני המוכרת בחנות הירקות בברצלונה הסתכלה עליי כאילו נחתתי מאיזה כוכב אחר - Cucmber? Que es eso? היא שאלה, והיה ברור לאן הגעתי. למרות שכל שלט, כל הודעה, כל מכונת כרטיסים וכל היבט אחר של החיים בעיר מתנהל ב-3 שפות (אנגלית, קטלאנית וספרדית), החוליה החלשה היא האנשים שכמו בכל מקום אוהבים את אזור הנוחות שלהם. ולמה שיתאמצו? הרי גם פה בארץ אנחנו פוגשים את הערבית בכל שלט רחוב, על כל אוטובוס ובכל הודעה רשמית, ובכל זאת מי שלא מדבר מהבית לא טורח ללמוד. אבל הפוסט הזה אינו על שפות או על ירקות, אלא על הזדמנויות. על ההזדמנות ללמוד אנגלית, שנמצאת בכל מקום סביבך אבל נראית בלתי ניתנת להשגה. על ההזדמנות ללמוד שפת תכנות חדשה או טכניקה חדשה אפילו בפרויקט הנוכחי שלך, למשל על ידי שילוב git או טכנולוגיות ענן, שפשוט לא נראית פרקטית ממיליון סיבות. עד שיום אחד מחליטים לנצל אותה, ומגלים שאותו חלום הקיף אותנו כל הזמן.

ToCode
1 420
# זה ייקח רק דקה זה ייקח רק דקה? באמת? כולל כתיבת הבדיקה? כולל כתיבת התיעוד לפיצ'ר? כולל הרצת הבדיקות הישנות ותיקון או מחיקה של בדיקות לא רלוונטיות? כולל ניסוח הודעת קומיט טובה? כולל דחיפה לשרת והתמודדות עם קונפליקטים? תיקונים מהירים ששוברים 50 דברים אחרים לא מוסיפים נקודות לאף אחד. עדיף לקחת אוויר ולכתוב את התיקון בצורה נכונה. החצי שעה שאולי חסכת עכשיו תעלה הרבה יותר בעוד חודשיים.

ToCode
1 420
# עדיפה ארכיטקטורה טובה על מעקף טוב אז בריאקט עובדים על תיעוד חדש ואחת הדוגמאות שם תפסה את העין של סבסטיאן קרלוס וזכתה לכינוי "התבנית המכוערת ביותר בריאקט". זה הקוד:
function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}
הרעיון הוא שהקומפוננטה CountLabel מקבלת את הערך של count מבחוץ וצריכה להציג מגמה - האם הערך הנוכחי גדול או קטן מהערך הקודם. בשביל זה היא שומרת את הערך הקודם ב State ואז אנחנו תקועים וצריכים לעדכן את הסטייט בתוך הקוד הראשי של פונקציית CountLabel. גם בתיעוד של ריאקט מודים שזו לא אופציה טובה, אבל שבמצבים מסוימים היא הדרך היחידה לפתור בעיה. הקונטקסט לסיפור הזה הוא שעד עכשיו ההמלצה היתה להשתמש ב useEffect כדי לעדכן משתנה סטייט בעקבות שינוי של prop, ועכשיו הם ממליצים לוותר על ה useEffect ולעדכן את הערך ישירות בפונקציה הראשית. האמת היא ששתי האופציות לא טובות, ושתיהן מהוות בסך הכל מעקף עבור ארכיטקטורה גרועה. פיתרון נכון לשמירת מגמה יהיה לשמור את המגמה יחד עם הסטייט המקורי - כלומר מי שמחזיק את count ואחראי על עדכון שלו הוא גם זה שצריך לשמור את המגמה. שמירת זיכרון של הערך הקודם היא תכונה של הסטייט, בדיוק כמו שמירה של משתנה הסטייט ב Local Storage או כל מניפולציה אחרת על הערך. וכך פיתרון עם Custom Hook הולך להיות הרבה יותר פשוט ויותר נכון מההצעה של סטייט נוסף בקומפוננטת ילד. זה נראה ככה:
import { useState } from "react";

function useTrendedState(initialValue) {
  const [value, setValue] = useState(initialValue);
  const [prevValue, setPrevValue] = useState(initialValue);

  function setter(...args) {
    setPrevValue(value);
    return setValue(...args);
  }

  return [value, setter, prevValue];
}

function CountLabel({ count, prevCount }) {
  let trend;
  if (count < prevCount) {
    trend = "Decreasing";
  } else if (count > prevCount) {
    trend = "Increasing";
  } else {
    trend = "Stable";
  }

  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

export default function App() {
  const [count, setCount, prevCount] = useTrendedState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <CountLabel count={count} prevCount={prevCount} />
    </>
  );
}
על הדרך הרווחנו קוד מפורש שמטפל במצב ש count לא השתנה. נכון שבגלל שזה מספרים ובגלל ש useState ממילא לא עושה כלום כשהוא נקרא עם הערך שכבר קיים שם המצב הזה לא אפשרי בקוד הנוכחי, אבל במצבים יותר מורכבים בהחלט אפשר לדמיין משמעות שונה לזהות שכן היתה מייצרת באג בהצעה שבתיעוד (למשל בעבודה עם מערכים או אוביקטים). כן פתחי מילוט זה חשוב, וכן טוב שיש אותם בפריימוורק, אבל אם כבר בוחרים לתעד פתח מילוט כזה עדיף לבחור דוגמה שבאמת דורשת אותו. ולא, אני לא יכול לחשוב על כזאת דוגמה.

ToCode
1 420
# פרסים בלימוד נושא חדש, פרסים יכולים להיות דרך קלה להגביר מוטיבציה בנקודות מפתח. לדוגמה- 1. אחרי שאצליח להתמיד שלושה חודשים בספורט אני קונה נעלי ריצה חדשות. 2. כשאסיים לכתוב את האפליקציה הראשונה שלי, אני קונה לעצמי אייפון חדש. 3. כשאסיים להקים מערכת בדיקות אוטומטיות לאתר, אני קונה לעצמי כרטיסים לכנס מקצועי בנושא בדיקות. הדבר החשוב בשימוש בפרסים הוא לבחור פרסים שיגבירו אצלכם את המוטיבציה ללמוד ויהיו קשורים למיומנות שאתם מנסים לשפר. כך תהיו בטוחים שאחרי הפרס תוכלו להמשיך בתהליך ואף להגביר את הקצב.

ToCode
1 420
sys.argv = sys.argv[:1]
    unittest.main()
    sys.exit(0)


print("Hello world")

# part 1
print(f"2 * 2 = {two_by_two()}")

<<<<<<< HEAD
=======
# part 2
print(f"3 * 3 = {three_by_three()}")



>>>>>>> ae4ea11 (add tests)
זאת ההדפסה בשורה האחרונה - אני מוחק אותה וממשיך את הריבייס:
$ git add .
$ git rebase --continue

[detached HEAD 82f6008] add tests
 1 file changed, 13 insertions(+)
Successfully rebased and updated detached HEAD.
נעביר את main למקום החדש שלו ונקבל:
$ git branch -f main HEAD
$ git switch main
$ git log --oneline --graph

* 82f6008 (HEAD -> main) add tests
* 3e0d862 add generic method
* 05df720 add script docs
* 5161eec fixed print statement
* c299ef3 add print statement
* 6adbc5b initial commit
עכשיו ההיסטוריה נראית הרבה יותר טוב, בלי שום זכר לענף שמוזג. אם בעתיד נרצה לחזור אליו, להוסיף עוד קומיטים ולמזג לא תהיה עם זה שום בעיה. העניין היחיד הוא שאם עשיתם push למאגר לפני הריבייס, תצטרכו לדחוף גירסה מעודכנת ולדרוס את מה ששיתפתם, וזה יכול לסבך אנשים אחרים שעובדים אתכם על הפרויקט ואולי כבר השתמשו בגירסה ששיתפתם.

ToCode
1 420
print(f"2 * 2 = {two_by_two()}")

<<<<<<< HEAD
# part 2
print(f"3 * 3 = {three_by_three()}")



=======
>>>>>>> parent of 91cdcd3 (fixed conflicts)
אני רואה בקובץ שהקטעים המסומנים הם באמת אלה שמתאימים לקוד שנכנס מהבראנץ שאני רוצה למחוק, משאיר רק את ה HEAD ומסיים עם הסקריפט הבא:
import unittest, sys

class TestMultMethod(unittest.TestCase):
    def test_mul_positives(self):
        self.assertEqual(6, mul(2, 3))

##
# This script does all kinds of nice things
# to help children learn math
#
def two_by_two():
    return mul(2, 2)

def mul(x, y):
    return x * y

# run self test with --test
if len(sys.argv) > 1 and sys.argv[1] == "--test":
    sys.argv = sys.argv[:1]
    unittest.main()
    sys.exit(0)


print("Hello world")

# part 1
print(f"2 * 2 = {two_by_two()}")
ומסיים את ה revert עם:
$ git add demo.py
$ git revert --continue
$ git log --oneline --graph

* 492cb53 (HEAD -> main) Revert printing 3 by 3
* ae4ea11 add tests
* 5ad40d1 add generic method
*   91cdcd3 fixed conflicts
|\
| * 6a8ccb0 add comments
| * 71b3207 fixed syntax
| * b2cc15b first try
* | 05df720 add script docs
|/
* 5161eec fixed print statement
* c299ef3 add print statement
* 6adbc5b initial commit
הקוד חזר למצבו לפני המיזוג, המיזוג מופיע בהיסטוריה וגם הביטול שלו ורק צריך לזכור לעולם לא להוסיף קומיט עם תיקון אחרי קומיט 6a8ccb0, כלומר לענף השבור, בגלל שאי אפשר למזג מחדש משהו שכבר מיזגנו. כן אפשר יהיה לעשות rebase לשלושת הקומיטים הבעייתיים ואז למזג את הגירסה החדשה המרובסת שלהם. ## איך לבטל את המיזוג עם rebase דרך אחרת לגשת לאותה בעיה, אם לא דחפתם עדיין את המאגר, היא לעשות rebase לקומיטים ae4ea11 ו 5ad40d1 כך שאותו הקוד שיש בהם "יולבש" על קומיט 05df720 שהיה לפני המיזוג. זה נראה ככה:
$ git rebase --onto 05df720 91cdcd3 ae4ea11
יש פה 3 מספרי קומיטים: 1. הראשון הוא הקומיט שממנו אנחנו רוצים להמשיך (בדיוק לפני המיזוג הבעייתי) 2. השני הוא קומיט המיזוג (הקומיט האחרון שאנחנו רוצים למחוק) 3. השלישי הוא הקומיט האחרון בפרויקט (הקומיט אליו אנחנו רוצים להגיע). אפשר להשתמש בשם main במקום מספר קומיט כאן. התוצאה היא כמובן קונפליקט אבל זה צפוי:
Auto-merging demo.py
CONFLICT (content): Merge conflict in demo.py
error: could not apply 5ad40d1... add generic method
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 5ad40d1... add generic method
הקוד עם הקונפליקט מסומן הוא:
##
# This script does all kinds of nice things
# to help children learn math
#
<<<<<<< HEAD
=======
def three_by_three():
    return mul(3, 3)
>>>>>>> 5ad40d1 (add generic method)

def two_by_two():
    return mul(2, 2)

def mul(x, y):
    return x * y

print("Hello world")

# part 1
print(f"2 * 2 = {two_by_two()}")
אני מוחק את הפונקציה המיותרת כדי לתקן את הקונפליקט וממשיך בריבייס, רק בשביל לקבל קונפליקט נוסף:
$ git add .
$ git rebase --continue

Auto-merging demo.py
CONFLICT (content): Merge conflict in demo.py
error: could not apply ae4ea11... add tests
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply ae4ea11... add tests
הקוד עכשיו עם הקונפליקט מסומן הוא:
import unittest, sys

class TestMultMethod(unittest.TestCase):
    def test_mul_positives(self):
        self.assertEqual(6, mul(2, 3))

##
# This script does all kinds of nice things
# to help children learn math
#
def two_by_two():
    return mul(2, 2)

def mul(x, y):
    return x * y

# run self test with --test
if len(sys.argv) > 1 and sys.argv[1] == "--test":

ToCode
1 420
# איך לבטל מיזוג של ענף ישן ב git בעבודה עם גיט אנחנו אוהבים לבנות כל פיצ'ר בענף משלו ורק אחרי שבדקנו והכל עובד למזג לתוך main (זאת לא הדרך היחידה לעבוד, אבל היא מספיק פופולרית כדי להיחשב למיינסטרים). בעיה שחוזרת לא פעם בשיטת עבודה כזו היא הצורך לבטל מיזוג של ענף, למשל כי אחרי שמיזגנו והעלינו לפרודקשן פתאום גילינו את הבאג ההוא שפספסנו בבדיקות. ## מאגר לדוגמה בשביל הדוגמה בניתי מאגר מסקריפט פייתון שהלוג שלו נראה ככה:
* ae4ea11 (HEAD -> main) add tests
* 5ad40d1 add generic method
*   91cdcd3 fixed conflicts
|\
| * 6a8ccb0 add comments
| * 71b3207 fixed syntax
| * b2cc15b first try
* | 05df720 add script docs
|/
* 5161eec fixed print statement
* c299ef3 add print statement
* 6adbc5b initial commit
אפשר לראות את הענף הממוזג בין הקומיט add comments לקומיט first try. האמת היא שיש עוד כמה ענפים שמוזגו פנימה, אבל רק בענף הזה היו קונפליקטים ולכן נוצר שם Merge Commit (זה קומיט מספר 91cdcd3). שאר המיזוגים עברו חלק עם Fast Forward. בנוסף אחרי כל מיזוג גם מחקתי את שם הענף. במאגר יש רק סקריפט פייתון יחיד בקובץ demo.py וזה התוכן שלו:
import unittest, sys

class TestMultMethod(unittest.TestCase):
    def test_mul_positives(self):
        self.assertEqual(6, mul(2, 3))

##
# This script does all kinds of nice things
# to help children learn math
#
def three_by_three():
    return mul(3, 3)

def two_by_two():
    return mul(2, 2)

def mul(x, y):
    return x * y

# run self test with --test
if len(sys.argv) > 1 and sys.argv[1] == "--test":
    sys.argv = sys.argv[:1]
    unittest.main()
    sys.exit(0)


print("Hello world")

# part 1
print(f"2 * 2 = {two_by_two()}")

# part 2
print(f"3 * 3 = {three_by_three()}")
והקומיטים שאני רוצה לבטל הם אלה שהוסיפו את הפיצ'ר של "הדפסת 3 כפול 3", הפיצ'ר הזה מורכב מהפונקציה three_by_three ומהודעת ההדפסה בשורה האחרונה של הסקריפט. מאז שינוי זה היו עוד שינויים שנגעו בכל המקומות בסקריפט ולכן צפויים קונפליקטים. ## איך לבטל את המיזוג עם revert דרך אחת לבטל את המיזוג היא פקודת revert. אבל יש עם זה שתי בעיות: 1. ריוורט היא פקודה מאוד רגישה לשינויים שקרו אחרי המיזוג, ולכן ככל שעברו במאגר יותר שינויים אחרי הקומיט שאתם רוצים לבטל, כך ה revert יהיה פחות אמין. ריוורט בעצם מייצר קומיט חדש שמוחק את הקוד שהקומיט הבעייתי הכניס. 2. אם אי פעם בעתיד תרצו למזג מחדש את הענף (למשל כי תמצאו את הבעיה ותתקנו אותה), גיט לא ימזג מחדש את הקומיטים שכבר "מוזגו" ו"רוברטו" (reverted), כי ה revert לא משנה דברים בהיסטוריה אלא הוא בסך הכל קומיט חדש. זה אומר שאם ניקח את הדרך הזאת ובעתיד נרצה לתקן משהו בענף שכבר מוזג נצטרך לעשות rebase לכל הקומיטים באותו ענף - כלומר לקומיטים 6a8ccb0, 71b3207 ו b2cc15b - כי אחרת שלושתם לא ימוזגו מחדש לעולם. בכל מקרה ואם אתם מוכנים לוותר על מיזוג עתידי בואו נתקדם. אני מזכיר שהריפו הוא:
$ git log

* ae4ea11 (HEAD -> main) add tests
* 5ad40d1 add generic method
*   91cdcd3 fixed conflicts
|\
| * 6a8ccb0 add comments
| * 71b3207 fixed syntax
| * b2cc15b first try
* | 05df720 add script docs
|/
* 5161eec fixed print statement
* c299ef3 add print statement
* 6adbc5b initial commit
בגלל שהמיזוג בוצע ב Merge Commit אני יכול לייצר (נו, לנסות לייצר) את קוד ההיפוך ישירות מאותו merge commit. בגלל שזה merge commit אני צריך לציין איזו רגל שלו מייצגת את הענף הראשי, כלומר זה שהקוד מוזג אליו. המיספור מתחיל מ-1 ולכן אצלי המספר הוא 1:
$ git revert 91cdcd3 -m 1
התוצאה היא כמובן קונפליקט אבל אפשר לחיות עם זה. ככה נראה הקובץ עם הקונפליקט:
import unittest, sys

class TestMultMethod(unittest.TestCase):
    def test_mul_positives(self):
        self.assertEqual(6, mul(2, 3))

##
# This script does all kinds of nice things
# to help children learn math
#
<<<<<<< HEAD
def three_by_three():
    return mul(3, 3)
=======
>>>>>>> parent of 91cdcd3 (fixed conflicts)

def two_by_two():
    return mul(2, 2)

def mul(x, y):
    return x * y

# run self test with --test
if len(sys.argv) > 1 and sys.argv[1] == "--test":
    sys.argv = sys.argv[:1]
    unittest.main()
    sys.exit(0)


print("Hello world")

# part 1

ToCode
1 420
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");
    }

    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
    }

    fn required_size(&mut self, _: Vec2) -> Vec2 {
        self.board.size.map_x(|x| 2 * x)
    }
}


fn main() {
    // Creates the cursive root - required for every application.
    let mut siv = cursive::default();

    let game = GameView::new();
    let tx = siv.cb_sink().clone();

    thread::spawn(|| {
        loop {
            thread::sleep(Duration::from_millis(500));
            game.step();
        }
    });

    thread::spawn(move || {
        // game.board.step(Direction::Up);
        loop {
            thread::sleep(Duration::from_millis(500));
            tx.send(Box::new(|si| {
                si.call_on_name("game", |view: &mut GameView|  {
                    let still_alive = view.step();
                    if !still_alive {
                        panic!("You lose");
                    }
                }).unwrap();
            })).unwrap();
        }
    });

    siv.add_layer(game.with_name("game"));
    
    siv.set_fps(24);
    // Starts the event loop.
    siv.run();
}
אותו תוכלו למצוא גם במאגר הגיטהאב של הסידרה בקישור: https://github.com/ynonp/rust-8-days/tree/main/day7%20-%20snake%20gui/snake ## תרגילים להרחבה 1. כרגע המשחק קצת משעמם כי לא משנה כמה תפוחים נאכל המהירות תישאר זהה (step נקראת פעם בחצי שניה). תחילה עדכנו את הקוד כך שיספור ויציג נקודות. 2. אחרי שיש לכם נקודות עדכנו את הקוד כדי שככל שיש יותר נקודות מהירות המשחק תגדל. 3. הוסיפו אפשרות יציאה באמצע - לחיצה על כפתור q מסיימת את המשחק. 4. הוסיפו מסך Game Over אחרי שהנחש מתנגש בקיר או בעצמו. אתם כרגיל מוזמנים לפרסם את הפיתרונות שלכם כאן (או לינקים לפיתרונות אם תבחרו להעלות אותם לגיטהאב) ואנחנו ניפרד מהנחש ומחר נמשיך לדבר על ראסט והאינטרנט.

ToCode
1 420
אחרי שנציג את ה View אם משתמש ילחץ על כפתור אוטומטית cursive ייכנס לפונקציה on_event וייתן לנו הזדמנות לטפל בלחיצה. אבל במשחק סנייק יש עוד דבר שצריך לעשות - להזיז את הנחש. ואת זה אנחנו צריכים לעשות לפי שעון, בלי קשר לאירועים שקרו או לא. בפריימוורקים רבים אחרים יש דרך להגדיר אירוע timeout על View, וכך להריץ קוד כל X שניות. ב cursive לא מצאתי דרך להפעיל שעון ברקע שיפעיל אירוע, ולכן הרעיון השני שלי היה להריץ Thread ברקע, שכל חצי שניה יפעיל את הפונקציה step של Game. זה התברר כהרבה יותר מסובך ממה שקיוויתי בגלל מודל הבעלות של Rust. הבלוק הראשון שאני רוצה להדביק הוא המימוש של GameView, שכולל את הפונקציה step:
impl GameView {
    pub fn new() -> Self {
        return GameView {
            board: game::Game::new(),
            direction: Direction::Down,
        };
    }

    pub fn step(&mut self) -> bool {
        return self.board.step(self.direction);
    }
}
בעצם בנוסף לכל מה שכבר סיפרתי על ה View שלי, הוספתי לו עכשיו עוד שתי פונקציות - הפונקציה new שיוצרת GameView חדש, והפונקציה step שמפעילה את הפונקציה step של המשחק שמוצמד ל View. בקוד ה main אני כותב:
let mut siv = cursive::default();

let game = GameView::new();
let tx = siv.cb_sink().clone();

thread::spawn(move || {
    // game.board.step(Direction::Up);
    loop {
        thread::sleep(Duration::from_millis(500));
        tx.send(Box::new(|si| {
            si.call_on_name("game", |view: &mut GameView|  {
                let still_alive = view.step();
                if !still_alive {
                    panic!("You lose");
                }
            }).unwrap();
        })).unwrap();
    }
});
ברור למה אני מפעיל Thread חדש כדי שירוץ ברקע (עשינו משהו דומה בדוגמת התקשורת), אבל שימו לב שאני לא יכול מתוך ה Closure של ה Thread לקרוא ל game.step סתם כך. כלומר קוד כזה לא היה מתקמפל:
thread::spawn(|| {
    loop {
        thread::sleep(Duration::from_millis(500));
        game.step();
    }
});
בגדול בגלל שאף אחד לא יכול להבטיח ל Rust ש game יישאר חי כל זמן שה Thread נשאר חי (אולי זה משתנה פנימי של הפונקציה), אז ראסט צריך להעביר את הבעלות על game לתוך ה Closure, אבל אם הוא יעשה את זה לא נוכל להשתמש ב game מחוץ ל Closure. במלים אחרות למרות שאנחנו יודעים שה Thread המשני והראשי יישארו חיים לאותו משך זמן, ראסט לא יודע את זה ולכן דורש שנבחר מי משניהם ינהל את Game. וזאת בחירה שאין לי איך לעשות. וזה מביא אותי לגירסה הקצת יותר מסורבלת של הקוד שמטרתה להשאיר את game בבעלות התהליכון הראשי, ולהפעיל את הפונקציה step באמצעות שליחת הודעה מה Thread המשני. הפונקציה siv.cb_sink של ספריית cursive מייצרת צינור תקשורת איתו אני יכול לשלוח הודעות מתהליכוני רקע ליישום הראשי, הפקודה send של צינור התקשורת שולחת את ההודעה ותוכן ההודעה הוא Closure בעצמו שמקבל Reference של אוביקט ה siv הראשי. מתוך ה Reference אני מפעיל את call_on_name כדי לקבל Reference ל Game View, ודרכו אני מפעיל את step, כל זה בלי שאף אחד ייקח בעלות על שום דבר. אני קורא לקוד כזה "מס ראסט", כי אני עובד יותר קשה רק בשביל להתאים לשיטת העבודה של ראסט. לאורך זמן נצטרך להחליט אם הערך של ראסט - כלומר תוכניות בטוחות יותר - שווה את המחיר. אם נסכם את כל קטעי הקוד מהפוסט ועוד קצת קטעים של חיבורים נגיע לתוכן המלא של main.rs:
use cursive::{
    event::{Event, EventResult, Key},    
    view::{Nameable},    
    Printer, Vec2,
};
use game::{Direction};
use std::{thread};
use std::time::Duration;
mod game;

struct GameView {
    // Actual board, unknown to the player.
    board: game::Game,
    direction: Direction,
}

impl GameView {
    pub fn new() -> Self {
        return GameView {
            board: game::Game::new(),
            direction: Direction::Down,
        };
    }

    pub fn step(&mut self) -> bool {
        return self.board.step(self.direction);
    }
}

impl cursive::view::View for GameView {
    fn draw(&self, printer: &Printer) {
        printer.print(self.board.snake.front().unwrap(), match self.direction {
            Direction::Up => "^",
            Direction::Down => "V",