הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

אילוצים פיקטיביים

06/02/2023

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

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

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

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

function update() {
  var i;
  for (i = 3; i <= 10; i++) {
    labelID = "line_R" + i;
    if (document.getElementById("num").value >= i) {
      document.getElementById(labelID).style.display = "list-item";
    } else {
      document.getElementById(labelID).style.display = "none";
    }
  }
}

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

העלות של קיצורי דרך (ולמה כל כך מסובך לכתוב gitignore)

05/02/2023

ניסיתי לבקש השבוע מ gitignored ליצור לי קובץ gitignore לפרויקט פייתון. זאת היתה ההצעה שלו:

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

אחסוך לכם את הספירה - יש פה 160 שורות. מאה ושישים. אני מנחש שרק חלק קטן מהאנשים שישתמשו בתבנית כזאת באמת יקראו את כל השורות כדי לחשוב מה רלוונטי ומה לא. ובדומה לקיצורי דרך אחרים גם פה יש Trade Off.

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

בפיתוח תוכנה הסיטואציה היא אף פעם לא "כמה שיותר יותר טוב"; לכל בחירה יש עלות, וכדאי לחפש אותה לפני השימוש בכלי החדש.

היום למדתי: הגדרת פרוטוקול ב Python

04/02/2023

מנגנון Type Hints של פייתון מציע דרך להוסיף הגדרת טיפוסים למשתנים ופונקציות שלנו. כלים כמו mypy יכולים אז לעבור על הקוד ולזהות מתי ניסינו לעשות משהו שלא יעבוד בגלל בעיית טיפוסים ולהציל אותנו מבאגים טפשיים.

המשך קריאה

מתי עוצרים?

03/02/2023

בתיעוד של Redux מוצגת הדוגמה הבאה ל Reducer:

function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + action.payload }
    case 'counter/decremented':
      return { value: state.value - action.payload }
    default:
      return state
  }
}

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

  1. אין Context - מי שיסתכל על הקוד הזה בעוד שבועיים לא ידע מתי פעולה מסוימת נקראת, מה החשיבות שלה ביישום באופן כללי.

  2. אין טיפוסים - לא ברור מה צריך להיות הערך של action.payload ואיך הוא מחובר ל action.type בלי לקרוא ולהבין את הקוד עצמו.

  3. שימוש במחרוזות בתור טיפוסים של actions-ים אומר שאם מישהו ירצה לשנות שם של פעולות הוא יצטרך לעבור על כל הקבצים ביישום כדי לחפש את כל המקומות בהם המחרוזת מופיעה.

אז אין ספק שעם 10 שורות הכל עובד מעולה אבל ב 500 שורות כבר אי אפשר לתחזק את הקוד. ועכשיו השאלה מתי עוצרים? מתי משכתבים את היסודות כי הלוגיקה של המערכת שלנו נהייתה מסובכת מדי? אני יכול לחשוב על 3 אפשרויות:

  1. אפשר לחפש מהתחלה בסיס שיגדל טוב - לדוגמה אפשר להתחיל את הפרויקט עם Redux Toolkit ו TypeScript ולקבל יסודות טובים יותר מהשניה הראשונה.

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

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

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

לתפוס את הרגע

02/02/2023

יושב מתכנת בחברת תוכנה כלשהי ומקבל דיווח על באג - הבילדים נשברים. אחרי קצת מחקר מתגלית הבעיה: סקריפט הבניה של התוכנה שלהם מוריד קוד מגיטהאב בתור קובץ tar.gz ובודק Checksum על הקובץ שירד כדי לראות שקיבל את הקובץ הנכון. רק אם ה Checksum מתאים הוא מריץ את הבניה. ומסתבר שלאחרונה ה Checksum-ים על קבצי ה tag.gz התחילו להשתנות.

פרטים בקישור: https://github.com/orgs/community/discussions/45830

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

וזה רגע שצריך לתפוס.

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

It's been known for forever that these files aren't stable

GitHub doesn't guarantee the stability of checksums for automatically generated archives. annndd its gonee

This is why I generally upload tarballs to my releases. Artifacts attached to a release won't change.

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

ואז נתתי ל Chat GPT לכתוב לי את ה SQL

01/02/2023

סיפרתי לכם כבר על התעודות שהוספתי לאתר, ואיך כתבתי שאילתת SQL לא מאוד מתוחכמת כדי להבין אם משתמש ראה את כל השיעורים בקורס כדי לדעת אם אפשר לשלוח תעודה.

אלא שאז כמה אנשים ביקשו אפשרות להדפיס תעודות ישנות, כלומר תעודות על קורסים שהם כבר לא יכולים לגשת אליהם כי המנוי שלהם נגמר. הפיתרון לזה היה לייצר מסך "רשימת תעודות" שיראה לכם במקום אחד את כל הקורסים שסיימתם עם אפשרות להוריד תעודה לכל קורס. אבל עכשיו יש לנו בעיה חדשה - השאילתה שבודקת אם תלמיד סיים קורס תלויה במזהה הקורס. הפעלה שלה בלולאה על כל הקורסים מייצרת את באג N+1 הידוע לשימצה. הגיע הזמן לעדכן את השאילתה כדי להוציא את כל הקורסים שתלמיד סיים.

