en
Feedback
ToCode

ToCode

Open in Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Show more
1 420
Subscribers
+124 hours
+17 days
-430 days
Posts Archive
ToCode
1 420
דברים קטנים כשלא מבינים, גם דברים קטנים נראים מלחיצים. הם גורמים לאנשים טובים להיבהל ולחפש פיתרונות מהירים, רק בשביל להבטיח לעצמנו שהבעיה לא גדולה כמו שזה נראה- ״למה הקונטיינר שלי לא נדלק?!״ ״למה אין עדיין רווחים מהמערכת?!״ ״למה הדף נטען כל כך לאט?!״ לפעמים הפיתרון באמת ייקח דקה. לפעמים יותר. הדבר החשוב הוא לשים לב שככל שמבינים יותר כך נלחצים פחות, ויודעים יותר טוב מה לעשות. בהתמודדות עם אתגר לא מוכר שווה לבחור באחת משתי דרכים: 1. ללמוד את עולם התוכן ברמה מספיק טובה כדי להתמודד עם האתגר הזה. 2. - או - לשלם למישהו שלמד כבר את עולם התוכן כדי שיעזור לנו לפתור את האתגר. הטעות שכדאי להיזהר ממנה היא ללמוד רק קצת מתוך עולם התוכן "מספיק בשביל לפתור את הבעיה" ואז לפתור דברים עקום. התוצאה בדרך כלל תהיה שהבעיה תישאר ורק תיראה עוד יותר מסובכת.

ToCode
1 420
סוגריים אז כמו תמיד אחרי שאני מבלה יותר מדי זמן בלדבג משהו טפשי אני הולך לכתוב את זה כאן כדי לא לשכוח וכדי שלפחות נלמד משהו מהסיפור, והיום אנחנו רוצים לדבר על סוגריים וסקאלה. נו, רוצים זאת מילה גדולה, אף אחד לא רוצה לדבר על סוגריים. ובכל זאת לפעמים צריך ובמיוחד כשסוגריים יכולים להיות מבלבלים. נתחיל בקוד-
  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }.recover { err => 30 }

    println(result)
והשאלה מה הערך של result? בשביל לענות על זה נתחיל עם דוגמת קוד יותר פשוטה ונוריד את ה recover. עכשיו בלוק ה then ובלוק ה else שקולים:
  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }
ו result יהיה שווה לאוביקט Failure שה Exception שלו תלויה בתוצאה של החישוב האקראי. פקודת recover בסקאלה שמופעלת על אוביקט Try משנה אותו מ Failure ל Success עם הערך שמופיע ב recover. הדוגמה הבאה לכן גם צפויה:
  @main
  def parens(): Unit =
    val result = if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }

    println(result.recover { err => 30 })
