• בלוג
  • ארבעה עקרונות של תכנות פונקציונאלי שתוכלו למצוא ב Python

ארבעה עקרונות של תכנות פונקציונאלי שתוכלו למצוא ב Python

23/03/2020

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

אגב- ביום חמישי הקרוב אני ודני נעביר וובינר על תכנות פונקציונאלי עם Clojure. אם בא לכם לשמוע יותר על תכנות פונקציונאלי ממש שווה לבוא. פרטים בקישור https://www.tocode.co.il/workshops/95.

1. פונקציות פונקציות פונקציות

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

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

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

"""
First-class function
The language supports:

    1. passing functions as arguments to other functions
    2. returning them as the values from other functions
    3. assigning them to variables or storing them in data structures
"""

from math import sqrt, factorial
from functools import partial
from operator import add

# 1. Passing functions as arguments to other functions
print(list(map(factorial, range(10))))

# 2. Returning functions from other functions
plus_one = partial(add, 1)
print(plus_one(10))

# 3. Storing functions in variables and Data Structures
twice = lambda x: x * 2
print(twice(10))

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

x = twice

לבין:

x = twice(10)

במקרה הראשון נשמור ב x את הפונקציה עצמה, ובמקרה השני את תוצאת הפעלת הפונקציה על המספר 10.

2. פונקציות טהורות

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

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

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

"""
Pure functions (or expressions) have no side effects (memory or I/O)
"""

def twice(x):
    return x * 2

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

3. מידע קבוע

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

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

# Mutable Code
x = [10, 20, 30, 40]
x.append(50)

# Immutable Code
x = [10, 20, 30, 40]
x = x + [50]

או ערבוב אלמנטים ברשימה:

import random

# Mutable Code
x = [10, 20, 30, 40]
random.shuffle(x)
print(x)

# Immutable Code
x = [10, 20, 30, 40]
print(random.sample(x, k=len(x)))

ואפילו בעבודה עם numpy אנחנו יכולים לבחור לשנות את המערך או לייצר מערך חדש:

import numpy as np

# Mutable Code
arr = np.arange(100, dtype=float).reshape(10, 10)
arr[arr % 2 == 0] /= 2
print(arr)

# Immutable Code
arr = np.arange(100).reshape(10, 10)
brr = np.where(arr % 2 == 0, arr / 2, arr)
print(brr)

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

4. רקורסיה

העובדה שמידע לא משתנה מייצרת בעיה וגם הזדמנות בכל מה שנוגע ללולאות: אם המידע שלנו לא יכול להשתנות, איך בדיוק נגדיר משתנה רץ בלולאה? הרי אותו i שרץ מ-1 עד 10, בכל איטרציה של הלולאה משתנה ב-1.

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

# Iterative code
def find_available_filename_iter(base):
    i = 0
    while os.path.exists(f"{base}_{i}"):
        i += 1
    return f"{base}_{i}"

print(find_available_filename_iter('hello'))

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

# Recursive code
def find_available_filename_rec(base, i=0):
    name = f"{base}_{i}"
    if not os.path.exists(name):
        return name

    return find_available_filename_rec(base, i + 1)

print(find_available_filename_rec('hello'))

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

ביום חמישי בבוקר אני ודני נדבר על כל העקרונות האלה ואיך הם מתבטאים בשפת Clojure, שהיא שפת תכנות פונקציונאלית. אם אתם רוצים ללמוד יותר על תכנות פונקציונאלי זו תהיה הזדמנות מצוינת. מוזמנים להצטרף בחינם בקישור: https://www.tocode.co.il/workshops/95.