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

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

ג'אסט דו איט - הרצת סקריפטים עם just במקום npm

22/09/2024

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

בואו נראה דוגמה קצרה איך just יכול לעזור לנו בפרויקט node כש npm מתחיל להסתבך. נניח שיש לי קובץ package.json כזה:

{
  "name": "demo-project",
  "version": "1.0.0",
  "description": "A simple demo project",
  "main": "index.js",
  "scripts": {
    "start": "npm run build && NODE_ENV=production node index.js",
    "build": "echo building the app",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^2.0.22",
    "babel-cli": "^6.26.0"
  }
}

שימו לב לבלוק הסקריפטים ובמיוחד לסקריפט start:

"start": "npm run build && NODE_ENV=production node index.js",

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

publish: "npm run build && npm run start"

אז במצב כזה ה build ירוץ פעמיים, כי publish יקרא לו ואז start גם יפעיל אותו.

כשה npm מתחיל להסתבך, just יכול להציל את המצב. ג'אסט הוא כלי שרץ משורת הפקודה, אבל אנחנו יכולים להוסיף אותו לקובץ ה package.json בתור תלות נוספת לפיתוח כדי שאנשים לא יצטרכו להתקין אותו בנפרד:

  "devDependencies": {
    "just-install": "2.0.2",
  }

אחרי שהוספנו והתקנו אותו אנחנו יוצרים קובץ בשם justfile בתיקייה הראשית של הפרויקט עם תוכן שנראה כמו Makefile:

default:
  just --list

start: build
  NODE_ENV=production
  node index.js

build:
  echo building app

ועכשיו אפשר למחוק את כל בלוק הסקריפטים מ package.json ולעבור לעבוד עם just. אני מריץ:

$ npx just
just --list
Available recipes:
    build
    default
    start

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

$ npx just start
echo building app
building app
NODE_ENV=production
node index.js
starting app

ואני רואה ש start הריץ גם את build וגם את פקודת ה node. בשביל להוסיף סקריפט publish ל justfile אני כותב:

publish: build start
  echo publish

ובהפעלה ברור ש build רץ רק פעם אחת:

$ npx just publish
echo building app
building app
NODE_ENV=production
node index.js
starting app
echo publish
publish

התיעוד של just מצוין ויש לו הרבה פחות פינות חדות בהשוואה ל make וגם ChatGPT מכיר אותו די טוב אז לא תהיה לכם בעיה להיעזר ב AI כשצריך. אפשר לקרוא יותר על הכלי בגיטהאב שלהם בקישור: https://github.com/casey/just/

ניסוי דינו - מה אפשר לעשות היום עם Deno ו next.js

21/09/2024

בדף התיעוד של דינו יש רשימה של Web Frameworks שדינו תומך בהם ואני מודה שהפריט הראשון ברשימה בלבל אותי. כלומר שמעתי את אנשי דינו מדברים על תמיכה ב next.js אבל לא הבנתי שהכל כבר מוכן, ואם זה מוכן לא ברור למה האנשים ב vercel לא נותנים יותר פירסום לסיפור. אני מפרסם פה כמה ניסויים שעשיתי עם השילוב, לצערי בלי מסקנות מלהיבות.

המשך קריאה

היום למדתי להיזהר מהפונקציה contains בקלוז'ר

20/09/2024

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

הפונקציה keys בקלוז'ר מקבלת מפה ומחזירה את המפתחות שלה. זה קל:

user=> (keys {:a 10 :b 20 })
(:a :b)

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

user=> (keys ["a" "b" "c"])
Error printing return value (ClassCastException) at clojure.lang.APersistentMap$KeySeq/first (APersistentMap.java:168).
class java.lang.String cannot be cast to class java.util.Map$Entry (java.lang.String and java.util.Map$Entry are in module java.base of loader 'bootstrap')

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

user=> (contains? {:a 10 :b 20} :a)
true

בגלל ש keys לא עובדת על מערכים, חשבתי שגם contains? לא אמורה לעבוד על מערכים, וקצת הופתעתי לגלות שהיא דווקא עובדת ואפילו לפעמים צודקת:

user=> (contains? [1 2 3] 2)
true

אבל זה היה רק מוקש. למעשה contains? על מערכים בקלוז'ר בודקת אם הפרמטר שהיא קיבלה הוא אינדקס במערך - ולא ערך. הדוגמה הקודמת עבדה רק במקרה. הנה כמה דוגמאות יותר טובות:

user=> (contains? [1 2 3] 0)
true
user=> (contains? [1 2 3] 3)
false
user=> (contains? ['a' 'b' 'c'] 0)
true
user=> (contains? ['a' 'b' 'c'] 'a')
false
user=> (contains? ['a' 'b' 'c'] 9)
false

