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

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

ביטויים רגולאריים ואנאגרמות... אוי ויי

20/11/2019

לעולם לא אשכח את ההתרגשות שלי ביום ה 4/12/2017 כאשר בשעה שבע בבוקר אריק ווסטל פתח את החידה הרביעית של Advent Of Code אותה שנה. הסיבה למסיבה היתה תוכן החידה, שבמבט ראשון נראה לי כמו הזדמנות נפלאה לכתוב ביטויים רגולאריים ולהיכנס לרשימת הפותרים המהירים של התחרות.

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

\b(\w+)\b.*\b\1\b

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

import fileinput, re

count = 0

pat = re.compile(r'\b(\w+)\b.*\b\1\b')

with fileinput.input() as file:
    for line in file:
        if not pat.search(line):
            count += 1

print(count)

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

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

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

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

dad can add

אנחנו נשנה את המשפט ל:

add acn add

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

import fileinput, re

count1 = 0
count2 = 0

pat = re.compile(r'\b(\w+)\b.*\b\1\b')

with fileinput.input() as file:
    for line in file:
        if not pat.search(line):
            count1 += 1

        line2 = re.sub(
                r'\b(\w+)\b',
                lambda m: ''.join(sorted(m.group(1))),
                line)

        if not pat.search(line2):
            count2 += 1

print(count1)
print(count2)

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

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

שעה ביום

19/11/2019

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

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

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

רק ל CTO יש זמן ללמוד טכנולוגיות חדשות כל היום.

רק יועצים טכנולוגיים מצליחים למצוא זמן לכתוב בלוג מקצועי.

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

בואו נלמד שפה חדשה עם Python

18/11/2019

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

המשך קריאה

זה עוד רחוק?

17/11/2019

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

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

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

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

כי כל השאלות האלה מסיחות את הדעת מהדבר האמיתי שצריך לעשות עכשיו - שזה להמשיך לכתוב ולמחוק קוד עד שהוא יעבוד.

מימוש רשימה מקושרת בגישה פונקציונאלית

16/11/2019

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

המשך קריאה

איך לשמור מערך בסטייט של React Component

15/11/2019

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

המשך קריאה

לא בכיוון

14/11/2019

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

המשך קריאה

דוגמת ריפקטורינג ב React עם Custom React Hook

13/11/2019

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

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

import React from 'react';
import ReactDOM from 'react-dom';
import { useState, useEffect } from 'react';
import _ from 'lodash';
import $ from 'jquery';

const App = () => {
  const [id, setId] = useState(1);
  const [character, setCharacter] = useState({});
  const [filmId, setFilmId] = useState(1);
  const [film, setFilm] = useState({});

  useEffect(function() {
    setFilm({});
    const req = $.getJSON(`https://swapi.co/api/films/${filmId}/`, function(data) {
      setFilm(data);
    });
    return function cancel() {
      req.abort();
    }

  }, [filmId]);

  useEffect(function() {
    setCharacter({});
    const req = $.getJSON(`https://swapi.co/api/people/${id}/`, function(data) {
      setCharacter(data);
    });
    return function cancel() {
      req.abort();
    }
  }, [id]);

  const characterNameText = (character.name == null ?
    '[Loading please wait]' :
    character.name);

  const filmNameText = (film.title == null ?
    '[Loading please wait]' :
    film.title);

  const filmIds = (character.films != null ?
    character.films.map(f => f.match(/(\d+)\/$/)[1]) :
    _.range(1, 8));

  return (
    <div>
      <h1>ID: {id}</h1>
      Character ID:
      <select onChange={(e) => setId(e.target.value)}>
        {_.range(1, 11).map(id => (
          <option value={id}>{id}</option>
        ))}
      </select>
      Film ID :
      <select onChange={(e) => setFilmId(e.target.value)}>
        {filmIds.map(id => (
          <option value={id}>{id}</option>
        ))}
      </select>
      <hr />
      <h2>Character Name: {characterNameText}</h2>
      <h2>Film Name: {filmNameText}</h2>
    </div>
  )
};


const root = document.querySelector('main');
ReactDOM.render(<App />, root);

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

איך מתקנים? כל מה שצריך הוא להוציא את הקוד הכפול לפונקציה:

export function useRemoteData(endpoint) {
  const [id, setId] = useState(1);
  const [data, setData] = useState({});

  useEffect(function() {
    setData({});
    const req = $.getJSON(`https://swapi.co/api/${endpoint}/${id}/`, function(data) {
      setData(data);
    });
    return function cancel() {
      req.abort();
    }

  }, [id]);

  return [id, setId, data];
}

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

const App = () => {
  const [characterId, setCharacterId, character] = useRemoteData('people');
  const [filmId, setFilmId, film] = useRemoteData('films');
  // ...

וכן לריאקט ממש לא אכפת שאתם קוראים מפונקציית הפקד לפונקציה נוספת והיא זו שקוראת בפועל ל useEffect ו useState. למעשה האפשרות הזו להוציא קוד החוצה היא הסיבה המרכזית למעבר ל React Hooks. הפונקציה useRemoteData נקראת Custom Hook וזה כל מה שצריך לדעת בשביל לשתף קוד מבוסס סטייט בין קומפוננטות שונות או לבצע שימוש חוזר בקוד כזה בתוך אותה קומפוננטה.

הערכות זמנים

12/11/2019

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

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

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

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

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

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

להתחיל בקטן

11/11/2019

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

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

כי הבעיה היא לא השפה - באמת. יש אנשים שכותבים קוד נפלא עם השפה שבחרת.

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

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

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