לאחר זמן רב של למידה ועבודה עם תכנות מונחה עצמים, לקחתי צעד אחורה לחשוב על מורכבות המערכת.
"Complexity is anything that makes software hard to understand or to modify.
" - ג'ון אוטרהוט
עשיתי מחקר, מצאתי מושגי תכנות פונקציונליים כמו אי-משתנות ותפקוד טהור. מושגים אלה הם יתרונות גדולים לבניית פונקציות ללא תופעות לוואי, כך שקל יותר לשמור על מערכות - עם יתרונות אחרים.
בפוסט זה אספר לכם יותר על תכנות פונקציונאלי, וכמה מושגים חשובים, עם הרבה דוגמאות קוד.
מאמר זה משתמש ב- Clojure כדוגמה לשפת תכנות להסבר תכנות פונקציונלי. אם אינך מרגיש בנוח עם שפה מסוג LISP, פרסמתי גם את אותו פוסט ב- JavaScript. התבונן: עקרונות תכנות פונקציונליים ב- Javascript
מהי תכנות פונקציונלי?
תכנות פונקציונלי הוא פרדיגמת תכנות - סגנון של בניית מבנה ואלמנטים של תוכניות מחשב - המתייחס לחישוב כהערכה של פונקציות מתמטיות ונמנע משינוי מצב ונתונים משתנים - ויקיפדיהפונקציות טהורות

