# Pure Functions — פונקציות טהורות ו-Side Effects<no value>

## מה נלמד?

- מה זו **Pure Function** (פונקציה טהורה).
- מה זה **Side Effect** (תופעת לוואי) — ואילו סוגים קיימים.
- המלכודת של **שינוי רשימה שהתקבלה כפרמטר**.
- איך כותבים **גרסה טהורה** של פונקציה.

## למה צריך את זה?

הנה פונקציה שנראית תמימה:

```python
total = 0


def add_to_total(price):
    global total
    total += price
    return total


print(add_to_total(10))
print(add_to_total(10))
```

**פלט:**

```text
10
20
```

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

## הסבר מהבסיס

### מה זו Pure Function

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

1. **אותו קלט ‏←‏ תמיד אותה תוצאה.** התוצאה תלויה רק בפרמטרים — לא במשתנים גלובליים, לא בשעה, לא בקלט מהמשתמש.
2. **אין Side Effects.** היא לא משנה שום דבר מחוץ לה. כל מה שהיא "מוציאה" יוצא דרך `return`.

### מה נחשב Side Effect

| Side Effect | דוגמה |
|---|---|
| שינוי משתנה גלובלי | `global total` ואז `total += price` |
| שינוי רשימה או מילון שהתקבלו | `prices[i] = ...` על הרשימה שהגיעה כפרמטר |
| קלט ופלט מול המשתמש | `print(...)`, `input(...)` |
| כתיבה לקובץ או למסד נתונים | `open(..., "w")`, `INSERT` |

{{< box "analogy" "אנלוגיה: מכונת מיץ" >}}
פונקציה טהורה היא כמו מסחטת מיץ: מכניסים 3 תפוזים — יוצאת כוס מיץ. תמיד אותה כמות, והמסחטה לא נוגעת בשום דבר אחר במטבח. פונקציה לא טהורה היא מסחטה שלפעמים גם מזיזה את המקרר — ופתאום משהו אחר בבית לא עובד, ואין לכם מושג למה.
{{< /box >}}

## תחביר

אין תחביר מיוחד — זו **דרך לכתוב** פונקציות:

```python
# pure: result depends only on the parameters
def add_price(total, price):
    return total + price


# not pure: reads and changes something outside
def add_to_total(price):
    global total
    total += price
    return total
```

## דוגמת קוד ראשונה

אותה פונקציה מתחילת השיעור, בגרסה טהורה. `week-2/pure_total.py`:

