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

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

היום למדתי: שינוי שם שם מפתח במפה בקלוז'ר

17/07/2024

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

>>> d = {}
>>> d[[1, 2, 3]] = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

או ב JavaScript כל מפתח שנבחר יהפוך ל String אוטומטית:

> o = {}
{}
> o[2] = 10
10
> o['2']
10

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

3.1.1 :001 > h = { name: "ynon" }
 => {:name=>"ynon"}
3.1.1 :002 > h[:nickname] = h.delete :name
 => "ynon"
3.1.1 :003 > h
 => {:nickname=>"ynon"}

היה לי מילון עם מפתח בשם name ובשביל להחליף אותו ל nickname היה צריך להוציא את המפתח הישן מהמילון ולשים חדש עם אותו ערך.

וקלוז'ר? דווקא היא הפתיעה לטובה עם פונקציית rename-keys שמקבלת מפה ומפה של שינויים ומשנה את השמות של המפתחות לפי מפת השינויים. כמובן שבקלוז'ר אנחנו מדברים על מבני נתונים Immutable ולכן אנחנו לא באמת משנים מילון אלא מייצרים מילון חדש עם המפתחות בשמות החדשים:

user=> (clojure.set/rename-keys {:name "ynon"} {:name :nickname})
{:nickname "ynon"}

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

טיפ CSS - אפקט שורות מתחת לטקסט

16/07/2024

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

<div class="border-b border-gray-400">
  Hello World
</div>

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

<div class="border-b border-gray-400 mb-2">
  Line 1
</div>
<div class="border-b border-gray-400 mb-2">
  Line 2
</div>
<div class="border-b border-gray-400 mb-2">
  Line 3
</div>

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

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

<div class="p-4 w-full max-w-md mx-auto">
  <div class="relative">
    <div class="absolute inset-0 bg-[size:100%_2rem] bg-[linear-gradient(#999_1px,transparent_1px)] bg-[position:0_-1px]"></div>
    <p class="relative leading-8 pt-[1px]">
      This text now starts from the first line. It appears as if it's written on the notebook lines, aligning perfectly with each line to create a realistic effect.
    </p>
  </div>
</div>

במקום לצייר קו מתחת לכל שורה אני מגדיר אלמנט עם גרדיינט לינארי ברקע. הגרדיינט הוא שאחראי על ציור השורות וה line-height (שנקבע על ידי המאפיין leading של טיילווינד) גורם לטקסט להתאים בגובה לגובה של השורות בתמונת הרקע. כשיש יותר טקסט אין שום בעיה ובאופן אוטומטי הטקסט יחולק בין השורות.

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

<div class="p-4 w-full mx-auto">
  <div class="relative">
    <div class="absolute inset-0 bg-[size:100%_2rem] bg-[linear-gradient(#999_1px,transparent_1px)] bg-[position:0_-1px]"></div>
    <p class="relative leading-8 pt-[1px] w-full h-32 ">
    </p>
  </div>
</div>

נ.ב. בשביל לשחק עם הדוגמאות הכי קל לבקר ב https://play.tailwindcss.com/ ולהדביק שם את קוד הטיילווינד כדי לראות את התוצאה בצד ימין של המסך.

כשהתרגיל המחשבתי עובד נגדך

15/07/2024

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

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

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

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

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

פיתרון Advent Of Code 2023 יום 21

14/07/2024

יום 21 של Advent Of Code הציג חידת מפה ממש חמודה בחלק הראשון שהפכה לכאב ראש בחלק השני (שעליו דילגתי). בואו נראה על מה מדובר ולמה וויתרתי על החלק השני, יחד עם הפיתרון של החלק הראשון בשפת סקאלה.

המשך קריאה

היום למדתי להיזהר מקבצי pyc

13/07/2024

החברים ב JFrog פירסמו השבוע פוסט שתפס אותי בהפתעה: https://jfrog.com/blog/leaked-pypi-secret-token-revealed-in-binary-preventing-suppy-chain-attack/

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

def _fetch_github_file(github_repository="owner/repo", ref="main", access_token=None, filename="Dockerfile"):
    headers = {
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
        "Authorization": "Bearer 0d6a9bb..."
    }
    if access_token is not None:
        headers['Authorization'] = f'token {access_token}'
        ...

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

