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

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

רגע, למה זה עובד?

12/12/2022

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

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

וזה מביא אותנו לכמה בעיות-

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

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

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

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

פיתרון Advent Of Code 2022 יום 6 ביוניקס

11/12/2022

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

mjqjpqmgbljsphdztnvjfqwrcgsmlb

ואנחנו רואים שבקריאה משמאל לימין הפעם הראשונה שמופיעים 4 תווים שונים היא התווים jqpm ולכן האינדקס של התו שאחריהם (האות g) הוא 7.

בשביל למצוא אותו אפשר לתת ל bash לעבוד:

  1. נרוץ בלולאה עם משתנה לולאה i שמתחיל מ-1.
  2. לכל ערך של i נחתוך מהמחרוזת את 4 התווים החל מהמקום ה i בעזרת cut.
  3. נשלח את egrep לחפש תו כפול במחרוזת. אם הוא לא מצא אפשר לצאת מהלולאה ולהדפיס את i+4-1

בהנחה שהקלט נשמר בקובץ input.txt תוכנית ה bash המתאימה היא:

#!/bin/bash

i=1
while cat input.txt | cut -c $i- | cut -c -4 | egrep '(\w).*\1' >& /dev/null
do
    (( i++ ))
done

echo $(( i + 4 - 1))

או בשורה אחת:

i=1; while cat input.txt| cut -c $i- | cut -c -4 | egrep  '(\w).*\1' >& /dev/null ; do; (( i ++ )); done; echo $(( i + 4 - 1))

שלוש נקודות לקחת מהתוכנית:

  1. הרבה פעמים נוח לפצל תנאי לכמה תנאים ב Pipeline. במקרה שלנו בשביל לחתוך 4 תווים החל ממקום i אפשר לשבור את השיניים בשביל ליצור פקודת cut אחת, אבל יותר קל לפצל את זה לשתי פקודות cut אחת אחרי השניה, אחת מתחילה מהמקום ה i והשניה חותכת 4 תווים.

  2. מאוד נוח שב bash פקודת while מבצעת פקודה שהיא מקבלת שוב ושוב כל עוד הפקודה מצליחה.

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

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

גיט הוא בסך הכל גרף

10/12/2022

  • אתה יודע מאוד עוזר לחשוב על גיט בתור גרף מכוון.

  • גרף מה?

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

  • אבל איך אוטובוס קשור לגיט?

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

  • מה הכוונה כיוון הנסיעה?

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

  • עדיין לא הבנתי. איך הקומיט קשור לתחנת אוטובוס?

  • בוא ננסה את זה בקוד. אני יוצר פרויקט חדש עם גיט בתיקיה הנוכחית:

$ git init .

כותב שטויות באיזה קובץ ושומר אותו בקופסה - זאת התחנה הראשונה:

$ echo hello world > readme.txt
$ git add readme.txt
$ git commit -m 'initial commit'

אני יכול לראות את כל התחנות עם פקודת git log, אבל כרגע יש שם רק אחת:

$ git log --oneline
f812a16 initial commit

לתחנה האחת שלי אגב יש שם - f812a16. זה מזהה ייחודי של התחנה הזאת. נמשיך לבנות עוד כמה תחנות במסלול:

$ echo second stop > status.txt
$ git add .
$ git commit -m 'second stop'
[main b39bd7e] second stop

$ echo third stop > status.txt
$ git add .
$ git commit -m 'third stop'
[main 43ea893] third stop

$ echo fourth stop > status.txt
$ git add .
$ git commit -m 'fourth stop'
[main fb7fdb6] fourth stop

הלוג עכשיו מראה את כל התחנות:

$ git log --oneline

fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit

אבל הדבר החשוב כאן הוא הכיוון: כל תחנה "מצביעה" על התחנה שבאה לפניה. אני יכול לראות את זה עם cat-file:

$ git cat-file commit fb7fdb6

tree 2394fdb509358a49d452574536bec528d119ba06
parent 43ea893b8bda3420d7a794ef14a7246d94f88ced
author ynonp <ynonperek@gmail.com> 1670589664 +0200
committer ynonp <ynonperek@gmail.com> 1670589664 +0200

