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

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

מה הופך מימוש לקשה לקריאה

24/06/2024

לא משנה אם אנחנו מדברים על JavaScript, על פייתון, על פרל או על ראסט, יש כמה מאפיינים שיכולים להפוך מימוש של אלגוריתם לקשה במיוחד לקריאה. אנטון זייאנוב ריכז מימושים של UUID7 ב 32 שפות בקישור כאן: https://antonz.org/uuidv7/

ואני חשבתי שזו הזדמנות מצוינת לחפש את הקושי.

המשך קריאה

חיפוש תמונות ב pixabay מתוך סקאלה

23/06/2024

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

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

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

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

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

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

package dictionary.images

import com.typesafe.scalalogging.Logger
import common.ClaudeSyncClient.getClass
import io.circe.generic.auto.*
import io.circe.parser.*
import sttp.client3.*
import sttp.client3.circe.*

import sttp.model.Uri

import scala.util.chaining.*

case class ImageHit(id: Int,
                     pageURL: String,
                     `type`: String,
                     tags: String,
                     previewURL: String,
                     previewWidth: Int,
                     previewHeight: Int,
                     webformatURL: String,
                     webformatWidth: Int,
                     webformatHeight: Int,
                     largeImageURL: String,
                     imageWidth: Int,
                     imageHeight: Int,
                     imageSize: Int,
                     views: Int,
                     downloads: Int,
                     collections: Int,
                     likes: Int,
                     comments: Int,
                     user_id: Int,
                     user: String,
                     userImageURL: String)

case class ApiResponse(total: Int,
                        totalHits: Int,
                        hits: List[ImageHit])

object Pixabay {
  private val API_KEY: String = System.getenv("PIXABAY_API_KEY")
  val client: SimpleHttpClient = SimpleHttpClient()
  val logger: Logger = Logger(getClass)

  def searchImages(query: String): ApiResponse =
    val queryParams = Map(
      "key" -> API_KEY,
      "q" -> query,
      "image_type" -> "photo",
      "page" -> "1"
    )
    val baseUrl = uri"https://pixabay.com/api/"
    val uriWithParams: Uri = uri"$baseUrl?$queryParams"
    println(uriWithParams)
    basicRequest
      .get(uriWithParams)
      .response(asJson[ApiResponse])
      .pipe(client.send)
      .body match
        case Right(response) => response
        case Left(err) =>
          logger.error("Pixabay error: ", err)
          throw Exception(s"Error getting images from pixabay", err)

  @main
  def testImageSearchPixabay(): Unit =
    val response = searchImages("raisin")
    println(response.hits.head.previewURL)

}

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

scalacOptions ++= Seq("-Xmax-inlines", "100")

גרמלין נשך אותי

22/06/2024

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

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

import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerTransactionGraph
import org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal
import scala.jdk.CollectionConverters._

val graph = TinkerTransactionGraph.open
val g = traversal.withEmbedded(graph)

ואז יוצר כמה פריטים:

g.addV("item").next()
g.addV("item").next()
g.addV("item").next()

וגם יכול להדפיס את המידע עליהם עם השאילתה:

scala> g.V().hasLabel("item").limit(2).elementMap().toList.asScala

val res21: scala.collection.mutable.Buffer[java.util.Map[Object, Object]] = Buffer({id=0, label=item}, {id=1, label=item})

עכשיו לחלק שהפתיע אותי (למרות שבמבט שני על הקוד האשמה כולה עליי). נניח שאני רוצה לפצל את השאילתה לשני חלקים, קודם לקחת את כל ה IDs של הצמתים שיצרתי ורק אחר כך להתחיל מעבר חדש על הצמתים לפי ה ID ולהדפיס את המידע. אני יכול להתפתות ולכתוב:

scala> val allItems = g.V().hasLabel("item").limit(2).toList.asScala.toList
val allItems: List[org.apache.tinkerpop.gremlin.structure.Vertex] = List(v[0], v[1])

scala> g.V(allItems: _*).elementMap().toList
val res22: java.util.List[java.util.Map[Object, Object]] = [{id=0, label=item}, {id=1, label=item}]

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

scala> val allItems = g.V().hasLabel("x").limit(2).toList.asScala.toList
val allItems: List[org.apache.tinkerpop.gremlin.structure.Vertex] = List()

scala> g.V(allItems: _*).elementMap().toList
val res23: java.util.List[java.util.Map[Object, Object]] = [{id=0, label=item}, {id=1, label=item}, {id=2, label=item}]

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

והלקח מהסיפור - לא להשתמש ב spread operator, או לפחות אם משתמשים בו תמיד לבדוק מה קורה כשהרשימה ריקה.

כבר לא

21/06/2024

מה יותר מהיר, שאילתה אחת עם JOIN או מספר שאילתות וחיבור הנתונים בזיכרון?

מה יותר מהיר, חיבור כל קבצי ה JavaScript לקובץ אחד ארוך או שליחת 50 פניות לשרת ל 50 קבצים שונים?

מה יותר מהיר, הוספה או עדכון?

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

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

למי חשוב ה git diff?

19/06/2024

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

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

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

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

מה לפני איך

18/06/2024

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

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

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

  1. מי צריך את הפיצ'ר הזה?

  2. מה הבעיה שהפיצ'ר אמור לפתור לאותו לקוח?

  3. מה אותו לקוח עושה היום?

  4. איך הפיצ'ר ישנה את ה Workflow של הלקוח?

  5. איזה פיצ'רים אחרים עשויים להשפיע על אותה בעיה?

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

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

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

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

ככל שחברנו ה AI הולך לעזור יותר בכתיבת קוד, כך היכולת להסתכל על ה"מה" לפני ה"איך" הופכת להיות חלק בלתי נפרד מהעבודה שלנו.

פוחד אפילו לנסות (סביבת תרגול)

17/06/2024

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

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

אבל בפרויקט צד? בפרויקט ללימודים? בקורס?

אחת המטרות של פרויקטים נטולי סיכון היא לקבל מרחב בטוח להתנסות, מרחב שבו אפשר לכתוב אפליקציה בגישת Micro Services גם אם זה Overkill, להכניס בסיס נתונים מבוזר (או 5 בסיסי נתונים), לעצב דף בית תלת מימדי עם Three.js וכן גם לכתוב את ה Front End בלי אף פריימוורק ואת צד השרת ב Rust. לא יודע איזה מהרעיונות האלה יעבוד לפרויקט שלכם וכמה מפרויקטי הלימודים האלה באמת יצליחו להתרומם, אבל מה שבטוח שבסיום הכתיבה תהיה לכם הבנה טובה של שיטת עבודה חדשה, ואולי חלק משיטות העבודה האלה יעזרו לכם גם לפרויקטים אמיתיים.

סינכרון קבצי הגדרות בין מכונות עם yadm

16/06/2024

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

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

המשך קריאה

עוד כמה טיעונים נגד טייפסקריפט

15/06/2024

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

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

type Table = Array<Array<number>>;

function duplicateRow(table: Table, rowIndex: number): Table {
    if ((rowIndex >= table.length) || (rowIndex < 0)) {
        throw new Error("row index out of range")
    }

    return [...table.slice(0, rowIndex), table.at(rowIndex), ...table.slice(rowIndex)];
}

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

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

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