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

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

הצילו! ספריית הקוד הפתוח שאני צריך כבר לא מתוחזקת

08/02/2023

בעולם של היום הדבר הכי קל בפרויקט זה להוסיף תלות בעוד ספריית קוד פתוח. צריכה הצפנה? יש מודול ב pip שעושה את זה. צריך לפתוח קובץ זיפ? פשוט תריץ npm install. לפעמים נדמה לי שחצי מהעבודה שלנו היא למצוא את המודול המתאים בשביל לפתור את הבעיה.

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

ואם זה גם המקרה שלכם הנה כמה רעיונות פרקטיים שאפשר לנסות:

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

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

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

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

למה למחוק קוד?

07/02/2023

אודי הרשקוביץ כתב בסיפור על הבראנץ הנדיב-

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

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

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

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

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 שהיא עובדת מולו, ונצטרך לעבור על הקומפוננטות אחת אחת כדי לאסוף את כל הקריאות.

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