בגלל שלא משנה מה הוגרל result החזיק בכל מקרה Failure, אז הפעלת recover תשנה את הערך ל Success של 30 וזה מה שיודפס:
Success(30)
אבל מה עם קטע הקוד הראשון שהדבקתי? עכשיו אנחנו מבינים מה רציתי שהוא יעשה - רציתי שהוא יפעיל את recover על הערך שחזר מה if, לפני שנשמר ב result. זה לא מה שקרה. בשביל להבין מה כן קרה צריך קודם כל למחוק את הסוגריים המסולסלים:
    val result = if (Random.nextInt() > 0)
                    Try { throw new Exception("20") }
                 else Try { throw new Exception("30")
כדאי לחשוב על ה"בלוק" בסקאלה בתור פרמטר ל if או ל else. לכן אין בעיה לכתוב את השורה בלעדיו ולכן הגירסה הראשונה של הקוד שקולה לגירסה הזו:
val result = if (Random.nextInt() > 0)
                Try { throw new Exception("20") }
                else
                Try { throw new Exception("30") }.recover { _ => 30 }
עכשיו זה ברור - ה recover השפיע רק על בלוק ה else ולא על כל ה if. הקוד החזיר Success כשהמספר האקראי היה 0 או שלילי, ובמספרים חיוביים החזיר Failure. כשמבינים איך זה עובד קל גם לתקן:
  @main
  def parens(): Unit =
    val result = (if (Random.nextInt() > 0) {
      Try { throw new Exception("20") }
    } else {
      Try { throw new Exception("30") }
    }).recover { _ => 30 }

    println(result.recover { err => 30 })

ToCode
1 420
ואז גיליתי אני כותב עכשיו בוט לטלגרם שכולל הודעות עם כפתורים, למשל דמיינו הודעה על הפוסט הזה שכוללת כפתור "לייק". המימוש מסתבר מחזיר אותנו לאתגר הקלאסי של כפתורים בממשק - איך להוסיף מידע מעניין לאירוע הלחיצה על הכפתור. ואני אסביר. כשמשתמש לוחץ על כפתור "לייק" מופעלת פונקציה. אותה פונקציה צריכה להבין לאיזה פוסט צריך לסמן את הלייק לפני שתוכל ללכת לעדכן את הפרטים בבסיס הנתונים. פונקציה שמטפלת בלחיצה על כפתור מקבלת את הפרמטרים הבאים: flags, game, peer, msg_id, data, password, ולכן הדבר שנראה לי הכי הגיוני כשניגשתי לממש את זה פעם ראשונה היה לשים את המידע החשוב בשדה data של הכפתור, ואז לקבל את המידע הזה בטיפול בלחיצה. בדוגמה של הלייק נשים את ה URL של הפוסט בתור data, נקבל אותו בפונקציה שתטפל בלחיצה כל הכפתור והכל טוב. (ואז גיליתי) ופה הסיפור נהיה מעניין כי אחרי שהתחלתי לבדוק את הבוט ראיתי שחלק מהכפתורים עובדים וחלק לא עובדים. המשך מחקר הביא אותי לגילוי העצוב שיש מגבלת אורך על אותו callback data של 64 תווים. עם ה URL-ים הקצרים לא היתה בעיה אבל כשהיה URL ארוך מספיק הקוד לא עבד. לנקודות האלה של "עכשיו גיליתי" יש פוטנציאל לחשוף משהו מעבר למימוש הספציפי, ויכולת לדחוף אותנו לפיתרונות מהירים שיפגעו בנו בטווח הרחוק. במקרה של הבוט הנה כמה אפשרויות: 1. אפשר להישאר עם המבנה של שמירת URL-ים בכפתורים, אבל פשוט לעבור דרך שירות קיצור URL-ים כדי לוודא שכולם קצרים יותר מ 64 תווים. 2. אפשר לייצר ב DB מזהה אקראי אחר קצר יותר לכל פוסט, נקרא לו "מזהה קצר בשביל כפתור טלגרם" ולכתוב אותו בתור הערך של ה data. 3. אפשר לשים לב לפרמטר נוסף msg_id ברשימת הפרמטרים של טיפול בלחיצה. בעזרת מזהה ההודעה אני יכול להגיע להודעה שכללה את הכפתור ולהסתכל שם מה היה ה URL של הפוסט. 4. אפשר ללכת עוד צעד ולשמור ב DB כל הודעה שהבוט שולח, עם מפתח זר לפוסטים. עכשיו כשאני אקבל msg_id אני לא צריך לחפש בהיסטוריית ההודעות בטלגרם ויכול לעשות את הבדיקה אצלי ב DB. "ואז גיליתי" זו נקודה שהופכת אירוע שבכלל לא חשבתי עליו כשכתבתי את הקוד למשהו מעניין. ואז גיליתי, ועכשיו אפשר לחשוב קצת יותר לעומק על פיתרון, ועל הריפקטורינג שצריך בשביל להגיע לאותו פיתרון, ואולי לחשוב איך לבנות את זה כדי שיהיה קל לעבור לפיתרון Fallback נוסף אם אני אגלה עוד בעיות. "ואז גיליתי" אלה ההזדמנויות שלנו לשפר את מבנה הקוד כדי להתמודד טוב יותר עם שינויים בעתיד.

ToCode
1 420
כעת יש לנו דף אינטרנט וניתן לפרסר אותו ולהתחיל לשחק איתו. כדי לפרסר את ה-HTML לעץ DOM, בחרנו ב- goquery. העבודה עם מחרוזות Raw HTML כאן היא די מאתגרת ואנחנו צריכים להמיר אותן לאובייקט שניתן לעבוד איתו. במבט לאחור, ייתכן שהיה עלינו להשתמש ב-Rod כדי להעביר את רכיבי ה-DOM אך בזמן האמת, היינו זקוקים לפרסר HTML קל משקל וללא תלות ו – goquery היה נראה כמו פתרון טוב. זו פונקציית הפיענוח:
func ParseHTML(web_data string)*goquery.Document {
// load html string to go query html parser
 doc, err := goquery.NewDocumentFromReader(strings.NewReader(web_data))
 if err != nil {
  log.Fatal(err)
 }
    return doc
}
לפני שנמשיך עם מניפולציות ה-DOM, בואו ונסתכל על פונקציית השמירה. חשוב לזכור שעלינו לשמור את הנכסים הקיימים באמצעות ה- ETAG על מנת להמנע מלמלא את הבקט שלנו בקבצים מאותו הסוג. תחילה נבנה קליינט HTTP שיסייע בהורדת הנכסים:
//go http client constructor
func goclient(url_path string) (*http.Response, error) {
 // set client with proxy and time out of 10 secs for perf increase
 var client *http.Client
 if GetEnv("HTTP_PROXY") != "" {
  proxyUrl, _ := url.Parse(GetEnv("HTTP_PROXY"))
  client = &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)}}

 } else {
  client = &http.Client{Timeout: 10 * time.Second}
 }

 req, err := http.NewRequest("GET", url_path, nil)
 if err != nil {
  return nil, errRequest
 }
 req.Header.Add("Accept", GetEnv("accept"))
 req.Header.Add("User-Agent", GetEnv("userAgent"))
 // get response from client req
 resp, err := client.Do(req)
 if err != nil {
  log.Printf("error getting resource %s \\n", err)
 }
 return resp, nil
}
כעת אנו יכולים להוריד את הנכס.
// get file buffer asset from url
func getAssetContent(url_path string) (string, []byte, error) {
 reader, err := goclient(url_path)
 if err != nil {
  return "", []byte{}, errAssetContent
 }

 if reader != nil {
  buffer, err := ioutil.ReadAll(reader.Body)
  if err != nil {
   fmt.Println("error reading reader as buffer ", err)
  }
  // get hash of content
  hash := fmt.Sprintf("%x", md5.Sum(buffer))
  return hash, buffer, nil
 }

 return "", nil, nil
}
ונכתוב פונקציית שמירה פשוטה המבוססת על פונקציית בדיקת S3
//cache already uploaded asset
func cachelink(link string) (bool, string, string, error) {
 etag, err := goclient(link)
 if err != nil {
  return false, "", "", err
 }

 path, _ := url.Parse(link)
 // get file extension from file url
 filetype := filepath.Ext(path.Path)
 if etag != nil {
  // cache files with their etag values
  etag_val := etag.Header.Get("Etag")
  var fileExist bool
  if etag_val != "" {
   // check if file exist in cache
   fileExist = S3KeyCache.Check(etag_val)
  } else {
   // use sha value if file etag value does not exist
   hash, _, err := getAssetContent(link)
   if err != nil {
    return false, "", "", errCacheLink
   }

   if hash != "" {
    etag_val = hash
    fileExist = S3KeyCache.Check(hash)
   }
  }
  // construct new key with file extension
  return fileExist, etag_val, filetype, nil
 }

 return false, "", "", nil
}
וסוף סוף, יש לנו את פונקציית ההעלאה. אנו נשתמש ב-sync.mutex של golang כדי לנצל את התמיכה של השפה במקביליות:
// fetchAndUploadContent fetches assets and uploads to S3
func fetchAndUploadContent(waitGroup *sync.WaitGroup, mutex *sync.Mutex, info fetchUploadJobInfo) {
 defer waitGroup.Done()

 _, buffer, err := getAssetContent(*info.url)
 if err != nil {
  return
 }

 if buffer != nil {
  newKey := UploadToS3(*info.filename, buffer, *info.filetype)

  mutex.Lock()
  defer mutex.Unlock()

  // add etag to cache
  S3KeyCache.Add(*info.filename)
  info.node.SetAttr(*info.attr, newKey)
 }
}

