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

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

איך להפוך את כתיבת ה CSS לקצת פחות מתסכלת

23/11/2020

מתכנתים לא אוהבים לכתוב CSS ויש לזה כל מיני סיבות. אפילו אנשי Front End רבים שפגשתי העדיפו להתמקד בצד הטכני יותר של פיתוח JavaScript או React ולהשאיר את ה CSS למעצבים. במקביל ככל שהעולם של ה CSS התפתח כך זה רק עוד יותר הפחיד רבים מאיתנו: עכשיו ללמוד Flexbox ו grid? ובאיזה דפדפנים כל דבר עובד? וממילא מה שאני לא אעשה זה לא יראה טוב אז בשביל מה להתאמץ.

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

המשך קריאה

לא להאמין ש C++ קיבלה את האופרטור הזה לפני JavaScript

22/11/2020

כבר שנים שמדברים על אופרטור ה Pipeline ב JavaScript שיהפוך את הכתיבה הפונקציונאלית בשפה לקלה יותר, אבל נכון להיום הוא עדיין ב stage 1 ולא נראה שמתקדם. והנה פתאום פגשתי את המשפט הבא בתיעוד על ranges של C++20:

Range adaptors accept viewable range as their first arguments and return a view. If an adaptor takes only one argument, it can also be called using the pipe operator: if C is a range adaptor object and R is a viewable_range, these two expressions are equivalent:

ואם אתם לא בטוחים מה זה אומר הם גם נותנים שם את דוגמת הקוד הבאה להמחשה:

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    std::vector<int> ints{0,1,2,3,4,5};
    auto even = [](int i){ return 0 == i % 2; };
    auto square = [](int i) { return i * i; };

    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
        std::cout << i << ' ';
    }
}

שימו לב לקו האנכי:

  1. מתחילים בוקטור של המספרים מ-0 עד 5.

  2. שולחים אותו לפונקציה filter כדי להשאיר רק את המספרים הזוגיים.

  3. את התוצאה שולחים לפונקציה transform (שבכל מקום אחר היינו קוראים לה map) ומפעילים את square על כל אחד מהמספרים הזוגיים שקיבלנו

סך הכל יודפס:

0 4 16

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

דרישות משתנות

21/11/2020

דרישות חדשות יכולות להגיע משתי סיבות:

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

  2. או שהלקוח שינה את דעתו.

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

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

אליאס לקובץ האחרון

20/11/2020

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

הפקודה הראשונה שקופצת לראש היא ls -t שמסדרת את רשימת הקבצים לפי תאריכי עדכון. הפקודה הבאה מציגה את 3 הקבצים העדכניים ביותר בתיקיה:

$ ls -rtlh | tail -3

אבל זה עדיין דורש copy-paste אם רוצים "לעשות משהו" עם הקובץ האחרון. עד שאנחנו נזכרים בסימן ה $() המופלא של bash. אם הדבר העדכני ביותר הוא תיקיה אז הפקודה הבאה תיכנס אליה:

cd $(ls -rt | tail -1)

ובשביל שיהיה קל יותר להשתמש ברצף אני יוצר עבורו alias:

alias latest='ls -rt | tail -1'

ועכשיו אפשר לכתוב פקודות כמו:

rm $(latest)

כדי למחוק את הקובץ האחרון שהורדתי, או:

$ xdg-open $(latest)

כדי להריץ אותו בתוכנית ברירת המחדל של המערכת.

הצהרת כוונות

19/11/2020

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

בעזרת while וגישה דרך אינדקס למחרוזת הפיתרון עשוי להיראות פשוט:

def reduce_polymer(polymer):
    i = 0
    while i < len(polymer) - 1:
        if reducts_with(polymer[i], polymer[i+1]):
            polymer = polymer[0:i] + polymer[i+2:]
            i -= 1
            continue

        i += 1

    return polymer


def reducts_with(unit1, unit2):
    return abs(ord(unit1) - ord(unit2)) == 32

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

