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

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

חיפוש סיפרה כפולה ב Clojure בהשוואה לפייתון

05/12/2019

אני ממשיך ללמוד קלוז'ר עם AoC ובתרגיל היום המשימה המרכזית היתה לכתוב קוד שיזהה סיפרה כפולה במספר. זה לא נשמע נוראי עד שאתם נזכרים שאסור שהרצף יהיה ארוך יותר משתי ספרות, כלומר בעוד ש 12234 מתקבל יפה, החבר שלו 122234 לא עובר את הבדיקה.

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

def chunks(seq):
    chunk = []

    for val in seq:
        if len(chunk) == 0:
            chunk.append(val)
            continue

        if chunk[-1] == val:
            chunk.append(val)
            continue

        yield(chunk)
        chunk = [val]

    else:
        yield(chunk)

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

import itertools

def find_sequence_of_size(number, sequence_size=2):
    for key, chunk in itertools.groupby(str(number)):
        if len(list(chunk)) == sequence_size:
            return True

    return False

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

הקוד המתאים בקלוז'ר נראה כך:

(defn only-two-adjacent-digits
  [num]
  (let [
        snum (str num)
        chunks (partition-by identity snum)
        ]
    (not (empty? (filter (fn [chunk] (= (count chunk) 2)) chunks)))))

אפקט חדר הכושר

04/12/2019

ביום הראשון 50,852 מתכנתים הגישו לאריק ווסטל את הפיתרון לחידה. ביום השני המספר ירד ל 34,373 והיום נכון לזמן כתיבת הפוסט אנחנו על 13,481, כך לפי דף הסטטיסטיקה של הפרויקט. הירידה הולכת עוד להימשך ואנחנו מכירים את התופעה מכל אתגר ארוך שהיינו בו.

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

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

שימוש חוזר בקוד עם קלוז'ר ומפות

03/12/2019

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

https://adventofcode.com/2019/day/2

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

(defn process-add
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        ]

    (assoc program idx3 (+ val1 val2))
    ))

(defn process-mul
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        ]

    (assoc program idx3 (* val1 val2))
    ))

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

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

(defn process
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        op (get operators opcode)
        ]

    (case opcode
      1 (assoc program idx3 (+ val1 val2))
      2 (assoc program idx3 (* val1 val2))
    )))

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

שפות תכנות רבות (וקלוז'ר ביניהן) מאפשרות להעביר את הפונקציה עצמה של הכפל או החיבור בתור פרמטר, או לשמור אותם בתור ערכים ב Hash Map. שמירה כזו אומרת שנוכל "לשלוף" את הפעולה מה Hash Map במקום לכתוב פקודת switch/case ארוכה שמכסה את כל המקרים.

שכתוב הקוד שישתמש ב Hash Map נותן את הפונקציה הבאה:

(def operators 
  {
   1 +
   2 * 
   })

(defn process
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        op (get operators opcode)
        ]

    (assoc program idx3 (op val1 val2))
    ))

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

הודעת השגיאה "can only recur from tail position" ב Clojure

02/12/2019

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

Can only recur from tail position

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

(defn factorial-no-tail
  [n]
  (cond
        (< n 1) n
        :else (* n (recur (dec n)))))

הפונקציה factorial-no-tail היא פונקציה רקורסיבית פשוטה לחישוב עצרת, שאם תרשמו אותה בתוכנית קלוז'ר התוכנית תסרב להתקמפל ולרוץ. אז מה קורה כאן?

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

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

4! = 4 * 3!
             3 * 2!
                   2 * 1!
                         1
=>
4! = 4 * 3 * 2 * 1 = 24

בחישוב 4! נרשום בצד את 4 ואז נלך לחשב את 3!. בשביל למצוא את 3! נרשום בצד את 3 ואז נלך לחשב את 2!, בשבילו נרשום בצד את 2 ונלך לחשב את 1! ולשמחתנו 1! הוא 1. עכשיו מתחילים לגלגל אחורה ובעזרת ה-1 מגלים ש 2! הוא 2, בעזרתו אפשר לגלות ש 3! הוא 6 ואיתו נגלה ש 4! הוא 4.

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

(defn factorial-tail
  [n]
  (loop [n n acc 1]
    (cond
      (< n 1) acc
      :else (recur (dec n) (* n acc)))))

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

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

(defn factorial-recursive
  [n]
  (cond
        (< n 1) 1
        :else (* n (factorial-recursive (dec n)))))

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

הלינקיה סיכום 2019

01/12/2019

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

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

המשך קריאה

כללים ויוצאים מן הכלל

30/11/2019

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

data = { name: 'my item', price: 15 }

שנראה ממש דומה לקוד הבא בשפה JavaScript:

data = { name: 'my item', price: 15 }

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

data = { 'a' => 10, 'b' => 20 }

או נסתכל על פייתון ונקבל את הרושם שהלולאה הזו:

for i in range(10):
    print(i)

דומה באיזשהו אופן ללולאה הזו מ JavaScript (עם קצת עזרה של lodash):

for (let i in _.range(10)) {
    console.log(i);
}

ושתי התוכניות באמת מדפיסות את אותו דבר אבל עושות משהו אחר לגמרי. במציאות המילה of ב JavaScript היא הדרך לדמות את ההתנהגות של פייתון:

for (let i of _.range(10)) {
    console.log(i);
}

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

מה עוצר אותך?

29/11/2019

מה עוצר אותך מללמוד את הטכנולוגיה החדשה הזאת שכולם מדברים עליה? מלנסות לכתוב אפליקציית מובייל ב React Native? מלכתוב פרויקט צד ב Clojure או ב Elixir או אפילו ב Node.JS? מלהתחיל להשתמש כמו שצריך ב Git?

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

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

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

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

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

חידת ריאקט: דברים שאי אפשר לעשות עם Custom Hooks

28/11/2019

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

שימו לב לקוד הבא שמגדיר Custom Hook כדי ליצור באופן אוטומטי אלמנט Input יחד עם שדה בסטייט שמתאים לו:

function useTextField() {
  const [value, setValue] = useState('');

  function Input(props) {
    return <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
  }

  return [Input, value, setValue];
}

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

והחידה של היום - מה קורה בתיבה שלא אמור היה לקרות? איפה הבאג בקוד שגורם לבעיה? ואיך בכל זאת הייתם כותבים את הקוד הזה בצורה שעובדת?

יודעים את התשובה? מוזמנים לכתוב הסבר בתגובות.

דוגמאות שלא עובדות

27/11/2019

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

>>> import asyncio

>>> async def main():
...     print('hello')
...     await asyncio.sleep(1)
...     print('world')

>>> asyncio.run(main())
hello
world

זה עובד. וזה מדפיס hello ו world בדיוק כמו שהם כתבו שיקרה. וזה גם משתמש בהמון מילים של asyncio כמו await, async ו asyncio.run. ובכל זאת אם לא הייתי יודע מה כל הפקודות האלה עושות לא חושב שהייתי מרוויח הרבה מקריאת הדוגמא כאן.

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

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

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

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

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

טיפ HTML - הגדרת type על כפתור בטופס

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

<form>
  <label>
    type something:
    <input type="text" name="text" />
  </label>

  <button>Save</button>
</form>

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

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

<form>
  <label>
    type something:
    <input type="text" name="text" />
  </label>

  <button type="button">Save</button>
</form>