ToCode
1.4K subscribers
3.25K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
גם השמות הלא נכונים שלכם הם חוב טכני
שמות תמיד היו חשובים. שם לא נכון של עמודה בבסיס נתונים יכול לבלבל משתמשים חדשים וגם וותיקים, ואותו דבר לגבי פונקציות, שמות קבצים, שמות מחלקות ואפילו שמות של פרויקטים. כן לתת שמות לדברים תמיד היה קשה.

ואז הגיע AI.

שימו לב לשלושת הפרומפטים הכמעט זהים:

Write tests for the following python function:

def count_lines(x):
...


Write tests for the following python function:

def count_lines(url):
...


Write tests for the following python function:

def count_lines(file_path):
...


Write tests for the following python function:

def count_lines(fp):
...


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

אין בעיה בוס, מתחילים בקובץ הזה:

import dynamic from 'next/dynamic'

// Use dynamic import to prevent hydration errors with canvas
const SnakeGame = dynamic(() => import('@/components/SnakeGame'), {
ssr: false
})

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-4 bg-gray-100">
<h1 className="text-3xl font-bold mb-6">Snake Game</h1>
<SnakeGame />
</main>
)
}


הי קרסר הקוד לא מתקמפל - הוא מתלונן שאי אפשר לקרוא ל dynamic בלי SSR מתוך קומפוננטת צד שרת.

אין בעיה בוס אני על זה. בוא נעשה ככה:

1. ניצור קובץ חדש בשם SnakeGameWrapper שיתחיל ב use client ונעביר אליו את כל הקוד מ page.

2. נעדכן את page שיטען את הקובץ החדש הזה.

---

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

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

import * as Phaser from 'phaser';

const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
pixelArt: true,
transparent: false,
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: []
};

new Phaser.Game(config);


מתקין את פייזר עם:

npm add phaser


ונכנס למסך הפיתוח עם:

npm run dev


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

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

import * as Phaser from 'phaser';

export default class MainScene extends Phaser.Scene {}


השמות שנראה בדוגמה הם:

1. הפונקציה preload שנקראת לפני שטוענים את הסצינה ונועדה לאתחל רכיבים גרפיים של הסצינה.

2. הפונקציה create שנקראת כשצריך להפעיל את הסצינה.

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

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

מתחילים עם preload:

  preload() {
const graphics = this.add.graphics();
graphics.fillStyle(0x00ff00, 1);
graphics.fillCircle(25, 25, 25);
graphics.generateTexture('greenBall', 50, 50);
graphics.destroy(); // Clean up graphics after texture is created
}


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

הפונקציה create כבר יותר מעניינת:

create() {
// Now you can safely use the texture
this.ball = this.physics.add.sprite(100, 100, 'greenBall');
this.ball
.setVelocity(200, 200)
.setCollideWorldBounds(true);
}


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

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

  update() {
const {width, height} = this.game.config;

if (this.ball.body.left <= 0) {
this.ball.body.velocity.x = 200;
}
if (this.ball.body.right >= width) {
this.ball.body.velocity.x = -200;
}
if (this.ball.body.top <= 0) {
this.ball.body.velocity.y = 200;
}
if (this.ball.body.bottom >= height ) {
this.ball.body.velocity.y = -200;
}
}


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

את הקוד המלא שמרתי בקובץ main-scene.js:
import * as Phaser from 'phaser';

export default class MainScene extends Phaser.Scene {
constructor() {
super('MainScene');
}

preload() {
const graphics = this.add.graphics();
graphics.fillStyle(0x00ff00, 1);
graphics.fillCircle(25, 25, 25); // Draw circle at the center of a 50x50 texture
graphics.generateTexture('greenBall', 50, 50); // Generate texture
graphics.destroy(); // Clean up graphics after texture is created
}

create() {
// Now you can safely use the texture
this.ball = this.physics.add.sprite(100, 100, 'greenBall');
this.ball
.setVelocity(200, 200)
.setCollideWorldBounds(true);
}

update() {
const {width, height} = this.game.config;

if (this.ball.body.left <= 0) {
this.ball.body.velocity.x = 200;
}
if (this.ball.body.right >= width) {
this.ball.body.velocity.x = -200;
}
if (this.ball.body.top <= 0) {
this.ball.body.velocity.y = 200;
}
if (this.ball.body.bottom >= height ) {
this.ball.body.velocity.y = -200;
}
}
}


