• בלוג
  • בואו נכתוב DSL בפייתון

בואו נכתוב DSL בפייתון

26/05/2018

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

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

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

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

add x 20
add y 50
add z x
add z y
sub z 2
print z

התוכנית כוללת 3 פקודות: add, sub ו print. כל פקודה מקבלת שם של אוגר כפרמטר ראשון וערך כפרמטר שני. הפקודה add מוסיפה את הערך לאוגר, הפקודה sub מחסירה את הערך מהאוגר ו print מדפיסה את ערך האוגר. בסיום תוכנית הדוגמא נקבל את הפלט 68.

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

הקוד עבור המחלקות לפקודות השונות:


class Cmd:
    def __init__(self, args):
        self.args = args

    def get_int_or_value(self, name, mem):
        if name in string.ascii_letters:
            return mem[name]
        else:
            return int(name)

class CmdAdd(Cmd):
    def run(self, mem):
        reg, arg = self.args
        mem[reg] += self.get_int_or_value(arg, mem)

class CmdPrint(Cmd):
    def run(self, mem):
        arg = self.args[0]
        print(self.get_int_or_value(arg, mem))

class CmdSub(Cmd):
    def run(self, mem):
        reg, arg = self.args
        mem[reg] -= self.get_int_or_value(arg, mem)

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

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

COMMANDS = {
        'add': CmdAdd,
        'print': CmdPrint,
        'sub': CmdSub,
        }

ונרצה לייצר מחלקה של Parser שתרוץ על הקלט ותייצר את האוביקטים המתאימים לטיפול בכל שורה:

class MyParser:
    def __init__(self, fname):
        self.fname = fname
        self.memory = defaultdict(int)

    def parse(self, line):
        cmd_name, *args = line.split()
        return COMMANDS[cmd_name](args)

    def run(self):
        with open(self.fname) as f:
            for line in f:
                cmd = self.parse(line.strip())
                cmd.run(self.memory)

p = MyParser('dsldemo.txt')
p.run()

כמה נקודות לשים לב אליהן בתוכנית:

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

  2. קל מאוד לשנות את מבנה הקלט באמצעות שינוי הקוד ב MyParser. מוזמנים כתרגיל לנסות לעבור למבנה קלט כזה:

x += 20
y += 50
z += x
z += y
z -= 2
z

ותגלו שבסך הכל אתם צריכים לשנות את הקוד של MyParser כדי שיחפש את שם הפקודה בתור המילה השניה, להחליף את שמות הפקודות מ add ל += ואם אין שם לפקודה לבחור print. כל עוד MyParser יודע לקחת שורה ולשבור אותה לפקודה ופרמטרים הכל בסדר.

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