//add upload to a go routine
func schedule_upload(path string, node *goquery.Selection, attr string, waitGroup *sync.WaitGroup, mutex *sync.Mutex) {
 fileExist, etag, fileType, err := cachelink(path)
 if err != nil {
  return
 }

ToCode
1 420
פוסט אורח - בניית זחלן רשת Crawling Engine הכותב הוא Oragbakosi Valentine, בשלוש השנים האחרונות הוא עובד כמפתח תוכנה בגיטסטארט ולפני כן עבד בGoSquare. גיטסטארט היא פלטפורמת Code as a Service שהופכת את מה יש לכם בבקלוג (backlog) לקוד באיכות גבוהה ובו זמנית מטפחת קהילה הולכת וגדלה של מפתחים ברחבי העולם. בתחילת השנה השיקה גיטסטארט את פעילותה בישראל והחלה לתמוך בצוותי פיתוח מקומיים. תיאור הפרויקט אחד הלקוחות שלנו, חברה קטנה שנותנת שירותי brand awareness ללקוחות ברחבי העולם ביקשה מאיתנו לפתח זחלן רשת (engine crawler) ו-פלטפורמת העתקת דפי רשת (web cloning platform). הפרויקט הזה מצא חן בעיני במיוחד בזכות זה שהוא נתן לנו הזדמנות ללמוד לעומק כיצד זחלני רשת פועלים ואיך דפדפנים מתמודדים עם רינדור ופירסור של HTML. הפרויקט כלל: 1. פיתוח של שירות תמיכה בהעתקת דפי אינטרנט, ואחסון של כל הנכסים (assets) בבקט של S3. 2. פיתוח של שירות היכול לזחול באתרי מסחר אלקטרוניים ולבחור פריטי מידע מסויימים מדפי מוצרים, לדוגמא: שם מוצר, מחיר, תמונות, תיאור וכו'. (השירות צריך לתמוך בכל סוגי אתרי המסחר האלקטרוני). הקוד כולו נכתב ב Go. בואו נמשיך ונראה את החלקים המרכזיים והמעניינים בפרויקט. שירות העתקה כדי להעתיק אתר באופן מיידי, בחרנו בדרך הפעולה הבאה: 1. להוריד את קובץ ה- HTML של העמוד 2. לשמור את ה-HTML בזיכרון מטמון ברדיס 3. לפרסר את ה-HTML לעץ ה-DOM שניתן להפנות אליו בקלות 4. לעבור על עץ ה-DOM ולשלוף ממנו assets כמו קישורים, תמונות, קבצי JavaScript, קבצי CSS וכו'. 5. להעביר את הנכסים האלה לזיכרון מטמון ולהעלות לבקט S3 (במידה והאתר ירד, באופן זה יהיה לנו עותק של כל הנכסים). 6. לשמור את תכונת ה-Etag של האובייקט כך שאם נתקל בנכס דומה, לא יהיה עלינו להעלות מחדש קבצים מיותרים. 7. להחליף קישורי נכסים עם ה-URL ב-S3 של עץ ה-DOM. 8. לשמור את עץ ה-DOM בחזרה כמחרוזת HTML נוכל להשתמש בגישה המסורתית ולהשתמש בקליינט הדיפולטיבי של golang כדי להוריד את עמוד האינטרנט כפי שמוצג למטה:
res, err := http.Get(link)
    if err != nil {
        log.Fatal(err)
    }
    content, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
        log.Fatal(err)
    }
    html := string(content)