ובקובץ main.js אני מעדכן את הקונפיגורציה כדי להציג את הסצינה:

import * as Phaser from 'phaser';
import MainScene from './main-scene';
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
pixelArt: true,
transparent: false,
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: [MainScene]
};

new Phaser.Game(config);


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

אפשר ללמוד עוד על פייזר דרך אינסוף מדריכים באתר שלהם כאן:
https://phaser.io/

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

def return_example():
try:
print("In try block")
return "Try block return"
except Exception as e:
print(f"Exception: {e}")
finally:
print("In finally block")
return "Finally block return"

result = return_example()
print(f"Result: {result}")


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

In try block
In finally block
Result: Finally block return


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

https://peps.python.org/pep-0765/

אגב דוגמה מעניינת מה PEP של קוד אמיתי שנופל בבור הזה היתה:

try:
...
except:
return NotImplemented
finally:
return some_value


הרעיון כאן היה שמישהו רצה להחזיר ערך מסוים אם היה Exception או ערך אחר אם לא היה, אבל השימוש ב return בתוך finally ממסך על ה return של ה except וכך תמיד מוחזר הערך שמוגדר ב finally.
פיתוח משחק סנייק עם AI - מה כן עבד
לפני כמה ימים כתבתי כאן על הכישלון שלי בפיתוח משחק סנייק עם קרסר. הפרומפט "תפתח לי משחק סנייק", באופן די צפוי, יצר קוד גרוע ומשחק שרק נראה עובד אבל מכיל באגים וארכיטקטורה גרועה, כך שכל תיקון של אחד שבר 3 אחרים. בפוסט היום אני רוצה להראות מה כן לעשות - איך לגרום ל AI לפתח קוד ממש בסדר, ומה זה אומר על התפקיד של מתכנתים בעולם של Vibe Coding.

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

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

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


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

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

https://github.com/ynonp/vibe-coding-snake-game/tree/17c67dcee0cc3e6514440f09bb6449478dae0108

זה מתחיל בקובץ חוקים כללי לפרויקט שמתאר את מבנה התיקיות והטכנולוגיות:

---
description:
globs:
alwaysApply: true
---
* Project Overview *

You are an experienced game developer implementing a DOM based snake game using:
1. React
2. Next.JS 15 (App Router)
3. Tailwind CSS 4
4. Vitest

* Directory Structure *

1. \/src\ contains source files
2. \/tests\ contains test files, in a top-level test separation
3. \/src/components/dom\ are components that render game items to DOM
4. \/src/app\ is the root for the application pages
5. \/src/lib\ stores general utilities or logic, including server actions
6. \/src/hooks\ stores custom hooks

* Components *
* Store DOM rendering logic in components
* Component should be small (less than 200 lines). Their logic is saved in hooks
* Think carefully before using \useEffect\. There's probably a better way.
* Do not use \useCallback\ or \useMemo\. Be smart about the location of component state.
* Create new components as needed

* Hooks *
* Store the client side component logic in custom hooks
* Hooks help us maintain small components
* Create new hooks as needed

* Tests *
* vitest config file is /vitest.config.mts
* Create tests for any feature you implement.
* Use existing mocks where available, for example use fake timers in the tests.
* Do not create new mocks or stubs for the tests.

* Style *
* Prefer using built-in tailwind 4 classes
* All tailwind theme settings is defined in \src/app/globals.css\.
* Do not use a dedicated config file.


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

אחרי זה הלכתי לתיקיית קבצי המקור ויצרתי כמה קבצים. תחילה קובץ game.tsx בשביל הקומפוננטה של המשחק:

'use client';

