# Higher-Order Functions — פונקציות שמקבלות ומחזירות פונקציות<no value>

## מה נלמד?

- מה זו **Higher-Order Function** (פונקציה מסדר גבוה).
- איך **מעבירים פונקציה כפרמטר** — ולמה בלי סוגריים.
- הפונקציות המובנות **`map`** ו-**`filter`**.
- הפרמטר **`key`** של `sorted`, `max` ו-`min`.
- **פונקציה שמחזירה פונקציה**.

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

שתי פונקציות כמעט זהות:

```python
def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result


def square_all(numbers):
    result = []
    for n in numbers:
        result.append(n * n)
    return result
```

הלולאה **זהה** — רק הפעולה על כל איבר שונה. כל פעולה חדשה (חצי, פלוס 10, עיגול...) דורשת עוד פונקציה שלמה, וזו בדיוק הכפילות ש-DRY אוסר.

**הרעיון:** לכתוב את הלולאה **פעם אחת**, ולהעביר את **הפעולה עצמה** כפרמטר.

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

### פונקציה היא אובייקט

כבר ראינו בשיעור "פונקציות לעומק" שב-Python פונקציה היא **אובייקט** — כמו מספר או רשימה:

- `double` (בלי סוגריים) — **הפונקציה עצמה**. אפשר לשמור אותה במשתנה או להעביר אותה.
- `double(5)` (עם סוגריים) — **קריאה** לפונקציה. התוצאה היא מה שהיא מחזירה.

### Higher-Order Function

**Higher-Order Function** היא פונקציה ש:

- **מקבלת** פונקציה אחרת כפרמטר, **או**
- **מחזירה** פונקציה.

כבר השתמשתם באחת: `sorted(products, key=lambda p: p[1])` — `sorted` מקבלת פונקציה בפרמטר `key`.

{{< box "analogy" "אנלוגיה: מכונת כביסה עם תוכניות" >}}
מכונת כביסה יודעת "לעבור על כל הבגדים". **מה** לעשות לכל בגד — את זה בוחרים בתוכנית: כביסה עדינה, רגילה או ייבוש. לא קונים מכונה חדשה לכל תוכנית — מעבירים לה את התוכנית. Higher-Order Function היא המכונה, והפונקציה שמעבירים היא התוכנית.
{{< /box >}}

## תחביר

```python
def apply_to_all(numbers, action):     # action is a function
    result = []
    for n in numbers:
        result.append(action(n))       # call it for each item
    return result


apply_to_all([1, 2, 3], double)        # pass it: no parentheses
list(map(double, [1, 2, 3]))           # built-in: map
list(filter(is_even, [1, 2, 3, 4]))    # built-in: filter
sorted(words, key=len)                 # key: sort by the result of len
```

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

`week-2/apply_to_all.py`:

```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

**פלט:**

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

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

| שורה | הסבר |
|---|---|
| `def apply_to_all(numbers, action):` | `action` הוא פרמטר רגיל — רק שמצפים שיהיה בו **פונקציה**. |
| `result.append(action(n))` | קוראים לפונקציה שקיבלנו, על כל איבר. |
| `apply_to_all([1, 2, 3], double)` | מעבירים את `double` **בלי סוגריים** — את הפונקציה, לא את התוצאה שלה. |
| `lambda n: n * n` | פונקציה קצרה בלי שם, שנוצרת במקום. |
| `round` | גם פונקציה מובנית אפשר להעביר. |

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

**`map`, `filter` ו-`key`.** `week-2/hof_builtins.py`:

```python
prices = [100, 250, 40, 80]

with_fee = list(map(lambda p: p + 10, prices))
cheap = list(filter(lambda p: p < 100, prices))
print(with_fee)
print(cheap)

words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len))
print(max(words, key=len))

students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
print(max(students, key=lambda s: s[1]))
```

**פלט:**

```text
[110, 260, 50, 90]
[40, 80]
['fig', 'kiwi', 'banana', 'cherry']
banana
('Dana', 92)
```

- **`map(func, items)`** — מפעילה את `func` על **כל** איבר. `list(...)` הופך את התוצאה לרשימה.
- **`filter(func, items)`** — שומרת רק את האיברים ש-`func` מחזירה עליהם `True`.
- **`key=len`** — `sorted` ו-`max` משווים לפי **התוצאה** של `len` על כל מילה, ולא לפי המילה עצמה.

**פונקציה שמחזירה פונקציה.** `week-2/make_multiplier.py`:

```python
def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply


