• בלוג
  • זה לא באג, זה פיצ'ר

זה לא באג, זה פיצ'ר

16/06/2016

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

1. התאמה מדורגת של ביטוי רגולרי

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

use v5.18;

use List::Util qw/sum/;

my $text = <<END;
this page describes the syntax of regular expressions in Perl
END

my @words = $text =~ /(\b\w+\b)/g;

my $sum = 0;
foreach my $word (@words) {
  my @chars = split //, $word;
  $sum += sqrt(sum(map ord, @chars));
}

say "Sum Sqrt(ord(letters)) = $sum";

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

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

use v5.18;

use List::Util qw/sum/;

my $text = <<END;
this page describes the syntax of regular expressions in Perl
END

my $sum = 0;
while($text =~ /(\b\w+\b)/g) {
  my $word = $1;
  my @chars = split //, $word;
  $sum += sqrt(sum(map ord, @chars));
}

say "Sum Sqrt(ord(letters)) = $sum";

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

2. זהירות דרקונים

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

use v5.18;

while(<>) {
  print "Fizz" if /\A(...)+\Z/g;
  print "Buzz" if /\A(.....)+\Z/g;
  print "\n";
}

שימו לב ל-g שמסתתר בסוף הביטויים הרגולריים. ביטורי רגולרי עם הדגל g שנכתב בהקשר סקלארי (במקרה הזה כתנאי ל if) מדליק את מצב ההתאמה המדורגת. המילה FizzBuzz לעולם לא תודפס כאן: אם אורך המילה מתחלק ב-3, כל התווים כבר "נתפסו" בשביל ההתאמה הראשונה וכשפרל מגיע להמשיך את ההתאמה ולחפש חלוקה ב-5 לא נשאר כלום במחרוזת וההתאמה השניה נכשלת.

והפתרון? פשוט להוריד את ה g.