```python
def add_price(total, price):
    return total + price


total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**פלט:**

```text
20
10
10
```

## פירוק הקוד שורה אחר שורה

| שורה | הסבר |
|---|---|
| `def add_price(total, price):` | כל מה שהפונקציה צריכה מגיע **בפרמטרים**. |
| `return total + price` | היא לא משנה כלום — רק מחשבת ומחזירה. |
| `total = add_price(total, 10)` | **מי שקורא** מחליט מה לעשות עם התוצאה: כאן — לשמור אותה ב-`total`. |
| `print(add_price(0, 10))` פעמיים | אותו קלט ‏←‏ אותה תוצאה, `10`, בכל פעם. |

## דוגמה נוספת

**המלכודת: שינוי רשימה שהתקבלה.** `week-2/discount_trap.py`:

```python
def add_discount(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * 0.9
    return prices


original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**פלט:**

```text
[90.0, 180.0]
[90.0, 180.0]
```

**`original` השתנה!** כמו שלמדנו בשיעור על Mutability, רשימה עוברת לפונקציה **כהפניה** — `prices` ו-`original` הם שני שמות לאותה רשימה. מי שהשתמש ב-`original` בהמשך התוכנית מקבל פתאום מחירים אחרים.

**הגרסה הטהורה — בונים רשימה חדשה:**

```python
def add_discount(prices):
    result = []
    for price in prices:
        result.append(price * 0.9)
    return result


original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**פלט:**

```text
[90.0, 180.0]
[100, 200]
```

עכשיו `original` נשאר בדיוק כמו שהיה, ו-`discounted` היא רשימה חדשה לגמרי.

## טעות נפוצה

**הטעות: לחשוב שכל פונקציה חייבת להיות טהורה.** תוכנה שלא מדפיסה, לא שומרת לקובץ ולא כותבת למסד — לא עושה שום דבר שימושי. Side Effects הם **חלק הכרחי** מכל תוכנה.

הכלל הוא **להפריד**:

- את **החישוב** — בפונקציות טהורות (`calculate_total`, `apply_discount`).
- את **התקשורת עם העולם** — במקומות מעטים וברורים (`print`, שמירה לקובץ).

```python
def calculate_total(prices):        # pure: easy to test
    return sum(prices)


def show_total(prices):             # side effect: printing
    print("Total:", calculate_total(prices))
```

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

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

### תרגיל 1: טהורה או לא?

לכל פונקציה — האם היא טהורה? אם לא, מה ה-Side Effect?

```text
def square(n):
    return n * n

def log_square(n):
    print("squaring", n)
    return n * n

def add_item(cart, item):
    cart.append(item)
    return cart

def with_item(cart, item):
    return cart + [item]
```

{{< solution >}}
| פונקציה | טהורה? | למה |
|---|---|---|
| `square` | **כן** | התוצאה תלויה רק ב-`n`, ושום דבר לא משתנה |
| `log_square` | **לא** | `print` הוא Side Effect — פלט למסך |
| `add_item` | **לא** | `append` משנה את הרשימה שהתקבלה (Mutation) |
| `with_item` | **כן** | `cart + [item]` יוצר רשימה **חדשה**, והמקורית לא נגעה |
{{< /solution >}}

### תרגיל 2: אותה קריאה — תוצאה אחרת

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total
```

הבעיה: כל קריאה ל-`add_to_total(10)` מחזירה תוצאה אחרת (10, ואז 20, ואז 30), כי הפונקציה תלויה במשתנה גלובלי ומשנה אותו.

שכתבו את הפונקציה כגרסה טהורה בשם `add_price(total, price)` — היא מקבלת את הסכום הנוכחי בפרמטר ומחזירה את הסכום החדש, בלי `global`. הריצו את קוד הבדיקה, שמראה שאותו קלט נותן תמיד אותה תוצאה:

```python
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**פלט צפוי:**

```text
20
10
10
```

{{< solution >}}
```python
def add_price(total, price):
    return total + price


total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

```text
20
10
10
```

כל מה שהפונקציה צריכה מגיע **בפרמטרים**, והתוצאה יוצאת רק דרך `return`. מי שקרא מחליט מה לעשות איתה.
{{< /solution >}}

### תרגיל 3: לא לשנות את הרשימה שהתקבלה

```python
TAX = 1.17

def apply_tax(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * TAX
```

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

כתבו גרסה טהורה של `apply_tax(prices)`, שמחזירה רשימה **חדשה** עם המחירים כולל מס (כל מחיר מעוגל לשתי ספרות) ולא נוגעת ברשימה שקיבלה. הריצו את קוד הבדיקה — השורה הראשונה מוכיחה שהרשימה המקורית לא השתנתה:

```python
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

**פלט צפוי:**

```text
[100, 50]
[117.0, 58.5]
```

{{< solution >}}
```python
TAX = 1.17


def apply_tax(prices):
    result = []
    for price in prices:
        result.append(round(price * TAX, 2))
    return result


prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

```text
[100, 50]
[117.0, 58.5]
```

`TAX` הוא קבוע שלא משתנה, ולכן קריאה שלו לא פוגעת בטהרה — אותו קלט עדיין נותן תמיד אותה תוצאה.
{{< /solution >}}

### תרגיל 4: להפריד חישוב מהדפסה

```python
def show_total(prices):
    total = 0
    for price in prices:
        total = total + price
    print("Total:", total)
```

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

- `calculate_total(prices)` — טהורה: רק מחשבת ומחזירה את הסכום;
- `show_total(prices)` — קוראת ל-`calculate_total` ומדפיסה את התוצאה.

קוד לבדיקה:

```python
show_total([10, 20, 30])
```

**פלט צפוי:**

```text
Total: 60
```

{{< solution >}}
```python
def calculate_total(prices):
    return sum(prices)


def show_total(prices):
    print("Total:", calculate_total(prices))


show_total([10, 20, 30])
```

```text
Total: 60
```

החישוב (`calculate_total`) טהור וקל לבדוק. ה-Side Effect (`print`) מרוכז במקום אחד וברור.
{{< /solution >}}

### תרגיל 5: שלושת הגדולים — בלי לשנות

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

```python
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

**פלט צפוי:**

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

{{< solution >}}
```python
def top_three(numbers):
    return sorted(numbers, reverse=True)[:3]


scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

`sorted` מחזירה רשימה **חדשה** ממוינת, ו-Slicing (`[:3]`) מחזיר עוד רשימה חדשה — הרשימה המקורית לא נגעה, ולכן הפונקציה טהורה.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Pure Function** = אותו קלט ‏←‏ תמיד אותה תוצאה, ובלי לשנות שום דבר בחוץ.
- **Side Effects**: שינוי גלובלי, שינוי רשימה או מילון שהתקבלו, `print`/`input`, כתיבה לקובץ או למסד.
- רשימה עוברת כהפניה — פונקציה שמשנה אותה משנה את **המקור**. הגרסה הטהורה בונה רשימה חדשה.
- פונקציות טהורות קלות לבדיקה, צפויות, וקל להשתמש בהן שוב.
- לא הכול יכול להיות טהור — מפרידים בין **חישוב** טהור לבין **תקשורת עם העולם**.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [Functional Programming HOWTO](https://docs.python.org/3/howto/functional.html#introduction) — פונקציות בלי Side Effects

<!-- docs:end -->