from aoc2018day5 import reduce_polymer


def test_aA():
    assert reduce_polymer("aA") == ""


def test_bug():
    assert reduce_polymer("aAff") == "ff"


def test_abBA():
    assert reduce_polymer("abBA") == ""


def test_abAB():
    assert reduce_polymer("abAB") == "abAB"


def test_dabAcCaCBAcCcaDA():
    assert reduce_polymer("dabAcCaCBAcCcaDA") == "dabCBAcaDA"

שעברה בלי בעיה.

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

...

עוד לא מצאתם? שימו לב לאינדקסים

...

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

הקוד הבא למשל מכניס את הפונקציה ללולאה אינסופית:

if __name__ == "__main__":
    print(len(reduce_polymer("aAaAffaA")))

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

  1. בשורת התנאי אנחנו בונים על זה ש i אינו שלילי, אבל לא מוודאים את זה.

  2. בשורת המחיקה אנחנו בונים על זה ש i אינו שלילי, ושוב לא דואגים לספר על זה לאף אחד.

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

def reduce_polymer(polymer: str) -> str:
    i = 0
    while i < len(polymer) - 1:
        if has_reduction(polymer, i):
            polymer = remove_reduction(polymer, i)
            i -= 1
            continue
        i += 1
    return polymer


def has_reduction(polymer: str, i: int):
    if not 0 <= i < len(polymer) - 1:
        raise ValueError(f"Index should be >= 0, got {i}")
    unit1 = polymer[i]
    unit2 = polymer[i + 1]
    return abs(ord(unit1) - ord(unit2)) == 32


def remove_reduction(polymer: str, i: int):
    if not 0 <= i < len(polymer) - 1:
        raise ValueError(f"Index should be >= 0, got {i}")
    return polymer[0:i] + polymer[i+2:]

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

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

איך לשמור שלא יוסיפו לכם Attribute בטעות למחלקה בפייתון

18/11/2020

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

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

נניח שיש לנו מחלקה בשם Person ולכל אוביקט מסוג Person אנחנו רוצים להגדיר את השדות:

  1. שדה id מסוג int.
  2. שדה name מסוג str.
  3. שדה friends מסוג מערך של Person.

הקוד באמצעות Type Hints יראה כך:

from typing import List

class Person:
    id: int
    name: str
    friends: List['Person']

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

p = Person()
x = Person()
y = Person()

# Works great
p.id = 0
p.name = 'test'
p.friends = [x, y]

# Fails: 10 is not a Person
p.friends.append(10)

# Fails: foo is not a valid attribute
p.foo = 'bar'

הפעלה של mypy על הקוד נותנת:

a.py:22: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "Person"
a.py:25: error: "Person" has no attribute "foo"

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

  1. פונקציית __init__ אוטומטית שמקבלת את כל הפרמטרים שהגדרתי למחלקה

  2. פונקציית __repr__ אוטומטית שמחזירה ייצוג ידידותי של האוביקט

  3. פונקציית __eq__ שבודקת שיוויון לפי כל אחד מהשדות

  4. פונקציה בשם dataclasses.asdict שלוקחת כל Data Class ומחזירה dict עם כל המאפיינים שלו.

סך הכל אחרי המרה לשימוש ב dataclass המחלקה שלי נראית כך:

from typing import List
from dataclasses import dataclass, field
import dataclasses


@dataclass
class Person:
    id: int
    name: str
    friends: List['Person'] = field(default_factory=list)


p = Person(0, 'foo')
x = Person(1, 'bar')
y = Person(2, 'buz')

print(dataclasses.asdict(p))

בזכות פונקציית __init__ שנוצרה אוטומטית אני יכול ליצור אוביקטים של Person ולהעביר אליהם את כל הפרמטרים עבורם לא מוגדר ערך ברירת מחדל (ויש Type Hint שמוודא את זה), ואני יכול לקרוא ל asdict כדי לקבל את id, name ו friends בתוך dict.

