-->

יום חמישי, 11 בספטמבר 2014

שפת Ruby: מה זה השטויות האלה?!

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

---

רובי היא שפת תכנות שנוצרה ע"י בחור יפני (עובדה שתתחבר לנו בהמשך) בשם יוקיהירו מאטצ'מוטו (שם חיבה: מאצ'). מאצ' לא מצא שפת תכנות "לטעמו" - ולכן יצר אחת.

רובי דומה בהיבטים רבים לשפת Python, מכיוון שהיא:
  • שפת high level, כמעט scripting language
  • שפת general purpose
  • שפת Dynamic typing
  • התחביר שלה דומה

החוויה הראשונה בהתקלות עם רובי, שלי לפחות, היא שהיא מעט מוזרה - דומה ל Perl ("יש אצלנו יותר מדרך אחת לעשות כל דבר"). בתחביר יש הרבה קיצורים, רחוקים מהשפה האנגלית ולא כ"כ אינטואטיביים למי שלא רגיל אליהם , למשל:

%w(abc def ghi)

(ללא מרכאות) הוא קיצור מקובל ל

["abc", "def", "ghi"]

דוגמה נוספת היא הבחירה במונחים:
  • string.downcase במקום string.lowercase 
  • string.gsub במקום string.replace (יש מתודה בשם replace, אך היא עושה משהו אחר)
  • to_s במקום toString, כאשר יש גם מתודה בשם to_str - אבל לא נכון להשתמש בה, אם המחלקה היא לא סוג של string בפני עצמו.
דבר אחרון שעשוי להיות דיי מוזר הוא השימוש הרב בסימני פיסוק: סימני "?" או "!" משמשים כקונבנציה על שמות של מתודות/פונקציות. יש שימוש ב <=, גם ב @, או $ בשמות של משתנים, ויש גם שימוש ב :|, &, וסימנים אחרים.

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

"@הצלחנו: ליצור (!), קוד :שהוא |ממש| כמו => פסקה? בשפה! האנגלית!" - כן, בטח!


---

רובי, בניגוד ל Python, היא שפת OO "אמיתית" עם classes כפי שאנו מכירים אותם בג'אווה / #C.
רובי הושפעה גם מ SmallTalk, משפת LISP ומ Perl. כן,... Perl השפה שעליה נאמר שהיא שפה לכתיבה-בלבד.

לזכותה של רובי יאמר שהיא אחת השפות שצמחו במהירות הגדולות ביותר, ויש לה קהילה גדולה, איכותית, ופעילה. חוזק גדול מאוד של הקהילה הוא תשתית ה FullStack Web Development בשם Ruby on Rails (בקיצור: RoR), אבל יש לה גם כח ונוכחות מעבר לכך. ניתן לציין את RSpec, Chef, Puppet, Vagrant, Cucumber - שהשפעתם על תעשיית התוכנה היא רבה.

מתכנת ששולט ב Ruby יכול לכתוב בה בקצב מהיר מאוד, יותר מאשר ב Python - כך טוענים.

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








התקנה


התקנת רובי פשוטה הרבה יותר על לינוקס.
משתמשי חלונות: כדאי להשתמש בלינק הזה, ולהשתמש בגרסת 32 bit, כי גרסת ה 64 היא בעייתית.
כמו כן כדאי להשתמש ברובי 2. רובי לא מזמן עשתה את המעבר מ 1.9 לגרסה 2. למרות החלפת הספרה לא היה פה שינוי דרמטי, אלא רק שינוי הדרגתי, קטן כנראה מהמעבר בין גרסה 1.8 לגרסה 1.9.

אם אתם רוצים לפתח (דאא!) אז תזדקקו דיי מהר גם ל Devkit. קובץ ההתקנה הוא בסך הכל ZIP שנפתח לאיזו תיקיה. בתיקיה זו (אני עובד בחלונות) הקלידו ב command line:

> ruby dk.rb init
> ruby dk.rb install

ה IDE החביב עלי הוא (כמובן) RubyMine.