אבל אם נעשה את זה נתקל במגבלה, הקליינט לא יחכה לטעינת כל הפריטים והנכסים בעמוד, וזה יכול להוות בעיה בממשק עם single-page applications (SPA) לכן החלטנו להשתמש ב- Headless Browser. בהורדת עמוד עם "http.Get", קוד ה- JavaScript המוגדר בדף אינו מופעל, ולכן לפעמים ניתן לקבל HTML ריק (כי ב-SPA, יש צורך ב- JavaScript כדי לרנדר את התוכן של הדף). חבילת Rod היא חבילה של golang שדומה ל- selenium ומפעילה דפדפן במצב headless. בשימוש ב ROD פונקציית ההורדה נראית כך:
func RodDownload(url_path string) string{
page := rod.New().MustConnect().MustPage(url_path)
  // set user agent header
  userAgentHeaderOverride := proto.NetworkSetUserAgentOverride{
   UserAgent: GetEnv("userAgent"),
  }
  page.SetUserAgent(&userAgentHeaderOverride)
  webPage := page.MustWaitLoad().MustHTML()

  // expire after 5 hours
  _, err = redis_conn.Do("SETEX", url_path, 18000, webPage)
  if err != nil {
   log.Fatal("error getting web page", err)
  }
  return webPage
]
חייבים לשמור את הדף לרדיס אם הדף אינו קיים שם כבר. אנו נשתמש ב-URL כמפתח ברדיס:
func downloadWebPage(url_path string) string {
 var redis_url string
  redis_url = GetEnv("REDIS_URL")
 redis_conn, err := redis.Dial("tcp", redis_url)
 if err != nil {
  // don't proceed if conn to redis fails
  log.Fatal("error connecting to redis server", err)
 }

 defer redis_conn.Close()

 webPage, err := redis.String(redis_conn.Do("HGET", url_path, url_path))
 if err != nil {
  // if redis key is abscent, create one with new content
  webPage= RodDownload(url_path)

 }
 return webPage
}