למידע נוסף על dataclasses שווה לקרוא את דף התיעוד בקישור: https://docs.python.org/3/library/dataclasses.html

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

17/11/2020

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

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

המשך קריאה

למה לא מזמינים אותי לראיון עבודה

16/11/2020

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

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

המשך קריאה

היום למדתי: שינוי פרמטר נכנס ב JavaScript (אל תנסו את זה בבית)

15/11/2020

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

function foo(x) {
  x *= 2;
  console.log(`00 x = ${x}`);
  function bar(y) {
    console.log(`11 x = ${x}; y = ${y}`);
    y *= 3;
    console.log(`22 x = ${x}; y = ${y}`);
  }
  return bar;
}

foo(2)(5);

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

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

מהן Callback Functions ב CTypes

14/11/2020

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

from ctypes import *
from ctypes.util import find_library

libm = CDLL(find_library("m"))
libm.pow.restype = c_double

print(libm.pow(c_double(2), c_double(3)))

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

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

נתבונן בקוד הבא בשפת C:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>

void *print_message_function( void *ptr );

int call_some_threads(printer_t done)
{
  time_t t;
  srand((unsigned) time(&t));
  pthread_t thread1, thread2;
  int  iret1, iret2;

  iret1 = pthread_create( &thread1, NULL, print_message_function, (void*) done);
  iret2 = pthread_create( &thread2, NULL, print_message_function, (void*) done);

  pthread_join( thread1, NULL);
  pthread_join( thread2, NULL); 

  printf("Thread 1 returns: %d\n",iret1);
  printf("Thread 2 returns: %d\n",iret2);
  return 0;
}

void *print_message_function( void *ptr )
{
  int waittime = rand() % 10;
  sleep(waittime);
  printf("[%d]\n",  waittime);
  printer_t callback = (printer_t) ptr;
  callback(waittime);
  return NULL;
}

הקוד מפעיל שני Threads (כל אחד מהם יחכה מספר שניות אקראי בין 0 ל 9) ומדפיס למסך כמה זמן הוא חיכה. אבל, ופה הטריק, כל Thread כזה מקבל גם מצביע לפונקציה. אנחנו לא יודעים מה תהיה הפונקציה - זו מתקבלת כפרמטר done לפונקציה call_some_threads - אבל בסוף הפונקציה print_message_function ואחרי שהדפסנו הודעת דיווח על התקדמות משפת C אנחנו מפעילים את אותה פונקציה שקיבלנו כפרמטר.

אנחנו קוראים לפונקציה שהתקבלה לפרמטר done בשם Callback Function בגלל שזוהי פונקציה באמצעותה קוד C קורא בחזרה לקוד פייתון שהפעיל אותו.

אחרי שאקמפל את קובץ ה C ואהפוך אותו לספריה דינמית אוכל להפעיל את הקוד הבא מ Python כדי להעביר פונקציית Python בתור ערך לפרמטר done:

from ctypes import *

mylib = cdll.LoadLibrary("./libmydemo.so")

@CFUNCTYPE(None, c_int)
def print_done(n):
    print(f"[Python] waited {n} seconds")

mylib.call_some_threads(print_done)

הדקורטור @CFUNCTYPE אומר ל ctypes שיש פה פונקציה שאפשר להעביר ל C, שהיא מחזירה None ומקבלת פרמטר יחיד מסוג int. זה מספיק בשביל לשלוח אותה בתור פרמטר לפונקציה call_some_threads שהוגדרה בקוד ה C. הרצת התוכנית תגרום ל:

  1. יצירת שני Threads מתוך קוד C.

  2. כל Thread מגריל מספר ומתחיל לחכות מספר שניות לפי הערך שהוגרל.

  3. כש Thread כלשהו מסיים את העבודה הפונקציה המתאימה ב C תיקרא ותדפיס הודעה

  4. אותה פונקציה לאחר מכן תפעיל את הפונקציה print_done של פייתון שהעברנו כפרמטר (ומופיעה בקוד C בשם done ולאחר מכן בשם callback).

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