fourth stop

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

  • רגע רגע, מה הכוונה "התחנה בה אתה נמצא?" - אפשר לנסוע בין התחנות?

  • ברור. זאת כל הפואנטה. הפקודה git switch לוקחת אותך לתחנה אחרת. הנה דוגמה:

$ git switch --detach b39bd7e

HEAD is now at b39bd7e second stop

מבט על הפרויקט יראה לנו את הקבצים כמו שהם היו כשהיינו בתחנה השניה. אבל השוס האמיתי הוא הפלט של git log:

$ git log --oneline
b39bd7e second stop
f812a16 initial commit

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

  • מה? מחקת לי את כל הפרויקט?? אז איך אני חוזר עכשיו לנקודת ההתחלה?

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

$ git branch -v
* (HEAD detached at b39bd7e) b39bd7e second stop
  main                       fb7fdb6 fourth stop

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

$ git switch main
Previous HEAD position was b39bd7e second stop
Switched to branch 'main'

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

$ git log --oneline | cat

fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit

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

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

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

git reset --hard b39bd7e

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

$ git log main --oneline| cat
b39bd7e second stop
f812a16 initial commit

$ git branch -v | cat
* main b39bd7e second stop

בספר הכתובות שלי השם main מודבק עכשיו ליד תחנה b39bd7e. התחנה fb7fdb6 עדיין נמצאת בשכונה, אבל בשביל להגיע אליה אני חייב להשתמש בכתובת המדויקת שלה. לפני שנשכח אני מוסיף אותה לספר הכתובות עם שם חדש:

$ git branch dev fb7fdb6

ועכשיו יש לי שתי כתובות בספר:

$ git branch -v
  dev  fb7fdb6 fourth stop
* main b39bd7e second stop

ואני שוב יכול לחזור לכתובת הרחוקה ביותר:

$ git switch dev
$ git log --oneline
fb7fdb6 fourth stop
43ea893 third stop
b39bd7e second stop
f812a16 initial commit
  • נשמע מלחיץ. ואיך זה מסתדר עם merge-ים? ריבייסים? צ'רי פיק?
  • כן צריך להיזהר עם גיט, אבל כשמבינים את המודל שלו הרבה יותר קל לצאת ממצבים מביכים. הייתי שמח להישאר ולקשקש אבל אני רואה את האוטובוס שלי מגיע. על מרג'ים, ריבייסים וכל השאר נצטרך להמשיך לדבר ביום אחר. אה, וכמעט שכחתי - יש בטוקוד קורס גיט נהדר שמסביר את הפקודות יחד עם המודל המנטלי שמתאים להן. נתראה בתחנה הבאה.

בדיקות יחידה טובות

09/12/2022

אלה כמה קריטריונים שאתם יכולים לאמץ כדי לראות אם בדיקות היחידה שלכם עובדות בשבילכם, או אתם בשבילן-

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

  2. הבדיקה נכשלת כשאתם שוברים משהו, וכשהיא נכשלת אתם מבינים בדיוק מה שברתם.

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

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

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

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

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

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

היום למדתי: סימון פונקציה בתור Deprecated ב TypeScript

08/12/2022

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

עם טייפסקריפט ו VS Code סימון כזה הוא יחסית פשוט:

  1. אנחנו מוסיפים את המילה @deprecated בהערת JSDoc מעל הגדרת הפונקציה אותה רוצים לסמן בתור Deprecated.

  2. באופן אוטומטי כש VS Code ימצא שהשתמשתי באותה פונקציה הוא יסמן קו באמצע של הקוד שמפעיל את הפונקציה (strike through) וכשאני אעבור על הסימון עם העכבר אני אוכל לראות את כל ההערה, שם בדרך כלל תהיה המלצה במה להשתמש במקום.

דוגמה? בטח. נסו להדביק את הקוד הבא בקובץ טייפסקריפט ותראו את הקסם:

/**
 * @deprecated The method should not be used. Use `bar` instead.
 */