double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))
```

**פלט:**

```text
10
15
```

`make_multiplier` היא "מפעל" שמייצר פונקציות: כל קריאה מחזירה פונקציה **חדשה**, שזוכרת את ה-`factor` שלה. איך בדיוק היא זוכרת — נראה בשיעור הבא, על פונקציות מקוננות.

## טעות נפוצה

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

```python
apply_to_all([1, 2], double())
```

```text
TypeError: double() missing 1 required positional argument: 'n'
```

**למה?** `double()` עם סוגריים **קורא** לפונקציה מיד — בלי פרמטר — ולכן היא נכשלת עוד לפני ש-`apply_to_all` מתחילה. מעבירים את הפונקציה **בלי סוגריים**: `apply_to_all([1, 2], double)`. הפונקציה שמקבלת אותה היא זו שתקרא לה.

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

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

### תרגיל 1: פונקציה כפרמטר

כתבו פונקציה `apply_to_all(numbers, action)` שמפעילה את `action` על כל מספר ומחזירה רשימה חדשה. השתמשו בה עם פונקציה רגילה `double`, עם `lambda` שמעלה בריבוע, ועם `round`.

```python
print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

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

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

{{< solution >}}
```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
```

```text
[2, 4, 6]
[1, 4, 9]
[2, 2]
```

מעבירים את הפונקציה **בלי סוגריים** (`double`, `round`). `apply_to_all` היא זו שקוראת לה.
{{< /solution >}}

### תרגיל 2: map ו-filter

```python
prices = [20, 75, 120, 45, 90]
```

1. השאירו רק מחירים מעל 50, בעזרת `filter`.
2. הוסיפו 10 לכל מחיר שנשאר, בעזרת `map`.

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

```text
[75, 120, 90]
[85, 130, 100]
```

{{< solution >}}
```python
prices = [20, 75, 120, 45, 90]

expensive = list(filter(lambda p: p > 50, prices))
raised = list(map(lambda p: p + 10, expensive))
print(expensive)
print(raised)
```

```text
[75, 120, 90]
[85, 130, 100]
```

`filter` שומרת רק את מה שה-lambda מחזירה עליו `True`. `map` מפעילה את ה-lambda על כל איבר. `list(...)` הופך את התוצאה לרשימה.
{{< /solution >}}

### תרגיל 3: key — לפי מה למיין

```python
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
```

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

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

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

{{< solution >}}
```python
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]

print(sorted(words, key=len))
print(max(words, key=len))

for name, grade in sorted(students, key=lambda s: s[1], reverse=True):
    print(name)
```

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

`key=len` — משווים לפי **התוצאה** של `len` על כל מילה, לא לפי המילה עצמה.
{{< /solution >}}

### תרגיל 4: פונקציה שמחזירה פונקציה

כתבו פונקציה `make_discount(percent)` שמחזירה פונקציה, שמקבלת מחיר ומחזירה אותו אחרי ההנחה.

```python
ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
```

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

```text
180.0
100.0
```

{{< solution >}}
```python
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply


ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
```

```text
180.0
100.0
```

`make_discount` היא "מפעל" — כל קריאה מייצרת פונקציה חדשה שזוכרת את ה-`percent` שלה.
{{< /solution >}}

### תרגיל 5: לקרוא במקום להעביר

```python
def double(n):
    return n * 2

print(apply_to_all([1, 2], double()))
```

1. איזו שגיאה תתקבל, ולמה?
2. מה התיקון?

{{< solution >}}
1. `TypeError: double() missing 1 required positional argument: 'n'`. הסוגריים ב-`double()` **קוראים** לפונקציה מיד — בלי פרמטר — עוד לפני ש-`apply_to_all` מתחילה.
2. מעבירים את הפונקציה עצמה, **בלי סוגריים**:

```python
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2], double))
```

```text
[2, 4]
```
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Higher-Order Function** = פונקציה שמקבלת פונקציה או מחזירה פונקציה. זה אפשרי כי פונקציה היא אובייקט.
- מעבירים פונקציה **בלי סוגריים** — הפונקציה שמקבלת אותה היא זו שקוראת לה.
- **`map`** — פעולה על כל איבר. **`filter`** — שומרת את מה שעומד בתנאי.
- **`key=`** ב-`sorted`, `max`, `min` — קובע **לפי מה** משווים.
- פונקציה יכולה **להחזיר** פונקציה — "מפעל" שמייצר פונקציות מותאמות.

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

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

- [Functional Programming HOWTO — Built-in functions](https://docs.python.org/3/howto/functional.html#built-in-functions) — map, filter ו-sorted עם פונקציה
- [sorted() — key](https://docs.python.org/3/builtins/functions.html#sorted) — פונקציה כפרמטר key

<!-- docs:end -->