האתגר בקצרה ברמת הטבלאות נשמע כך-

  1. יש טבלה בשם bundle שמחזיקה נתונים על קורסים. אנחנו רוצים לקבל את ה id של כל הקורסים שתלמיד סיים.

  2. יש טבלה בשם lessons שמחזיקה את כל השיעורים. לכל שיעור גם יש id ובגלל ששיעור שייך לקורס יש לו גם bundle_id.

  3. יש טבלה בשם user_in_lessons שבשביל הסיפור שלנו מספיק לחשוב שהיא מחזיקה שתי עמודות user_id ו lesson_id. אם יש בה שורה למשתמש ושיעור מסוים זה אומר שהמשתמש ראה את כל השיעור.

  4. טבלה אחרונה בסיפור היא users. אנחנו מתחילים עם id מסוים מטבלה זו.

המסלול בגדול הוא לקחת את המשתמש, להוציא את כל השיעורים שהוא עשה מסודרים לפי קורסים, לבדוק באיזה קורסים הוא עשה את כל השיעורים ולהחזיר רשימה של כל הקורסים האלה. ואת המשימה הזאת רק באנגלית הדבקתי בחלון השיחה של Chat GPT. התשובה באמת נראית כמו שאילתה:

SELECT bundles.id
FROM bundles
JOIN lessons ON bundles.id = lessons.bundle_id
WHERE NOT EXISTS (
  SELECT *
  FROM lessons AS l
  LEFT JOIN user_in_lesson AS uil ON l.id = uil.lesson_id
  WHERE uil.user_id = [user_id] AND l.bundle_id = bundles.id
) IS NULL;

ונכון, היא לא עובדת, אבל היי יש פה רעיון מעניין, באמת אפשר להשתמש ב Sub Queries כדי לרוץ על כל הקורסים בלולאה בתוך בסיס הנתונים. רק צריך לתקן את תתי השאילתות ומגיעים לגירסה שבסוף הכנסתי לאתר (מתורגמת כבר לריילס):

@bundles = Bundle
.joins(:lessons)
.where("
  (SELECT count(*)
   FROM lessons AS l
   LEFT JOIN user_in_lessons AS uil ON l.id = uil.lesson_id
   WHERE uil.user_id = ? AND l.bundle_id = bundles.id)
   =
  (SELECT count(*) FROM lessons where lessons.bundle_id = bundles.id)", user_id).distinct

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

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

עוד 3 פיצ'רים של ES13 בהם אתם אשכרה הולכים להשתמש

31/01/2023

ג'אווהסקריפט כל הזמן מתקדמת ולמרות שכבר ירדנו מרכבת ההרים שהיתה ES6, עדיין יש שיפורים קטנים שהופכים את החיים לקלים יותר כל יום. אז נכון כשחושבים על ES13 מיד קופץ לראש סימן הסולמית והיכולת להגדיר פונקציות פרטיות, אבל האמת שבזה אני לא רואה סיבה להשתמש. הנה 3 פיצ'רים אחרים שדווקא כן יכולים להיכנס לקוד שלי-

המשך קריאה

ואז הם הציעו "בואו נחליף את ה Backend"

30/01/2023

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

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

בשביל הדוגמה נדמיין Service שכתוב בריאקט ומתחבר לשרת Backend, אבל גם מתחבר לכל מיני APIs ברשת. בקוד יש כמה מאות קומפוננטות, חלקן לוקחות מידע מה Backend דרך SWR, חלקן מעדכנות מידע באמצעות fetch וקומפוננטות אחרות משתמשות בממשקים ייעודיים של APIs חיצוניים כדי לתקשר עם אותם APIs. ועכשיו נשאל "איפה אני מוצא רשימה של כל בקשות הרשת שה Service שלי מסוגל להוציא?"

במקרה הטוב אם הקוד קריא ביחס לשאלה הזאת יהיה לנו במערכת קובץ אחד שמטפל בכל הבקשות שיוצאות מהיישום ומהווה "נקודת יציאה" לכל Backend או API. הקומפוננטות תמיד יעברו דרך נקודת יציאה זו וכך בשביל לענות על השאלה צריך רק לפתוח את הקובץ.

במקרה הפחות טוב כל קומפוננטה תקרא בדרך שלה ל API או ה Backend שהיא עובדת מולו, ונצטרך לעבור על הקומפוננטות אחת אחת כדי לאסוף את כל הקריאות.

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

בואו נבנה לוח מודעות ב Rails בלי JavaScript

29/01/2023

אחת הדוגמאות הראשונות שאני מראה ל Web Sockets היא בניה של לוח מודעות משותף לכמה גולשים - כל פעם שגולש אחד שולח הודעה, ההודעה מיד מופיעה על המסך של כל האחרים. והיום ננסה לבנות את אותו לוח מודעות בלי לכתוב שורה של JavaScript, רק באמצעות המנגנונים המובנים ב Rails.

המשך קריאה

מעקפים

28/01/2023

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

function isInCanvas(x, y) {
    if (typeof x === 'string') {
        x = Number(x);
    }
    if (typeof y === 'string') {
        y = Number(y);
    }

    // check if (x,y) is in the canvas
}

וכן יש שתי בעיות עם מעקפים:

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

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

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