function foo(x: number) {
    return x + x;
}

function bar(x: number) {
    return x * 2;
}

const x = foo(10);
const y = bar(20);

ואם אין לכם פרויקט טייפסקריפט זמין אפשר לראות את האפקט גם לייב ב TypeScript Playground בקישור הזה.

שתי דרכים לבנות מערכת (או: עניין של העדפה)

07/12/2022

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

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

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

טיפ ריסלקט - שימו לב כשאתם מפעילים filter או map בתוך Selector

06/12/2022

ספריית Reselect עוזרת לנו לבנות פונקציות שמושכות מידע מ Redux Store. ריסלקט מגיעה כחלק מספריית Redux Toolkit ובשביל להשתמש בה בפרויקט אנחנו צריכים רק לייבא את הפונקציה המרכזית שלה createSelector:

import { createSelector } from "@reduxjs/toolkit";

בשביל להבין למה צריך את createSelector נצטרך להיזכר איך עובדת הפונקציה useSelector בה אנחנו משתמשים בתוך קומפוננטות:

  1. בתוך קוד קומפוננטה אני קורא ל useSelector כדי "לחבר" בין קוד הקומפוננטה לנתיב מסוים באוביקט ה State ב Redux.

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

const todos = useSelector((state: AppState) => state.todos);

כדי לגשת לכל מערך ה todos שיש בסטייט.

  1. כל פעם שמישהו באיזשהו מקום במערכת עושה dispatch, כל פונקציות ה useSelector יתעוררו ויפעילו את הפונקציות שהן קיבלו כפרמטרים. אלה שיחזירו ערך חדש יגרמו לרינדור מחדש של הקומפוננטה. זה אומר שבכל Action שישלח ל Store, הקוד שלי יסתכל על השדה todos וישווה אותו לערך הקודם.

  2. לפעמים אני רוצה להפעיל חישובים מורכבים בתוך useSelector. במצב כזה אולי לא משתלם להפעיל את כל החישוב מחדש, במיוחד אם אני יודע במה החישוב תלוי. לדוגמה נתבונן ב Selector הבא:

const todosCount = useSelector((state: AppState) => state.todos.length);

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

  1. הספריה Reselect מאפשרת לנו לתאר את הקשר בין Selector-ים שונים, וכך לחסוך חישובים מיותרים. סלקטור יחושב מחדש רק אם חלק מהתלויות שלו השתנו.

בואו נראה עוד דוגמה קצת יותר מורכבת אבל עדיין בעולם של Todos:

import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "./store";

const todos = (state: AppState) => state.todos;

export const todosThatStartWithA = createSelector(todos, (todos) =>
  todos.filter((t) => t.message.startsWith("a"))
);

export const finishedTodosThatStartWithA = createSelector(
  todosThatStartWithA,
  (todos) => todos.filter((t) => t.completed)
);

export const numberOfFinishedTodosThatStartWithA = createSelector(
  finishedTodosThatStartWithA,
  (finishedTodosThatStartWithA) => {
    console.log(`Recalculating the length`);
    return finishedTodosThatStartWithA.length;
  }
);

יש לי כל מיני שאלות שקשורות ל todos, למשל:

  1. מי ה todos במערך שההודעה שלהם מתחילה ב a ?

  2. מי ה todos במערך שההודעה שלהם מתחילה ב a, והם מסומנים בתור completed ?

  3. כמה todos יש במערך שגם ההודעה שלהם מתחילה ב a וגם מסומנים בתור completed ?

השימוש ב createSelector מספק דרך נוחה לשלב כמה Selectors, וגם לחשב מחדש כל Selector רק כשהתלויות שלו באמת משתנות. אבל בואו לא נתבלבל, אפילו Reselect אינו קוסם.

