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

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

הפיצ'ר שעדיין חסר לי ברידאקס

29/06/2024

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

(def state (reagent/atom {:foo {:bar "BAR"}
                          :baz "BAZ"
                          :quux "QUUX"}))
;; Now create a cursor
(def bar-cursor (reagent/cursor state [:foo :bar]))

(defn bar-component []
  (js/console.log "bar-component is rendering")
  [:div @bar-cursor])

התוכנית מגדירה משתנה אחד בשם state שבו מוחזק כל הסטייט של היישום (כמו ה store ברידאקס) ואז עוד משתנה בשם bar-cursor שהוא סוג של מצביע לתוך הסטייט. הקריאה מה cursor, קצת כמו קריאה מ store דרך useSelector יוצרת חיבור בין הקומפוננטה לחלק הזה בסטייט כך שכשהמידע בתוך הנתיב [:foo :bar] ישתנה גם הקומפוננטה תרונדר מחדש, אבל יש פה שני כוחות על שחסרים ברידאקס:

  1. ה cursor מיוצר מחוץ לקומפוננטות (לא כזה מלהיב, גם ברידאקס הייתי יכול להגדיר Selector בקובץ נפרד).

  2. אפשר לעדכן את ה State דרך ה Cursor.

היכולת השניה היא הדבר הגדול. ברידאקס בשביל להשיג אפקט דומה אני צריך להגדיר Reducer ו Action וכל ה Boilerplate.

הטיית הפעלתנות

28/06/2024

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

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

  2. כמות המאמץ הדרושה בשביל "ללכת אחורה", "להישאר במקום" או "לזוז קדימה" לא פרופורציונלית לתוצאות.

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

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

ומה אם נתחיל מאפס?

27/06/2024

חבר שאל אותי על עיצוב מחדש לאתר ישן שכתוב ב Bootstrap. הוא חיפש Theme יפה יותר אבל כל דבר שהוא מצא דרש יותר מדי התאמות. השאלה הבאה שלי היתה "ומה אם נתחיל מאפס?", ופתאום היתה שתיקה שהבהירה שעלינו על משהו.

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

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

ואז יום אחד

26/06/2024

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

או-

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

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

פיתרון Advent Of Code יום 19 בסקאלה

25/06/2024

אני כבר לא זוכר כמה זמן עבר מאז חידת ה Advent Of Code הקודמת שפתרתי כאן בסקאלה, אבל יצא שתוכניות השתנו היום וסוף סוף היה לי זמן להמשיך לפרק הבא. למי שעוקב אנחנו בחידה 19 מתוך 25 מה שאומר שיש סיכוי לא רע שעד סוף השנה אצליח לסיים את כל ה 25 חידות של שנה שעברה.

המשך קריאה

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

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 קבצים שונים?

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

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

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