ToCode
1 420
משחקים עם חישוב מקבילי בסקאלה אחת הבעיות של עבודה לא מבוססת Java בתוך ה JVM היא שיש יותר מדי דרכים לעשות דברים, ולא תמיד ברור במה לבחור. במקרה של סקאלה ומקביליות זה נהיה מסובך כי יש גם שיקולים של ארכיטקטורה ותכנות פונקציונאלי. בשביל המשחק רציתי לספור כמה מספרים ראשוניים יש עד 10 מיליון, ולפצל את החישוב לכמה תהליכונים תוך שימוש בשתי גישות פשוטות למקביליות ובהשוואה עם חישוב סדרתי. הגישה המקבילית הראשונה היתה פשוט לפתוח Future לכל מספר כדי לזהות אם הוא ראשוני, ולתת ל Java לשגר את התהליכונים. הגישה המקבילית השניה היתה הספריה parallel-collections שמציעה מימוש של map מקבילי. וכן נצטרך לעשות פוסט המשך עם cats-effect. טוב קוד? יאללה. זאת התוכנית:
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.util.Random
import concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import java.net.{URI, URL}
import java.util.concurrent.{CompletableFuture, Executors}
import scala.language.implicitConversions
import scala.util.chaining._
import scala.collection.parallel.CollectionConverters._


object futures {

  private def isPrime(n: Int): Boolean =
    2.to(Math.sqrt(n.toDouble).toInt).forall(n % _ != 0)

  @main
  def virtualThreadsDemo(): Unit =
    val s0 = System.nanoTime()
    1.to(10000000)
      .map(n => Future { isPrime(n) })
      .map(Await.result(_, Duration.Inf))
      .count(identity)
      .pipe(println)

    val s1 = System.nanoTime()

    1.to(10000000)
      .map(isPrime)
      .count(identity)
      .pipe(println)

    val s2 = System.nanoTime()

    1.to(10000000)
      .par
      .map(isPrime)
      .count(identity)
      .pipe(println)

    val s3 = System.nanoTime()

    println(s"1 thread = ${s2 - s1}")
    println(s"* thread = ${s1 - s0}")
    println(s"pmap     = ${s3 - s2}")
}
וכן בשביל המשחק כתבתי אותה גם בפייתון כדי שנוכל להשוות זמנים:
import time
import multiprocessing
import math

