רובי קשה שפה

בחודשים האחרונים התחלתי לחזור לראות ולשמוע podcast בצורת ווידאו (שיותר קל לי להתרכז על מה מדברים) וגם בצורת audio (שהרבה יותר קשה לי להתרכז בו כי זה יותר מידי "באוויר" כאשר מדובר בטכנולוגיה). ה podcastים הם על שפת רובי, ועל rails.

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

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

הבעיה ברובי (כמו עוד כמה שפות תכנות בעיקר דינמיות), היא שיש לה 3 צורות פיתוח שונות, שאפשר לשלב בין כולן או להשתמש רק באחת מהן, דבר שמן הסתם לא מוסיף, אבל מספק יכולות מדהימות לשפה עצמה.

רובי בנוייה לפי 3 צורות פיתוח:

  1. תכנות פרוצדורלי
  2. תכנות מונחה עצמים
  3. תכנות שטוח – אפשר פשוט לזרוק את הקוד באמצע הקובץ בלי פונקציות ובלי אובייקטים והוא ירוץ.

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

בשביל להבין את הלך החשיבה צריך להבין גם איך הדברים עובדים, כך שלמשל תכנות מונחה העצמים של רובי אינו זהה לתכנות מונחה העצמים של פיתון לדוגמה. בעוד שפיתון מכילה תכנות מונחה עצמים שהוא implicit (כלומר נגזר), ברובי (ובהרבה שפות אחרות כדוגמת פסקל) התכנות הוא explicit (כלומר ישיר). מה הכוונה ? נגיד ויש לי מחלקה, ואני יורש במחלקה הזו מחלקה שתהיה האב שלנו. ברובי הירושה תתנהג כמו שהתרגלנו בתכנות "רגיל", בו המתודות של מחלקת האב נמצאות גם במחלקת הבן בעצם העובדה שירשנו את מחלקת האב. בפיתון למשל צריך להגיד מתי אנחנו רוצים להשתמש במתודות הנמצאות במחלקת האב בשביל שהן יהיו נגישות ואז בשימוש, אחרת הן לא גלויות. הגישה של פיתון פחות נפוצה אבל היא גם לא כזו מהפכנית.