def _fetch_github_file(github_repository="owner/repo", ref="main", access_token=None, filename="Dockerfile"):
    headers = {
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    if access_token is not None:
        headers['Authorization'] = f'token {access_token}'
        ...

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

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

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

איך להתחיל לפתח Miniapp לבוט בטלגרם

12/07/2024

אחת היכולות המלהיבות של בוטים בטלגרם היא האפשרות "לברוח" מהבוט לדף ווב, כאשר דף הווב מקבל המון מידע מטלגרם כדי לייצר מראה אחיד בין הבוט לאפליקציה. פירוט מלא על מיניאפס אפשר למצוא בדף התיעוד של טלגרם כאן: https://core.telegram.org/bots/webapps.

ואם כבר יש לכם בוט אלה השלבים בקצרה:

  1. יוצרים תיקייה על המחשב עבור הבוט. אפשר לבנות ווב אפ מתוחכם או בשביל הדוגמה קובץ html פשוט. המינימום שצריך זה קובץ index.html וקובץ manifest.json.

קובץ ה manifest.json נראה ככה:

{
  "name": "My Telegram Mini App",
  "short_name": "Mini App",
  "description": "A brief description of what your Mini App does",
  "version": "1.0.0",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0088cc",
  "icons": [
    {
      "src": "/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

והקובץ index.html נראה ככה:

<!DOCTYPE html>
<html>
<head>
    <link rel="manifest" href="/manifest.json">
</head>
<body>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script>
    const webapp = window.Telegram.WebApp;
    webapp.ready();
</script>
</body>
</html>
  1. מתקינים https://telebit.cloud או ngrok או כלי דומה כדי לקבל URL ציבורי לסרביס שרץ אצלכם על המחשב.

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

  3. מפעילים את טלביט לפורט של השרת שיצרתם. בדוגמה שלי אני מפעיל:

python -m http.server

כדי לקבל שרת ווב סטטי לקבצים על פורט 8000 ולכן אני מפעיל:

~/telebit http 8000

כדי לחבר את הפייתון ל URL הציבורי שקיבלתי מטלביט.

  1. שולחים הודעה ל Botfather עם הפקודה /newapp.

  2. עונים על המון שאלות. השאלה היחידה שחשובה שם היא ה URL של ה Web App. מכניסים שם את ה URL שקיבלנו מטלביט (זה שמחובר לשרת ווב שרץ אצלנו על המחשב).

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

  4. אחרי שמסיימים לפתח תוכלו להעלות את דף הווב לשרת אמיתי ולעדכן את הכתובת באמצעות שליחת הודעת /editapp ל botfather.

שמונה מיומנויות גיט שממש שווה להכיר בעל פה

11/07/2024

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

  1. להוסיף קבצים, לעשות קומיט ולדחוף את השינויים לשרת מרוחק.

  2. להסתכל איך נראה קובץ בקומיט אחר.

  3. לעבור בין ענפים, מקומיים או מרוחקים.

  4. לראות את רשימת כל השרתים המרוחקים איתם הפרויקט שלכם מסונכרן.

  5. לראות מה היה הקומיט שגרם לשורה מסוימת להצטרף לקוד, או לבלוק מסוים להימחק.

  6. לקבל רשימה של שמות הקבצים שהשתנו בקומיט מסוים.

  7. למחוק ענפים מקומיים או מרוחקים.

  8. לפתור קונפליקטים אחרי merge או cherry-pick או rebase או כל דבר אחר שיוצר אותם.

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

בדיוק מה שביקשת

10/07/2024

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

הלכתי לאדון קלוד היום וביקשתי:

create an encrypted messages with deno

זה הקוד שקיבלתי - ובבקשה אל תשתמשו בו בקוד שלכם:

import { encode, decode } from "https://deno.land/std/encoding/base64.ts";
import { Aes } from "https://deno.land/x/crypto/aes.ts";
import { Cbc } from "https://deno.land/x/crypto/block_modes.ts";

function encrypt(message: string, key: string): string {
  const iv = crypto.getRandomValues(new Uint8Array(16));
  const aes = new Aes(key);
  const cbc = new Cbc(aes, iv);

  const encoder = new TextEncoder();
  const encodedMessage = encoder.encode(message);

  const encryptedData = cbc.encrypt(encodedMessage);

  const combined = new Uint8Array(iv.length + encryptedData.length);
  combined.set(iv);
  combined.set(encryptedData, iv.length);

  return encode(combined);
}

function decrypt(encryptedMessage: string, key: string): string {
  const combined = decode(encryptedMessage);
  const iv = combined.slice(0, 16);
  const encryptedData = combined.slice(16);

  const aes = new Aes(key);
  const cbc = new Cbc(aes, iv);

  const decryptedData = cbc.decrypt(encryptedData);

  const decoder = new TextDecoder();
  return decoder.decode(decryptedData);
}

// Example usage
const key = "mySecretKey123456"; // 16, 24, or 32 bytes long
const message = "Hello, this is a secret message!";

const encrypted = encrypt(message, key);
console.log("Encrypted:", encrypted);

const decrypted = decrypt(encrypted, key);
console.log("Decrypted:", decrypted);

לא צריך יותר מדי להתעמק בשביל לראות שהמפתח הסודי שמור בתוך קוד התוכנית ושהקוד לא חותם על התוכן המוצפן מה שיאפשר לפורצים עתידיים להשתמש בתוכנית שלי כדי לפרוץ את ההצפנה באמצעות שינוי המידע המוצפן ושליחתו למפענח (מה שמכונה Padding Oracle Attack).

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

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

אחרי כמה זמן זה מפסיק להיות קשה?

09/07/2024

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

  1. מתי זה יתחיל להיות כיף?

  2. מתי אתחיל להתגעגע לזה אם אדלג על שיעורים?

  3. מתי אגלה לבד משהו שלא מצאתי קודם בספר?

  4. מתי אוכל לפגוש אנשים חדשים שגם לומדים את הדבר הזה?

  5. מתי אצליח משהו שאף פעם לא הצלחתי קודם?

  6. מתי לא ארצה להפסיק?

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

מימוש חלקי בסקאלה דרך מיקסינים

08/07/2024

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

class MyBigHandler {
  def handle(message: String): String =
    message match
      case "error" => "Handling error"
      case "warning" => "Handling warning"
      case "info" => "Handling info"
      case "zzz" => "MyHandler"
      case _ => "Unhandled message"
}

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

trait MessageHandler {
  def handle(message: String): String = "Unhandled Message"
}

trait ErrorHandler extends MessageHandler {
  abstract override def handle(message: String): String = message match {
    case "error" => "Handling error"
    case _ => super.handle(message)
  }
}

trait WarningHandler extends MessageHandler {
  abstract override def handle(message: String): String = message match {
    case "warning" => "Handling warning"
    case _ => super.handle(message)
  }
}

trait InfoHandler extends MessageHandler {
  abstract override def handle(message: String): String = message match {
    case "info" => "Handling info"
    case _ => super.handle(message)
  }
}

class MyHandler extends MessageHandler
  with ErrorHandler
  with WarningHandler
  with InfoHandler {

  override def handle(message: String): String =
    message match
      case "zzz" => "MyHandler"
      case _ => super.handle(message)
}

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