ונשארתי עם השאלה - אם האינדקסים של מערך הם המפתחות שלו, למה keys לא מחזירה את רשימת האינדקסים? ואם הם לא מפתחות, למה contains? בודקת אם ערך שהיא קיבלה הוא אינדקס חוקי במערך?

הצעה לפרויקט - תוסף VS Code שמדבג עם Chat GPT

19/09/2024

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

  1. ב VS Code אפשר לכתוב תוסף שמתממשק עם ה Debugger, למשל דרך https://code.visualstudio.com/api/references/vscode-api#DebugAdapterTracker

  2. מתוך ה Callbacks אפשר לגלות מה הקוד שבו עצרנו, מה ערכי המשתנים ומה ה Stack Trace ברגע העצירה.

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

  4. מוסיפים תיבת Chat לחלון הדיבאג ששולחת שאלות ל Chat GPT ומציגה את התשובות בחלונית.

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

נ.ב.ב. באותו נושא cursor ai לוקח כיוון אחר אבל סופר מעניין. אם לא ניסיתם אותו מאוד מומלץ. זה נראה כמו VS Code משולב עם חלון שיחה עם AI, וה AI ממש רץ על כל הקבצים ומעדכן את הקוד לפי הבקשות שלכם. אולי יום אחד הם יוסיפו תמיכה ב Debugger. בכל מקרה מאחר והם לא קוד פתוח זה לא עוזר בינתיים.

בסוף זה תמיד הטיפול בשגיאות

18/09/2024

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

create_user(!!params[:quickjoin]) do |user|
  user.after_confirmation
end

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

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

clean_up_passwords resource
set_minimum_password_length
return redirect_to quickjoin_path, alert: resource.errors.full_messages.join(', ') if params[:quickjoin] == '1'

respond_with resource

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

ב Rails הפונקציה respond_with היא קיצור לזה:

def create
  @user = User.new(params[:user])
  respond_to do |format|
    if @user.save
      flash[:notice] = 'User was successfully created.'
      format.html { redirect_to(@user) }
      format.xml { render xml: @user }
    else
      format.html { render action: "new" }
      format.xml { render xml: @user }
    end
  end
end

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

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

חמש דקות עם flet

17/09/2024

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

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

המשך קריאה

תרגיל SQL - מחיקת שורות כפולות

16/09/2024

נתונה טבלה עם פריטים שנוצרה על ידי הפקודה:

create table items(id integer primary key, name text);

INSERT INTO items (name) VALUES
    ('Item A'),
    ('Item B'),
    ('Item C'),
    ('Item A'),
    ('Item D'),
    ('Item E'),
    ('Item B'),
    ('Item F'),
    ('Item G'),
    ('Item A'); 

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

המשך קריאה

שלושה עקרונות חשובים להצלחה בפרויקטים

15/09/2024

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

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

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

לא יודע לתכנת

14/09/2024

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

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

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

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

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

ספריות בסיסי נתונים וטיפול בהבדלים בין בסיסי הנתונים

13/09/2024

ב Rails יצרתי טבלה בבסיס הנתונים עם קוד שנראה בערך כך:

create_table :users do |t|
  t.string :username
  t.string :email
  t.timestamps
end

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

ב Sequelize הממשק דורש יותר קוד אבל עדיין הספריה מטפלת בהבדלים בין בסיסי נתונים:

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Users', {
      id: {
        type: Sequelize.INTEGER,
        autoIncrement: true,
        primaryKey: true,
        allowNull: false
      },
      username: {
        type: Sequelize.STRING,
        allowNull: false
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false
      },
      createdAt: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.NOW
      },
      updatedAt: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.NOW
      }
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Users');
  }
};

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

export const users = mysqlTable('users', {
  id: bigint('id', { mode: 'number' }).primaryKey().autoincrement(),
  fullName: varchar('full_name', { length: 256 }),
}, (users) => ({
  nameIdx: index('name_idx').on(users.fullName),
}));

וזו מיגרציה בקיסלי שכתובה בצורה מפורשת עבור Postgresql:

  await db.schema
    .createTable('person')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('first_name', 'varchar', (col) => col.notNull())
    .addColumn('last_name', 'varchar')
    .addColumn('gender', 'varchar(50)', (col) => col.notNull())
    .addColumn('created_at', 'timestamp', (col) =>
      col.defaultTo(sql`now()`).notNull(),
    )
    .execute()

למה הם עושים את זה? ומה עדיף?

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

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

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

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

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