תכנות דינמי ושפת Groovy

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

מלחמת השפות

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

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

תחביר בסיסי

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

System.out.println(new Date().getTime())

//output a long number

הביטוי המקביל בשפת Groovy יראה כך

println new Date().time

סוכר סינטקטי

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

//avoid null pointer exceptions

def ref

println ref?.toString()

//GString templates

def name="joe"

println "Hello ${name}"

//define list with range, iterate with 'each' and run a clojure

[1..3].each{println it}

//map

def map = [name:'Joe',age:100,height:99.9]

println map.name

//define a list

def nums=[1,2,3,4,5]

//print the penultimate object

nums[-2]

//filter the list

nums.findAll{num-> (num %2 )==0}

//simple SQL

sql = Sql.newInstance(connStr, driverClass)

sql.eachRow("select * from persons", { println it.name } );

מהאנקדוטות הנ"ל רואים את הסינטקס המקוצר של השפה.

תכנות דינמי

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

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

def add(x,y){

        x+y

}

add(3,2) //return 5

add("Hello ", "World") //return "Hello world"

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

זה העיקרון הראשון  – אין חובה להגדיר טיפוס עבור מצביע. כל פניה בסגנון x.y היא חוקית (בזמן קומפילציה) ורק בזמן ריצה נבדקת חוקיות הפניה בפועל.

את אותה הפונקציה אפשר להגדיר גם באופן הבא:

def add = {x,y -> x+y}

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

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

[1..3].each{println it}

מזה נובע גם העיקרון השלישי – שימוש רב בשיקוף (reflection) ושינוי המבנה של מחלקות ומשתנים בזמן ריצה.

בואו נראה כמה דוגמאות:

def talkingString = "ABC"

talkingString.metaClass."sayHello" = {println "hi"}

talkingString.sayHello()

בדוגמא רואים הוספה דינמית של פונקציה למחלקה String (אפילו שזו לא מחלקה שאנו יצרנו !).

דוגמא נוספת:

class Stam {

}

def stam = new Stam()

stam.saySomething()

כאן כמובן נזרקת שגיאה – הפונקציה אינה מוגדרת (השגיאה כאמור נזרקת רק בזמן ריצה!).

נוסיף למחלקה את הפונקציה הבאה

    def methodMissing(String name, args) {

                "method ${name} is not defined"

     }

ונריץ שוב. הפעם לא תהיה שגיאה, ויודפס

method sayHello is not defined

הפוקנציה methosMissing מיירטת קריאות לפונקציות שאינן מוגדרות.

ניתן לממש אותה בצורה יותר מעניינת:

 def methodMissing(String name, args) {

    if (name.startsWith("say")){

        def word = name.substring(3)

        word

       }

    else

        "methos ${name} is not defined"

     }

כאן אנו מגיבים לכל קריאה בסגנון

sayXYZ()

על ידי החזרת המחרוזת "XYZ".

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

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

getMoshesAge(), getSarasHeight() …

למעשה יש דוגמא טובה לזה בGrails  – אם יצרתי domain בשם Blog אשר יש לו את המשתנה Name אני יכול לבצע שאילתא מול בסיס הנתונים על ידי הפקודה הבאה

Def blog = Blog.findByName(someName)

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

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

כתיבת תגובה