درمورد پرولوگ about prolog/
1. تاریخچه همان طور که گفته شد در پیدایش این زبان کلمرار در مارسی فرانسه بوده است(1972). و اولین سیستم مورد استفاده قرار گرفته شده توسط آلن فیلیپ راسل پیاده سازی شده است و بعدا توسط رابرت کوالسکی ارتقا یافته است. اما زمزمههای ایجاد یک زبان منطق گرا از دهه 70 میلادی از شمال امریکا شکل گرفته است. بعدا در نسل پنجم کامپیوترها از پرلوگ برای نوشتن کرنل سیستم عامل نیز در ایجاد پروژه سیستم FGCS استفاده شده است. 2. انواع داده ها نوع داده در پرلوگ به صورت ترم ها تعریف میشود که این ترم ها میتوانند اتم ،اعداد ،متغیرها و یا ترکیبی از ترم های دیگر باشند. اتم ها به طور کلی هیچ معنای ذاتی ندارند و یک سری رشته از حروف یا ... هستند که خواننده پرلوگ آنها را تجزیه کرده است. اتم کلمات آشکار در کد میباشند که هیچ نحو خاصی برای آنها در نظر گرفته نشده است مثل : x, blue,some,atom اعداد که میتوانند به صورت اعداد شناور و یا صحیح باشند و حتی اعدا گویا متغیر که یک رشتهٔ متشکل از حروف است که میتواند نشان دهندهٔ یک واژه باشند و ارزش آنها با توجه به پرلوگ مقداردهی داده میشود. یک واژه مرکب (عمل کننده یا functor) ترکیبی از اتم ها است که به صورت یک متغیر با آن رفتار می کنیم و نیز مجموعهای از استدلال هاست که یک نتیجه نهایی درست یا غلط را دربرمی گیرد. .واژههای مرکب در یک پرانتز تعریف میشوند و به انها عبارت های مرکب نیز اطلاق میشود. مثل
truck_year('Mazda', 1986) 'Person_Friends'(zelda,[tom,jim])
لیست ها که یک حالت خاص عبارت ها ترکیبی هستند و ساختمان داده ایی پیشرفته برای نگهداشت استدلال ها و منطق هاست و به طور کلی لیستی از اتم ها هستندو بوسیله پرانتز، نقطه و کاما نشان داده میشود. رشتهها که مجموعه ایی از کارکترها هستند برای نگهداری یونیکدها و نام های شخصیت های محلی.
3. برنامه نویسی در Prolog برنامه پرلوگ مجموعهای از روابط است که توسط بندهای خاص تعریف شده اند . این بندها محدود به بند های horn و تورینگ است که زیر مجموعه کاملی از منطق منظور اول است (first-order predicate logic) . بندها به دو دستهٔ قوانین و حقیقت ها تقسیم میشوند . یک مثال از قانون: Head :- Body. سر : -- بدن است. سر یک عضوی از بدن است . و بعد با پرس و جو های انجام شده با توجه به قوانین موجود و حقایق اولیه نتایج ثانویه که حقایق جدیدی هستند شکل میگیرد. پرس و جو ها میتوانند براساس لیست های پیوندی نیز باشد و طبق قوانین از پیش تعیین شده نتایجی را در اختیار کاربر گذاشت . مثل اندازه لیست . عنصر آخر لیست و ... . بهمین خاطر مجموعهای از کتابخانههای این زبان شکل گرفته است و در راستای آن هم دستوراتی برای چاپ دادهها و امثال آن شکل گرفته است . 4. بررسی بررسی ابتدا با یک پرس و جو شروع میشود و با توجه به حقایق موجود که درست یا غلط است نتیجه منتقل میشود. اساس کار بدین گونه است که بهدنبال اولین غلط و یا تکذیب مساله مییابد و با توجه به نوع مساله در صورت نیافتن آن همین روال را برای دیگر دادهها نیز انجام میدهد و در صورت حصول نتیجه حقیقت دادههای قبلی بصورت بازگشتی معلوم میگردد. با این استراتژی که Backtracking نیز گفته میشود اساس یافت جواب در زبان پرلوگ اطلاق میشود. به طور مثال:
mother_child(trude, sally).
father_child(tom, sally). father_child(tom, erica). father_child(mike, tom).
sibling(X, Y) :- parent_child(Z, X), parent_child(Z, Y).
parent_child(X, Y) :- father_child(X, Y). parent_child(X, Y) :- mother_child(X, Y).
و سوال پرسیده شده در دنباله آن
?- sibling(sally, erica).
Yes
طبق قانون وقتی دونفر خواهر و برادرند که والدین آنها یکی باشد پس زبان به دنبال والدین میگردد و آنها را بر می گرداند و جواب را به جای گزارههای مذکور می گذارد و طبق قانون اگر دو گزاره یکی باشند درست برمی گرداند. پس روش کلی روشی بازگشتی اثبات بندهاست و جایگزینی نتایج آنها. 5. حلقهها و بازگشت الگوریتمهای Iterative (پشت سرهمی) میتوانید بوسیله predicates بازگشتی به اجرا درآیند. سیستم های Prolog معمولا به روش بهینه سازی معروفی به نام تماس دم (tco) پایبندند که ملتزم به بازگشت قطعی است و حتما در این روش بهینه سازی می بایست به دم برسد. بهمین خاطر در روش بازگشتی از یک پشته با یک نوع محدودیت بهره میگیرد و بدین وسیله میتواند حلقههای متعدد و بازگشتی را با اطمینان به بازگشت به جواب را جوابگو باشد.
6. خنثی سازی
ایجاد یک مسند نفی و شکست باعث خنثی کردن یک استدلال میشود. حالت دیگری نیز در بر دارد بدینگونه که برای اثبات یک استدلال دنباله بازگشتی آنرا تا رسیدن به یک دم و یا انتها میرود و در صورتی که نتواند حقیقتی برای آن پیدا کند در حقیقت نتوانسته آنرا اثبات کند اما شاید هم حقیقتی مربوط به نفی آن پیدا شود که در صورت آنرا نفی میکند.
7. ملاحظات عملیاتی
یکسری قوانینی تعبیه شده است که در آن کاربر با استفاده از آن میتواند از ایجاد لوپ های تودرتو جلوگیری کند. و البته با نظم دادن به دستورات میتوان از ایجاد آنها جلوگیری کرد به طور مثال:
predicate1(X) :-
predicate2(X,X).
predicate2(X,Y) :-
predicate1(X),
X \= Y.
برای پرسش
?- predicate1(atom).
ایجاد یک حلقه بی نهایت توسط کاربر میکند ولی اگر دستورات را اینگونه نظم دهیم چنین اتفاقی نمی افتد
predicate2(X,Y) :-
X \= Y,
predicate1(X).
8. DCGs و تجزیه
یک نماد ویژهای به نام گرامرهای بند قطعی (DCGs) وجود دارد . یک قاعده تعریف شده به عنوان --> به جای -: که با استفاده از آن میتوان به تجزیه کردن و ایجاد یک رابط مناسب برای یک لیست بکار گرفت. در زیر یک مثال از این مورد را می بینید:
<sentence> ::= <stat_part> <stat_part> ::= <statement> | <stat_part> <statement> <statement> ::= <id> = <expression> ; <expression> ::= <operand> | <expression> <operator> <operand> <operand> ::= <id> | <digit> <id> ::= a | b <digit> ::= 0..9 <operator> ::= + | - | * و با استفاده از DCG sentence(S) --> statement(S0), sentence_r(S0, S). sentence_r(S, S) --> []. sentence_r(S0, seq(S0, S)) --> statement(S1), sentence_r(S1, S).
statement(assign(Id,E)) --> id(Id), [=], expression(E), [;].
expression(E) --> term(T), expression_r(T, E). expression_r(E, E) --> []. expression_r(E0, E) --> [+], term(T), expression_r(plus(E0,T), E). expression_r(E0, E) --> [-], term(T), expression_r(minus(E0, T), E).
term(T) --> factor(F), term_r(F, T). term_r(T, T) --> []. term_r(T0, T) --> [*], factor(F), term_r(times(T0, F), T).
factor(id(ID)) --> id(ID). factor(digit(D)) --> [D], { (number(D) ; var(D)), between(0, 9, D)}.
id(a) --> [a]. id(b) --> [b]. همان طور که می بینید علاوه بر مزیت های گفته شده موجب بزرگتر شدن کد نیز میشود.
9. برنامه نویسی سفارشی
یکی دیگر امکاناتی که در پرلوگ با توجه به ماهیت آن بوجود آمده است برنامه نویسی سفارشی است . با توجه به روند BachTracking و روش بهینه سازی بازگشت قطعی میتوانیم حالاتی را ایجاد کنیم که از آن طریق برنامه به طور خودکار و تنها با داشتن یک قاعده ساده به دنبال جواب در یک لیست پیوندی بگردد. به طور مثال شمارش اعداد و پیدا کردن اعداد اول. perfect(N) :-
between(1, inf, N), U is N // 2,
findall(D, (between(1,U,D), N mod D =:= 0), Ds),
sumlist(Ds, N).
10. متا - مترجم و انعکاس
یک زبان homoiconic فراهم شده است و بسیاری از امکانات را برای انعکاس را در اختیار ما می گذارد.. این زبان امکان نوشتن meta-circular evaluator را نیز فراهم می سازد که اغلب از آن به عنوان متا- مترجم یاد میشود. بدلیل آنکه برنامههای به زبان پرلوگ دنبالهای از اصطلاحات خود این زبان است بهمین دلیل خواندن آن راحت و مکانیزم ساده ایی جهت ترجمه را خواستار است.
11. روش های پیاده سازی
برای بهره وری از کد Prolog معمولا به صورت کد ماشین انتزاعی ترجمه میشود و اغلب تحت تاثیر مجموعه دستورات ثبت نامی براساس ماشین انتراعی وارن (Warren Abstract Machine (WAM)) است.برای پیاده سازی انتزاعی متناسب به نوع اصطلاحات و اطلاعات در زمان کامپایل است. برای ترجمه بهتر و نزدیک تر بودن به زبان ماشین واقعی برای عملکرد بهتر نیاز به تحقیقات مبتنی بر جامعه منطقی برنامه ریزی شده است که دو کار اساسی براساس قواعد منطقی انجام میدهد یک باینری کردن عبارات و بند ها و دیگری فراهم کردن پشته مبتنی بر ماشین مجازی.در نسل پنجم سعی شده است ماشین ها و سیستم های مبتنی بر پرلوگ نیاز های سخت افزاری این نوع برنامه سازی منطقی را نیز فراهم سازند تا سرعت اجرای آن هزاران برابر شود. بعلاوه این پرلوگ این توانایی را نیز دارد که با پردازش موازی بندها سرعت را بهبود بخشد.
12. مثال ها
در اینجا مثال هایی از برنامه ساخته شده به زبان پرلوگ را می بینیم.
12.1 . الگوریتم مرتب سازی سریع ((QuickSort
partition([], _, [], []). partition([X|Xs], Pivot, Smalls, Bigs) :-
( X @< Pivot ->
Smalls = [X|Rest],
partition(Xs, Pivot, Rest, Bigs)
; Bigs = [X|Rest],
partition(Xs, Pivot, Smalls, Rest)
).
quicksort([]) --> []. quicksort([X|Xs]) -->
{ partition(Xs, X, Smaller, Bigger) },
quicksort(Smaller), [X], quicksort(Bigger).
12.2 . ماشین تورینگ turing(Tape0, Tape) :-
perform(q0, [], Ls, Tape0, Rs),
reverse(Ls, Ls1),
append(Ls1, Rs, Tape).
perform(qf, Ls, Ls, Rs, Rs) :- !. perform(Q0, Ls0, Ls, Rs0, Rs) :-
symbol(Rs0, Sym, RsRest),
once(rule(Q0, Sym, Q1, NewSym, Action)),
action(Action, Ls0, Ls1, [NewSym|RsRest], Rs1),
perform(Q1, Ls1, Ls, Rs1, Rs).
symbol([], b, []). symbol([Sym|Rs], Sym, Rs).
action(left, Ls0, Ls, Rs0, Rs) :- left(Ls0, Ls, Rs0, Rs). action(stay, Ls, Ls, Rs, Rs). action(right, Ls0, [Sym|Ls0], [Sym|Rs], Rs).
left([], [], Rs0, [b|Rs0]). left([L|Ls], Ls, Rs, [L|Rs]). مثالی ساده از برنامه پیاده سازی ماشین تورینگ rule(q0, 1, q0, 1, right). rule(q0, b, qf, 1, stay).
?- turing([1,1,1], Ts). Ts = [1, 1, 1, 1] ;
12.3 . برنامه نویسی پویا
برنامه Prolog زیر استفاده میکند برنامه نویسی پویا برای یافتن توالی مشترک طولانی از دو فهرست در زمان چند جملهای: پایگاه داده بند برای memoization استفاده شده است :
- dynamic(stored/1).
memo(Goal) :- ( stored(Goal) -> true ; Goal, assertz(stored(Goal)) ).
lcs([], _, []) :- !. lcs(_, [], []) :- !. lcs([X|Xs], [X|Ys], [X|Ls]) :- !, memo(lcs(Xs, Ys, Ls)). lcs([X|Xs], [Y|Ys], Ls) :-
memo(lcs([X|Xs], Ys, Ls1)), memo(lcs(Xs, [Y|Ys], Ls2)),
length(Ls1, L1), length(Ls2, L2),
( L1 >= L2 -> Ls = Ls1 ; Ls = Ls2 ).
مثال پرس و جو ?- lcs([x,m,j,y,a,u,z], [m,z,j,a,w,x,u], Ls). Ls = [m, j, a, u]
13. ضمیمه ها
درحال حاضرتلاش در راستای توسعه این زبان برای گسترش قابلیت های برنامه سازی در جهات مختلف میباشند که این قابلیت ها عبارتند از محدودیت های برنامه نویسی منطقی (CLP)(بیشتر در تنظیمات صنعتی مانند جدول های زمان بندی) ، برنامه نویسی شی گرای منطقی (OOLP) ، همزمانی ، منطق خطی و قابلیت همکاری با پایگاههای دانش. بدین منظور نسخههایی همچون نمونههای زیر تهیه شده است: HiLog و λProlog تمدید Prolog با بالاتربردن ویژگی های برنامه نویسی سفارشی. اف منطق گسترش Prolog با قاب / اشیاء را برای نمایش دانش. OW Prolog ایجاد شده است به منظور پاسخ به عدم Prolog است از گرافیک و رابط. Logtalk یک شیء گرا منطق زبان برنامه نویسی است (بهمراه ایجاد خاصیت چندریختی و وراثت و همچنین چند نخی و همزمانی) Prolog - MPI یک منبع باز SWI - Prolog میباشد که برای محاسبات توزیع شده از طریق واسط پیام رسان اینکار را انجام میدهد. Oblog کوچک ، قابل حمل ، شیء گرا را به فرمت - Prolog توسط McDougall مارگارت از EdCAAD ، دانشگاه ادینبورگ است.
14. زبان های مرتبط
Datalog که در واقع یک زیر مجموعه از Prolog است و شرایط ترکیب را بخوبی اجازه نمیدهد و در مقابل به Prolog ، Datalog تورینگ نشده است. ویژوال پرلوگ که بیشتر به عنوان PDC prolog شناخته میشود و شی گرا میباشد و با ایجاد محیطی شی گرا فهم را در هنگام کدزنی بالا میبرد. InterProlog که یک کتابخانه برنامه نویسی شده بین جاوا و پرلوگ است. JPL که پلی بین جاوا و پرلوگ است. Prova یک زبان اسکریپ نویسی بر اساس پرلوگ است .
15. Prolog Gnu چیست؟
gnu Prolog (همچنین به نام gprolog) یک کامپایلرکد باز توسط دانیل دیاز با یک اشکال زدایی محیط تعاملی برای Prolog در دسترس در یونیکس و ویندوزمی باشد. همچنین پشتیبانی از برخی از الحاقات را به Prolog از جمله محدودیت های برنامه نویسی بیش از یک دامنه محدود ، با استفاده از تجزیه grammars بند تصریح شده ، و یک سیستم عامل و رابط را داراست. کامپایلر تبدیل کد منبع را تبدیل به بایت کد میکند که میتواند توسط یک دستگاه مترجم و یا مفسر وارون انتزاعی آن را به کد اجرایی رایج تبدیل کند.
عناوين متن
زبانهاي برنامهنويسيAI، برنامهنويسي تابعي ، برنامهنويسي تابعي در Lisp ، A- Syntax (نحو) و semantic هاي (معاني) Lisp ، ليست انواع داده ، تعريف توابع جديد ، تعريف ساختارهاي كنترلي ، تعريف توابع بازگشتي ، توابع مرتبه بالا ، ساير زبانهاي برنامهنويسي تابعي غير از Lisp ، برنامهنويسي منطقي در Prolog ، ساير روشهاي برنامهنويسي
واژه نامه
بندهاي برنامه Prolog شامل مجموعهاي از جملات بنام بندها هستند كه براي نشان دادن دادهها و برنامهها بكار ميروند.
تابع مرتبه بالا تعريف تابعي است كه اجازه ميدهد آرگومانها يا مقدار بازگشتي تابع، مقدار توابع باشد. نماد ساختار ليستها اغلب نشاندهنده نحوه استفاده از ليست ساختاري داده هستند، كه يك عنصر ليست ممكن است نماد يا ليست ديگر باشد. ليستها ساختاري مركزي Lisp هستند كه براي نشان دادن دادهها و برنامهها بكار ميروند. بازگشت تكنيكي الگوريتمي براي انجام يك كار است كه يك تابع با بعضي از قسمتهاي كار خودش را فراخواني ميكند.
محاسبات نمادين برنامهنويسي AI (اساساً) شامل دستكاري نمادها است نه اعداد. اين نمادها ميتوانند اشياء در جهان و ارتباط بين آن اشياء را نشان دهند- ساختارهاي پيچيده نمادها نياز به دانش ما از جهان دارند. واژه ساختار اساسي دادهها در Prolog واژهاي است كه ميتواند يك ثابت، يك متغير يا يك ساختار باشد. ساختارها موضوعات ريز محاسبات گزارهاي را نشان ميدهند و شامل يك عملگر نام و يك پارامتر ليست هستند.
زبانهاي برنامهنويسي هوش مصنوعي(AI)
ابزار اصلي بررسي و ساخت برنامههاي كامپيوتري هستند كه ميتوانند در شبيهسازي فرايندهاي هوشمند مانند يادگيري، استدلال و فهم اطلاعات نمادين بكار بروند. هر چند اخيراً زبان كامپيوتر اصولاً براي استفاده از كامپيوترها براي انجام محاسبات با اعداد طراحي شده بود، اما بزودي دريافتند كه رشتهاي از بيتها نه تنها اعداد بلكه ميتوانند اشياي دلخواه را نيز نمايش دهند. عمليات روي ويژهگيها يا نمادها ميتواند با استفاده از قوانين براي ايجاد، انتساب يا دستكاري نشان داده شود. اين تصور از محاسبات نمادين بعنوان تعريف الگوريتمهايي كه هر نوع اطلاعات را پردازش ميكنند و بنابراين ميتواند براي شبيهسازي هوش انسان بكار برود مناسب است.
بزودي برنامه نويسي با نمادها كه نياز به سطح بالايي از چكيدگي دارند توليد ميشوند، غير از امكاناتي كه با زبانهاي برنامه نويسي مخصوص پردازش اعداد ممكن بود مانند فرترن
I-زبانهاي برنامه نويسي AI
ر AI خودكار كردن يا برنامهنويسي همه جنبههاي شناخت انساني بوسيله بنيادهاي شناخت علمي روشهاي نمادين و غير نمادين AI، پردازش زبان طبيعي، ديد كامپيوتري و سيستمهاي تكامل يا سازگار مطرح ميشود. لازم است دامنه مسئلههاي خيلي پيچيده در ابتداي مرحله برنامهنويسي يك مسئله AI معين، مشخص شود كه كافي نيست. تنها بوسيله تعامل و افزايش اصلاحات خصوصيات بسيار دقيق ممكن است. در حقيقت مسئلههاي معمول AI به بسياري از زمينههاي خاص گرايش دارند، بنابراين روشهاي ذهني بايد بوسيله توليد و آزمايش روشها بطور تجربي توسعه يابند(مشهور به نمونه سازي سريع). در اينصورت برنامهنويسي AI بطور قابل توجهي با روشهاي استاندارد مهندسي نرمافزار متفاوت بوده زيرا برنامهنويسي معمولا از يك مشخصات رسمي با جزئيات شروع ميشود. در برنامهنويسي AI پيادهسازي در واقع جزئي از پردازش مشخصات مسئله است. به اقتضاي طبيعت مسئلههاي AI برنامهنويسي AI مزاياي بسياري دارد اگر زبانهاي برنامه نويسي، برنامهنويسAI را آزاد بگذارند و در بسياري از ساختارهاي فني محدود نكنند (مانند ساختار انواع دادهاي جديد سطح پايين، دستيابي دستي به حافظه). ترجيحاً سبك برنامهنويسي اعلاني براي استفاده در ساختارهاي پيشساخته دادهاي سطح بالا(مانند ليستها و درختها) و عمليات(مانند تطبيق الگوها) مناسب است، بنابراين محاسبات نمادين سطح خلاصهسازي بيشتري نسبت به آنچه كه با زبانهاي دستوري استاندارد مانند فرترن، پاسكال يا C امكانپذير خواهد بود را پشتيباني ميكند. البته طبقهبندي خلاصه سازي آسان نيست، زيرا تدوين برنامههاي AI روي كامپيوترهاي استاندارد وان نيومن نميتواند به كارآمدي زبانهاي دستوري باشد. هر چند يك مسئله مسلم AI فهم آن است (حداقل جزئيات) امكان دارد با تنظيم مجدد آن به شكل خصوصيات جزئي شده با بكار بردن يك زبان دستوري پياده سازي مجدد شود. با توجه به نيازمنديهاي محاسبات نمادين و برنامهنويسي AI دو الگوي جديد برنامهنويسي كه به سبك دستوري پيشنهاد ميشوند بوجود ميآيد: سبك برنامهنويسي تابعي و منطقي. هر دو بر مبناي رياضيات طرحريزي شدهاند، يعني نظريه توابع بازگشتي و منطق رسمي. اولين زبان برنامهنويسي AI كاربردي كه هنوز هم بطور گسترده استفاده ميشود زبان برنامهنويسي Lisp است كه در اواخر دهه 1950 توسط جان مك كارتي توسعه يافته است. Lisp برمبناي نظريه توابع رياضي و خلاصهسازي Lambda است. تعدادي از كاربردهاي مهم و موثرAI در Lisp نوشته شده است. كه ما بعضي از جزئيات اين زبان برنامهنويسي را در اين مقاله شرح خواهيم داد. در اوايل دهه 1970 يك الگوي برنامهنويسي جديد بنام برنامهنويسي منطقي بر اساس محاسبات گزارهاي بوجود آمد. اولين و مهمترين زبان برنامهنويسي منطقي Prolog است كه توسط آلن كالمرار، رابرت كوالسكي و فيليپ راسل توسعه يافته است. مسئلهها در prolog بصورت حقايق، بديهيات و قوانين منطقي براي استنباط حقايق جديد بيان ميشوند. Prolog با قانون رياضي در محاسبات گزارهاي و نتايج نظري بدست آمده در زمينه اثبات قضيه خودكار در اواخر دهه 1960 بنا نهاده شده است.
II- برنامه نويسي تابعي
يك تابع رياضي نگاشتي از يك مجموعه (دامنه) به مجموعه ديگر(برد) است. تعريف يك تابع توصيف اين نگاشت است كه يا بطور صريح بوسيله شمارش و يا بطور ضمني بوسيله يك عبارت است. تعريف يك تابع بوسيله نام تابع كه بدنبال آن ليستي از پارامترها در داخل پرانتز قرار دارند و به دنبال آن نيز عبارت توصيفي نگاشت است مشخص مي شود مانند:
X يك عدد حقيقي است cube(X) ≡ X X X , where X is a real number.
آلونسو چارچ توابع بي نام را با استفاده از نمادLambda معرفي مي كند. يك عبارت Lambda پارامترها و نگاشت تابع را با استفاده از عملگر X مشخص مي كند, مانند λ (X)X X X آن خودش تابع است, بنابراين شرح بكار رفته در مثال تابع بي نام با يك آرگومان مشخص است. براي مثال:(λ (X) X X X)(4).
برنامه نويسي در يك زبان تابعي شامل ساختمان تعريف توابع و بكاربردن كامپيوتر براي ارزيابي عبارات است. يعني بكاربردن توابع با آرگومانهاي واقعي. كار اصلي برنامه نويسي پس از ساخت يك تابع براي يك مساله خاص تركيب توابع تعريف شده قبلي با توجه به اصول رياضي است. كار اصلي كامپيوتر ارزيابي توابع فراخواني شده و چاپ حاصل مقادير تابع است. در اين روش كامپيوتر مشابه يك كامپيوتر جيبي معمولي بكار مي رود البته بسيار انعطاف پذيرتر و قدرتمندتر. يك خاصيت برنامه نويسي تابعي اين است كه اگر عبارت به خوبي مقداردهي شود آنگاه ترتيب انجام ارزيابي كامپيوتر در نتايج ارزيابي تاثيري ندارد. بنابراين نتيجه ارزيابي يك عبارت تنها مقدار آن است. بدين معني است كه در يك زبان تابعي ناب اثرات جانبي وجود ندارد. اثرات جانبي در مدل موقعيت هاي حافظه به متغيرها متصل شده اند.بنابراين در يك زبان برنامه نويسي ناب در مفهوم زبانهاي دستوري متغير وجود ندارد. روشهاي اصلي كنترل جريان، بازگشت (تكرار) و عبارات شرطي هستند. اين كاملاً با زبانهاي دستوري در مفهوم اساسي كنترل ترتيب و تكرار متفاوت است. برنامه نويسي تابعي نيز خصوصيات توابع مرتبه بالا را پشتيباني مي كند. تابع مرتبه بالا تعريف تابعي است كه اجازه مي دهد آرگومانها يا مقدار بازگشتي تابع, مقدار توابع باشند. همه اين جوانب با هم مخصوصاً آخري از اصلي ترين مزاياي سبك برنامه نويسي تابعي در برابر سبك برنامه نويسي دستوري هستند. خلاصه برنامه نويسي تابعي سطح بالايي از درجه پيمانه اي بودن را فراهم مي كند. وقتي يك مسئله با تقسيم آن به مجموعه اي از زير مسئله ها تعريف مي شود, موضوع اصلي روشهايي است كه مي توان زير مسئله ها را به يكديگر چسباند. بنابراين براي افزايش قابليت پيمانه اي بودن يك مسئله مفهومي, ابتدا بايد نوع جديدي از چسب در زبان برنامه نويسي فراهم شود- قدرت اصلي برنامه نويسي تابعي .
III- برنامه نويسي تابعي در Lisp
isp اولين زبان برنامه نويسي تابعي است: آن براي پشتيباني محاسبات نمادين با استفاده از ليستهاي پيوندي بعنوان ساختار مركزي داده ها ابداع شده بود ( Lisp يعني پردازشگر ليست). جان مك كارتي دريافت كه روشهاي كنترل جريان توابع رياضي (بازگشت و تكرار) وسيله نظري مناسبي براي انجام محاسبات نمادين هستند. علاوه براين مفاهيم خلاصه سازي تابعي و كاربرد تابعي تعريف شده در محاسبات Lambda , سطح بالايي از خلاصه سازي موردنياز براي مسئله هاي AI مشخص شده را فراهم مي كنند.
Lisp در سال 1958 توسط مك كارتي ابداع شد و اولين نگارش محيط برنامه نويسي Lisp در سال 1960 آماده شد كه شامل يك مفسر, يك كامپايلر و مكانيسم تخصيص و بازپسگيري حافظه پويا بود (بعنوان مجموعه فضاي هرز شناخته شده است). يكسال بعد اولين زبان استاندارد با نام Lisp1.5 معرفي شد. پس از آن تعدادي از نسخه ها و محيط هاي برنامه نويسي Lisp توسعه يافته اند. مانند MacLisp، FranzLisp، InterLisp، CommonLisp، Scheme هر چند آنها در بعضي جزئيات خاص متفاوتند ولي هسته Syntax (نحو) و Semantic (معني) آنها اساساً يكسان است. هسته را در جاي ديگر معرفي خواهيم كرد. پر استفاده ترين نسخههاي
Lisp ، Common Lisp و scheme هستند. در اين مقاله ما Common Lisp را براي نشان دادن جنبه هاي مختلف Lisp با مثالهاي معمولي انتخاب كرده ايم. هرچند مثالها نيز به راحتي مي توانند در نسخه هاي ديگر Lisp سازگار شوند.
Syntax .A. (نحو) و semantics (معاني) Lisp
1. عبارات نمادين: عناصر نحوي Lisp عبارات نمادين ناميده مي شوند (كه به صورتS-expressionsشناخته شدهاند). داده ها و توابع (يعني برنامه هاي Lisp ) بصورت عبارات نمادين نشان داده شده اند كه مي توانند اتم ها يا ليست ها باشند. اتم ها كلمه اي شبيه اشيا هستند. اتمها وابسته به نوع كاراكترهايي كه براي شكل دادن يك اتم مجازند مي توانند به انواع مختلفي تقسيم شوند. انواع اصلي عبارتنداز:
Numbers:1 234-43.14159265358979 -7.5 6.02E+23
Symbols:SymbolSym23another-one t false NILBLUE
Strings: ”This is a string””977?” ”setq””He said: \” I’m here.\” ”
توضيح اينكه هرچند نماد خاصي مثل BLUE استفاده ميشود چون مفهوم مشخص براي برنامهنويسي دارد، اما بزودي Lisp تنها ترتيبي از حروف يا تنها يك نماد است. ليستها بندي شبيه اشياء هستند. يك ليست شامل يك پرانتز باز( دنبالهاي از اعداد دلخواه كه بوسيله فاصله خالي از هم جدا ميشوند) و يك پرانتز بسته هستند. هر عنصر ليست ميتواند يك اتم يا ليست باشد. اينها مثالهايي از ليستها هستند:
(This is a list) ((this) ((too))) () (((((((())))))))
(a b c d) (john mary tom) (loves john ?X)
(* (+ 3 4) 8) (append (a b c) (1 2 3))
(defun member (elem list)
(if (eq elem (first list)) T
(member elem (rest list))))
توضيح اينكه در بسياري از مثالها عناصر ليست خود ليستها هستند.چنين ليستهايي، ليستهاي تو در تو ناميده ميشوند. در مورد تو در تويي محدوديتي وجود ندارد. براي مثال يكي از قويترين Lisp ها را شرح ميدهيم: پيچيدهترين اشياء را به راحتي ميتوان نوشت. تنها چيزي كه در نظر گرفته ميشود درستي عدد داخل پرانتزهاست. مهم توضيح اين است كه معني وابسته به يك ليست نمايش ويژه يا اتم در ليست نمايش وارد نميشود. به اين معني كه همه عبارات نمادين كه در بالا توصيف شده است از لحاظ نحو برنامههاي Lisp را اصلاح ميكنند ولي الزاماً از لحاظ معني (semantic) برنامهها رااصلاح نميكنند.
2. Semantics (معاني): هسته هر سيستم برنامهنويسي Lisp مفسر است كه كارش محاسبه مقدار براي يك عبارات نمادين داده شده است. اين فرآيند ارزيابي نام دارد. نتيجه يا مقدار يك عبارت نمادين، يك عبارت نمادين است. كه بعد از كامل شدن ارزيابي برگردانده شده است. توضيح اينكه در واقع Lispداراي Semantics (معاني) عملياتي است كه با يك تعريف رياضي دقيق از نظريه تابع بازگشتي بدست ميآيد.
حلقه خواندن- محاسبه- چاپ چگونه ميتواند مفسر Lisp را فعال كرده و براي محاسبه عبارات نمادين و بنابراين اجراي واقعي برنامههاي Lisp بكار برود؟
مسئلههاي Prolog بصورت حقايق، بديهيات و قوانين منطقي براي استنباط حقايق جديد بيان مي شوند . Prolog با قانون رياضي در محاسبات گزاره اي و ونتايج نظري بدست آمده در زمينه اثبات قضيه خودكار در اواخر دهه1960 بنا شده است. مفسر Lisp در واقع بعنوان يك تابع معمولاً بنام eval و جزئي از هر محيط برنامهنويسي Lisp است تعريف شده است (مانند تابعي كه پيشساخته نام دارد). آن بوسيله فراخواني حلقه خواندن- محاسبه- چاپ در يك سيستم Lisp جاسازي ميشود، وقتي يك عبارت نمادين توسط كاربر داده مي شود ابتدا به داخل سيستم Lisp خوانده ميشود( خواندن هم يك تابع پيشساخته است). سپس مفسر Lisp كه via نام دارد تابع eval را فراخواني ميكند تا عبارت نمادين را محاسبه و نتيجه عبارت نمادين را با چاپ در دستگاه كاربر برگرداند ( شگفتآورنيست گفتن اينكه چاپ هم يك تابع پيشساخته است). وقتي سيستم Lispدر كامپيوتر شروع به اجرا ميشود اين حلقه خواندن- محاسبه- چاپ بطور خودكار شروع به اجرا كرده و بوسيله علامت ويژه اعلان Lisp در ابتداي خط جديد به كاربر علامت ميدهد در اين مقاله ما علامت سئوا ل (?) را به عنوان اعلان Lisp بكار خواهيم برد. براي مثال:
( 4 3 +) ?
7
هر وقت سيستم Lisp اجرا شود حلقه خواندن- محاسبه- چاپ فعال خواهد بود.
عبارت نمادين ( 4 3 + ) كه بوسيله هكر Lisp وارد شده است بوسيله مفسر Lisp بصورت فراخواني تابع جمع تفسير شده و نتيجه عبارت نمادين در ابتداي خط جديد 7 چاپ ميشود ارزيابي مفسر Lisp مطابق سه قانون زير انجام ميشود:
1- يكساني: يك عدد، يك رشته يا نمادهاي t و nil خودشان را ارزيابي ميكنند (بر ميگردانند) به اين معني كه ارزش عدد 3،3 و ارزش رشته ”house”، رشته ”house”است. نمادt مقدار t برميگرداند كه به معناي true تفسير ميشود وnil ، nil به معني false برميگرداند
2- نمادها: ارزيابي يك نماد عبارت نمادين مربوط به آن را برميگرداند. ( چگونگي اش را در زير نشان خواهيم داد) بنابراين اگر ما فرض كنيم نماد names به ليست
(john mary tom) وابسته است آنگاه ارزيابي names آن ليست را نتيجه ميدهد. اگر نماد color را به نماد green وابسته كنيم آنگاه green بعنوان مقدار color برگردانده ميشود.
به بيان ديگر نمادها بعنوان متغيرهايي كه به مقاديري متصل(باند) شدهاند تفسير ميشوند.
3- ليستها: هر ليست بعنوان يك فراخواني تابع تفسير ميشود. مفسر اول ليست دلالت بر تابعي دارد كه بايد براي بقيه عناصر( بالقوه خالي) كه آرگومانهاي آن تابع را نشان ميدهند بكار رود. در واقع آرگومانهاي يك تابع قبلا بصورت نمادهاي پيشوندي مشخص ميشوند. اين مزيت را دارد كه توابع به سادگي ميتوانند با تعداد دلخواهي آرگومان مشخص و استفاده شوند. ليست خالي ( ) داراي عبارت نمادين nil بعنوان مقدارش ميباشد. توضيح اينكه نماد nil در واقع داراي دو معني است: يك نمايش مقدار منطقي false و ديگري نمايش ليست خالي. هر چند ممكن است اين يك بيت فرد بنظر برسد، ولي در واقع در Lisp مشكلي در شناسايي مفهوم nil بكاررفته وجود ندارد.
ولي بطور كل آرگومانها قبل از اينكه توابع مقادير آنها را استفاده كنند ارزيابي ميشوند. اولويت ارزيابي ترتيبي از آرگومانها از چپ به راست است. يك آرگومان ممكن است يك اتم يا يك ليست باشد،درهر حالت بعنوان يك فراخواني تابع تفيسر شده و مفسر Lisp براي ارزيابي آن فراخواني ميشود. براي مثال، ارزيابي زير در سيستم Lisp يك تابع به حساب ميآيد:
?(max 4 (min 9 8) 7 5)
8
در اينجا آرگومانها 5, 7, (min 9 8), 4 هستند كه در اولويتي قبل از تابعي به نام max كه نتيجه مقادير آرگومانها را به كار ميبرد ارزيابي ميشوند. آرگومان اول 4 ، يك عدد است پس مقدار آن 4 است. آرگومان دوم (min 9 8) است كه خودش يك فراخواني تابع است. بنابراين بايد قبل از آرگومان سوم فراخواني شود، (min 9 8) بايد توسط مفسر Lisp ارزيابي شود. چون ما بايد مفسر Lispرا براي بعضي آرگومانها در طول ارزيابي همه فراخوانيهاي توابع استفاده كنيم ميتوان گفت مفسر Lisp بصورت بازگشتي فراخواني شده است. مفسر Lisp همان مراحل را به كار ميبرد، پس آرگومان اول 9 قبل از آرگومان دوم 8، ارزيابي ميشود. با بكار برروي تابع min حاصل 8 ميشود يعني تابع كوچكترين عدد يك مجموعه از اعداد صحيح را محاسبه ميكند. براي تابع بيروني max هم به اين معني است كه آرگومان دوم آن 8 ارزيابي ميشود.
آرگومانهاي بعدي 7و5هستند كه نتيجه ارزيابي آنها مقادير 7و5 ميشود. حال تابع بزرگترين عدد كه max نام دارد ميتواند ارزيابي شود كه 8 برميگرداند. اين مقدار نهايي، مقدار فراخواني همه توابع ميباشد. از آنجايي كه گفته ميشود مفسر Lisp هميشه سعي ميكند مقدار يك نماد يا تفسير يك ليست بعنوان يك فراخواني تابع را تشخيص دهد ما چگونه ميتوانيم با نمادها و ليستها بعنوان داده رفتار كنيم؟ براي مثال، اگر ما ليست (peter walks home) را وارد كنيم، آنگاه مفسر Lisp فوراً يك خطا ميدهد كه چيزي شبيه اين خطا ميگويد: تابع peter ناشناخته است (مفسرLisp بايد بقدري باهوش باشد كه بتواند ابتدا كنترل كند كه آيا تعريف تابعي براي نام تابع تعيين شده وجود دارد يا نه، قبل از اينكه هر آرماگوني را ارزيابي كند). يا اگر ما فقط house را وارد كنيم، آنگاه مفسر Lisp با خطايي شبيه اين خطا خاتمه مييابد: مقداري به house متصل نيست (تخصيص نيافته است). حل اين مسئله كاملاً آسان است. زيرا عنصر اصلي هر ليست بعنوان نام تابع تفسير ميشود،هر سيستم Lisp با يك تابع پيشساخته quote ميآيد كه يك عبارت نمادين را بعنوان آرگومان پذيرفته و اين عبارت نمادين را بدون ارزيابي آن برميگرداند. براي مثال: ليست(quote(peter walks home)) ، به سادگي مقدار
(peter walks home) را برميگرداند، و براي (quote house)، آن house را بر ميگرداند. از آنجايي كه تابع quote زياد استفاده ميشود، ميتوان آن را با كاراكتر ويژه ' بيان كرد. بنابراين براي مثال بالا ميتوانيم معادل’(Peter walks home) و’house را مشخص كنيم. برنامهها بعنوان داده، يعني تابع quote به ما امكان ميدهد تا با فراخواني تابع بعنوان داده رفتار كنيم. براي مثال: (quote (max 4 (min 9 8) 7 5)) يا ’(max 4 (min 9 8) 7 5)
قبلاً گفتيم كه مفسر Lisp يك تابع يكتايي پيشساخته است كه eval نام دارد. آن صريحاً آرگومانهايش را وادار ميكند تا مطابق قوانين مذكور در بالا ارزيابي شوند. در بعضي حالات، آن ميتواند مقابل تابع quote قرار بگيرد بنابراين به وضوح لازم است كه يك ليست بعنوان داده مشخص شود تا سيستم Lisp بتواند يك فراخواني تابع تفسير شود، ما ميتوانيم(eval ’(max 4 (min 9 8) 7 5)) را مشخص كنيم كه مقدار 8 را بطوري كه در بالا توصيف شد بر ميگرداند. به همان صورت مشخص كردن (eval ’(peter walks home)) سبب يك خطاي Lisp ميشود زيرا Lisp سعي ميكند يك تابع peter فراخواني كند. مزيت اصلي رفتار برنامهها بعنوان داده اين است كه ما ميتوانيم برنامههاي Lisp (توابع) را طوري تعريف كنيم كه قادر به ساخت يا توليد برنامهها باشند بطوريكه ابتدا ليست نمايش متناظر را ساخته و سپس با استفاده از تابع eval ، مفسر Lisp را به منظور ارزيابي ليست ايجاد شده بعنوان يك تابع فراخواني ميكند. شگفتآور نيست كه به اقتضاي اين خصوصيات، Lisp هنوز زبان برنامهنويسي برتر در زمينه برنامهنويسي ژنتيك AI است.
وقتي مقادير را به نمادها تخصيص ميدهيم كه برنامهنويسي برنامههاي كاربردي
real-life به ذخيره مقاديري محاسبه شده در يك متغير نياز داشته باشد تا اگر در آينده در برنامه ديگري نياز باشند از هزينه محاسبه مجدد آن جلوگيري شود. در يك نگارش كاملاً تابعي Lisp مقدار يك تابع تنها به تعريف تابع و مقدار آرگومانهايش در فراخواني بستگي دارد. براي اينكه Lisp را يك زبان كاربردي بكنيم (كاربردي حداقل در اين مفهوم كه بتواند بر روي كامپيوترهاي وان نيومن به خوبي اجرا شود)، ما نياز به روشي داريم تا مقادير را به نمادها تخصيص دهيم.common Lisp با يك تابع پيشساخته بنام Setq ميآيد. Setq دو آرگومان ميخواهد: نماد (بنام متغير) كه يك مقدار به آن متصل شده است و يك عبارت نمادين كه بايد مقداري را فراهم كند. مفسر Lisp ارزيابي Setq را در روش خاصي انجام ميدهد بطوريكه آرگومان اول Setq را ارزيابي ميكند(متغير)، اما مقدار آرگومان دوم Setq را به متغير متصل ميكند(براي فهم چگونگي اتصال يك مقدار به يك نماد نياز به جزئيات فني زيادي خواهيم داشت كه در اين معرفي كوتاه نميتوان به آن پرداخت). مقدار آرگومان دوم Setq مقدار Setq را بر ميگرداند. اينها مثالهايي هستند:
?color
error: unbound symbol color
?(setq color ’green)
green
?(setq max (max 3 2 5 1))
3
توضيح اينكه در واقع Setq حالت مفسر Lisp را تغيير ميدهد تا دفعه بعدي كه همان متغير استفاده ميشود، داراي مقدار بوده و بنابراين مفسرLisp قادر به بازگرداندن آن خواهد بود. اگر اين اتفاق نيفتد آنگاه مفسر Lisp يك اخطار خواهد داد زيرا نماد متصل نشده است.
(گام 2 مفسر Lisp پيدا نشد). بنابراين آن ميگويدكه Setq يك اثر جانبي توليد ميكند زيرا حالت مفسر Lisp بطور پويا تغيير ميدهد. وقتي استفاده از Setq اجباري شد، به هرحال متوجه شد كه در واقع از مسير semantics (معاني) Lisp ناب دور ميشود. پس Setq بايد با دقت بسيار استفاده شود.
B. نوع داده ليست
برنامهنويسي در Lisp در واقع به معني تعريف توابعي است كه روي ليست عمل ميكنند. مانند ايجاد، پيمايش،كپي، تغيير و حذف ليستها. از آنجايي كه اين در Lisp مركزي است، هر سيستم Lisp بر مبناي مجموعهاي از توابع پيشساخته ابتدايي كه بطور موثري عمليات اصلي ليست را پشتيباني ميكند ميآيد. ما بطور خلاصه يكي از مهمترين آنها معرفي ميكنيم. ابتدا نوع گزارهاي، ما ميدانيم كه يك عبارت نمادين جاري يا يك ليست است يا نيست (يعني يك اتم). اين كار بوسيله تابع Listp انجام ميشود كه هر عبارت نمادين expr را بعنوان آرگومان پذيرفته و اگر expr ليست باشد نماد t و در غير اين صورت nil برميگرداند. مثالها هستند (ما از فلش راست => براي نشان دادن نتيجه فراخواني تابع استفاده خواهيم كرد):
(listp ’(1 2 3))==>t
(listp ’( ))==>t
(listp ’3)==>nil
در انتخاب عناصر ليست دو تابع اساسي براي دستيابي به عناصر يك ليست وجود دارد: car وcdr هر دو تابع يك ليست را بعنوان آرگومان ميپذيرند. تابع car اولين عنصر ليست يا اگر ليست خالي از آرگومان باشد nil بر ميگرداند،و cdr همان ليست را بطوري كه عنصر اول آن حذف شده است يا اگر ليست خالي از آرگومان بود nil برميگرداند. مثالها:
(car ’(a b c)) ==>a (cdr ’(a b c)) ==>(b c)
(car ’( )) ==>nil(cdr ’(a)) ==>nil
(car ’((a b) c))==>(a b)
با استفاده از ترتيبي از فراخوانيهاي توابع car و cdr ميتوان يك ليست را از چپ به راست و از عناصر بيروني به سمت عناصر داخلي ليست پيمايش كرد.
براي مثال، در طول ارزيابي (car (cdr ’(see the quote))) مفسر Lisp ابتدا عبارت
(cdr ’(see the quote))را ارزيابي خواهد كرد كه ليست (the quote) را برميگرداند، سپس به تابع car پاس ميشود كه نماد the را بر ميگرداند. اينها مثالهايي ديگر هستند:
(car (cdr (cdr ’(see the quote)))) ==>quote
(car (cdr (cdr (cdr ’(see the quote))))) ==>nil
(car (car ’(see the quote))) ==>?
در طول ارزيابي مثال اخير چه اتفاقي خواهد افتاد؟ ارزيابي (car ’(see the quote)) نماد see را برميگرداند.سپس اين به عنوان آرگومان به فراخواني بيروني car پاس ميشود. چون تابع car يك ليست را به عنوان آرگومان مي پذيرد پس مفسر Lisp بلافاصله ارزيابي ديگر را با خطايي مانند اين خطا متوقف خواهد كرد: سعي ميشود Car SEE بدست آيد ولي Listp نيست. يك توضيح كوتاه تاريخي: نامهاي Car,cdr از روشهاي قديمي هستند زيرا آنها در اولين نگارش Lisp كه بر مبناي مجموعه عمليات كد ماشين كامپيوتر انتخاب و پياده سازي شده بودند (car از محتواي ثبات آدرس استفاده ميكند و cdr از محتواي ثبات كاهش استفاده ميكند). به منظور نوشتن كد Lisp خواناتر، common Lisp يا در تابع first و rest بوجود آمد. ما نامهاي قديمي را استفاده ميكنيم تا براي خواندن و فهم كد AI Lisp قديمي قادر باشيم. براي ساخت ليستها، يك تابع ابتدايي Cons مانند Car و cdr وجود دارد كه براي ساخت يك ليست بكار ميرود. Cons دو عبارت نمادين را ميپذيرد كه اولي بعنوان يك عنصر جديد در جلوي دومي وارد ميشود. در مثالهاي زير ملاحظه ميكنيد:
(cons ’a ’(b c)) ==>(a b c)
(cons ’(a d) ’(b c))==>((a d) b c)
(cons (first ’(1 2 3)) (rest ’(1 2 3))) ==>(1 2 3)
در اصل، Cons و ليست خالي با هم براي ساخت ليستهاي خيلي پيچيده كافي هستند، براي مثال:
(cons ’a (cons ’b (cons ’c ’( )))) ==>(a b c)
(cons ’a (cons (cons ’b (cons ’c ’( ))) (cons ’d ’( )))) ==>(a (b c) d)
چون اين كار كاملاً طاقتفرساست، سيستمهاي Lispبسياري با توابع ليست پيشساخته بسيار پيشرفته بوجود ميآيند. براي مثال، تابع List با تعداد دلخواهي عبارت نمادين يك ليست ميسازد، و تابع append با الحاق آرگومانهايش كه بايد ليست باشند يك ليست جديد ميسازد. equal تابعي است كه اگر عناصر و ترتيب آنها در دو ليست يكسان باشد t ، در غير اين صورت nil بر ميگرداند. مثال:
(list ’a ’b ’c) ==>(a b c)
(list (list 1) 2 (list 1 2 3)) ==>((1) 2 (1 2 3))
(append ’(1) (list 2)) ==>(1 2)
(append ’(1 2) nil ’(3 4))==>(1 2 3 4)
(equal ’(a b c) ’(a b c)) ==>t
(equal ’(a b c) ’(a c b)) ==>nil
C. تعريف توابع جديد
برنامهنوسي در Lisp با تعريف توابع جديد انجام ميشود. در اصل اين به اين معني است كه: مشخص كردن ليستها در يك روش نحوي معين. مشابه تابع setq كه بوسيله مفسر Lisp در يك روش خاص رفتار ميكرد. تابع خاص defun است كه براي ايجاد اشياي تابع جديد توسط مفسر Lisp بكار ميرود. defunيك نماد دال برنام تابع، يك ليست از پارامترها(ممكن است خالي باشد) براي تابع جديد و تعداد دلخواهي از عبارات نماديني كه بدنه تابع جديدرا تعريف ميكند را به عنوان آرگومانهايش ميپذيرد. اين تعويض از يك تابع ساده به نام my-sum است كه دو آرگومان ميپذيرد و با استفاده از تابع پيشساخته آنها را جمع ميكند.
(defun my-sum (x y)
(+ x y))
اين عبارت به همان روشي كه بعنوان يك تابع فراخواني ميشود در سيستم Lisp وارد ميشود. ارزيابي يك تعريف تابع نام تابع را بعنوان مقدار برميگرداند، اما يك شئ تابع را بعنوان اثر جانبي ايجاد خواهد كرد و وقتي Lisp شروع به اجرا ميكند آن را به مجموعه تعاريف توابع شناخته شده توسط سيستم Lisp اضافه ميكند (حداقل مجموعه توابع پيشساخته)
توضيح اينكه در اين مثال بدنه شامل تنها يك عبارت نمادين است. هر چند بدنه ميتواند شامل ترتيب دلخواهي از عبارات نمادين باشد مقدار آخرين عبارت نمادين از بدنه مقدار تابع را تعيين ميكند. به اين معني است كه در واقع همه عناصر بدنه بي تاثير هستند مگر اينكه اثرات جانبي تصميمگيري توليد كنند.
لسيت پارامتر تابع جديدmy-sum به ما ميگويد وقتي فراخواني ميشود درست دو عبارت نمادين را بعنوان آرگومان ميپذيرد. بنابراين اگر شما(my-sum 3 5) را در سيستمLisp وارد كنيد مفسرLisp قادر خواهد بود كه تعريف براي نام تابع مشخص شده بيابد و سپس آرگومانهاي داده شده را از چپ به راست پردازش كند وقتي اين كار انجام شد آن مقدار هر آرگومان را مطابق پارامتر مشخص شده در ليست پارامتر تعريف تابع وصل خواهد كرد(تخصيص خواهد داد) در مثال ما بدين معني است كه مقدار آرگومان اول كه3 است(3 همان عدد3 است كه خودش را ارزيابي كرده است) به پارامترx متصل ميكند. سپس مقدار آرگومان دوم كه 5 است به پارامترy متصل ميشود. چون مقدار يك آرگومان به يك پارامتر متصل ميشود، اين روش فراخواني با مقدار ناميده شده است. بعد از مقداريابي براي همه پارامترها مفسرLisp قادر به ارزيابي بدنه تابع خواهد بود. مثال بدين معني است كه ( 3 5 +) فراخواني خواهد شد. نتيجه فراخواني8 است كه بعنوان نتيجه فراخواني(my-sum 3 5) برگردانده ميشود. بعد از تكميل فراخواني تابع اتصالات موقت پارامترهايx وy حذف ميشوند. هنگامي كه يك تعريف تابع جديد در سيستمLisp وارد ميشودميتواند به عنوان جزئي از تعريف تابع جديد به همان روش كه بعنوان تابع پيش ساخته استفاده شده است بكار برده شود بطوريكه در مثال زير نشان داده شده است.
(defun double-sum (x y)
(+ (my-sum x y) (my-sum x y)))
كه با دوبار فراخوانيmy-sum جمع آرگومانهايش را دو برابر خواهد كرد اين مثال ديگري از يك تعريف تابع است نشان دادن استفاده از عبارات نمادين چندگانه در بدنه تابع است.
(defun hello-world () (print ”Hello World!”) ’done)
اين تعريف تابع پارامتري ندارد زيرا ليست پارامتر آن خالي است بنابراين وقتي(hello-world) فراخواني ميشود مفسرLisp بلافاصله (print ”Hello World!”) را ارزيابي و رشته
”Hello World!”را روي نمايشگر شما بعنوان يك اثر جانبي چاپ ميكند سپس نماد’done را ارزيابي خواهد كرد وdone را به عنوان نتيجه فراخواني تابع برميگرداند.
D. تعريف ساختارهاي كنترلي
هر چنداكنون تعريف توابع جديد با تعريف توابع پيش ساخته و توابعي كه كاربر تعريف ميكند ممكن است برنامهنويسي درLisp بسيار خسته كننده خواهد شداگر كنترل جريان اطلاعات بوسيله شاخههاي شرطي ممكن نبود شايد بارها تكرار ميشد تا اينكه يك روند توقف اجرا شود گزينشLisp بر مبناي ارزيابي توابع است توابع كنترل تستهايي روي عبارات نمادين واقعي انجام ميدهد و ارزيابي عبارات نمادين متناوب را بسته به نتايج انتخاب ميكنند تابع اساسي براي تعيين اثباتهاي شرطي درcond،Lisp است.cond تعداد دلخواهي آرگومان راميپذيرد هر آرگومان يك بخش ممكن را بيان ميكنند و بعنوان يك ليست نمايش داده شده كه عنصر اول يك تست و بقيه عناصر اعمال (عبارات نمادين) هستند كه اگر تست انجام شود ارزيابي ميشوند مقدار آخرين عمل به عنوان مقدار پيشنهادي برگردانده ميشود همه آرگومانهاي ممكنcond (يعني بخشها) تا زماني كه بخش اول بطور مثبت تست شوداز چپ به راست ارزيابي ميشوند درآن حالت مقدار آن بخش مقدار كل تابعcond است. در واقع اين مفهوم بسيار پيچيده تر از آن است اجازه دهيد تابعverbalize-prop زيركه يك مقدار احتمال را بيان ميكند. به عنوان يك عدد حقيقي فرض ميكنيم.
(defun verbalize–prop (prob-value)
(cond ((> prob–value 0.75) ’very-probable)
((> prob–value 0.5) ’probable)
((> prob–value 0.25) ’improbable)
(T ’very-improbable)))
وقتي(verbalize-prop 0.33) فراخواني ميشود مقدار واقعي آرگومانها به پارامترprop-value متصل ميشود.سپسcond با آن اتصالات ارزيابي ميشود very-probable)’((>prop-value)است.> يك گزاره پيش ساخته است كه تست ميكند كه آيا آرگومان اول از دومي بزرگتر است،چونpropvalue،0.33 است. بهnil ارزيابي ميشود كه به معني انجام نشدن تست است. بنابراين ارزيابي اين بخش پيشنهادي بلافاصله پايان مييابد. و سپس پيشنهاد
((> prob–value 0.5) ’probable)ارزيابي ميشود كه تابع تست باز هم nilبرميگرداندبنابراين ارزيابي هم پايان مييابد. سپس ((prop-value 0.25) ’improbable) ارزيابي ميشود حال با بكار بردن تابع تستT برگردانده ميشود كه به معني انجام تست است.آنگاه همه اعمال اين بخش كه بطور مثبت تست شده است. ارزيابي ومقدار آخرين عمل به عنوان مقدارcond برگردانده ميشود در مثال ما تنها عملimprobable’ تعيين ميشود كه مقدارimprobable (غيرمحتمل) را برميگرداند از آنجايي كه اين مقدارcond را تعيين ميكند و عبارت cond تنها عبارت بدنه تابعverbalize-prop است. نتيجه فراخواني improbable ,((verbalize-prop 0.33) است. توضيح اينكهاگرما (verbalize- prop 0.1)را وارد كنيم مقدارvery- improbable را برميگرداند زيرا تست هر سه با شكست مواجه شده و بايد بخش (T ’very-improbable)ارزيابي شوددر اين حالت نمادT به عنوان تستي كه هميشهT برميگرداند استفاده شده است بنابراين مقدار اين پيشنهاد
very- improbable است.
E. تعريف توابع بازگشتي
دومين روش اصلي براي تعريف كنترل جريان درLisp تعاريف توابع بازگشتي هستند. تابعي كه از تعريفش بعنوان جزئي از تعريفش استفاده ميكند بازگشتي نام دارد. بنابراين، يك تعريف بازگشتي، تا جايي كه امكان دارد مسئلهاي را به قسمتهاي كوچكتر تقسيم ميكند سپس اين قسمتهاي كوچكتر را با استفاده از توابع مشهور و جمع پاسخهاي يكسان حل كرده و حل برنامه را كامل ميكند. بازگشت يك روش طبيعي براي كنترل ساختارهاي دادهاي است كه اندازه معيني ندارد. مانند ليستها، درختها و گرافها. بنابراين براي مسئلههايي كه در يك فاصله از حالات دنبال حل كانديد ميگردند مناسب است.
Lisp اولين زبان برنامهنويسي كاربردي بود كه با روش معين تعريف تعاريف بازگشتي را پشتيباني كرده است. ما از دو مثال كوچك براي نشان دادن بازگشت درLisp استفاده خواهيم كرد. اولين مثال براي تعيين طول يك ليست طويل دلخواه استفاده ميشود. طول يك ليست برابر تعداد عناصر آن است. تابع بازگشتي آن به صورت زير است.
(defun length (list)
(cond ((null list) 0)
(T (+ 1 (length (cdr list))))))
وقتي يك تعريف بازگشتي تعريف ميشود. ما بايد حالتهاي اساسي راشناسايي كنيم يعني آن قسمتهايي كه نميتوانند بيشتر تجزيه شوند. مسئله اندازه وابسته به ليست است. كوچكترين مسئله اندازه در ليست، ليست خالي است. بنابراين اولين چيزي كه ما بايد مشخص كنيم تستي براي شناسايي ليست خالي است و تعيين اينكه طول ليست خالي بايد چقدر باشد تابع پيشساخته null تست ميكند كه آيا اين ليست خالي است در اين صورت t برميگرداند. از آنجايي كه ليست خالي بدون عنصر است تعريف ميكنيم كه طول ليست خالي صفر باشد كار ديگري كه بايد انجام شود تجزيه مسئله اندازه به قسمتهاي كوچكتر است كه همان مسئله ميتواند براي فسمتهاي كوچكتر استفاده شود. تجزيه ليست ميتواند با استفاده از توابع cdr,car انجام شود. به اين معني كه ما بايد تعيين كنيم تا وقتي كه ليست خالي پيدا شود عنصر اول و بقيه عناصر ليست چه كار بكنند. از آنجايي كه ما ازقبل ليست خالي را بعنوان حالت اساسي شناسايي كرديم، ميتوانيم فرض كنيم تجزيه برروي ليستي شامل حداقل يك عنصر انجام خواهد شد. بنابراين هر بار كه قادر خواهيم بود تا با بكار بردن cdr بقيه عناصر ليست را بدست آوريم، ما يك عنصر اضافي پيدا كرديم كه بايد براي افزايش تعداد عناصر ليست قبلا شناسايي شده بوسيله يك استفاده ميشود. استفاده از اين تعريف تابع(length ’( )) بلافاصله صفر برخواهد گرداند و اگر
(length ’(a b c)) را فراخواني كنيم، نتيجه 3 خواهد بود زيرا براي اينكه ليست خالي شود بايد سه فراخواني بازگشتي member انجام دهيم بعنوان مثال دوم، تعريف بازگشتي را در نظر ميگيريم كه تست ميكند كه آيا عنصر داده شده در ليست داده شده قرار دارد اگر عنصر براستي در ليست پيدا شود زير ليستي كه با عنصر پيدا شده شروع ميشود را برميگرداند اگر عنصر پيدا نشوددnil برگردانده ميشود مثال فراخوانيها هستند.
(member ’b ’(a f b d e b c)) ==> (b d e b c)
(member ’k ’(a f b d e b c)) ==> nil
مشابه تعريف بازگشتي ما ليست خالي را به عنوان حالت اساسي استفاده ميكنيم برايmember ليست خالي به اين معني است كه عنصر مورد سوال در ليست پيدا نشود. بنابراين ما بايد يك ليست را تا زماني كه عنصر مورد سوال پيدا ميشود يا ليست خالي است تجزيه ميكنيم تجزيه با استفاده ازcar وcdr انجام ميشود.car براي استخراج عنصر اول ليست به كار ميرود كه ميتواند براي كنترل اينكه با عنصر مورد سوال برابر است استفاده شود در اين حالت ميتوانيم پردازشهاي اضافي را مستقيماً متوقف كنيم اگر برابر نبود آنگاه بايد تابعmember را براي بقيه عناصر تا خالي شدن ليست بكار ببريم بنابراين ميتواند به صورت زير تعريف شود.
(defun member (elem list)
(cond ((null list) nil)
((equal elem (car list)) list)
(T (member elem (cdr list)))))
F. توابع مرتبه بالا
درLisp توابع ميتوانند بعنوان آرگومان استفاده شود تابعي كه بتواند توابع را بعنوان آرگومانهايش بپذيرد تابع مرتبه بالا ناميده ميشود. مشكلات فراواني وجود دارند كه يكي پيمايش يك ليست(يا يك درخت يا يك گراف) است كه بايد براي هر ليست عنصر تابع معيني استفاده شود. براي مثالfliter تابعي است كه تستي براي عناصر ليست بهكار ميبرد و آنهايي كه شكست ميخورند را حذف ميكند. نگاشتها توابعي هستند كه همان تابع را روي هر عنصر ليست به كار ميبرند تا ليستي از نتايج را برگردانند. تعاربف توابع مرتبه بالا ميتواند براي تعريف توابع عمومي پيمايش ليست استفاده شود كه آنها از توابع خاصي كه براي پردازش عناصر ليست بكار ميروند خلاصه ميشوند (چكيده ميشوند). به منظور پشتيباني تعاريف مرتبه بالا يك تابع خاص است كه يك تابع و دنبالهاي از آرگومانها را به عنوان آرگومان ميپذيرد و آن تابع را در آرگومانهاي آنها به كار ميبرد. بعنوان مثال با استفاده ازfuncall، تابع عموميfliter را تعريف خواهيم كرد كه ميتواند به اين صورت فراخواني شود:
(fliter ’(1 3 -9 -5 6 -3) #’plusp) ==>(1 3 6)
plusp يك تابع پيش ساخته است كه كنترل ميكند آيا يك عدد داده شده مثبت است يا نه؟ اگر باشد آن عدد را برميگرداند در غير اين صورتnil برميگرداند نماد خاص# بكار ميرود تا به مفسرLisp بگويد كه مقدار آرگومان يك شي تابعي است . تعريف به صورت زير است:
(defun fliter (list test)
(cond ((null list) list)
((funcall test (car list))
(cons (car list) (fliter (cdr list) test)))
(T (fliter (cdr list) test))))
اگر ليست خالي باشد آنگاه بسادگي برميگردد در غير اين صورت تابع تست روي عنصر اول ليست بكار ميرود. اگر تابع تست موفق شودcons بكار ميرود تا ليست حاصل را با استفاده از اين عنصر و همه عناصري كه در طول فراخواني بازگشتيfliter ازcdr و تابع تست استفاده ميكنند بسازد. اگر تابع تست براي عنصر اول با شكست مواجه شود اين عنصر بسادگي با بكاربردنfliter بصورت بازگشتي روي عناصر باقيمانده پرش ميكند. يعني اين عنصر نميتواند جزئي از ليست حاصل باشد تابع ميتواند براي بسياري از توابع مختلف تست استفاده شود مانند:
(fliter ’(1 3 A B 6 C 4) #’numberp) ==> (1 3 6 4)
(fliter ’(1 2 3 4 5 6) #’even) ==> (2 4 6)
به عنوان مثال ديگري از تعريفfliter تابع مرتبه بالا، ماميخواهيم يك تابع نگاشت ساده تعريف كنيم كه يك تابع روي همه عناصر يك ليست بكاررفته، ليستي از همه مقادير برميگرداند. اگر تابع my-map را فراخواني كنيم آنگاه تعريفي شبيه اين داريم:
(defun my-map (fn list)
(cond ((null list) list)
(T (cons (funcall fn (car list)) (my-map fn (cdr list))))))
اگر يك تابع Double وجود داشته ياشد كه تنها عدد را دو برابر كند آنگاه يك فراخواني ممكن my-map به اين صورت ميتواند باشد:
(my-map #’double ’(1 2 3 4))==> (2 4 6 8)
بارها شده كه يك تابع بايد يكبار استفاده ميشد. بنابراين اگر ما بتوانيم مستقيما تعريفي از يك تابع بعنوان آرگومان از تابع نگاشت فراهم كنيم كاملا مناسب خواهد بود براي اينكار تعريف عبارت lambda را پشتيباني ميكند. ما قبلا به طور غير رسمي نمادسازي عبارات را در بخش II بعنوان تعريف توابع بي نام يا مستعار معرفي كرديم. در Lisp عبارات lambda با استفاده از نوع خاصي از lambda تعريف ميشوند نوع عمومي عبارت lambda به اين صورت است:
(lambda ( parameter . . . ) body . . . )
يك عبارت lambda امكان ميدهد تا ما تعريف تابع را از نام تابع تشخيص دهيم عبارات lambda ميتوانند به جاي نام تابع در تابع funcall استفاده شوند مانند عبارت كه تابع double ما ميتواند باشد:
(lambda (x) (+ x x))
براي مثال: فراخواني تابع my-map بالا ميتواند با استفاده از عبارت lambda مجدداً به صورت زير بيان شود:
(my-map
’(lambda (x) (+ x x)) ’(1 2 3 4) ==> (2 4 6 8)
يك عبارت lambda يك شئ تابعي بر ميگرداند كه به نام تابع متصل نيست در تعريف
my-map ، پارامتر fn را بعنوان متغير نام تابع استفاده ميكنيم. وقتي شكل lambda محاسبه شد مفسر Lisp شئ تابعي را به متغير نام تابع متصل خواهد كرد. به اين طريق يك پارامتر تابع بصورت يك نام تابع پويا استفاده ميشود. نماد # صروري است تا به Lisp بگويد كه نه تنها يك شئ تابعي را وصل كند بلكه بايد اتصالات محلي و سراسري مقادير وابسته به شئ تابعي را نيز نگه دارد. اين تنها با استفاده از عملگر quote امكانپذير نخواهد بود (متأسفانه به دليل محدوديت جا جزئيات بيشتري داده نميشود).
G. ساير زبانهاي برنامهنويسي تابعي غير از Lisp
ما Lisp را به عنوان نماينده اصلي زبان برنامهنويسي تابعي معرفي كرديم (مخصوصاً نسخه پر استفاده Common Lisp )، زيرا هنوز هم زبان برنامهنويسي پر استفادهاي براي تعدادي از مسئلههاي هوش مصنوعي مانند فهم زبان طبيعي، استخراج اطلاعات، يادگيري ماشين، برنامهريزي AI يا برنامهنويسي ژنتيك است. دركنار Lispتعدادي از زبانهاي برنامهنويسي تابعي ديگر توسعه يافتند. ما بطور خلاصه دو عضو مشهور را ذكر ميكنيم، ML و Haskell.
ML برگرفته از Meta-Language است يك زبان برنامهنويسي تابعي با دامنه ايستاست. تفاوت اصلياش با Lisp درsyntax (نحو) است (كه بيشتر شبيه پاسكال است)، و يك نوع سيستم چند ريختي محض است (يعني بكاربردن انواع قوي و نوع استنتاجي بوسيله متغيرهايي كه نياز به اعلان ندارند). نوع هر متغير اعلان شده و عبارت ميتواند در زمان كامپايل تعيين شود. MLتعريف انواع داده خلاصه را پشتيباني ميكند، به صورتي كه در مثال زير شرح داده شده است:
datatype tree = L of int
| int * tree * tree;
خوانده ميشود’’ هر درخت دو دويي داراي يك برگ شامل يك عدد صحيح و يا يك گره
شامل يك عدد صحيح و دو درخت است( زير درختها)‘‘ در مثال بعدي، مثالي از تعريف يك تابع بازگشتي كه روي يك ساختار درخت بكار ميرود نشان داده شده است:
fun depth(L ) = 1
| depth(N(i,l,r)) =
1 + max(depth l, depth r);
تابع depth نگاشتي از درختها به اعداد است. عمق هر برگ 1 است و عمق هر درخت ديگر 1 بعلاوه بيشترين عمق زير درختهاي چپ و راست آن است.
Haskell شبيه ML است: Syntax مشابهي بكار ميبرد، دامنهاش هم ايستاست و از همان روش استنتاج استفاده ميكند. با ML در اين تفاوت دارد كه يك زبان كاملاً تابعي است. به اين معني است كه به اثرات جانبي اجازه نداده و شامل هيچ نوع ويژگي دستوري نيست، در اصل متغير و جملات انتسابي ندارد. بعلاوه از يك تكنيك ارزيابي كند استفاده ميكند، كه زير عبارت را ارزيابي نميكند تا موقع نياز مقدارش معلوم باشد. ليستها رايجترين ساختار داده در Haskell هستند. براي مثال [1,2,3] ليستي از سه عدد صحيح 3,2,1 است ليست [1,2,3] در Haskell در واقع خلاصهنويسي شده ليست 1:(2:(3:[ ] )) است، كه[ ] ليست خالي است و: عملگري ميانوندي است كه آرگومان اولش را جلوي آرگومان دومش اضافه ميكند( يك ليست). بعنوان مثالي از يك تابع كاربر تعريفي كه روي ليستها عمل ميكند، مسئله شمارش تعداد عناصر در يك ليست با تعريف تابع length ملاحظه ميشود.
length :: [a] -> Integer
length [ ] = 0
length (x:xs) = 1 + length xs
خوانده ميشود’’طول ليست خالي 0 است، و طول ليستي كه عنصر اولش x است و بقيه xs است،1 بعلاوه طول xs است‘‘. در Haskell تابع invocation احضار با تطبيق الگو راهنمايي ميكند، براي مثال طرف چپ معادله داري الگوهايي مانند[ ] و x:xs است. در يك كاربرد تابع اين الگوها با پارامترهاي واقعي تطبيق داده ميشوند [ ] ) تنها با ليست خالي مطابقت ميكند، و x :xs با هر ليست با حداقل يك عنصر با موفقيت تطبيق ميكند، x به عنصر اول و xs به بقيه ليست متصل ميشوند). اگر تطبيق موفقيتآميز باشد طرف راست معادله ارزيابي و بعنوان نتيجه كاربرد برگردانده ميشود. اگر با شكست مواجه شود معادله بعدي سعي ميشود، و اگر همه معادلات با شكست مواجه شوند، حاصل يك خطا ميشود.
اين پايان كوتاه ما از’’سفر در Lisp ‘‘ است. ما تنهاي توانستيم جنبه بسيار مهم Lisp را مطرح كنيم. خوانندگان علاقمند به جزئيات خاص بيشتر بايد حداقل يكي از كتابهاي مذكور در آخر مقاله را كنكاش كنند. بقيه اين مقاله معرفي الگوي برنامهنويسي ديگري بنام Prolog است كه در برنامهنويسي AI بطور گسترده مورد استفاده قرار ميگيرد.
IV.ساير روشهاي برنامهنويسي
در اين مقاله قبلاً زبانهاي AI را با روشهاي برنامهنويسي دستوري مقايسه كرديم. زبانهاي شيء گرا به الگوي برنامهنويسي مشهور ديگري تعلق دارند. در اين جور زبانها اولين وسيله براي تعيين مسئلهها، تعيين خلاصه ساختارهاي داده است كه كلاسها، اشياءنام دارند. يك كلاس شامل يك ساختار