הרעיון הבסיסי הראשון שאנו לומדים כשאנחנו רוצים להבין תכנות פונקציונלי הוא פונקציות טהורות . אבל מה זה באמת אומר? מה הופך פונקציה לטהורה?
אז איך נדע אם פונקציה היא pure
או לא? הנה הגדרה קפדנית מאוד של טוהר:
- היא מחזירה את אותה תוצאה אם ניתנים אותם טיעונים (היא מכונה גם
deterministic
) - זה לא גורם לתופעות לוואי נצפות
היא מחזירה את אותה תוצאה אם ניתנים אותם טיעונים
תאר לעצמך שאנחנו רוצים ליישם פונקציה המחשבת את שטח המעגל. פונקציה טמאה תקבל radius
כפרמטר, ואז תחשב radius * radius * PI
. ב Clojure, המפעיל מגיע ראשון, כך radius * radius * PI
הופך להיות (* radius radius PI)
:
מדוע זו פונקציה טמאה? פשוט משום שהוא משתמש באובייקט גלובלי שלא הועבר כפרמטר לפונקציה.
עכשיו דמיין כמה מתמטיקאים טוענים PI
שהערך הוא למעשה 42
ומשנים את הערך של האובייקט הגלובלי.
הפונקציה הטמאה שלנו תביא כעת ל 10 * 10 * 42
= 4200
. לאותו פרמטר ( radius = 10
), יש לנו תוצאה אחרת. בואו נתקן את זה!
TA-DA?! כעת נעביר את I
ערך ה- P כפרמטר לפונקציה. אז עכשיו אנחנו רק ניגשים לפרמטרים המועברים לפונקציה. אין הxternal object.
- עבור הפרמטרים
radius = 10
&PI = 3.14
, תמיד תהיה לנו אותה התוצאה:314.0
- עבור הפרמטרים
radius = 10
&PI = 42
, תמיד תהיה לנו אותה התוצאה:4200
קריאת קבצים
אם הפונקציה שלנו קוראת קבצים חיצוניים, זה לא פונקציה טהורה - תוכן הקובץ יכול להשתנות.
יצירת מספרים אקראיים
כל פונקציה המסתמכת על מחולל מספרים אקראיים אינה יכולה להיות טהורה.
זה לא גורם לתופעות לוואי נצפות
דוגמאות לתופעות לוואי נצפות כוללות שינוי אובייקט גלובלי או פרמטר המועבר על ידי הפניה.
כעת אנו רוצים ליישם פונקציה כדי לקבל ערך שלם ולהחזיר את הערך המוגדל ב -1.
יש לנו את counter
הערך. הפונקציה הטמאה שלנו מקבלת את הערך ומקצה מחדש את הדלפק עם הערך המוגדל ב -1.
התבוננות : מוטציה מתייאשת מתכנות פונקציונלי.
אנו משנים את האובייקט הגלובלי. אבל איך נכין את זה pure
? פשוט החזירו את הערך שהוגדל ב- 1. פשוט ככה.
ראה שהפונקציה הטהורה שלנו increase-counter
מחזירה 2, אך counter
הערך עדיין זהה. הפונקציה מחזירה את הערך המצטבר מבלי לשנות את ערך המשתנה.
אם אנו מקפידים על שני כללים פשוטים אלה, קל יותר להבין את התוכניות שלנו. כעת כל פונקציה מבודדת ואינה מסוגלת להשפיע על חלקים אחרים במערכת שלנו.
פונקציות טהורות יציבות, עקביות וצפויות. בהינתן אותם פרמטרים, פונקציות טהורות תמיד יחזירו את אותה התוצאה. אנחנו לא צריכים לחשוב על מצבים שלאותו פרמטר יש תוצאות שונות - כי זה לעולם לא יקרה.
יתרונות פונקציות טהורות
הקוד בהחלט קל יותר לבדיקה. אנחנו לא צריכים ללעוג לשום דבר. כדי שנוכל לבדוק יחידות פונקציות טהורות בהקשרים שונים:
- בהינתן פרמטר
A
→ צפה שהפונקציה תחזיר ערךB
- בהינתן פרמטר
C
→ צפה שהפונקציה תחזיר ערךD
דוגמה פשוטה תהיה פונקציה לקבל אוסף מספרים ולצפות שהוא יגדיל כל רכיב באוסף זה.
אנו מקבלים את numbers
האוסף, משתמשים map
עם inc
הפונקציה כדי להגדיל כל מספר ולהחזיר רשימה חדשה של מספרים מצטברים.
עבור input
[1 2 3 4 5]
, הצפוי output
יהיה [2 3 4 5 6]
.
חוסר שינוי
לא משתנה לאורך זמן או שלא ניתן לשנותו.
כאשר הנתונים אינם ניתנים לשינוי, מצבם אינו יכול להשתנותאחרי שהוא נוצר. אם אתה רוצה לשנות אובייקט בלתי משתנה, אתה לא יכול. במקום זאת, אתה יוצר אובייקט חדש עם הערך החדש.
ב- Javascript אנו משתמשים בדרך כלל for
בלולאה. for
להצהרה הבאה זו יש כמה משתנים משתנים.
עבור כל איטרציה, אנו משנים i
את sumOfValue
המדינה ואת המדינה . אך כיצד נתמודד עם שינויים באיטרציה? רקורסיה! חזרה לקלוז'ור!
אז הנה לנו sum
הפונקציה שמקבלת וקטור של ערכים מספריים. recur
קופץ בחזרה loop
עד שנקבל את הווקטור הריק (רקורסיה שלנו base case
). עבור כל "איטרציה" נוסיף את הערך total
לצבר.
עם רקורסיה, אנו שומרים על המשתנים שלנובלתי ניתן לשינוי.
תצפית : כן! אנו יכולים להשתמש reduce
בכדי ליישם פונקציה זו. נראה זאת Higher Order Functions
בנושא.
מקובל מאוד לבנות את המצב הסופי של אובייקט. דמיין שיש לנו מחרוזת, ואנחנו רוצים להפוך מחרוזת זו ל- url slug
.
ב- OOP ברובי היינו יוצרים כיתה, נניח UrlSlugify
,. ולמחלקה זו תהיה slugify!
שיטה להפוך את קלט המחרוזת ל- a url slug
.
יפה! זה מיושם! כאן יש לנו תכנות הכרחי האומרות בדיוק מה אנחנו רוצים לעשות בכל slugify
תהליך - תחילה אותיות קטנות, ואז הסר רווחים לבנים חסרי תועלת ולבסוף, החלף את הרווחים הלבנים הנותרים במקפים.
אך אנו משתנים את מצב הקלט בתהליך זה.
אנו יכולים להתמודד עם מוטציה זו על ידי ביצוע הרכב פונקציות, או שרשור פונקציות. במילים אחרות, התוצאה של פונקציה תשמש כקלט לפונקציה הבאה, מבלי לשנות את מחרוזת הקלט המקורית.
כאן יש לנו:
trim
: מסיר מרחב לבן משני קצות המחרוזתlower-case
: ממיר את המחרוזת לכל אותיות קטנותreplace
: מחליף את כל מקרי ההתאמה עם החלפה במחרוזת נתונה
אנו משלבים את כל שלוש הפונקציות ונוכל "slugify"
למחרוזת שלנו.
אם כבר מדברים על שילוב פונקציות , אנחנו יכולים להשתמש comp
בפונקציה כדי לחבר את כל שלוש הפונקציות. בואו נסתכל:
שקיפות התייחסותית