רובי כמו פסקל, מאפשרת לרשת תוכן של מתודות (override הוא המונח היותר נכון להשתמש בו) כולל לבנאים (דבר שג'אווה וגם ++C לא מאפשרים), מה שאומר שמי שמגיע מג'אווה או ++C יתקשה להתרגל לרעיון שאפשר לנצל פולימורפיזם בצורה מלאה ולא חלקית.

ברוב השפות הדינמיות, כאשר גם רובי וגם פיתון בהם, אי אפשר לעשות העמסת פונקציות (או overload בלשון המקצועית), דבר שגם מצריך חשיבה שונה מאשר שפות כדוגמת פסקל, ג'אווה, ++C ואחרות.

לרובי יש שימוש ב module. מודול הוא הדבר הכי קרוב ל interface שאפשר לחשוב, אבל הוא ממש לא מתקרב interface. הוא למעשה קרוב יותר לירושה מרובה. בנוסף מודול גם מסוגל לשנות את ההתנהגות של המחלקה שלנו (אם הוא מוגדר פנימית עד כמה שאני מבין), ולספק לנו יכולות שאי אפשר לממש בדרך אחרת (לפחות עד כמה שאני מבין). הגישה של מודול שונה לגמרי ממה שאנחנו מכירים ברוב השפות הפופולריות (שאני מכיר), ולכן מאוד קשה לתת משהו מהצד השני.

עוד דבר שאין ברובי, זה אופרטורים. אני יודע מה אתם הולכים להגיד: "אבל מה זה ה '=' הזה שאתה עושה למשתנים ?" ובכן זו פונקציה לכל דבר ועניין. למעשה כל מה שאנחנו מכירים בתור אופרטור בשפות תכנות הפופולריות, ברובי הוא פונקציה לכל דבר ועניין. ככה גם אפשר לעשות operator overload בלי באמת לעשות את זה. הפונקציות שאנחנו מתייחסים אליהן כאופרטורים, מוגדרות במחלקה שאנחנו עובדים איתה (בטח רציתם לשאול את זה).

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

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

19 מחשבות על “רובי קשה שפה

  1. ik_5 מאת

    בשביל זה אני צריך או להתחיל להשתכר (ואני בכלל לא נוהג להשתכר) או להיות מסומם (ובשביל זה צריך להתחיל לקחת סמים).

    כי זו הדרך היחידה שאני אמצא איזשהו הגיון בטירוף 🙂

  2. ik_5 מאת

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

    אני מאוד מכבד את השפה והטכנולוגיה, אבל אני פשוט לא מצליח למצוא את עצמי עובד איתה בלי באיזשהו שלב לרצות להרביץ לגווידו (או מאיר, כי הוא מעצבן אותי בקשר לפיתון בלי קשר) 🙂

  3. Shai

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

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

    לגבי module, הוא אכן דומה מאד לירושה מרובה. בעולם של פייתון קוראים לדבר כזה mix-in class, אבל אף אחד שם לא מנסה לאכוף את זה של-mix-in אין data members (שזה, אם הבנתי נכון, המצב ברובי).

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

  4. ik_5 מאת

    שי, בוא נעשה ניסוי (תתעלם מזה שאין הזכה):

    class A
    def hello(self)
    print "hello"

    class B(A):
    def hi(self):
    hello(self)

    תקבל הודעת שגיאה:
    NameError: global name 'hello' is not defined

    אתה צריך להגדיר במיוחד שאתה לוקח את hello מ A
    כלומר:

    class B(A):
    def hi(self):
    A.hello(self)

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

  5. Shai

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

    class B(A):
    def hi(self):
    self.hello()
    # ignore this comment it is only here to fix RTL issues

    כמו שהמתודה hi שלך כתובה, תקבל את אותה הודעת שגיאה גם אם היא תהיה כתובה בתוך class A; הדבר שצריך להתייחס אליו במפורש הוא לא האב, אלא המופע self.

  6. ארתיום

    (דבר שג’אווה וגם ++C לא מאפשרים)

    אתה יכול להסביר ביותר פירוט? למה אתה מתכוון? האם הבנתי מכון ש־C++‎ לא מעביר כברירת מחדל את הפרמטרים לאבא. כך אם האם הבנתי נכון:


    class Foo {
    int x_;
    public:
    Foo()
    {
    x_=0;
    }
    Foo(int x)
    {
    x_=x;
    }
    };

    class Bar : public Foo {
    public:
    Bar(int x) :
    Foo(x)
    {
    }
    // Must Forward Only for non default constructor;
    // for default is OK
    };

    in C++0x

    class Bar: public Foo {
    public:
    using Foo::Foo;
    // Forwards all constructors
    };

  7. ארתיום

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

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

  8. ik_5 מאת

    שי, אני לא יכול להשתמש ב self.hello(self) כי הוא לא שייך ל B, ובשביל להפעיל אותו אני חייב להגיד לו להשתמש במתודה של A עם ה instance של B. אתה מוזמן להפנות אותי לתיעוד שאומר אחרת (‎ או להראות איך לעשות את זה בלי לקבל שגיאות).

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

    type
    TSky = class
    constructor Create; virtual;
    end;

    TSun = class(TSky)
    contructor create; override;
    end;

    constructor TSky create;
    begin
    Do something
    end;

    constructor TSun.Create;
    begin
    if Something = 0 then
    inherited Create;
    end;
    // comment for bidi issues

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

  9. Shai

    עידו,

    הראיתי איך לעשות את זה בלי לקבל שגיאות, ואיכשהו הצלחת להחמיץ…

    אתה כתבת שתי צורות לא נכונות. בתגובה הראשונה שלך,

    hello(self) # fails because it only looks for hello as global

    ובתגובה השניה שלך,

    self.hello(self) # fails because of redundant argument

    ואילו אני כתבתי בתגובה השניה שלי את הצורה הנכונה,

    self.hello() # works

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

    ארתיום,

    עידו משתמש במונח "לרשת" בצורה קצת מוזרה, כתרגום ל-override במקום ל-inherit. הוא טוען, ובצדק, שבג'אווה וגם ++C אתה לא יכול להחליף את המימוש של בנאי — בנאים ב-class בן הם תמיד בנוסף, ולא במקום, הבנאים שב-class האב.

    אני חושב שתוספת הגמישות הזאת בעייתית, כי היא פוגעת בהפרדה בין ממשק למימוש; כל בן יכול לכתוב בנאי שלא קורא לבנאי של האב, ואז שינויי מימוש של האב — ללא כל שינוי בממשקים, כולל בסמנטיקה שלהם — יכולה לשבור את הבן. בג'אווה ו-++C, כדי שדבר כזה יקרה, המשתמש-הבן צריך לעשות דברים בוטים שמדליקים נורות אזהרה — reflection בג'אווה, מניפולציות מצביעים מלוכלכות ב-++C.

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

  10. ik_5 מאת

    שי, אוקי עכשיו הבנתי ואתה צודק.

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

    ב ++C ובג'אווה, בשביל שהבן יקבל את מה שהאב אתחל, צריך להשתמש באתחולים סטטיים (כלמר סוגריים מסולסלים בהגדרת הקלאס אם אני זוכר נכון -> כבר מזמן שלא תכנתתי בג'אווה וטוב שכך), או ליצור מתודות שהיוצרים השונים יקראו להם. וזה די גורם למצב שאי אפשר ליישם את DRY.

  11. ארתיום

    עידו, אתה יכול בבקשה להסביר מדוע אני צריך (באיזה מצב אני צריך) דבר כזה.

    ההפניה הסביר היחידה שמצאתי ברשת היא: http://www.felix-colibri.com/papers/oop_components/delphi_virtual_constructor/delphi_virtual_constructor.html#why_virtual_constructors

    אבל שם virtual constructor זה בסה"כ נראה כמו factory design patters שכלל לא קשור לבנאים.

    אתה יכול להביא דוגמה מהחיים שאתה משתמש בזה? (אולי הפניה לאיזה svn עם קוד קיים).

    אני אסביר מה אני לא מבין:

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

  12. ארתיום

    או קיי, עכשיו ראיתי את התגובה…

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

    class TSky {
    	// These always should be
    	TAngleInSky *angels;
    protected:
    	TStar *stars;
    	
    	struct DoNotCreateMe {};
    	TSky(DoNotCreateMe marker)
    	{
    		angels=new TAngleInSky[10];
    	} 
    public:
    	TSky()
    	{
    		angels=new TAngleInSky[10];
    		stars=new TStar[10];
    	}
    };
    
    class TSun {
    public:
    	TSun() : TSky(DoNotCreateMe())
    	{
    		starts=NULL;
    	}
    };
    

  13. ik_5 מאת

    ב99% מהפעמים אתה פשוט יורש את יוצר האב. כלומר תמיד.

    בפסקל ה Constructor לא באמת מאתחל את הזכרון, מה שמאתחל את הזכרון אלו מתודות שמתרחשות לפני, אבל הן שקופות בד"כ. הצגתי את זה בפוסט שלי על singleton, איך האתחול מתבצע בעצם:
    אתה יכול ד"א לראות את היחידה הבאה:
    http://svn.freepascal.org/svn/fpc/trunk/rtl/objpas/fgl.pp
    (גם generics שאתה מאוד אוהב, וגם הדגמה לשימוש)

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

  14. Shai

    ארתיום,

    מה שיש בפסקל וחסר בג'אוה ו-++C (ובמפתיע, גם ב-#C, למרות שמי שתכנן אותה היה אמור לדעת יותר טוב) הוא מה שקוראים בפייתון class method: פונקציה שלא שייכת לאובייקט אלא ל-class, אבל מחוברת למבני הנתונים הסטאטיים של ה-class (וכאילו מתייחסת ל-class בתור האובייקט שלה). זה מאפשר לך התנהגות דמויית פונקציה וירטואלית, בפונקציה סטאטית; בנאי הוא רק מקרה פרטי של הסיפור הזה.

    זה קשור, כמובן, בעבותות ל-reflection; הסיטואציה הקלאסית היא של class שנטען באופן דינמי, ועכשיו אתה רוצה לייצר אובייקט שלו — אתה מתחיל עם מטא-אובייקט (אובייקט שמייצג את ה-class עצמו, בניגוד ל-instance) ביד, ומפעיל עליו מתודות.

    דוגמה (שלא עובדת) ב-++C:

    struct A {
    static void f() { cout<<"in A"<< eol; }

    static void g() { f(); }
    };
    struct B : public A {
    static void f() { cout<<"in B"<< eol; }
    };
    void main() {
    B::g();
    }
    // The call in main doesn't call B::f

    היתה לי דוגמה ב-#C שבה רציתי כזה דבר, גם כן בהקשר של טעינה דינמית. שם, התיעוד מספיק מעורפל בשביל שהפרשנות הטבעית שלו תהיה שכן צריך לקרוא למתודה בבן (לפחות היה — מדובר על C#2). כמובן, מיקרוסופט סגרו את הבאג בטענה שזה לא באג.

  15. ארתיום

    הבנתי… כלומר זה משהו שהייתי מממש את זה כך ב־C++‎:


    ////////////
    // MAIN
    ////////
    class Figure {
    public:
    virtual void draw();
    // META CLASS INFORMATION
    struct Meta {
    virtual Figure *create(int x,int y) = 0;
    virtual string name() = 0;
    };
    };

    ///////
    // Module
    ///////
    class Ellipse : public Figure {
    public:
    Ellipse(int x,int y);
    ...
    // META CLASS INFORMATION
    struct Meta : public Figure::Meta {
    virtual Ellipse *create(int x,int y) {...}
    virtual string name() {...}
    };
    };

    Figure::Meta &module()
    {
    static Ellipse::Meta ellipse;
    return ellipse;
    }

    // FIX FOR BIDI

  16. ארתיום

    עידו… אתה חייב למצוא פתרון לבלוג כדי שאפשר יהיה להדביק קוד..

    אתה יכול להגדיר ב־CSS:

    .code { direction:ltr; text-align:left }‎

    אחרת זה סיוט לקרוא את מה שכתוב כאן

  17. ik_5 מאת

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

להשאיר תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s