למרות שהכתיב נותן הרגשה כאילו ה Selectors תלויים אחד בשני, בפועל בגלל ששני ה Selectors שבאמצע משתמשים ב filter (ולכן מייצרים מערך חדש), מצב התלויות הוא שכל ה Selectors תלויים ב state.todos. ברגע שמערך ה todos משתנה מכל סיבה שהיא, כל ה Selectors יחושבו מחדש, כי todosThatStartWithA ו finishedTodosThatStartWithA תמיד מחזירים מערך חדש. אם נריץ את הקוד נגלה שכל שינוי ב todos, גם אם ה todos שהשתנו לא מתחילים ב a, עדיין גרם לחישוב מחדש של ה Selector האחרון.

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

הצצה לעולם פוסט GPT3

05/12/2022

שירות הצ'ט החדש של OpenAI פותח צוהר לעולם עתידני בו (הרבה יותר קרוב ממכוניות מעופפות. כנראה גם הרבה יותר קרוב ממכוניות שנוסעות לבד) בו מחשבים יקראו דרישות של אנשי מוצר ויכתבו בעצמם את רוב הקוד. כבר היום אנחנו יכולים לבקש ממחשב לכתוב תוכניות פשוטות והוא די מבין את המשימה, למשל אני ביקשתי ממנו ב Playground:

Implement a program that reads a file and writes its lines backwards

וקיבלתי את הקוד הבא בפייתון:

import sys

def main():
    if len(sys.argv) != 2:
        print("Usage: python reverse.py <file>")
        sys.exit(1)

    file = sys.argv[1]
    with open(file, 'r') as f:
        lines = f.readlines()
        lines.reverse()
        for line in lines:
            print(line, end='')

if __name__ == "__main__":
    main()

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

אז מה, להחליף מקצוע? נדמה לי שהמסקנה היא בדיוק הפוכה:

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

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

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

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

בחזרה לבאגים, בדוגמה אחרת ביקשתי מ GPT3 לייצר לי Unix Pipeline שסוכם את כל המספרים שהוא קיבל מהקלט הסטנדרטי עם השאילתה:

create a unix pipeline to sum all the numbers read from stdin

הוא הציע שני רעיונות, כל אחד עם הבעיות שלו:

# Option 1
cat | tr ' ' '\n' | awk '{ sum += $1 } END { print sum }'

# Option 2
cat | paste -sd+ | bc

בראשון ה cut וה tr מיותרים; השני לא עובד על מק ואיתו כנראה על עוד כמה וריאנטים של יוניקס כי paste אצלנו לא קוראת מ STDIN.

ולמרות הכל הסיפור הזה מרשים ומרגש. אם עוד לא קשקשתם עם GPT3 לכו להירשם עכשיו. טירוף מה שהולך שם.

ושוב awk הציל את היום

04/12/2022

הבטחתי לעצמי שאני לא אעשה את כל ה Advent Of Code-ים השנה, ואני באמת מתכנן להפסיק - אבל עד שזה יקרה אני מקווה להשתמש בחידה של היום כדי להראות עוד כמה טריקים של יוניקס ו awk שאולי יעזרו לכם גם מחוץ לארץ האלפים.

המשך קריאה

איך קוד מתקלקל (דוגמת טייפסקריפט + ריאקט)

03/12/2022

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {
    // component implementation ...
}

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

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {
    // component implementation ...
}

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

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type PinkProps = {
    page?: number;
    itemsPerPage?: number;
    items: Array<{ id: number, text: string }>;
};

type OrangeProps = Pick<PinkProps, "page"|"itemsPerPage"> & {
    items: Array<{ id: number, visible: boolean }>
};

function isValid(item: OrangeProps['items'][number]) {
    // validate the item from orange props
}

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

type Paginated = {
    page?: number;
    itemsPerPage?: number;
}

type TextItem = {
    id: number;
    text: string;
}

type ToggleItem = {
    id: number;
    visible: boolean;
}

type PinkProps = Paginated & {
    items: Array<TextItem>;
};

type OrangeProps = Paginated & {
    items: Array<ToggleItem>;
}

function isValid(item: ToggleItem) {
    // validate the item from orange props
}

function Pink({ page=1, itemsPerPage=25, items }: PinkProps) {}


function Orange({ page=1, itemsPerPage=25, items}: OrangeProps) {}

ארוך יותר, כן, אבל הרבה יותר ברור וקל להרחבה.

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