در مورد الگوریتم ماشین حساب ما استفاده از یک بافر برای گرفتن عبارت بطور کامل و سپس تجزیه کردن اجزای (Parse) آن از لحاظ فنی غیر ممکن نیست و تنها بدلیل صورت مسئله قادر به انجام آن نیستیم. اما تصور کنید که اگر قرار بود مرورگرهای وب (Web Browsers) ابتدا تمام محتوای یک صفحه را بخواندند و سپس آن را تجزیه کرده و نمایش دهند چه مقدار زمان کاربر و سرویس دهنده وب به هدر میرفت و ترافیک بیهودهای برروی خطوط ارتباطی حاصل میشد (در اکثر موارد ما با دیدن تنها چند خط از یک صفحه به صفحه دیگری میرویم).
در مورد ماشین حساب ورودی ما تنها یک عبارت ریاضی است که زیاد هم بزرگ نخواهد بود٬ اما متنی که توسط یک کامپایلر تجزیه تحلیل میشود میتواند به چندین میلیون خط هم برسد. در این حالت اگر کامپایلر نیاز داشت که تمام خطوط را در حافظه قرار دهد به چه میزان حافظه نیاز داشت تا تنها به این نکته پی ببرد که آیا خطایی در متن برنامه وجود دارد یا نه؟
در مواردی که ورودی ما از الگوی خاصی (Syntax) تبعیت میکند٬ به منظور یافتن الگوریتم مناسب برای تجزیه اجزا (Parse) و تفسیر معنای (Semantics) هر جزء از Syntax Diagram استفاده میشود.
همانند فلوچارت که میتواند برای نوشتن یک الگوریتم بکار رود٬ Syntax Diagram هم روشی دیگر برای نوشتن یک الگوریتم است.
حال به یافتن الگوریتم ماشین حساب میپردازیم تا در این مثال به چگونگی استفاده از Syntax Diagram آشنا شوید.
برای ساده کردن عبارات از دوران ابتدایی آموختهایم که:
- هر عبارت داخل پرانتز خود میتواند به عنوان عبارتی مستقل محاسبه شده و در نهایت بصورت یک عدد در عبارت اولیه جایگزین شود.
- علامتهای مثبت و منفی همواره پیش از یک عدد یا یک پرانتز قرار دارند (عملگرهای یگانی).
- حاصل ضرب و تقسیم تمام اعدادی که با عملگرهای ضرب و تقسیم به هم مرتبط هستند بدون نیاز به اولویتبندی قابل محاسبه بوده و حاصل میتواند در عبارت اولیه جایگزین گردد (عملگرهای ضرب و تقسیم).
- حاصل جمع و تفریق تمام اعدادی که مابین عملگرهای جمع و تفریق قرار دارند بدون نیاز به اولویتبندی قابل محاسبه هستند و حاصل میتواند در عبارت اولیه جایگزین گردد (عملگرهای جمع و تفریق دودویی).
با این معلومات و با شروع از کلیات تا رسیدن به جزئیات (Top-Down Design) اقدام به طراحی الگوریتم میکنیم.
ماشین حساب:
ورودی میتواند حرف X باشد که در این صورت خارج میشویم یا یک عبارت و به دنبال آن علامت تساوی (=) که در این صورت دوباره از اول کار شروع میکنیم.
عبارت:
یک عبارت تشکیل شده است از یک جمله یا حاصل جمع یا تفریق دو یا چند جمله.
جمله:
یک جمله تشکیل شده است از یک ضریب یا حاصل ضرب یا تقسیم دو یا چند ضریب.
ضریب:
یک ضریب میتواند در ابتدا یکی از علامتهای مثبت یا منفی باشد و سپس، یا یک عدد یا یک عبارت داخل پرانتز (عبارت تعریفی است بازگشتی).
عدد:
یک عدد تشکیل شده است از یک نقطه (.) و سپس یک یا چند رقم، یا تنها یک یا چند رقم٬ یا یک یا چند رقم و سپس یک نقطه (.) و پس از آن یک یا چند رقم.
رقم:
یک رقم یکی از حروف 0 تا 9 است.
خوب الگوریتم ما به همین سادگی نوشته شد. در هر مرحله٬ هرگاه ورودی با آنچه که در Syntax Diagram مربوطه انتظارش را داریم نخواند به معنی این است که عبارت مورد محاسبه نادرست وارد شده است.
در بالا برای ساده کردن Syntax Diagram ها فاصلهها را ندید گرفتهام ولی همانجور که پیداست به غیر از داخل یک عدد٬ در بقیه موارد لازم است که فاصلهها را ندید بگیریم. برای ایم منظور با استفاده از تابع GetChar که در صورت مسئله عنوان شده است٬ تابع دیگری مینویسیم که در آن بر حسب پارامتر تابع بتوانیم فاصلهها را رد کنیم. جدا از آن این تابع آخرین حرف خوانده شده را در داخل یک متغیر که توسط همه توابع قابل دسترسی است ذخیره میکند.
var
C: Char;
const
BlankChars = [' ', #9, #10, #13];
procedure FetchNextChar(SkipBlanks: Boolean);
begin
repeat
C := GetChar;
until not (SkipBlanks and (C in BlankChars));
end;
و بدنبال توابع مربوط به هر Syntax Diagram نوشته شده است. هر تابع در صورت برخورد با خطا مقدار False را برمیگرداند٬ والا مقدار برگشتی True است.
ماشین حساب:
procedure Calculator;
var
Value: Double;
begin
Writeln('Enter an expression or X to exit:');
FetchNextChar(True);
while C <> 'X' do
begin
if GetExpression(Value) and (C = '=') then
Writeln(Value)
else
begin
Writeln('Illegal expression!');
while C <> '=' do { ignores the rest of the illegal expression }
FetchNextChar(True);
end;
Writeln;
Writeln('Enter an expression or X to exit:');
FetchNextChar(True);
end;
end;
عبارت:
function GetExpression(var Expr: Double): Boolean;
var
NextTerm: Double;
Op: Char;
begin
Result := False;
if GetTerm(Expr) then
begin
Result := True;
while Result and (C in ['+', '-']) do
begin
Op := C;
FetchNextChar(True);
Result := False;
if GetTerm(NextTerm) then
begin
case Op of
'+': Expr := Expr + NextTerm;
'-': Expr := Expr - NextTerm;
end;
Result := True;
end;
end;
end;
end;
جمله:
function GetTerm(var Term: Double): Boolean;
var
NextFactor: Double;
Op: Char;
begin
Result := False;
if GetFactor(Term) then
begin
Result := True;
while Result and (C in ['*', '/']) do
begin
Op := C;
FetchNextChar(True);
Result := False;
if GetFactor(NextFactor) then
begin
if Op = '*' then
begin
Term := Term * NextFactor;
Result := True;
end
else if NextFactor <> 0 then
begin
Term := Term / NextFactor;
Result := True;
end;
end;
end;
end;
end;
ضریب:
function GetFactor(var Factor: Double): Boolean;
var
Negate: Boolean;
begin
Result := False;
Negate := False;
if C in ['+', '-'] then
begin
if C = '-' then
Negate := True;
FetchNextChar(True);
end;
if C = '(' then
begin
FetchNextChar(True);
if GetExpression(Factor) and (C = ')') then
begin
FetchNextChar(True);
Result := True;
end;
end
else if GetNumber(Factor) then
Result := True;
if Negate then
Factor := -Factor;
end;
عدد:
function GetNumber(var Number: Double): Boolean;
var
Digit: Integer;
Fraction: Double;
PointPos, P: Integer;
begin
Result := False;
Number := 0;
while GetDigit(Digit) do
begin
Number := Number * 10 + Digit;
FetchNextChar(False);
Result := True;
end;
if C = '.' then
begin
FetchNextChar(False);
PointPos := 0;
while GetDigit(Digit) do
begin
Fraction := Digit;
Inc(PointPos);
for P := 1 to PointPos do
Fraction := Fraction / 10;
Number := Number + Fraction;
FetchNextChar(False);
Result := True;
end;
end;
if C in BlankChars then
FetchNextChar(True);
end;
رقم:
function GetDigit(var Digit: Integer): Boolean;
begin
Result := False;
if C in ['0'..'9'] then
begin
Digit := Ord(C) - Ord('0');
Result := True;
end;
end;
برای تمرین سعی کنید با استفاده از Syntax Diagram عملگری برای به توان رساندن و چند تابع ریاضی (مثل Sin و Cos و ...) به ماشین حساب اضافه کنید.