import useSnake from "@/hooks/use-snake";
import useGameLoop from "@/hooks/use-game-loop";
/**
* Create a Snake game client component
*/
export default function Game() {
const snake = useSnake();
const apple = useApple();
const areColliding = useCollisionDetection(snake.body, apple.body);
const gameLoop = useGameLoop();

if (areColliding) {

}

return (
<div>
<h1>Snake</h1>
<Snake snake={snake} />
<Apple apple={apple} />
</div>
)
}


שלושה קבצים ריקים עבור הלוגיקה שרציתי שתישמר בתור hooks עם השמות use-apple.ts, use-collision-detection.ts, use-game-loop.ts ו use-snake.ts. יצרתי גם קובץ טיפוסים בשם types/game.ts עם התוכן הבא:

export type GameObject = Coordinates | Array<Coordinates>;

export type Snake = {
body: Array<Coordinates>;
direction: Direction;
};

export type Coordinates = {
x: number;
y: number;
};

export type Direction = 'up' | 'down' | 'left' | 'right';

export type GameState = {
snake: Snake;
food: Coordinates;
gameOver: boolean;
};

export type GameConfig = {
width: number;
height: number;
gameOver: boolean;
speed: number;
}


יותר מזה, רציתי לוודא שבלולאת המשחק ה UI ישתמש ב requestAnimationFrame אז כתבתי בקובץ use-game-loop.ts את התוכן הבא:

import { useEffect } from "react";

export default function useGameLoop({
update,
isRunning
}: {
update: () => void;
isRunning: boolean;
}) {
let animationFrame: number;

function tick() {
if (isRunning) {
update();
animationFrame = requestAnimationFrame(tick);
}
}

useEffect(() => {
tick();
return () => cancelAnimationFrame(animationFrame);
}, [isRunning]);
}


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

ניסיון שלישי - מבנה בסיסי עובד, AI משלים
אחרי שנתתי למספר מודלים ליצור משחק סנייק לפי ההתחלה של הבסיס שכתבתי בחלק הקודם הבנתי שהבעיה לא במודלים אלא בפרומפט וחזרתי לשולחן השרטוטים. הפעם המשכתי את המשחק בעצמי עד שהגעתי ל Game Loop שמזיזה נחש. הקוד כלל את הקובץ use-game-loop.ts השלם:

import type { RefObject } from 'react';
import { useRef, useEffect } from "react";

function gameLoop(
nextAnimationFrameRef: RefObject<number>,
lastTime: number,
accumulatorRef: RefObject<number>,
updatesPerSecond: number,
update: () => void,
) {
const timeStep = 1 / updatesPerSecond;
const currentTime = performance.now();
let deltaTime = (currentTime - lastTime) / 1000;
accumulatorRef.current += deltaTime;

while (accumulatorRef.current >= timeStep) {
update();
accumulatorRef.current -= timeStep;
}
nextAnimationFrameRef.current = requestAnimationFrame(() => gameLoop(nextAnimationFrameRef, currentTime, accumulatorRef, updatesPerSecond, update));
}

export default function useGameLoop(
updatesPerSecond: number,
update: () => void,
) {
let accumulatorRef = useRef(0);
const nextAnimationFrameRef = useRef(0);

useEffect(() => {
if (updatesPerSecond > 0) {
gameLoop(nextAnimationFrameRef, performance.now(), accumulatorRef, updatesPerSecond, update)
return () => {
cancelAnimationFrame(nextAnimationFrameRef.current);
}
}
}, [updatesPerSecond, update])
}


קובץ use-snake עם הרבה יותר תוכן:

import { Coordinates, Direction, Snake } from "@/types/game";
import { useState } from "react";

/**
* Creates a new snake object
* Bind keyboard events to move the snake
*/
export default function useSnake(): Snake {
const [body, setBody] = useState([{x: 20, y: 20}]);
const [direction, setDirection] = useState<Direction>('right');
const [grow, setGrow] = useState(2);

return {
body: body,
direction,
update: () => {
setBody(body => moveBody(body, direction, grow))
if (grow > 0) {
setGrow(g => g - 1);
}
},
grow: (addition: number) => {
setGrow(g => g + addition)
}
}
}
function moveBody(body: Array<Coordinates>, direction: Direction, grow: number) {
const head = body[0];
const newHead = {
'right': () => ({x: head.x + 1, y: head.y}),
'left': () => ({x: head.x - 1, y: head.y}),
'up': () => ({x: head.x, y: head.y - 1}),
'down': () => ({x: head.x, y: head.y + 1}),
}[direction]();
if (grow > 0) {
return [newHead, ...body]
} else {
return [newHead, ...body.slice(0, body.length - 1)]
}
}