בואו ליישם square function
:
לפונקציה (טהורה) זו תמיד תהיה אותה פלט, בהינתן אותה קלט.
העברת "2" כפרמטר של square function
הרצון מחזירה תמיד 4. אז עכשיו אנחנו יכולים להחליף את זה (square 2)
עם 4. זהו! התפקיד שלנו הוא referentially transparent
.
בעיקרון, אם פונקציה מניבה בעקביות את אותה תוצאה עבור אותה קלט, היא שקופה בהתייחסות.
פונקציות טהורות + נתונים בלתי ניתנים לשינוי = שקיפות התייחסותית
עם המושג הזה, דבר מגניב שאנחנו יכולים לעשות הוא להזכיר את הפונקציה. דמיין שיש לנו את הפונקציה הזו:
(+ 5 8)
שווה 13
. פונקציה זו תמיד תביא ל 13
. כדי שנוכל לעשות זאת:
והביטוי הזה תמיד יביא ל 16
. אנו יכולים להחליף את הביטוי כולו בקבוע מספרי ולהזכיר אותו.
מתפקד כישויות מהשורה הראשונה

הרעיון של פונקציות כישויות ממדרגה ראשונה היא כי פונקציות גם כאל ערכים ועל משמש נתונים.
ב- Clojure מקובל להשתמש defn
בהגדרת פונקציות, אך זהו סוכר תחבירי בלבד עבור (def foo (fn ...))
. fn
מחזירה את הפונקציה עצמה. defn
מחזיר a var
אשר מצביע על אובייקט פונקציה.
פונקציות כישויות מהשורה הראשונה יכולות:
- מתייחסים אליו מקבועים ומשתנים
- העבירו אותו כפרמטר לפונקציות אחרות
- להחזיר אותו כתוצאה מפונקציות אחרות
הרעיון הוא להתייחס לפונקציות כאל ערכים ולהעביר פונקציות כמו נתונים. בדרך זו אנו יכולים לשלב פונקציות שונות כדי ליצור פונקציות חדשות עם התנהגות חדשה.
דמיין שיש לנו פונקציה שמסכמת שני ערכים ואז מכפילה את הערך. משהו כזה:
עכשיו פונקציה שמחסרת ערכים והחזרת הכפיל:
לפונקציות אלו יש הגיון דומה, אך ההבדל הוא פונקציות האופרטורים. אם נוכל להתייחס לפונקציות כאל ערכים ולהעביר אותם כארגומנטים, נוכל לבנות פונקציה המקבלת את פונקציית האופרטור ולהשתמש בה בתוך הפונקציה שלנו. בואו נבנה את זה!
בוצע! עכשיו יש לנו f
ויכוח, ומשתמשים בו לעיבוד a
ו- b
. עברנו את פונקציות +
ו- -
כדי להלחין עם double-operator
הפונקציה וליצור התנהגות חדשה.
פונקציות מסדר גבוה יותר
כאשר אנו מדברים על פונקציות מסדר גבוה יותר, אנו מתכוונים לפונקציה שאחת מהן:
- לוקח פונקציה אחת או יותר כטיעונים, או
- מחזיר פונקציה כתוצאה שלה
double-operator
הפונקציה שהטמענו לעיל הנה פונקציה מסדר גבוה כי זה לוקח תפקיד מפעיל כטיעון ומשתמש בו.
אתה כנראה כבר שמעת כבר על filter
, map
, ו reduce
. בואו נסתכל על אלה.
לְסַנֵן
בהינתן אוסף, אנו רוצים לסנן לפי תכונה. פונקציית המסנן מצפה ש- true
או false
ערך יקבע אם הרכיב צריך או לא צריך להיכלל באוסף התוצאות. בעיקרון, אם הביטוי להתקשרות חוזרת הוא true
, פונקציית המסנן תכלול את האלמנט באוסף התוצאות. אחרת זה לא יקרה.
דוגמה פשוטה היא כשיש לנו אוסף של מספרים שלמים ואנחנו רוצים רק את המספרים הזוגיים.
גישה חובה
דרך חובה לעשות זאת באמצעות Javascript היא:
- ליצור וקטור ריק
evenNumbers
- לחזור על
numbers
הווקטור - דחף את המספרים הזוגיים
evenNumbers
לווקטור
אנו יכולים להשתמש filter
בפונקציה של הסדר הגבוה יותר כדי לקבל את even?
הפונקציה ולהחזיר רשימה של מספרים זוגיים:
בעיה מעניינת אחת שפתרתי בנתיב ה- FP של Hacker Rank הייתה בעיית ה- Filter Array . הרעיון הבעייתי הוא לסנן מערך נתון של מספרים שלמים ולהפיק רק את הערכים שהם פחות מערך מוגדר X
.
פיתרון Javascript הכרחי לבעיה זו הוא משהו כמו:
אנו אומרים בדיוק מה הפונקציה שלנו צריכה לעשות - לחזור על האוסף, להשוות את הפריט הנוכחי עם האוסף x
, ולדחוף את האלמנט הזה resultArray
אם הוא עובר את התנאי.
גישה הצהרתית
אך אנו רוצים דרך הצהרתית יותר לפתור בעיה זו, ולהשתמש גם filter
בפונקציה של הסדר הגבוה יותר.
פתרון הצהרת הצהרה יהיה בערך כך:
התחביר הזה נראה קצת מוזר מלכתחילה, אבל קל להבין אותו.
#(> x
%) היא רק פונקציה אנונימית שמקבלת e
את x ומשווה אותה עם כל אלמנט בקולקציה n
. % מייצג את הפרמטר של הפונקציה האנונימית - במקרה זה האלמנט הנוכחי בתוך t he fil
ter.
אנחנו יכולים לעשות זאת גם עם מפות. דמיין שיש לנו מפה של אנשים עם שלהם name
ו age
. ואנחנו רוצים לסנן רק אנשים מעל גיל מסוים שצוין, בדוגמה זו אנשים מעל גיל 21.
סיכום קוד:
- יש לנו רשימה של אנשים (עם
name
וage
). - יש לנו את הפונקציה האנונימית
#(< 21 (:age
%)). זכור ש- th
e% מייצג את האלמנט הנוכחי מהאוסף? ובכן, מרכיב האוסף הוא מפת אנשים. אם אנוdo (:age {:name "TK" :age 2
6}), הוא מחזיר את ערך הגילe,
26 במקרה זה. - אנו מסננים את כל האנשים על סמך פונקציה אנונימית זו.
מַפָּה
הרעיון של מפה הוא להפוך אוסף.
map
השיטה הופכת אוסף ידי החלת פונקציה לכל מרכיביו ובניית אוסף חדש הערכים חזרו.
בואו נקבל את אותו people
אוסף לעיל. איננו רוצים לסנן לפי "מעל גיל" כעת. אנחנו רק רוצים רשימה של מיתרים, משהו כמו TK is 26 years old
. אז המחרוזת הסופית עלולה להיות :name is :age years old
שם :name
ועל :age
כן סממנים מכל אלמנט people
האוסף.
באופן Javascript הכרחי, זה יהיה:
באופן קלוזורי הצהרתי, זה יהיה:
כל הרעיון הוא להפוך אוסף נתון לאוסף חדש.
בעיה מעניינת נוספת של דירוג האקרים הייתה בעיית רשימת העדכונים . אנחנו רק רוצים לעדכן את הערכים של אוסף נתון בערכים המוחלטים שלהם.
לדוגמא, הקלט [1 2 3 -4 5]
צריך שהפלט יהיה [1 2 3 4 5]
. הערך המוחלט של -4
הוא 4
.
פתרון פשוט יהיה עדכון במקום לכל ערך אוסף.
אנו משתמשים Math.abs
בפונקציה כדי להפוך את הערך לערכו המוחלט ולעשות את העדכון במקום.
זו לא דרך פונקציונלית ליישם פתרון זה.
ראשית, למדנו על אי-שינוי. אנו יודעים כמה חשוב אי-משתנות בכדי להפוך את הפונקציות שלנו לעקביות וצפויות יותר. הרעיון הוא לבנות אוסף חדש עם כל הערכים המוחלטים.
שנית, מדוע שלא נשתמש map
כאן בכדי "להפוך" את כל הנתונים?
הרעיון הראשון שלי היה לבנות to-absolute
פונקציה שתתמודד עם ערך אחד בלבד.
אם הוא שלילי, אנו רוצים להפוך אותו לערך חיובי (הערך המוחלט). אחרת, אנחנו לא צריכים לשנות את זה.
כעת, כשאנחנו יודעים לעשות absolute
עבור ערך אחד, נוכל להשתמש בפונקציה זו כדי לעבור כוויכוח map
לפונקציה. האם אתה זוכר ש- higher order function
יכול לקבל פונקציה כוויכוח ולהשתמש בה? כן, מפה יכולה לעשות את זה!
וואו. כל כך יפה! ?
לְהַפחִית
הרעיון של צמצום הוא לקבל פונקציה ואוסף, ולהחזיר ערך שנוצר על ידי שילוב הפריטים.
דוגמה נפוצה שאנשים מדברים עליה היא לקבל את הסכום הכולל של הזמנה. דמיין שאתה היית באתר קניות. הוספה Product 1
, Product 2
, Product 3
, ו Product 4
לעגלת הקניות שלך (לפי סדר). כעת אנו רוצים לחשב את הסכום הכולל של עגלת הקניות.
באופן הכרחי, היינו חוזרים על רשימת ההזמנות ומסכמים כל סכום מוצר לסכום הכולל.
באמצעות reduce
, אנו יכולים לבנות פונקציה שתטפל amount sum
בה ולהעביר אותה כטיעון reduce
לפונקציה.
הנה לנו shopping-cart
, הפונקציה sum-amount
שמקבלת את הזרם total-amount
, current-product
והאובייקט sum
אליהם.
get-total-amount
הפונקציה משמשת ידי שימוש ו החל .reduce
shopping-cart
sum-amount
0
דרך נוספת להשיג את הסכום הכולל היא להלחין map
ו reduce
. למה אני מתכוון בזה? נוכל להשתמש בה map
כדי להפוך את shopping-cart
אוסף amount
הערכים ואז פשוט להשתמש reduce
בפונקציה עם +
הפונקציה.
get-amount
מקבל את אובייקט המוצר ומחזיר רק את amount
הערך. אז מה שיש לנו כאן הוא [10 30 20 60]
. ואז reduce
משלב את כל הפריטים על ידי הוספת. יפה!
בדקנו כיצד כל פונקציה מסדר גבוה עובדת. אני רוצה להראות לך דוגמה כיצד נוכל לחבר את כל שלוש הפונקציות בדוגמה פשוטה.
אם מדברים על shopping cart
, דמיין שיש לנו את רשימת המוצרים בהזמנה שלנו:
אנו רוצים את הסכום הכולל של כל הספרים בסל הקניות שלנו. פשוט כמו זה. האלגוריתם?
- סנן לפי סוג ספר
- להפוך את עגלת הקניות לאוסף של סכומים באמצעות מפה
- שלב את כל הפריטים על ידי הוספתם עם להפחית
בוצע! ?
אֶמְצָעִי
ארגנתי כמה משאבים שקראתי ולמדתי. אני משתף את אלה שנראו לי מעניינים באמת. לקבלת משאבים נוספים, בקר במאגר Github של תכנות פונקציונלי שלי .
- משאבים ספציפיים לאודם
- משאבים ספציפיים ל- Javascript
- משאבים ספציפיים לסיומת
מבוא
- לימוד FP ב- JS
- מבוא לעשות FP עם Python
- סקירה כללית של FP
- מבוא מהיר ל- JS פונקציונלי
- מה זה FP?
- ז'רגון תכנות פונקציונלי
פונקציות טהורות
- מהי פונקציה טהורה?
- תכנות פונקציונלי טהור 1
- תכנות פונקציונלי טהור 2
נתונים בלתי ניתנים לשינוי
- DS בלתי ניתן לשינוי לתכנות פונקציונלי
- מדוע מצב משתנה משותף הוא שורש כל רע
- שיתוף מבני בקלוז'ר: חלק 1
- שיתוף מבני בקלוז'ר: חלק 2
- שיתוף מבני בקלוז'ר: חלק 3
- שיתוף מבני בקלוז'ר: חלק אחרון
פונקציות מסדר גבוה יותר
- JS אלוקנטי: פונקציות סדר גבוה יותר
- פונקציית כיף מהנה סינון
- פונקציית כיף מהנה מפה
- פונקציה מהנה מהנה בסיסית להפחית
- פונקציית כיף מהנה מתקדם להפחית
- פונקציות סדר גבוה יותר
- מסנן פונקציה טהור
- מפה פונקציונאלית בלבד
- צמצום פונקציונלי בלבד
תכנות הצהרתי
- תכנות הצהרתי לעומת הכרחי
זהו זה!
היי אנשים, אני מקווה שהיה לך כיף לקרוא את הפוסט הזה, ואני מקווה שלמדת הרבה כאן! זה היה הניסיון שלי לשתף את מה שאני לומד.
הנה המאגר עם כל הקודים ממאמר זה.
בוא ללמוד איתי. אני משתף משאבים ואת הקוד שלי במאגר זה של תכנות פונקציונליות למידה .
אני מקווה שראית כאן משהו מועיל עבורך. ונתראה בפעם הבאה! :)
הטוויטר שלי וגית'וב. ☺
TK.