התקנת רובי תוסיף לכם אפליקציה בשם irb (שהוא ה Interactive Ruby Shell, להפעלה מה commandline / shell) שבו תוכלו לבדוק במחזור feedback של שניות פקודות שלא ברור לכם בדיוק כיצד הן מתנהגות. אפשר להתקין את wirble שמאפשר autocomplete ו syntax highlighting ב irb.

כחלק מהתקנת רובי מגיעה גם אפליקציה בשם gem, שהוא ה Package manager של רובי (ממש כמו אלו של לינוקס, ממנה הושפעו הכלים של רובי לא-מעט). בעזרת gem מתקינים חבילות רובי. החבילה הראשונה שאני ממליץ עליה היא rspec ספריית בדיקות היחידה / BDD (שבדמותה נוצרה Jasmine של ג'אווהסקריפט - למי שמכיר). פשוט הקלידו בשורת הפקודה:

> gem install rspec







בואו נצא ידי חובה וניצור hello world קטן:


ברובי יש 2 מתודות להדפסה:
print שהיא המקבילה ל System.out.print בג'אווה.
ו puts (קיצור של put string. כמה טוב שלא בחרו לקרוא לה disuninject_string) שהיא המקבילה של System.out.println בג'אווה, ובה נשתמש בד"כ.

שתי הדוגמאות למעלה תקינות לחלוטין בשפת רובי, אולם הדוגמה השנייה נחשבת לעדיפה.
מדוע?
  • ברובי שימוש בסוגריים להפעלת פונקציה הוא אופציונלי, אפשר פשוט לרשום בזה אחר זה את הפרמטרים, עם רווחים.
  • ספציפית: בפקודת puts לא מקובל להשתמש בסוגריים בכלל.
  • מחרוזות ברובי יכולות להיות עם גרש אחד או גרשיים, אם כי escape chars הם ישימים רק במחרוזת עם גרשיים. כלומר 'n\' שקולה ל "n\\" - כי היא מקבלת \ כתו רגיל.
  • הסיבה לכך: מחרוזת עם גרש מועתקת לזיכרון כפי שהיא - ללא פענוח, ומחרוזת עם גרשיים - עוברת פענוח.
  • הדרך המקובלת ברובי היא להשתמש כמה שאפשר במחרוזות עם גרש אחד, כך שהשימוש בגרשיים ירמז שיש במחרוזת escape chars (או טריקים אחרים)


טבילת אש ראשונה




  1. שמות מחלקות חייבים להתחיל באות גדולה.
    כאשר אנו משתמשים באות גדולה בתחילת שם משתנה - הוא הופך לקבוע. אפשר לומר שגם מחלקה היא סוג של קבוע, כי הרי המחלקה (לא המופעים שלה) - לא משתנה מרגע שהוגדרה.
    המחלקה שלנו יורשת מהמחלקה LivingCreature.
  2. ה Constructor הוא מתודה (מתודות מוגרות ע"י def, קיצור של define) בשם initialize. אין צורך להגדיר טיפוסים (int, string וכו') של משתנים ברובי - זו שפה דינמית.
  3. משתני מופע (נקראים instance variables, מה שנקרא בג'אווה "members") מוגדרים ע"י סימן @ בתחילת השם. שימו לב שאת משתני המחלקה מגדירים רק בתוך מתודות, ולא בתוך המחלקה - כפי שמקובל בג'אווה. עצם השימוש במשתנה עם שם שמתחיל ב@ - מגדיר אותו כ member של המחלקה.
  4. משתני מחלקה (המקבילים ל static members בג'אווה) מוגדרים ע"י 2 סימני @.
    שימוש במשתני מחלקה נחשב כפרקטיקה לא מומלצת ברובי, בדומה לג'אווה.
  5. שמות מתודות, משתנים וקבצים ברובי נעשים ע"י שימוש ב snake_case (בהקבלה ל camelCase שבשימוש בג'אווה).
    מקובל מאוד שמתודה או פונקציה ללא פרמטרים - לעולם לא תופיע עם סוגריים.
    משתמשים ב PascalCase (אות גדולה ראשונה) להגדרות שמות מחלקות ומודולים.
    עבור קבועים, מקובל מאוד להשתמש ב SCREAMING_SNAKE_CASE (בדומה לג'אווה).
  6. אוקיי... הנה קוד קצת מוזר. זהו התחביר (המוזר) ברובי למשפט if מקוצר. משפטי if רגילים דורשים שימוש ב end.
    התחביר של המשפט הוא: <עשה X> אם <תנאי>, במקרה שלנו: "עשה את הפרש הגילאים" אם "הגיל קטן מה lifespan הממוצע".
    אבל ביצוע חישוב וזריקה של התשובה לאוויר - היא "עשייה"?
    כן. מכיוון שהשורה האחרונה במתודה / פונקציה ברובי - היא ערך ההחזרה (אפשר להשתמש ב return כדי להחזיר ערך מאמצע הפונקציה).
    אין פונקציות מסוג void ברובי - תמיד פונקציה תחזיר ערך. אם לא הגדרנו אותו / אין ערך בשורה האחרונה - אזי יחזור nil.
  7. נעבור להרצה:
    אנו יוצרים מופע חדש של המחלקה: ה new נמצא בצד ההפוך לזה שאנו רגילים בגא'ווה, והוא מקבל את הארגומנטים עבור ה constructor.
    הפעלה של מתודות - מקובל מאוד לשרשר ברובי, והרבה פונקציות בספריות הסטנדרטיות של רובי מחזירות reference לעצמן לצורך כך.
    כפי שכבר ציינו, אין צורך בסוגריים בהפעלת time_to_live, כי היא לא מקבלת ארגומנטים.
הערך שנותר לבחור בן 21 לחיות (ע"פ התוכנה) - הוא שישים שנה.
כאשר שואלים את התוכנה על בחור בן 90, היא לא מבצעת כלום (כי התנאי במשפט ה if לא מתקיים) - ולכן חוזר nil.
כשנשלח nil ל puts - הוא מדפיס שורה ריקה.



שונות





רובי מספקת דרך מובנה לבצע formatting בסיסי של מחרוזות. ע"י שימוש ב {}# (בתוך מחרוזת עם גרשיים), רובי תשתול במחרוזת ערכים שזמינים ב scope. אהבתי!





בדומה לפייטון, ניתן לבצע ברובי השמה מרובה (a,b = c,d).

שפות high level כמו רובי מעודדות את המתכנתים להשתמש בחופשיות במחרוזות בכדי לתאר "מצבים חוזרים". בשפת C היינו משתמשים בצורך זה ב const int, בג'אווה היינו משתמשים ב enum. ל JVM יש גם מנגנון של String Pooling שמאפשר לייצר אותה מחרוזת כמה פעמים - אך לנהל רק עותק אחד בזיכרון. אני מניח שיכולת זו מבוססת על תהליך הקומפילציה של ג'אווה.

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

הערה: symbols הם לא ממש מחרוזות, ולא ניתן להפעיל עליהם מתודות של מחרוזות, אולם נשמר הקשר ש:

:hello.object_id == hello.object_id 

בעוד
"hello".object_id != "hello".object_id 


בדוגמה למעלה a ו b הן מחרוזות, בעוד c ו d הם symbols
בשורת ה puts השתמשתי בכפל עם מחרוזת, שזהו קיצור לפקודת join על מערך. כך אני גורם ל puts להדפיס שורה אחת עם פסיקים - ממש כמו שמופיע בהערה באותה השורה.

עוד דבר קטן שאנו רואים בדוגמה הוא את קונבנציית השמות למתודות של "!": בעוד המתודה chop מחזירה עותק של המחרוזת a שהוסר ממנה הסימן האחרון (אנו לא עושים כלום עם הערך שחזר בדוגמה), המתודה !chop מסירה מהמחרוזת b את הערך האחרון ומחזירה nil.

לא ניתן להשתמש ב"!" בכל מתודה שרוצים: מי שכתב את המחלקה string יצר שתי מתודות: chop ו !chop.




מערך ברובי, הוא המקבילה ל ArrayList בג'אווה. מבנה הנתונים הזה פשוט כ"כ שימושי! היישום מאוד דומה לפייטון, וניתן לגשת לערכים מהסוף בקלות בעזרת שימוש במספרים שליליים (למשל -1 מגיע לסוף הרשימה) - וזה מאוד נוח.




לולאות ברובי דומות מאוד לפייטון או ג'אווהסקריפט: פורמט for ... in.
בדומה לג'אווהסקריפט (שם לולאה עוברת גם על פרמטרים של אובייקט האב), יש בלולאת for רגילה - התנהגות שלרוב איננה רצויה: הערך שהוגדר (item במקרה שלנו) - ממשיך לחיות מעבר ל scope של המערך.

לפעמים זה שימושי, פעמים רבות אחרות זה מבלבל - ולכן כדאי לעבוד עם התחביר השני (שדומה מאוד ל each של jQuery). התחביר השני שקול לחלוטין, מלבד כך שהערך שהוגדר (item2 בדוגמה) חי רק ב scope של הלולאה.

השימוש של התחביר השני רחב יותר מאשר סתם לולאות והוא נקרא block, הוא בעצם סוג של inline function שמעובר למתודה (each או select).

הנה דוגמה של שימוש במתודה select של מערך (סוג של filter: ערך ההחזרה הוא רק משתנים עבורם היה ערך חיובי). התחביר ~= הוא קיצור להתאמה של מחרוזת (color) ל regular expression (שאותה כותבים בין קווים נטויים, במקרה שלנו הופעת האות e).
כפי שאתם זוכרים, השורה האחרונה בפונקציה - היא ערך ההחזרה.




עוד מבנה נתונים "טבעי" בשפת רבי הוא ה Hash (המקבילה ל Map או HashTable בג'אווה).
התחביר הוא key => value. כשנתקלתי לראשונה בקטע קוד שכזה - לא שיערתי שמדובר בעצם ב Map: החץ נראה "חשוב מדי" מכדי לשמש כסימן הפרדה פשוט.

מאז גרסה 1.9 רובי הגדירה תחביר מקוצר חדש ל Hash שהמפתחות שלו הם symbols (שזה סוג של best practice). הרי הוא לפניכם.

דבר קטן שמוצא-חן בעיני הוא שרובי מקבלת פסיק עודף לאחר האיבר האחרון במערך או ב Hash (דוגמה ראשונה) - מבלי להתלונן. חדל סדר תחזוקת פסיקים לאחר copy-paste. זה מאוד הגיוני עבורי: הרי הקומפיילר עובד בשבילנו, ולא להיפך.


כמה השלמות חשובות


self ברובי יכול להזכיר this בג'אווה, אבל ההתנהגות שלו שונה. אם הוגדר בתוך מתודה - הוא יצביע על המתודה, אם בתוך מחלקה ומחוץ למתודה - אזי על המחלקה, ואם מחוץ למחלקה - הוא יצביע על המרחב הגלובלי "main".

ברובי ניתן לארגן את המחלקות / קבועים / וכו' בתוך modules (דומה ל packages ב ++C או #C)
כדי לנווט בתוך ה module - אנו משתמשים בסימן ::


:: מאפשר גישה למה שנחשב "קבועים" ברובי: מודולים, מחלקות, ומשתנים קבועים (כולם מתחילים באות גדולה).


בלוקים הם "חתיכות של קוד" שאנו שולחים בפרמטר לפונקציה (שימושים נפוצים הם פונקציות כמו each, filter, וכו')
הנה התחביר של הבלוק:

מקור

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



סיכום


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

שפת רובי היא מורכבת ועשירה, והיכולת "לקרוא כל קוד ברובי" - דורשת כנראה למידה ארוכה בהרבה מפוסט זה (שלא לדבר על היכולת "לכתוב כל קוד ברובי"). לרובי יש ספריות ספציפיות שיש להכיר, טכניקות של metaprogramming - שנמצאות בשימוש לא מובטל, ועוד.

מפרשן (Interpreter) ברירת המחדל של רובי נקרא MRI (קיצור של Matz's Ruby Interpreter). הוא הסטנדרט, הוא עושה את העבודה - אבל נחשב אטי, אטי יותר מ Python - לדוגמה.

האם זה באמת נכון ומתי? קשה ממש לומר. יש הרבה מבחני ביצועים שמראים דברים שונים. לכל הפחות: זהו נושא בדיון.
עוד מגבלה של MRI היא שאין לו באמת multi-threading, ואין לו parallelism (קרי: שימוש ב GPU, SIMD, וכו'). אלטרנטיבה מקובלת לסביבה שכזו היא JRuby - המפרשן של רובי שרץ על JVM (מהיר יותר, multi-threaded וכו').

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


שיהיה בהצלחה!



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


----

קישורים:

Ruby tricks to make your code more fun and less readable - השלמה טובה לפוסט. חבל לי לשכתב את מה שנכתב כאן בצורה יפה.

מדריך Coding Style לרובי, שימושי ומלמד.

סשן ביוטיוב: Functional Development in Ruby


5 תגובות:

  1. פוסט מעולה, קלעת בול.
    תודה!

    השבמחק
  2. בכמה מילים - מה זה grails ולמה יש על החרטה הזה באזז?

    השבמחק
    תשובות
    1. אני לא חושב של grails יש כ"כ באז....

      Grails (במקור Groovy on Grails) הוא המקבילה של שפת Groovy / JVM - ל stack הפיתוח לווב שנקרא Ruby on Rails (אותו הזכרתי בפוסט).

      הדמיון - הוא כמובן רב (convention over configuration, בדיקות כחלק מובנה, נסיונות לקצר מאוד את הקוד (DRY) וכו'), וההבדל העקרי הוא שמשתמשים בשפת גרובי במקום רובי (מן וריאציה של ג'אווה שהופכת לדינמית. התאימות לג'אווה קצת "תוקעת" אותה, לדעתי).

      הבדלים אחרים מעניינים היא עבודה מעל Rich Domain Object (בעזרת Hibernate) במקום ישירות מול בסיס הנתונים (בעזרת Active Records ב RoR), אינטגרציה עמוקה ל Spring Framework והבניה multi-threaded בבסיס (מה שקל לעשות מעל ה JVM ולא קיים כ"כ בשפת רובי).

      סה"כ Grails קלה יותר ללמידה מרובי (אין בסיס נתונים, רובי שפה לא כ"כ פשוטה), אבל היא "כבדה" הרבה יותר (כלומר: כמות קוד, ותלויות) כי היא מביאה איתה את Spring ו Hibernate. יש לה לכאורה את "כל המרכיבים הנכונים" - אבל זה לא מבטיח הצלחה. אבל עד כמה שידוע לי אין לה קהילה גדולה, בטח לא כמו של RoR ובכלל עכשיו כל המעבר לפיתוח אינטנסיבי של UI צד-לקוח מעמיד את העתיד של שתי הספריות בסימן שאלה.

      בקיצר: לא מבין על איזה באזז אתה מדבר...

      ליאור

      מחק
  3. היי, זו שוב אני עם השגיעוט ;)
    בואו נצא ידע חובה וניצור hello world קטן - ידי חובה...
    שמות מחלקות חייבות להתחיל באות גדולה. שמות חייבים- לא חייבות (שם ארוך או שם ארוכה?)
    אבל אלו ההערות היחידות...
    תודה רבה - למדתי משהו על הקוד המוזר שאני רואה מרצד על כל מיני מסכים סביבי... (מוזר למתכנתי DOT NET :) )

    השבמחק
    תשובות
    1. תודה רבה לך על השגיאותון!

      השפה הזו מוזרה אגב... לא רק לאנשי NET. :)

      מחק