def isprime(n):
    for i in range(2, int(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True

if __name__ == "__main__":
    pool = multiprocessing.Pool(5)

    s0 = time.time_ns()
    print(sum(pool.map(isprime, range(10_000_000))))
    s1 = time.time_ns()
    print(s1 - s0)

והתוצאות לפחות אצלי על המחשב:
1 thread = 2025199791
* thread = 3463589500
pmap     = 674844834
python   = 14593734000
לא סיפרתי קודם אבל ניסיתי גם להחליף את ה Executor שמריץ את ה Threads לכזה שמשתמש ב Virtual Threads של Java אבל התוצאות לא עשו חשק לדבר על זה אז קברתי את הניסוי. מה למדתי? 1. חישובים בפייתון עובדים לאט. גם כשניסיתי להריץ את התוכנית בפייתון בלי multiprocessing זה לא עזר. 2. אי אפשר סתם ליצור Thread לכל מספר. ככל שהמשימה מסובכת שווה להשקיע זמן ולחשוב איך לחלק אותה למספר תהליכונים. 3. לא סתם בחרתי 10 מיליון. במספרים קטנים יותר (אפילו מיליון) כמעט לא היה הבדל בין תהליכון אחד למספר תהליכונים. חשוב להבין טוב את המשימה לפני שבונים פיתרון מבוסס תהליכונים כדי לא לעשות "אופטימיזציות" מיותרות.

ToCode
1 420
המאזניים של AI וקוד מצד אחד AI מאפשר לכתוב קוד מהר יותר, ומצד שני AI (היום) לא יודע להציע אבסטרקציות שיובילו לכתיבת פחות קוד. רק כשנבין את זה נוכל לראות למה מתכנתים מסיימים משימות מהר יותר עם קופיילוט. כבר התרגלנו להסתכל על קוד קיים בתור נטל, ולחלום על פרויקטי Green Fields, והנה מגיע ה AI ומתחיל תמיד מאפס, ומראה לנו שאין שום בעיה להרים מאפס דף נחיתה בלי לקחת את ה CSS-ים של הפרויקט (כי הוא כבר יעצב ויכתוב CSS), או להוסיף עוד שליפה מבסיס הנתונים בלי להשתמש באבסטרקציות שכבר קיימות בקוד, כי הן לא בדיוק מתאימות ל Use Case הנוכחי. הבעיה שרק להוסיף קוד זו לא אסטרטגיה טובה עבור Senior Developers. זה מעגל שטני שגורם לקוד להיות יותר קשה לתחזוקה לאורך זמן, והקופיילוט בתורו מתעלם מהקושי וממשיך לסבך את המערכת עד שכבר אי אפשר יהיה יותר להיעזר בו. ההבנה הזאת היא המפתח למעבר מג'וניורים לסניורים. ג'וניורים מחפשים לפתור בעיות, סניורים מחפשים לבנות אבסטרקציות כדי שיהיה קל יותר לפתור בעיות בעתיד.

ToCode
1 420
היום למדתי: נקודה בפוליגון ופיתרון AoC 2023 יום 10 חלק 2 בסקאלה לפני שבוע פרסמתי כאן את הפיתרון של החלק הראשון של יום 10 מ Advent Of Code האחרון בסקאלה, בו ראינו איך למצוא מעגל בגרף באמצעות DFS. החלק השני של התרגיל הציג בעיה מעניינת שנקראת Point In Polygon. בואו נראה איך זה עובד ואיך לפתור אותה עם אלגוריתם Ray Casting. האתגר - חיפוש נקודות בתוך המעגל בחלק הראשון של התרגיל ראינו איך למצוא מעגל בגרף, לדוגמה גילינו את המעגל הזה:
...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........
האתגר בחלק השני הוא למצוא את כל הנקודות שנמצאות בתוך המעגל - בדוגמה הזאת אלה 4 הנקודות שמסומנות ב I כאן:
...........
.S-------7.
.|F-----7|.
.||OOOOO||.
.||OOOOO||.
.|L-7OF-J|.
.|II|O|II|.
.L--JOL--J.
.....O.....
האלגוריתם Ray Casting אני חייב להודות שבמבט ראשון על התרגיל לא היה לי מושג איך לגשת אליו והייתי צריך לחפש קצת ברשת רעיונות עד שהגעתי לדף בויקיפדיה שמסביר על הבעיה. בגדול אנחנו רוצים לחשוב על הצורה המעגלית שלנו בתור פוליגון, ואז מכל נקודה אפשר לדמיין קו שיוצא מהנקודה שמאלה עד הקצה. אם נספור כמה פעמים הקו הזה מתנגש עם הצורה שלנו נוכל לדעת אם הנקודה נמצאת בתוך המעגל או בחוץ. נקודה שהקו שיוצא ממנה שמאלה מתנגש עם המעגל מספר אי זוגי של פעמים נמצאת בתוך המעגל (ואם התיאור פה לא היה מספיק מובן נסו לקרוא את ההסבר בויקיפדיה הם ממש מדויקים שם וגם יש ציור). האתגר היחיד שיש כאן במימוש הוא להבין מתי הקו יתנגש עם המעגל - למשל אם נחשוב על שורה כזאת:
|F|F-JF---7F7-L7L|7|
אז ה F וה J הראשונים מייצרים התנגשות כי המעגל הולך מלמטה למעלה, אבל ה F וה 7 שאחריהם לא צריכים להיספר כי המעגל מגיע מלמטה ויורד חזרה למטה, ולכן בקוד שמחפש התנגשויות צריך לבחור בכל צמד כזה של F ו J או 7 ו L את מי לספור. אני ספרתי את ה L ואת ה J אבל אפשר היה לספור גם את ה F וה 7 ולקבל את אותה תוצאה. קוד? ברור בשביל זה באנו. אחרי כל החפירה הפונקציה שבודקת אם נקודה נמצאת "בתוך" המעגל היא בסך הכל:
  def insideLoop(point: (Int, Int),
                 loop: Set[(Int, Int)],
                 map: Map[(Int, Int), Char]): Boolean =
    val crossings = point._2
      .to(0, -1)
      .map(c => (point._1, c))
      .filter(p => loop.contains(p))
      .map(map(_))
      .count(Set('|', 'L', 'J').contains(_))
    !loop.contains(point) && crossings % 2 == 1

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

ToCode
1 420
איך לתחזק פרויקט שכמעט לא צריך שינויים אין ספק שפיתוח מערכת הוא אתגר גדול, אבל כשמפתחים מערכת יום יום ושעה שעה יש גם ייתרון - אפשר להחזיק את הכל (או הרוב) בראש והכלים זמינים. שרת הפיתוח כבר באוויר, שרת הבדיקות רץ, ביצעת כבר התחברות למסך ה admin וכל הספריות בגירסאות החדשות ביותר. בפרויקט Legacy, בנוסף לאתגר של פיתרון בעיה ספציפית יש עוד המון אתגרים שקשורים לעובדה שאף אחד לא נגע בקוד כמה חודשים או שנים: 1. הקוד לא רץ. גם לא מתקמפל. 2. אין לי מושג מה הסיסמה למסך הניהול על שרת הבדיקות. היא היתה שמורה לי איפשהו אבל כבר מזמן שכחתי איפה. 3. התיעוד ברשת לא רלוונטי כי הפרויקט משתמש בספריות ישנות. 4. אין לי מושג מה התפקיד של כל חלק בקוד או איפה למצוא את החלקים שאני צריך לעדכן. אפשר לנסות לפתור את רוב הבעיות כאן עם כתיבת קבצי תיעוד ואולי אתם יותר טובים ממני בכתיבת קבצים כאלה, אבל לפחות במקרה שלי אני מצליח לכתוב קבצי תיעוד אבל אף פעם לא זוכר לפתוח אותם כשצריך משהו וככה מצטברים לי המון קבצי תיעוד לא רלוונטיים. מה כן עובד לי- 1. יצירת מנגנון הרצה סטנדרטי, גם לפרויקטים ישנים - כלומר צריך לוודא ש npm start או docker compose up עובדים ומעלים את הסביבה. זה תמיד שווה את ההשקעה. 2. כל פעם שאני מגיע לשנות קוד ולא מבין משהו בקוד ישן אני מוסיף תיעוד על אותה פונקציה שמסביר מה היא עושה ולמה. לאט לאט תיעוד נכתב ולאורך שנים יותר קל לי לתקן בעיות. 3. לגבי בדיקות - היתרון של בדיקה על תיעוד הוא שאפשר להריץ בדיקה. החיסרון שאם לא הרצת בדיקה חצי שנה יש לה סיכוי טוב להיכשל. אם אתם מצליחים זה מאוד עוזר שיש גיטהאב אקשן שמריץ את הבדיקות פעם בשבוע. חייב להודות שלא בכל הפרויקטים הישנים שלי אני מצליח לשמר את זה. 4. בעיה נוספת עם פרויקטים ישנים היא מעקפים קטנים שאנחנו שמים כי חייבים לתקן באג דחוף בפרודקשן ואין באמת זמן לעשות את זה נכון. וככה אנחנו רואים שינויי קוד על מכונת הפרודקשן שלא מגיעים לגיט, במיוחד במערכות ווב שם לא צריך לקמפל את הפרויקט. אומנם לא הצלחתי לגמרי למגר את התופעה הזאת, אבל כן אפשר לנסות כל פעם שרואים דבר כזה פשוט לעשות קומיט לשינוי גם אם אין לך הודעת קומיט מדויקת לכתוב, רק בשביל שהבן אדם הבא לא ייבהל. יש לכם עוד טיפים לתחזוקת פרויקטים פעם בכמה חודשים? אל תתביישו לשתף בתגובות.

ToCode
1 420
בסקאלה זה לא היה קורה (או: מה חדש בטייפסקריפט 5.4) השינוי הראשון ברשימת החידושים של טייפסקריפט 5.4 הוא שטייפסקריפט תתחיל לשמור מסקנות על טיפוסים לתוך Closures, או בדוגמה, הקוד הבא כבר יתקמפל בלי שגיאות:
function getUrls(url: string | URL, names: string[]) {
    if (typeof url === "string") {
        url = new URL(url);
    }

    return names.map(name => {
        url.searchParams.set("name", name)
        //  ~~~~~~~~~~~~
        // error!
        // Property 'searchParams' does not exist on type 'string | URL'.

        return url.toString();
    });
}
זו תבנית מוכרת ב JavaScript שמטרתה "לסדר" את המשתנים לפני שממשיכים. בדוגמה שלנו הפונקציה יכולה לקבל מחרוזת או אוביקט URL, כדי שיהיה נוח לאנשים להפעיל אותה, אבל בתוך קוד הפונקציה יותר קל לנו לעבוד עם אוביקטי URL ולכן בתחילת הפונקציה בודקים אם התקבלה מחרוזת ואם כן ממירים ל URL. גירסאות קודמות של טייפסקריפט לא קישרו את המשתנה url שבתוך הפונקציה הפנימית לשינוי שבוצע בתחילת הפונקציה החיצונית, ולכן בתוך הפונקציה הפנימית היה צריך לבצע את הבדיקה פעם נוספת או לבצע המרה יזומה (או להתעלם מהשגיאה). החל מגירסה 5.4 הקוד הזה יתחיל לעבוד בלי בעיה, שזה אחלה. אבל השאלה האמיתית היא האם ההמרה מ String ל URL בכלל צריכה להיות חלק מהפונקציה. עכשיו אני יודע TypeScript לא מחפשת להגיד לאנשים מה לעשות והמטרה היא לאפשר בדיקת טיפוסים שתעבוד על קוד שאנשי JavaScript היו שמחים לכתוב. ובכל זאת מעניין לשים לב לתבנית מקבילה בסקאלה לטיפול באותה בעיה. בסקאלה הגישה היא שהמרה מ String ל URL היא "יכולת" של ה String ולא של הפונקציה, או יותר נכון יכולת של String בתוך קובץ או מודול מסוים. בשביל שזה יעבוד אני מגדיר שם פונקציית המרה שנראית כך:
  implicit def stringToURL(url: String): URL =
    new URI(url).toURL
ואם אותה פונקציה נמצאת ב Scope אז אני יכול לכתוב פונקציה שמצפה לקבל URL ולהפעיל אותה עם String והכל פשוט יעבוד:
  def printHost(url: URL): Unit =
    println(url.getHost)

  @main
  def demo(): Unit =
    val url: String = "https://www.tocode.co.il"
    printHost(url)
הקומפיילר מפעיל באופן אוטומטי את פונקציית ההמרה ובעצם הפונקציה נקראת עם אוביקט מסוג URL. היתרון בגישה של סקאלה הוא שאנחנו חוסכים כפל קוד - במקום לכתוב את קוד ההמרה בתחילת כל פונקציה שמקבלת URL מספיק לכתוב פונקציה אחת ולהשאיר אותה ב Scope. גם מבחינת בדיקה של הפונקציה יותר קל להפריד את מנגנון "הכנת" הפרמטרים לפונקציה נפרדת וכך קל לבדוק כל אחת מהפונקציות בנפרד, ובמחינת קריאות גם אם במבט ראשון המנגון של סקאלה נראה מבלבל יש להם כפתור ב IDE שפותח את הסוכר התחבירי הזה ובלחיצת כפתור מראה לנו את ההמרות שמבוצעות בזמן ההפעלה.