קובץ קומפוננטה של נחש בשם snake.tsx:

import type { Snake, Coordinates } from "@/types/game";
import { coordinatesToStyle } from '@/lib/utils';
/**
* Renders the snake using DOM objects
* Each snake part should be a div with a class name of 'snake'
*/


export default function Snake({ snake }: { snake: Snake }) {
return (
<>
{snake.body.map((c, i) => (
<div
key={i}
className='w-5 h-5 bg-amber-900 absolute m-0 p-0'
style={coordinatesToStyle(c)}
/>
))}
</>
)
}


וקובץ משחק game.tsx שמייצר נחש, תפוח ו Game Loop ומתחיל להזיז דברים על המסך:

'use client';
import isColliding from "@/lib/is-colliding";
import useSnake from "@/hooks/use-snake";
import useGameLoop from "@/hooks/use-game-loop";
import useApple from "@/hooks/use-apple";
import Snake from "@/components/dom/snake";
import Apple from "@/components/dom/apple";
import PlayButton from "@/components/dom/play-button";
import { useState } from "react";
/**
* Create a Snake game client component
*/
export default function Game() {
const [isPlaying, setIsPlaying] = useState(false);
const snake = useSnake();
const apple = useApple();
const snakeEatsApple = isColliding(snake.body, apple.body);
const GAME_SPEED = 2; // move the snake 1 step every second

const gameLoop = useGameLoop(isPlaying ? GAME_SPEED : 0, () => {
snake.update();
});

if (snakeEatsApple) {
}

return (
<div>
<h1>Snake</h1>
<PlayButton isPlaying={isPlaying} toggle={() => setIsPlaying(p => !p)} />
<div className="border-purple-600 border w-[800px] h-[600px] relative mx-auto">
<Snake snake={snake} />
<Apple apple={apple} />
</div>
</div>
)
}


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

1. נחש יכול לאכול תפוח ולגדול.

2. שליטה בנחש עם כפתורי החצים או w, s, a, d.

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

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

אפשר לראות את כל הקוד שקלוד כתב בצורת diff בקישור הזה:
https://github.com/ynonp/vibe-coding-snake-game/commit/7476986d0f7cc35ab70aa670c349dfdf5a4c531e

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

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

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

https://github.com/mrkn/pycall.rb

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

require 'pycall/import'
include PyCall::Import

pyimport :cowsay

puts cowsay.cow('Hello World')


בשביל שזה יעבוד צריך להתקין את pycall, להתקין את פייתון ולהקפיד לקמפל אותו עם האפשרות להשתמש בו בתור ספריה עם הפקודה:

env CONFIGURE_OPTS="--enable-shared" pyenv install 3.13.3


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

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

בין הדברים שעלו:

1. אחרי שימוש בכלי השלמה מבוססי AI שכחתי איך לכתוב אפילו פונקציה בסיסית.

2. התרגלתי לתת ל AI לפתור בעיות וכך הפסקתי להנות מתכנות.

3. קבלת החלטות קטנות במהלך כתיבת הקוד עוזרת לי לקבל החלטות גדולות לגבי הארכיטקטורה ולהרגיש את המערכת.

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

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

גישת התשובה האחת בעייתית ממגוון סיבות:

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

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

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

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

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

פעם בצוותים אנושיים היינו מדברים על אנשים שכותבים Quick and Dirty ולכן אחרי שנה או שנה וחצי המוצר נתקע וכמו כדור גדול של בוץ אי אפשר לשנות אותו או להוסיף פיצ'רים חדשים.

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

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

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

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

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

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

4. קל לי יותר לקבל כל הצעה של ה AI בלי לקרוא את הקוד, טוב יותר למערכת שהקוד שנכנס עבר ביקורת ולא כולל שטויות או המצאות.

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