איך להתחיל ללמוד תכנות מאפס מהבית
השאלה הזאת חוזרת מספיק פעמים בכל מיני שיחות עם תלמידים ומתעניינים בשביל שיהיה מעניין לכתוב מדריך מסודר שיעזור לכם ולחברים שלכם להתחיל ללמוד תכנות מאפס מהבית.
טיפים קצרים וחדשות למתכנתים
השאלה הזאת חוזרת מספיק פעמים בכל מיני שיחות עם תלמידים ומתעניינים בשביל שיהיה מעניין לכתוב מדריך מסודר שיעזור לכם ולחברים שלכם להתחיל ללמוד תכנות מאפס מהבית.
אחת הסיבות לאבד מוטיבציה לביצוע משימה היא ההבנה (הכמעט בלתי נמנעת) שהמשימה יותר מסובכת ממה שחשבתי שתהיה. יש פה משהו מטלטל כי הנה הצלחתי להתגבר על כל החסמים הפסיכולוגיים ולשבת לעבוד, הייתי בטוח שהדבר הזה הולך לקחת שעתיים ובסוף אחרי שעתיים הקוד לא עובד ואין לי מושג לאן להמשיך.
הרבה פעמים הפחד לגלות שמשימות הן יותר מסובכות ממה שחשבנו מוביל אותנו לאמץ אמונות שגויות בקשר לעצמנו:
״אני צריכה מסגרת מוגדרת בשביל להתקדם וללמוד״
״אני מצליח לבצע משימות קטנות אבל לא לחבר את הכל לפרויקט גדול״
״אני פוחדת לעבוד שבועיים רק בשביל לגלות בסוף שהלכתי בדרך הלא נכונה״
״אני פוחד להיתקע ולגלות שהפרויקט הזה גדול עליי״
אז כן יש סיכוי טוב שהפרויקט הנוכחי שלך הולך להיות יותר מסובך ממה שחשבת שיהיה. יש סיכוי טוב שתרגיל שחשבת לסיים בשעתיים ייקח בסוף שבוע, ושבדרך תצטרך לכתוב אותו מחדש 3 או 4 פעמים. יש סיכוי די טוב שבפרויקט החדש הזה שאת מתחילה היום את הולכת ללכת בכיוונים ממש לא נכונים ותצטרכי לחזור אחורה, לתקן ולנסות שוב, יותר מפעם אחת.
ועכשיו שאנחנו יודעים את זה בואו נוסיף את ההסתבכויות להערכות הזמנים שלנו ונתמקד במחר בבוקר: להתישב שוב ליד המחשב, לנסות עוד רעיון, ולהתקדם עוד צעד. הפחד לא יעלם אבל אפשר לשים אותו בצד. ובעצם זאת הדרך היחידה ללמוד.
אחד החיבורים שאני אוהב הוא בין TypeScript ל React Hooks - בצד של ריאקט המעבר ל Function Components הפך את הקוד להרבה יותר פשוט מאשר בתקופה שעבדנו עם מחלקות, ובצד של TypeScript הוא מצליח להבין כמעט את כל מה שאני זורק עליו והתמיכה בטיפוסים עוזרת לכתוב קוד יותר יציב.
עד שמגיעים לכתוב Custom Hooks.
ניקח לדוגמא את הקוד הבא שמגדיר Custom Hook שפונה לשרת להביא מידע בתור JSON:
function useRemoteData(endpoint: string, id: string) {
const [data, setData] = useState<any|null>(null);
useEffect(function() {
setData(null);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
cancelToken: source.token,
});
req.then(function(response) {
// when we get response
setData(response.data);
});
return function cancel() {
source.cancel();
}
// code continues here
}, [id]);
return data;
}
הקוד עובד אבל שימו לב לשורה שמגדירה את סוג המשתנה data:
const [data, setData] = useState<any|null>(null);
בגלל שבתוך ה Custom Hook אני לא יודע מה יהיה הנתיב בשרת שיבקשו ממני להביא, ומה מבנה המידע שצריך לחזור ממנו (כי ה Hook נמצא בשימוש של מספר קומפוננטות), לא ידעתי איזה סוג משתנה לבחור לערך שהשרת מחזיר.
הבעיה עם הקוד הזה היא שברירת המחדל של קוד חיצוני שמשתמש בו היא לעבוד בלי הגדרות טיפוסים ותוך הסתמכות על הטיפוס any שחוזר מהפונקציה. במילים אחרות הקוד הבא מתקמפל ועובד ולא בודק שגיאות כתיב או גישה לשדות שלא צריכים להיות בתשובת השרת:
function FilmInfo(props: { id: string }) {
const { id } = props;
// Get character data ???
const data = useRemoteData('films', id);
if (data === null) {
return <p>Loading, please wait...</p>
}
return (
<div>
<p>title: {data.title}</p>
<p>release_date: {data.release_date}</p>
<hr />
</div>
)
}
בהנחה שאני יודע לגבי כל נתיב מה הוא אמור להחזיר, דרך אחת לגרום לקוד שמשתמש ב Hook לוודא שהוא משתמש רק בשדות שהוא צריך היא להשתמש בפקודה as מתוך הקוד שקורא ל hook, כלומר להחליף את השורה השניה בפונקציה בשורה הזו:
const data = (useRemoteData('films', id) as IDataFilm);
ולהוסיף באיזשהו מקום בקובץ את הגדרת הממשק IDataFilm. אבל זה לא באמת פותר לנו את הבעיה: לא כולם יודעים או זוכרים שכדאי להשתמש ב as כל פעם לפני שמפעילים פונקציה ומהר מאוד נתחיל לראות מתכנתים שמוותרים על זה. דרך קצת יותר ברורה היא להשתמש ב Generics. המילה Generics בסך הכל אומרת שהקוד שקורא ל Hook חייב להעביר גם את סוג המידע שהוא מצפה לקבל, ואז ה Custom Hook שלנו יראה כך:
function useRemoteData<T>(endpoint: string, id: string) {
const [data, setData] = useState<T|null>(null);
useEffect(function() {
setData(null);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const req = axios.get(`https://swapi.co/api/${endpoint}/${id}/`, {
cancelToken: source.token,
});
req.then(function(response) {
// when we get response
setData(response.data);
});
return function cancel() {
source.cancel();
}
// code continues here
}, [id]);
return data;
}
שני השינויים היחידים הם בשורת ההגדרה של הפונקציה ובשורת הגדרת הטיפוס שנשמר בסטייט. עכשיו הקוד הקודם שמשתמש ב Hook כבר לא מתקמפל כי לא אמרנו מה הטיפוס שאנחנו מצפים לקבל. בשביל להשתמש ב Hook נהיה חייבים להעביר את הטיפוס של תשובת השרת באופן הבא:
function FilmInfo(props: { id: string }) {
const { id } = props;
// Get character data ???
const data = useRemoteData<IDataFilm>('films', id);
if (data === null) {
return <p>Loading, please wait...</p>
}
return (
<div>
<p>title: {data.title}</p>
<p>release_date: {data.release_date}</p>
<hr />
</div>
)
}
המעבר ל Generics יצר אילוץ על כל מי שמשתמש ב Hook כך שעכשיו נהיה חייבים לציין את סוג המידע שאנחנו מצפים לו. גישה כזו מונעת טעויות הקלדה והופכת את תחזוקת הקוד להרבה יותר קלה, שכן כל פעם שנעשה Refactoring ונשנה שמות של שדות או נמחק שדות מתשובת השרת באופן אוטומטי TypeScript יוכל להגיד לנו איזה קומפוננטות הושפעו מהשינוי.
השילוש הקדוש בעבודה עם גיט הוא רצף שמתכנתים מדקלמים כמעט מתוך שינה: add, commit ו push. ובעוד שיחסית אנשים מצליחים להשתמש ב git reset אחרי שעושים add לדבר הלא נכון, בקומיט הסיפור שונה לגמרי.
כמעט כל המתכנתים שראיתי שעובדים בגיט מרגישים שאחרי commit חייבים לעשות push.
בואו נפתח את זה יחד. כשאתם עושים קומיט אתם שומרים את הגירסא הנוכחית של הפרויקט שלכם בצד. יכול להיות שהגירסא ששמרתם טובה ושאתם רוצים להשתמש בה, אבל גם יכול להיות שעוד כמה רגעים תגלו שהיא לא כזו טובה. אולי יש איזו שגיאת כתיב שפספסתם, אולי איזו בדיקה הפסיקה לעבור ולא ראיתם. אולי אפילו יש עוד תיקון ששכחתם להכניס אבל צריך להיות שם.
שימו לב ל-4 קומיטים לדוגמא:
9a550c6 (HEAD -> master) actually this looks better in a class
e837614 changed some texts
3aa43e8 moved code to function
3785d2d intiial commit
ה diff בין הקומיט הישן ביותר לחדש ביותר נראה כך:
diff --git a/a.rb b/a.rb
index 15aaec7..de05881 100644
--- a/a.rb
+++ b/a.rb
@@ -1 +1,8 @@
-puts "hello world"
+class Greeter
+ def hi
+ puts "Hello World"
+ end
+end
+
+g = Greeter.new
+g.hi
במצב כזה מבט על לוג הפרויקט לא ממש עוזר לי להבין מה מבנה הקוד ומה קרה בכל שלב, אלא כולל הרבה מידע מיותר על שלבי ביניים שאולי עברו לי בראש בזמן שכתבתי את הקוד אבל אין להם שום חשיבות כשמסתכלים אחורה על ההיסטוריה.
מה שיפה בגיט זה שעד שלא עשיתי push אפשר לשנות בלי בעיה את מבנה וסדר הקומיטים ואף אחד לא ישים לב. הנה בפרויקט דוגמא הקטן שלי אני יכול לכתוב:
$ git reset --soft 3785d2d
$ git add .
$ git commit -m 'Changed code to use object oriented syntax'
וקיבלתי לוג הרבה יותר נקי:
6a4e1ad (HEAD -> master) Changed code to use object oriented syntax
3785d2d intiial commit
אני לא מפסיד כאן שום מידע כיוון שההתלבטויות שלי אם לכתוב את הקוד בתור פונקציה או באותיות קטנות או גדולות זה לא דברים שצריך לשמור אותם.
לכן הטיפ שלי להיום - לא קרה כלום אם עשיתם קומיט ושניה אחרי זה התחרטתם. קומיט אחד טוב עם הודעה מפורטת שמסבירה מה קרה שם ומה ההגיון עדיף בהרבה על 20 קומיטים שמתארים את כל מה שעבר עליכם באותו יום ולא עוזרים להבין מה קרה בקוד.
נ.ב. רוצים ללמוד יותר לעומק איך לעבוד ב git ולשחק עם קומיטים כאילו היו כדורי ג'אגלינג? יש לי קורס וידאו שתפור בדיוק עליכם. מתחילים כאן: https://www.tocode.co.il/bundles/git/
טייפסקריפט עשתה דרך ארוכה מאז יציאתה לאוויר העולם באוקטובר 2012, יחד עם כל עולם האינטרנט והפיתוח לאינטרנט. היום היא נתמכת בכל סביבות הפיתוח המרכזיות ובכל מערכות ההפעלה, אבל יותר חשוב מזה - בפרויקט ריאקט היא משתלבת עם התחביר של ריאקט כמו כפפה ליד. הנה שלוש סיבות לשלב TypeScript בפרויקט ריאקט הבא שלכם:
אני לא מבין מה רוצים ממני
אין לי מושג איך לגשת לזה
יש לי רעיון אבל אין לי מושג איך ליישם אותו
התחלתי ליישם את הרעיון אבל זה לא עבד - לא ברור לי למה
הבנתי למה הרעיון שלי לא עבד. עכשיו שוב אין לי שום רעיון.
ממשיכים את 2-5 בלולאה, לפעמים כמה עשרות פעמים. כל פעם לומדים עוד משהו על הבעיה ועל עוד דרך שלא עובדת.
הצלחתי ליישם פיתרון שעובד!
אה, אבל בעצם יש את מקרה הקצה הזה שלא חשבתי עליו.
יש לי רעיון איך לתקן את מקרה הקצה הבעייתי.
ממשיכים את 8-9 בלולאה, לפעמים כמה עשרות פעמים.
יש לי קוד שעובד!
מה המשימה הבאה?
הרבה פעמים אנחנו רוצים לדלג על הלולאות ב-6 וב-10, ומדמיינים שאם נצליח להגיע ל-11 זה סימן שלמדנו את הנושא, לא משנה איך הגענו לשם. אבל זה לא עובד ככה: עיקר הלימוד קורה בתוך הלולאות, ובחיפוש האינטנסיבי אחרי הרעיון הבא.
אם המטרה שלכם היא לבנות מערכת כדאי מאוד להיעזר באיש מקצוע. למרות שפגשתי המון מתכנתים שמעדיפים ללמוד משהו בשביל לבנות בזה מערכת - כמעט תמיד התוצאה יוצאת הרבה יותר יקרה ופחות טובה ממה שהיינו מקבלים אם היינו לוקחים איש מקצוע שכבר מיומן בטכנולוגיה.
מצד שני אם המטרה היא ללמוד משהו חדש אז ברור שכדאי לבנות את התוכנה לבד ולנסות כמה שיותר רעיונות אפילו הכי הזויים שאתם יכולים לדמיין רק בשביל לראות מה עובד ומה לא. התחלנו את המסע הזה בשביל ללמוד וזה הולך להיות כיף בין אם תהיה מערכת בסוף ובין אם לא. כשהמטרה היא לימודים ברור שדברים הולכים להסתבך, וברור שכמה שתשקיעי יותר זמן בהתמודדות עם ההסתבכויות את תלמדי טוב יותר.
הבעיות בחיים מתחילות כשאנחנו מתבלבלים בין המטרות שלנו, או כשיש התנגשות בין מטרות. לדוגמא - כשהבוס אומר שצריך לבנות מערכת חדשה ואנחנו רוצים ללמוד ריאקט. השילוב מייצר לחץ בלתי אפשרי, מצד אחד לבנות את המוצר הכי מהר שאפשר ולהוכיח שהבחירה שלנו היתה טובה, ומצד שני ללמוד תוך כדי תנועה ולקוות לקלוע במטרה כבר מהניסיון הראשון.
יותר קל להסתכל על החיים כמשחק לטווח ארוך: ללמוד כל הזמן טכנולוגיות חדשות עם פרויקטי צד ומערכות פחות חשובות, ולהיכנס לבצע פרויקטים אמיתיים וקריטיים לארגון רק בטכנולוגיות שכבר הגעתם איתן לרמת בשלות מסוימת.
התגובה הראשונה שאני מקבל מחברים כשאני מספר שקניתי ארדווינו היא סוג של פרצוף ושאלת "ומה אתה הולך לבנות עם זה?". ויש בזה משהו - כמעט כל דבר שתרצה לבנות מישהו כבר פיתח ומוכר די בזול. העולם נהיה כזה שאפשר לקנות כמעט כל גאדג'ט בגרושים. ובכל זאת יש כיף בלבנות דברים לבד, גם אם הם לא רובוטים תלת מימדיים או חלליות שטסות לירח. בקיצור אם נתקעתם עם ארדווינו ואתם מחפשים מה לעשות איתו, קבלו 4 רעיונות פשוטים שמורכבים אך ורק מנורות, נגדים וכפתורים ושיעזרו לכם להתאמן על תכנות וגם על חיבור מעגלים חשמליים:
הנה שאלת ראיונות עבודה פשוטה שנתקלתי בה - נתון מערך של מספרים ואתם צריכים לכתוב קוד שימצא את הרצף הכי ארוך של מספרים עוקבים. לדוגמא במערך הזה:
arr = [1, 2, 3, 10, 2, 3, 4, 5, 6, 9, 9, 1, 2, 3, 1, 2, 3]
היינו רוצים לזהות שבאינדקס 4 של המערך יש רצף של 5 מספרים עוקבים, והוא הארוך ביותר מבין הרצפים במערך זה.
כשאני רואה כאלה שאלות האינטואיציה הראשונה שלי היא שכדאי להפריד בין הלוגיקה של "איפה יש רצפים" לבין הלוגיקה של חיפוש רצף ארוך ביותר. הראשונה כנראה ספציפית לשאלה ובשביל השניה בטח נוכל להשתמש במנגנונים קיימים של Python. במקרה שלנו אפשר לחשוב על המערך בתור אוסף של רצפים של מספרים עוקבים (חלקם ממש קצרים). במערך שבדוגמא נוכל למצוא את הרצפים הבאים של מספרים עוקבים:
(0, 3)
(3, 1)
(4, 5)
(9, 1)
(10, 1)
(11, 3)
(14, 3)
כל רצף מזוהה על ידי האינדקס בו מתחיל הרצף (המספר הראשון) ואורך הרצף (המספר השני). כך המערך מתחיל באינדקס 0 ברצף של שלושה מספרים עוקבים - המספרים 1, 2 ו-3 ואחריהם באינדקס 3 יש לנו רצף קצר יותר של מספר יחיד הוא המספר 10.
מרגע שזיהיתי שיש פה אוסף של רצפים אני יכול לקודד את הלוגיקה שמחפשת כאלה רצפים לפונקציה מסוג Generator, בואו נקרא לה consecutives. מימוש עשוי להיראות כך:
def consecutives(arr):
start = 0
i = start
while i < len(arr) - 1:
if arr[i] + 1 == arr[i + 1]:
i += 1
else:
yield(start, i - start + 1)
start = i + 1
i = start
yield(start, i- start + 1)
ואחרי שיש לנו את הפונקציה קל למצוא את כל הרצפים עם לולאת for רגילה:
for seq in consecutives(arr):
print(seq)
או למצוא את הרצף הכי ארוך עם פונקציית max:
print(max(consecutives(arr), key=lambda s: s[1]))
או למצוא את הרצף שסכום האיברים בו הוא הגדול ביותר (שוב עם max):
print(max(consecutives(arr), key=lambda s: sum(arr[s[0]:s[0]+s[1]])))
והתבנית שכדאי לזכור מכאן - הרבה פעמים שווה לנו להתאמץ לשנות קצת את הבעיה כדי שנוכל לפתור אותה בכלים שאנחנו כבר מכירים.
הפוסט הבא הוא יותר תזכורת בשבילי מאשר בשבילכם, למרות שאם אתם כותבים SQL יכול להיות שגם לכם זה יבוא בהפתעה. מסתבר שהשאילתה הבאה לא עובדת טוב בכלל:
SELECT courses.*, count(lessons.id) as lessons_count,
count(course_labels.id) as labels_count
FROM "courses"
LEFT OUTER JOIN "course_labels" ON "course_labels"."course_id" = "courses"."id"
LEFT OUTER JOIN "lessons" ON "lessons"."course_id" = "courses"."id"
GROUP BY courses.id
סיפור הרקע הוא שיש לנו טבלת קורסים, טבלת שיעורים וטבלת תוויות. בכל קורס יש הרבה שיעורים ולכל קורס יש גישה להרבה תוויות דרך טבלת חיבור בשם course_labels
. המטרה של השאילתה היא להוציא במכה אחת את כל הקורסים, יחד עם מספר התוויות שיש לכל קורס, יחד עם מספר השיעורים שיש בכל קורס.
הבעיה עם השאילתה היא שה join הכפול גורם לשכפול של כל השורות בטבלה השניה - במילים אחרות כך נראית התוצאה כשאני מוריד את ה count וה group by:
sqlite> SELECT courses.id, lessons.id as lessons_count, course_labels.id as labels_count_1 FROM "courses" LEFT OUTER JOIN "course_labels" ON "course_labels"."course_id" = "courses"."id" LEFT OUTER JOIN "lessons" ON "lessons"."course_id" = "courses"."id";
id|lessons_count|labels_count_1
1|1|6
1|2|6
1|3|6
1|5|6
2|4|
3||
ה id של ה course_label
היחיד במערכת הבדיקה שלי הוא 6, והוא מופיע 4 פעמים כדי להתאים ל-4 שיעורים שיש בקורס.
הפיתרון במקרה של ספירה הוא ממש פשוט ומורכב מהמילה היחידה distinct. הנה השאילתה המתוקנת:
SELECT
courses.*,
count(distinct lessons.id) as lessons_count,
count(distinct course_labels.id) as labels_count_1
FROM "courses"
LEFT OUTER JOIN "course_labels" ON "course_labels"."course_id" = "courses"."id"
LEFT OUTER JOIN "lessons" ON "lessons"."course_id" = "courses"."id"
GROUP BY courses.id