96
Common Lisp 12 խնդիր Արﬔն Բադալյան 2 մայիսի 2016

Common Lisp․ 12 խնդիր

Embed Size (px)

DESCRIPTION

Common Lisp ծրագրավորման լեզվի ներածություն է՝ 12 հայտնի խնդիրների օրինակներով։

Citation preview

Page 1: Common Lisp․ 12 խնդիր

Common Lisp

12 խնդիր

Արմեն Բադալյան

2 մայիսի 2016

Page 2: Common Lisp․ 12 խնդիր

Կարծիքների և առաջարկների համար�

[email protected]

2016, Armen BadalianThis work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License

Page 3: Common Lisp․ 12 խնդիր

Բովանդակٳթյٳն

1 Ֆակտորիալ 6

2 Գٳշակել մտապահած թիվը 13

3 Քառակٳսի հավասարٳմ 18

4 Մեծագٳյն ընդհանٳր բաժանարար 22

5 Պատկերի մակերեսը 27

6 Պարզ և կատարյալ թվեր 31

7 Թվերի վերածٳմը բառերի 36

8 Կառٳցել Մորզեի այբٳբեն 41

9 Տեքստի բառապաշարի վերլٳծٳթյٳն 49

10 Տարեվերջի քննٳթյٳններ 59

11 Կրկնվող ֆայլերի որոնٳմ 67

12 Վեկտորային գրաֆիկա 75

A Lisp ընտանիքի լեզٳներ 83

B Quicklisp 87

Առարկայական ցանկ 89

Գրականٳթյٳն 93

3

Page 4: Common Lisp․ 12 խնդիր
Page 5: Common Lisp․ 12 խնդիր

Առաջաբան

Լեզٳն, որը չի ազդٳմ ծրագրավորման մասին

ձեր պատկերացٳմների վրա, չարժե սովորել։

— Ալան Պերլիս

A language that doesn’t affect the way you

think about programming, is not worth knowing.

— Alan Perlis

Այս գիրքը մի փորձ է իմ երախտագիտությունը հայտնելու Լիսպ-աշխարհին, ևմի փորձ է Լիսպ-հանրությանը վարձահատույց լինելու այն հսկայական գիտելիք֊ների համար, որ ես ստացել եմ Lisp լեզուն և նրատարատեսակներն ուսումնասիրե֊լիս։ Սա նաև մի համեստ փորձ է սկսնակ ծրագրավորողներին ներկայացնել Com֊mon Lisp լեզուն հայերենով։

Ես նպատակ չեմ ունեցել ամբողջությամբ ներկայացնել Common Lisp լեզուն։Այն հարուստ հնարավորություններով ընդհանուր օգտագործման ծրագրավորմանլեզու է, որում ստանդարտացված են շուրջ հազար ֆունկցիաներ, մակրոսներ,հատուկ կառուցվածքներ ու փոփոխականներ (լեզվի ստանդարտի հիման վրագրված [14] գիրքն ունի 1030 էջ)։ Ավելին, ես նպատակ չեմ ունեցել լեզվի հնարավո֊րությունները ներկայացնել համակարգված ու ճշգրիտ. ամեն մի խնդրի լուծումքննարկելիս այս կամ այն միջոցը ներկայացվում է այնքան, ինչքան հարկավորէ տվյալ պահին։ Կարծում եմ, որ դա թույլ է տալիս ուշադրություն չդարձնելխնդրի լուծման հետ կապ չունեցող մանրամասների վրա։ Հաճախ ես խնդրիլուծման գեղեցկությունն ու արդյունավետությունը զոհել եմ ևս մի նոր լեզվականկառուցվածք ներկայացնելու օգտին։

5

Page 6: Common Lisp․ 12 խնդիր

Խնդիր 1

Ֆակտորիալ

Ֆունկցիոնալ ծրագրավորման հնարավորություններով օժտված լեզուների մասինպատմելիս սովորաբար որպես առաջին օրինակ ներկայացնում են դրական ամ֊բողջ թվի ֆակտորիալը հաշվելու խնդիրը։ Իսկ քանի որ Lisp լեզուն առաջինն էր,որ հնարավորություն տվեց ծրագրավորել ֆունկցիոնալ եղանակով, և CommonLisp լեզուն էլ մեծ Lisp ընտանիքի ամենահայտնի ներկայացուցիչներից է, ապաայստեղ էլ հետևենք այդ ավանդույթին և լեզվի հետ ծանոթությունը սկսենք հենցֆակտորիալի հաշվման խնդրի փոքր-ինչ ընդլայնված տարբերակից.

Դիցուք տրված է դրական ամբողջ թվերի N = (n1, n2, . . . , nk) ցուցակը, և պա֊հանջվում է կազմել նույն այդ ni թվերի ֆակտորիալների N ! = (n1!, n2!, . . . , nk!)

ցուցակը։Ցٳցակը, որ արդեն երկու անգամ հիշատակվեց խնդրի ձևակերպման մեկ

նախադասության մեջ, Lisp ընտանիքի լեզուների առանցքային հասկացություն֊ներից է։ Ամեն ինչ, թե՛ տվյալները, թե՛ ծրագրերը Lisp լեզվում ներկայացվում ենցուցակների տեսքով։ (Ավելի խիստ ասած՝ ամեն ինչ ներկայացվում է սիմվոլիկարտահայտٳթյٳնների (S-expression) [10] տեսքով, իսկ ցուցակները S-արտահայ֊տությունների պարզեցված ներկայացում են։) Ցուցակները պարփակված են «(»և «)» նիշերի մեջ, իսկ որպես տարրեր կարող են պարունակել կա՛մ Lisp լեզվիտարրական միավորներ՝ ատոմներ (թվեր, սիմվոլներ և այլն), կա՛մ ենթացուցակ֊ներ (այսպիսի ցուցակները կոչվում են ներդրված )։ Օրինակ�

(a1 654 "text" ("21" 6.78) 100)

ցուցակը պարունակում է «a1», «654», «"text"» և «100» ատոմները, ինչպես նաև«("21" 6.78)» ենթացուցակը, որն էլ իր հերթին կազմված է «"21"» և «6.78»ատոմներից։

Արտահայտության ատոմ լինելը կարելի է ստուգել atom պրեդիկատով, իսկցուցակ լինելը՝ listp պրեդիկատով։

6

Page 7: Common Lisp․ 12 խնդիր

(atom 2) ⇒ T(atom "abc") ⇒ T(atom (list 1 2 3)) ⇒ NIL(listp 3) ⇒ NIL(listp (list 6 8)) ⇒ TԱյս օրինակներում T սիմվոլով Lisp լեզուն ներկայացնում է բուլյան «ճշմարիտ»

արժեքը, իսկ NIL սիմվոլով՝ բուլյան «կեղծ» արժեքը։ (Պետք է նշել, որ nil-ը Com֊mon Lisp լեզվի միակ տրամաբանական «կեղծ» արժեքն է։ nil-ից բացի բոլորարժեքները համարվում են «ճշմարիտ», այդ թվում նաև 0 թվային արժեքը։) NILսիմվոլը կարող է հանդես գալ որպես նաև դատարկ ցուցակի ներկայացում։ Այսդեպքում այն համարժեք է () ցուցակին։ NIL-ը նաև լեզվի միակ սիմվոլն է, որիհամար atom և listp պրեդիկատները վերադարձնում են T արժեքը.

(atom nil) ⇒ T(listp nil) ⇒ T

Common Lisp լեզվի հիմնական խնդիրն է հաշվարկել ցուցակների տեսքովտրված արտահայտությունները։ Հաշվարկվում է ամեն ինչ, բոլոր արտահայտութ֊յունները, և ամեն ինչի համար ստացվում է հաշվարկման արդյունք։ Նշեցինք,որ atom և listp պրեդիկատներով կազմված արտահայտությունների հաշվարկիցստացվում են բուլյան արժեքներ՝ ներկայացված T և NIL հաստատուններով։ Եթեփորձենք հաշվարկել «456» ատոմը, ապա կստանանք 456 արժեքը: Եթե հաշվար֊կենք «(+ 4 8)» արտահայտությունը՝ կստանանք 12 արժեքը։ Եթե հաշվարկենք«(list 1 2 3)» արտահայտությունը, որտեղ list ֆունկցիան կառուցում և վե֊րադարձնում է իր արգումենտների ցուցակը, ապա կստանանք «(1 2 3)» ցուցակըորպես արժեք։

Երբ հաշվարկվում է ցուցակը, նրա առաջին տարրը մեկնաբանվում է որպեսֆունկցիայի, մակրոսի կամ Common Lisp լեզվի հատուկ կառուցվածքի անուն, ևայդանվան հետ կապված գործողությունը կիրառվում է ցուցակի հաջորդտարրերինկատմամբ։ Այլ կերպ ասած՝ հաշվարկվող ցուցակի առաջին տարրը գործողٳթ֊յٳնն (օպերատոր) է, իսկ հաջորդները (եթե այդպիսիք կան)՝ արգٳմենտները(օպերանդներ)։ Օրինակ, երբ որպես արտահայտություն հաշվարկվի «(* 2 3 4)»ցուցակը, ապա «*» սիմվոլը, որ Lisp լեզվի բազմապատկման ֆունկցիայի անուննէ, կկիրառվի «2», «3» և «4» արտահայտությունների նկատմամբ։

Նշեցինք, որ և՛ ծրագրերը, և՛ տվյալները ներկայացվում են ցուցակներով (կամսիմվոլիկ արտահայտություններով) և անխտիր հաշվարկվում են բոլոր արտահայ֊տությունները։ Բնական է՝ պետք է մի միջոց, որը թույլ կտա հարկ եղած դեպքումարտահայտությունները պաշտպանել հաշվարկումից և դրանք ներկայացնել որ֊պես տվյալներ։ Այդմիջոցը quote հատուկ կառուցվածքն է, որ ներկայանում է նաև«'» (ապաթարց) համառոտ գրառմամբ։

(quote (+ 4 5 6) ⇒ (+ 4 5 6)

7

Page 8: Common Lisp․ 12 խնդիր

'(+ 4 5 6) ⇒ (+ 4 5 6)(+ 4 5 6) ⇒ 15quote հատուկ կառուցվածն անհրաժեշտ է, օրինակ, այն դեպքում, երբ որպես

որևէ ֆունկցիայի արգումենտ փոխանցում ենք ցուցակ։ Օրինակ, «(1 2 3 4)»ցուցակի երկարությունը հաշվելու համար եթե այն հենց այս տեսքով փոխանցենքlist-length ֆունկցիային, ապա կստանանք հաշվարկման սխալ, որովհետևփորձ կարվի ցուցակի առաջին տարրը՝ «1» սիմվոլը, մեկնաբանել որպես օպերա֊տոր։ Իսկ «'(1 2 3 4)» տեսքով փոխանցելիս կստանանք ճիշտ արդյունք.

(list-length '(1 2 3 4)) ⇒ 4

Սկսեցինք ցուցակներից, խոսքը խոսք բերեց ու քիչ էր մնում մոռանայինք մերխնդրի մասին։ Եվ այսպես, թող f -ը լինի n դրական ամբողջ թվի ֆակտորիալըհաշվող ֆունկցիան՝ f(n) = n!։ Այն կարելի է սահմանել կամ որպես արտադրյալ՝

f(n) = 1 · 2 · . . . · n =n∏

k=1

k, (1.1)

կամ որպես (1.2) անդրադարձ (ռեկուրսիվ) առնչություն.

f(n) =

1, երբ n = 1,

n · f(n− 1), երբ n > 1 :(1.2)

Այս վերջին ֆունկցիայիտեսքը հուշում է, որայն Common Lisp լեզվով ծրագրավորե֊լու համարմեզ հարկավոր են.ա) ֆունկցիայի սահմանմանմեխանիզմ, բ) պայմանիստուգմանմեխանիզմ, գ) թվերի հավասարության ստուգման, բազմապատկման ևհանման գործողություններ։

Գլոբալ ֆٳնկցիայի սահմանման համար Common Lisp լեզուն նախատեսում էdefun մակրոսը։ Այն ստանում է սահմանվող ֆունկցիայի անունը, արգումենտներիցուցակը և մարմինը կազմող արտահայտությունների հաջորդականությունը.

(defun ⟨name⟩ (⟨arguments⟩) ⟨body⟩)

Օրինակ, r հիմքի շառավիղ և h բարձրություն ունեցող կոնի ծավալը

V =1

3hπr2 (1.3)

բանաձևով հաշվելու համար կարող ենք սահմանել հետևյալ ֆունկցիան.

1 (defun cone-volume (r h)2 (/ (* pi h r r) 3))

որի անունն է «cone-volume», արգումենտների ցուցակում «r» և «h» սիմվոլներնեն, իսկ մարմինն է «(/ (* pi h r r) 3)»արտահայտությունը, որը (1.3) բանաձևինախածանցային (prefix) գրառումն է: cone-volume ֆունկցիան սահմանելիս օգ֊տագործվել են նաև Common Lisp լեզվի pi (π) հաստատունը, / (բաժանում) և *(բազմապատկում) ֆունկցիաները։

8

Page 9: Common Lisp․ 12 խնդիր

Ծրագրերում պայմանի ստուգումով ճյուղավորումներ կատարելու համար Com֊mon Lisp լեզուն տրամադրում է if հատուկ կառուցվածքը.

(if ⟨condition⟩ ⟨then-branch⟩ ⟨else-branch⟩?)

Եթե ⟨condition⟩ արտահայտության հաշվարկված արժեքը տարբեր է nil արժեքից(ճշմարիտ է), ապա հաշվարկվում և վերադարձվում է ⟨then-branch⟩ արտահայ֊տության արժեքը, հակառակ դեպքում՝ ⟨else-branch⟩ արտահայտության արժեքը։Օրինակ�

(if (> 5 6) 1 0) ⇒ 0(if (< 5 6) 1 0) ⇒ 1

Թվերի համեմատման համար Common Lisp լեզուն ունի բոլոր վեց ֆունկցիա֊ները՝ = (հավասար է), /= (հավասար չէ), > (մեծ է), >= (մեծ է կամ հավասար),< (փոքր է) և <= (փոքր է կամ հավասար)։ Բոլոր այս ֆունկցիաները սպասումեն մեկ կամ ավելի արգումենտներ։ = ֆունկցիան վերադարձնում է T, եթե նրաբոլոր արգումենտների արժեքները հավասար են։ Նմանապես, /= ֆունկցիանվերադարձնում է T, եթե այն չունի իրար հավասար արժեքներով արգումենտներ։Օրինակ�

(= 1 2 3) ⇒ NIL(= 14 14) ⇒ T(/= 1 2 3) ⇒ T(/= 25 25) ⇒ NIL

> ֆունկցիան վերադարձնում է T այն դեպքում, երբ նրա արգումենտների արժեք֊ները ձախից աջ մոնոտոն նվազում են, իսկ >= ֆունկցիան «ճշմարիտ» արժեք էվերադարձնում, երբ իր արգումենտների արժեքները մոնոտոն չաճող են։ Օրինակ�

(> 9 8 7 6) ⇒ T(> 9 8 8 7) ⇒ NIL(>= 9 8 8 7) ⇒ T

Նույն կերպ < և <= ֆունկցիաները ստուգում են իրենց արգումենտների համապա֊տասխանաբար մոնոտոն աճող և մոնոտոն չնվազող լինելը։

(< 1 2 3 4) ⇒ T(< 1 2 2 3) ⇒ NIL(<= 1 2 2 3) ⇒ TԹվաբանական գումարման՝ +, և բազմապատկման՝ *, ֆունկցիաները վերա֊

դարձնում են իրենց արգումենտների արժեքների գումարն ու արտադրյալը համա֊պատասխանաբար։ Օրինակ�

(+ 1 2 3 4) ⇒ 10(* 1 2 3 4) ⇒ 24

Եթե ոչ մի արգումենտ տրված չէ, ապա + ֆունկցիան վերադարձնում է 0, իսկ *ֆունկցիան՝ 1։

9

Page 10: Common Lisp․ 12 խնդիր

Ի տարբերություն + և * ֆունկցիաների - (հանում) և / (բաժանում) ֆունկցիա֊ները սպասում են գոնե մեկ արգումենտ։ Այս ֆունկցիաների վարքը նկարագրվումէ հետևյալ կերպ.

(- a0 a1 . . . ak) ⇒ a0 − (a1 + . . .+ ak),(- a0) ⇒ −a0,(/ a0 a1 . . . ak) ⇒ a0/(a1 · . . . · ak),(/ a0) ⇒ 1/a0:

Հիմա, կարծես թե, կարող ենք սահմանել (1.2) բանաձևին համարժեք factorialֆունկցիան։ Այն ունի շատ պարզ տեսք.

1 (defun factorial (n)2 (if (= n 1)3 14 (* n (factorial (1- n)))))

Միակ նորությունն այն է, որ factorial ֆունկցիայի 4-րդ տողում n− 1 արտահայ֊տությունը հաշվելու համար օգտագործված է ոչ թե - ֆունկցիան, այլ 1- ֆունկ֊ցիան։ Այն վերադարձնում է իր արգումենտի մեկով նվազեցրած արժեքը։ Նախա֊պես սահմանված է նաև 1+ ֆունկցիան, որը վերադարձնում է իր արգումենտիարժեքը՝ ավելացրած 1։

factorial ֆունկցիան տեստավորելու համար սահմանենք test-factorialֆունկցիան, որը ստանում է n թիվը և նրա ֆակտորիալի սպասվող արժեքը՝ n!։test-factorial ֆունկցիան վերադարձնում է T, եթե n թվի factorial ֆունկցիա֊յով հաշված արժեքը հավասար է սպասվող n! արժեքին։

1 (defun test-factorial (n n!)2 (= (factorial n) n!))

Ժամանակն է նկատել, որ Common Lisp լեզվում իդենտիֆիկատորները կարողեն կազմված լինել համարյա կամայական սիմվոլներից։ Արդեն տեսանք, որ ֆունկ֊ցիայի անուներ կարող են լինել «+», «/» կամ «>=» նիշերը, կամ «1-» և «1+»թվանշանով սկսվող և տառ չպարունակող հաջորդականությունները, իսկ test-factorial ֆունկցիայում որպես արգումենտի անուն օգտագործված է n! իդեն֊տիֆիկատորը։

Զուտ հետաքրքրության համար կարող ենք factorial և test-factorialֆունկցիաները սահմանել հետևյալ անսովոր տեսքով (այնուամենայնիվ նշենք, որCommon Lisp ստանդարտն առաջարկում է անունների մեջ չօգտագործել ?, !, [, ],{ և } նիշերը).

1 ;;; factorial2 (defun ! (n) (if (= n 1) 1 (* n (! (1- n)))))

10

Page 11: Common Lisp․ 12 խնդիր

3 ;;; test-factorial4 (defun ?-! (n n!) (= (! n) n!))

Այժմ մի քանի տեստային օրինակներով համոզվենք, որ factorial ֆունկցիանիսկապես ճիշտ է հաշվում տրված թվի ֆակտորիալը։

(test-factorial 1 1) ⇒ T(test-factorial 4 24) ⇒ T(test-factorial 5 10) ⇒ NIL(test-factorial 20 2432902008176640000) ⇒ T(test-factorial 30 265252859812191058636308480000000) ⇒ TԵվ այսպես, կարող ենք փաստել, որ մեր խնդրի պահանջներից մեկն իրակա֊

նացված է. արդեն ունենք factorial ֆունկցիան, որը հաշվում է տրված դրականամբողջ թվի ֆակտորիալը։

Այժմ թող list-of-factorials ֆունկցիան N = (n0, n1, . . . , nk) ցուցակից ռե֊կուրսիվ եղանակով կառուցում էN ! = (n0!, n1!, . . . , nk!) ցուցակը։ ԵթեN -ը դատարկէ, ապա N !-ը նույնպես դատարկ կլինի։ Եթե N -ը դատարկ չէ, ապա անջատենքնրա առաջին տարրը՝ n0, և factorial ֆունկցիայով հաշվենք n0! արժեքը։ N -իմյուս տարրերից կազմենք N ′ = (n1, . . . , nk) ցուցակը, որի նկատմամբ նորիցկիրառելով list-of-factorials ֆունկցիան՝ կստանանք N ′! = (n1!, . . . , nk!)

ֆակտորիալների ցուցակը։ Պարզ է արդեն, որ N ! ցուցակը ստանալու համարբավական է n0! տարրը կցել N ′! ցուցակի սկզբից։

list-of-factorials ֆունկցիան սահմանելու համար օգտագործելու ենք Լիսպլեզվի car, cdr և cons ֆունկցիաները։

1 (defun list-of-factorials (ns)2 (if (endp ns)3 '()4 (cons (factorial (car ns))5 (list-of-factorials (cdr ns)))))

car ֆունկցիան վերադարձնում է իր արգումենտում տրված ցուցակի առաջինտարրը՝ «գլٳխը» (head)։ Եթե տրված ցուցակը դատարկ է, ապա վերադարձնումէ NIL։

(car '(1 2 3)) ⇒ 1(car '((1 2) 3 4)) ⇒ (1 2)

cdr և rest ֆունկցիաները վերադարձնում են տրված ցուցակի «պոչը» (tail)՝ առա֊ջինից բացի մյուս բոլոր տարրերի ցուցակը։ Եթե արգումենտը դատարկ ցուցակ է,ապա այս ֆունկցիաները նույնպես վերադարձնում են NIL։

(cdr '(1 2 3)) ⇒ (2 3)(rest '((1 2) 3 4)) ⇒ (3 4)

11

Page 12: Common Lisp․ 12 խնդիր

Երբ car և cdr ֆունկցիաները հնարավորություն են տալիս տրոհել ցուցակը(S-արտահայտությունը), cons ֆունկցիան կատարում է ճիշտ հակառակը՝ իրար էկցում տրված ⟨car ⟩ և ⟨cdr⟩ տարրերը և կառուցում է նոր ցուցակ.

(cons 'a '()) ⇒ (a)(cons 'a '(b c d)) ⇒ (a b c d)(const '(1 2) '(3 4)) ⇒ ((1 2) 3 4)

list-of-factorials ֆունկցիայի երկրորդ տողում օգտագործված endp ֆունկ֊ցիան վերադարձնում է T, եթե նրա արգումենտում տրված ցուցակը դատակ է՝ ()(կամ NIL)։ 4-րդ տողում factorial ֆունկցիան կիրառված է ns ցուցակի «գլխի»նկատմամբ, իսկ 5-րդ տողում ns ցուցակի «պոչի» նկատմամբ կիրառված է list-of-factorials ֆունկցիայի ռեկուրսիվ կանչ։ Այնուհետև այդ երկու արդյունքներնիրար են կցվում cons ֆունկցիայով։

Տեստավորել list-of-factorials ֆունկցիան, կնշանակի սահամանել մի նորֆունկցիա, որը ստանում է ամբողջ թվերի ցուցակ և այդ նույն թվերի ֆակտո֊րիալների ցուցակը, ապա առաջին ցուցակից list-of-factorials ֆունկցիայովստանում է ֆակտորիալների ցուցակը և համեմատում է տրված երկրորդ ցուցակիհետ։ Այդ նոր ֆունկցիան կարող է ունենալ այսպիսի տեսք.

1 (defun test-list-of-factorials (ns ns!)2 (equal (list-of-factorials ns) ns!))

Երկրորդ տողում գրված equal ֆունկցիան վերադարձնում է T, երբ տրված երկուօբյեկտները տրամաբանորեն հավասար են (ավելի պարզ ասած՝ նույնն է դրանցարտածվող տեսքը)։

Վերջապես, list-of-factorials ֆունկցիան կտեստավորվի հետևյալ եղա֊նակով.

(test-list-of-factorials '(3 5 10) '(6 120 3628800)) ⇒ T(test-list-of-factorials '(3 5 10) '(6 777 3628800)) ⇒ NIL

12

Page 13: Common Lisp․ 12 խնդիր

Խնդիր 2

Գٳշակել մտապահած թիվը

Մտապահած թիվը գٳշակելٳ խաղը բինար որոնման ալգորիթմի կիրառությանմիօրինակ է։ Խաղին մասնակցում են երկու հոգի։ Առաջին խաղացողը մտապահում էպայմանավորված [bl; bu]միջակայքի մի թիվ, իսկ երկրորդ խաղացողը, հերթակա֊նությամբ անվանելով մոտարկումներ, փորձում է գուշակել այդ թիվը։ Անվանվածմոտարկումներին առաջին խաղացողն արձագանքում է «ճիշտ է», երբ երկրորդխաղացողը գուշակել է թիվը, «փոքր է», երբ մոտարկումըփոքր է մտապահածթվիցև «մեծ է», երբ մոտարկումը մեծ է մտապահած թվից։

Խաղը մոդելավորելու համար պայմանավորվենք, որ խաղացողներից մեկը՝ ովմտապահում է թիվը, օգտագործողն (user) է, իսկ մյուս խաղացողը՝ ով գուշակումէ թիվը, ծրագիրն է։ Խաղը սկսվում է number-guessing-game ֆունկցիայի կանչով,որին որպես արգումենտ տրվում է խաղի միջակայքի վերին սահմանը՝ range, իսկներքևի սահմանն ընդունվում է զրոն։

1 (defun number-guessing-game (range)2 (format t "Մտապահեք 0-ից ~D միջակայքի ամբողջ թիվ։~%" range)3 (force-output)4 (sleep 4)5 (search-number-between 0 range))

Այս ֆունկցիայի 2-րդ տողում օգտագործված format ֆունկցիան նախատեսված էտեքստերի ֆորմատավորման համար։

(format ⟨destination⟩ ⟨control-string⟩ ⟨argument⟩∗)

⟨control-string⟩ պարամետրը պարունակում է ֆորմատավորման կանոնները։ Եթեայն սովորական տող է, ապա նույնությամբ արտածվում է ⟨destination⟩ հոսքի մեջ։Եթե պարունակում է ~ (թիլդա) նիշով սկսվող ֆորմատավորման հրահանգներ,ապա ⟨argument⟩-ներից հերթականը տեղադրվում է ձևավորման տողում՝ ըստտվյալ ֆորմատավորման հրահանգի։ Օրինակ, *standard-output* նախապեսսահմանված հոսքում արտածենք 72 թիվը՝ երկուական, 123 թիվը՝ ութական, 4122

13

Page 14: Common Lisp․ 12 խնդիր

թիվը՝ տասական և 3852 թիվը՝ տասնվեցական ներկայացումներով։ format ֆունկ֊ցիայի կիրառումը կունենա

1 (format t "BIN=~8,'0B; OCT=~O; DEC=~D; HEX=~4X~%"2 72 123 4122 3852)

տեսքը, իսկ *standard-output* հոսքում կարտածվի ահա այսպիսի արդյունք.

BIN=01001000; OCT=173; DEC=4122; HEX= F0C

Ֆորմատավորման հրահանգները սկսվում են ~ նիշով, որին հաջորդում ենիրարից ստորակետերով անջատված հրահանգի պարամետրերը (դրանք պար֊տադիր չեն) և բուն հրահանգի նիշը։ Օրինակ, այստեղ օգտագործված «~8,'0B»հրահանգը ցույց է տալիս, որ երկուական արտածման դեպքում պետք է հատկաց֊նել 8 դիրք և սկզբի չզբաղված դիրքերը լրացնել 0 նիշով1:

format ֆունկցիան ձևավորած տեքստն արտածում է ⟨destination⟩ պարամետ֊րով որոշվող հոսքի վրա և որպես արժեք վերադարձնում է NIL։ Եթե ⟨destination⟩-ըt է, ապա որպես արտածման հոսք ընդունվում է արտածման ստանդարտ հոսքը(*standard-output*)։ Իսկ եթե ⟨destination⟩-ը nil է, ապա format ֆունկցիանարտածում չի կատարում և ձևավորված տողը վերադարձնում է որպես արժեք։

force-output ֆունկցիան անմիջապես արտածման է ուղարկում բուֆերաց֊ված հոսքի պարունակությունը։ Սա օգնում է վերացնել արտածման հապաղում֊ները և շատ կարևոր է ինտերակտիվ աշխատանքի համար։ Եթե այս ֆունկցիայիարգումենտում որևէ հոսք տված չէ, ապա այն աշխատում է *standard-output*հոսքի հետ։

Ինչպես և երևում է անունից, sleep ֆունկցիան ծրագրի աշխատանքը դադա֊րեցնում է իրարգումենտումտրված վայրկյաններիտևողությամբ։ Մենքայս ֆունկ֊ցիան օգտագործել ենք խաղացողին թիվ մտապահելու 4 վայրկյան տրամադրելուհամար։

Նկարագրված number-guessing-game ֆունկցիան պարզապես դեկորացիաէ՝ խաղը սկսելու ձև։ Խաղի բուն տրամաբանությունը ծրագրավորենք search-number-between ֆունկցիայով, որն իր արգումենտներում ստանում է որոնմաններքևի՝ bl, և վերևի՝ bu, սահմանները։ Այնուհետև, ըստ բինար որոնման ալգորիթ֊մի, պետք է հաշվել սահմանների թվաբանական միջինը՝ m = (bl + bu)/2, և այդարժեքն առաջարկել որպես հնարավոր պատասխան։

Հարցերը տրվում և պատասխաններն ընթերցվում են նախապես սահմանված*query-io* հոսքի միջոցով։ Սա երկուղղված՝ միաժամանակ և՛ ներմուծման, և՛

1Այս գրքի ծավալները հնարավորություն չեն տալիս ամբողջությամբ ներկայացնել CommonLisp լեզվի ամենաճկուն և ամենահարուստ հնարավորություններով օժտված format ֆունկցիան։Հետագա օրինակներում դեռ առիթ կունենանք հանդիպելու այս ֆունկցիային, բայց մինչ այդլիարժեք տեղեկություններ կարելի է ստանալ [14] և [2] գրքերից։

14

Page 15: Common Lisp․ 12 խնդիր

արտածման հոսք է, որ նախատեսված է, ինչպես երևում է անունից, հարցուպա֊տասխան կազմակերպելու համար։ format ֆունկցիան հենց *query-io* հոսքիվրա է արտածում «Դուք մտապահել եք ... թի՞վը։» հարցը, և հենց այդ հոսքից էread-line ֆունկցիան կարդում պատասխանը։

Առաջարկված պատասխանին օգտագործողը կարող է արձագանքել «Այո»,եթե ծրագիրը ճիշտ է գուշակել մտապահած թիվը, և այս դեպքում խաղն ավարտ֊վում է։ Կարող է պատասխանել «<» կամ «>», որ համապատասխանաբար նշանա֊կում են, թե ծրագրի առաջարկած մոտակումը փոքր է կամմեծ է մտապահած թվից։Եթե օգտագործողը տվել է «<» պատասխանը, ապա որոնումը պետք է վերսկսել[m; bu]միջակայքում, եթե տրվել է «>» պատասխանը՝ ապա [bl;m]միջակայքում։

1 (defun search-number-between (b-lower b-upper)2 (let ((middle (round (+ b-lower b-upper) 2)))3 (format *query-io* "Դուք մտապահել եք ~d թի՞վը։" middle)4 (force-output *query-io*)5 (let ((ans (read-line *query-io*)))6 (cond ((string-equal ans "Այո")7 (format t "Խաղն ավարտված է։~%") t)8 ((string= ans ">")9 (search-number-between b-lower middle))10 ((string= ans "<")11 (search-number-between middle b-upper))))))

Common Lisp լեզվի let հատուկ կառուցվածքը իր մարմնում ստեղծում է լեքսի֊կական (լոկալ) փոփոխականներ և դրանց հետ կապում արժեքներ.

(let (⟨variable⟩∗) ⟨body⟩∗)

⟨variable⟩-ով տրվում է փոփոխականների հայտարարման ցուցակը։ Եթե փոփո֊խականին պետք է վերագրել նախնական արժեք, ապա կիրառվում է «(⟨variable⟩⟨value⟩)» տեսքը, հակառակ դեպքում նշվում է միայն փոփոխականի անունը, և այդդեպքում ⟨value⟩-ն համարվում է NIL։ Օրինակ, եթե հաշվարկվի

1 (let (a (b 123) (c "text"))2 (list a b c))

կոդը, ապա որպես արդյունք կստացվի «(NIL 123 "text")» ցուցակը, որտեղերևում է, թե a, b և c լոկալ փոփոխականներն ինչ արժեքներ են ստացել։

search-number-between ֆունկցիայի 2-րդ տողում let կառուցվածքի օգնութ֊յամբ middle լեքսիկական փոփոխականին է կապվել (bl+ bu)/2 բանաձևի արժեքը։round ֆունկցիան իր առաջին արգումենտը բաժանում է երկրորդին, ապա կլորաց֊նում է արդյունքը՝ թվաբանական հայտնի կանոնով։ Եթե երկրորդ արգումենտըտրված չէ, ապա այն համարվում է 1։

15

Page 16: Common Lisp․ 12 խնդիր

5-րդ տողում օգտագործողի պատասխանը read-line ֆունկցիայով ընթերց֊վում է *query-io* հոսքից և կապվում է ans փոփոխականի հետ։ Ծրագիրըսպասում է, որ ans փոփոխականը կարող է ունենալ երեք արժեք՝ «Այո», «<»և «>»։ Այս տարբերակների մեջ ընտրություն կատարելու համար օգտագործվածէ cond մակրոսը։ Եթե if կառուցվածքը թույլ է տալիս ընտրություն կատարելպայմանի ճշմարիտ կամ կեղծ լինելու տարբերակների մեջ, ապա cond մակրոսիմիջոցով կարելի է նկարագրել բազմաթիվ պայմաններ և ամեն մի պայմանինհամապատասխանեցնել արտահայտություններ։

(cond (⟨test⟩0 ⟨body⟩0)(⟨test⟩1 ⟨body⟩1)...(⟨test⟩k ⟨body⟩k))

Որպես cond մակրոսի հաշվարկման արդյունք հաշվարկվում և վերադարձվումէ այն առաջին ⟨body⟩i արտահայտության արժեքը, որի համար ⟨test⟩i պայմանըճշմարիտ է։ search-number-between ֆունկցիայի 6-րդ տողում string-equalֆունկցիայով ստուգվում է ans փոփոխականի և «Այո» տողի հավասար լինելը։(Այս string-equal ֆունկցիան անտեսում է մեծատառերի ու փոքրատառերիտարբերությունը. օգտագործողը կարող է որպես պատասխան ներածել «այո»կամ «ԱՅՈ»։) Եվ, եթե «(string-equal ans "Այո")» պայմանը ճշմարիտ է, ապաարտածվում է «Խաղն ավարտված է։» տողը և վերադարձվում է T արժեքը։ Եթե⟨test⟩i պայմաններից որևէմեկը ճշմարիտարժեք չի վերադարձնում, cond մակրոսիկիրառման արդյունքը համարվում է NIL։

* * *Common Lisp լեզվի մի քանի ազատ իրականացումների հետ աշխատանքի համառոտնկարագրությունը բերված է A հավելվածում։ Բայց այս օրինակի ցուցադրությունն ավար֊տին հասցնելու համար ենթադրենք, թե մեր համակարգում առկա է Armed Bear CommonLisp իրականացման abcl.jar ֆայլը։ Գործարկենք այն.

$ java -jar abcl.jar

Այս հրամանի կատարման սկզբում արտածվում է որոշ ինֆորմացիա, ապա Common Lispինտերպրետատորը սպասում է օգտագործողի հրամաններին.

CL-USER(1): _

Հիմա ենթադրենք՝ մեր սահմանած երկու ֆունկցիաները գրառված են ex02.lisp ֆայլումև փորձենք ինտերպրետատորի կատարման միջավայր բեռնել այդ ֆայլը։ Ծրագրերըպարունակող ֆայլերը կատարման միջավայր են բեռնվում load ֆունկցիայով.

(load "ex02.lisp")

16

Page 17: Common Lisp․ 12 խնդիր

Ծրագրի հաջող բեռնումից հետո կարող ենք սկսել խաղը։ Օրինակ, ուզում ենք խաղալ[0; 1000]միջակայքում և ծրագիրը առաջարկում է մտապահել մի թիվ.

(number-guessing-game 1000)Մտապահեք 0-ից 1000 միջակայքի ամբողջ թիվ։

Այնուհետև, եթե, օրինակ, մտապահենք 46 թիվը, ապա ծրագրի հետ կունենանք հետևյալերկխոսությունը։

Դուք մտապահել էիք 500 թի՞վը։ >Դուք մտապահել էիք 250 թի՞վը։ >Դուք մտապահել էիք 125 թի՞վը։ >Դուք մտապահել էիք 62 թի՞վը։ >Դուք մտապահել էիք 31 թի՞վը։ <Դուք մտապահել էիք 46 թի՞վը։ այոԽաղն ավարտված է։T

Եվ վերջում, ինտերպրետատորի հետ աշխատանքն ավարտում ենք quit ֆունկցիայով։

17

Page 18: Common Lisp․ 12 խնդիր

Խնդիր 3

Քառակٳսի հավասարٳմ

Քառակٳսի հավասարٳմ է կոչվում մեկ անհայտով երկրորդ աստիճանի բազմ֊անդամային հավասարումը, որի ընդհանուր տեսքն է.

ax2 + bx+ c = 0, a ̸= 0, (3.1)

Այս հավասարման մեջ x-ը անհայտն է, իսկ a-ն, b-ն և c-ն՝ հավասարման գործա֊կիցները։ Ըստ հանրահաշվի հիմնական թեորեմի [17] այս հավասարումն ունի ճիշտերկու կոմպլեքս արմատ, և այդ արմատները որոշվում են (3.2) բանաձևով.

x1,2 =−b±

√b2 − 4ac

2a, (3.2)

որտեղ ∆ = b2 − 4ac արտահայտությունը կոչվում է դիսկրիմինանտ: Հայտնի է,որ եթե ∆ ≥ 0, ապա քառակուսի հավասարման արմատներն արտահայտվում ենիրական թվերով և ∆ = 0 դեպքում դրանք համընկնում են։ Իսկ ∆ < 0 դեպքումհավասարման արմատներն արտահայտվում են կոմպլեքս թվերով։

Քառակուսի հավասարումը լուծելու համար նախ ստուգենք, որ a գործակիցըզրո չէ, այսինքն (3.1) հավասարումն իսկապես քառակուսային է, այնուհետև հաշ֊վենք ∆ դիսկրիմինանտը և x1, x2 արմատները՝ (3.2) բանաձևով։1: function Quadratic(a, b, c)2: if a ̸= 0 then3: ∆ = b2 − 4ac

4: v0 = − b2a, v1 =

√∆

2a

5: return v0 + v1, v0 − v1

6: end if7: end functionՊայմանի ստուգման if հատուկ կառուցվածքին և cond մակրոսին արդեն

ծանոթ ենք. այս դեպքում էլ կարող ենք օգտագործել դրանցից մեկը։ Բայց ծանո֊թանանք նաև երկու կարևոր մակրոսների՝ when և unless, որոնք if կառուցվածքիհետ կապված են ահա այսպիսի հարաբերություններով.

18

Page 19: Common Lisp․ 12 խնդիր

(when ⟨condition⟩ ⟨form⟩∗) ⇔ (if ⟨condition⟩ (progn ⟨form⟩∗) nil)(unless ⟨condition⟩ ⟨form⟩∗) ⇔ (if ⟨condition⟩ nil (progn ⟨form⟩∗))

Ավելի պարզ ասած, when մակրոսը օգտագործելիս ⟨form⟩∗ արտահայտություններիհաջորդականությունը հաշվարկվում է այն դեպքում, երբ ⟨condition⟩ պայմանըճշմարիտ է, իսկ unless մակրոսն օգտագործելիս մարմինը հաշվարկվում է պայ֊մանի կեղծ լինելու դեպքում։

if կառուցվածքի ⟨then-branch⟩ և ⟨else-branch⟩ ճյուղերում (տես էջ 9) կարելիէ գրել միայն մեկ արտահայտություն։ Հենց այդ պատճառով է, որ when և un-less մակրոսներին համարժեք արտահայտությունները կազմելիս օգտագործվել էprogn հատուկ կառուցվածքը։ Կարելի է ասել, որ progn կառուցվածքը կատարում էհաջորդման հրամանի դերը՝ հաշվարկում է իրեն տրված արտահայտություններըև որպես արդյունք վերադարձնում է վերջինի արժեքը։ Օրինակ.

(progn (+ 1 2) (- 1 2) (* 3 4)) ⇒ 12Այս ասվածից հետևում է, որ Quadratic փսևդոկոդի երկրորդ տողում գրված

պայմանը կարելի է գրել հետևյալ համարժեք տարբերակներով.

(if (/= 0 a) ...)(when (not (zerop a)) ...)(unless (zerop a) ...)

Այստեղ օգտագործված zerop պրեդիկատով ստուգվում է թվի զրո լինելը։ Նույնկերպ թվի դրական կամ բացասական լինելը կարելի է ստուգել համապատասխա֊նաբար plusp և minusp պրեդիկատներով։

let կառուցվածքը (էջ 15) լեքսիկական փոփոխականների հայտարարման ցու֊ցակում արժեքավորումները կատարում է զուգահեռաբար՝ ամենմի փոփոխականարժեք է ստանում մյուսից անկախ։ Այն դեպքում, երբ հարկավոր է փոփոխական֊ներնարժեքավորել հաջորդաբար, այսինքն՝ հնարավորություն տալ, որ հերթականփոփոխականի արժեքը հաշվարկելիս կարելի լինի օգտագործել իրենից առաջթվարկված փոփոխականների արժեքները, պետք է օգտագործել let* կառուց֊վածքը։ Օրինակ, հետևյալ կոդի հաշվարկումը վերադարձնում է (1 2 3) ցուցակը։

1 (let* ((a 1) (b (1+ a)) (c (+ a b)))2 (list a b c))

quadratic ֆունկցիան ծրագրավորելու համար մնում է պարզապես բառացիո֊րեն վերարտադրել այս օրինակի սկզբում բերված Quadratic փսևդոկոդը.

1 (defun quadratic (a b c)2 "Լուծում է հավասարումը և վերադարձնում է արմատները։"3 (unless (zerop a)4 (let* ((delta (- (* b b) (* 4 a c)))5 (vo (/ (- b) (* 2 a)))

19

Page 20: Common Lisp․ 12 խնդիր

6 (vi (/ (sqrt delta) (* 2 a))))7 (list (+ vo vi) (- vo vi)))))

Առայժմ ենթադրում ենք, որ Common Lisp լեզվով սահմանված ֆունկցիաներըվերադարձնում են միայն մեկ արժեք։ Դրա համար էլ այս ֆունկցիայի վերջինտողում հաշվարկված երկու արմատներից կազմվում է ցուցակ (list ֆունկցիայիօգնությամբ) և այդ ցուցակը վերադարձվում է որպես արժեք։ Հետագա օրինակ֊ներում կծանոթանանքմեխանիզմների, որոնք հնարավորություն են տալիս ֆունկ֊ցիայից վերադարձնել մեկից ավելի արժեքներ։

Գլոբալ ֆունկցիաներ սահմանող defun մակրոսը հնարավորություն է տալիսամենմինոր սահմանվող ֆունկցիայի հետ կապել նկարագրության (փաստաթղթի)տող։ Այն գրվում է ֆունկցիայի արգումենտների ցուցակից հետո և ֆունկցիայիմարմնից առաջ։ Օրինակ, quadratic ֆունկցիայի համար գրված է «Լուծում էհավասարումը և վերադարձնում է արմատները։»տողը։ Ֆունկցիայիայս ոչպարտդադիրատրիբուտը Lisp ծրագրում կարելի է ստանալ documentation ֆունկցիայով, որիառաջինարգումենտը ցույց էտալիսայն օբյեկտը, որիցպետք է վերցնել նկարագրությանտողը, իսկ երկրորդ արգումենտը նշում է օբյեկտի տիպը։

(documentation 'quadratic 'function)) ⇒"Լուծում է հավասարումը և վերադարձնում է արմատները։"

quadratic ֆունկցիան սահմանելուց հետո, օրինակ, 2x2+3x−1 = 0 հավասար֊ման արմատները որոշելու համար, Lisp լեզվի հաշվարկիչին պետք է խնդրենքհաշվարկել «(quadratic 2 3 -1)» արտահայտությունը.

(quadratic 2 3 -1) ⇒ (0.28077638 -1.7807764)

Common Lisp լեզուն հնարավորություն է տալիս ֆունկցիայի սահմանման ևկիրառման ժամանակ օգտագործել անվանված արգումենտներ։ Օրինակ, եթեհարկավոր լինի quadratic ֆունկցիայի կանչի ժամանակ հավասարման արգու֊մենտները փոխանցել իրենց անուններով, ապա կարող ենք սահմանել մի նորֆունկցիա.

1 (defun quadratic-1 (&key a b c)2 (quadratic a b c))

Արտգումենտների ցուցակում &key բառը ցույց է տալիս, որ ֆունկցիան կանչելիսարգումենտները կարող ենք փոխանցել ոչ թե ըստ նրանց հայտարարման հաջոր֊դականության, այլ կամայական կարգով՝ ամեն մի արգումենտից առաջ նշելովֆորմալ պարամետրը։ Օրինակ,

(quadratic :a 2 :b 3 :c -1) ⇒ (0.28077638 -1.7807764)(quadratic :c -1 :a 2 :b 3) ⇒ (0.28077638 -1.7807764)

Սահմանենք quadratic ֆունկցիանին տեստավորող test-quadratic ֆունկ֊ցիան։ Այն ստանում է երկու արգումենտ՝ (3.1) հավասարման գործակիցների (a b c)

20

Page 21: Common Lisp․ 12 խնդիր

ցուցակը և այդ գործակիցներով հավասարման արմատների (e1 e2) ցուցակը՝որպես սպասվող արժեք։ Հետո test-quadratic ֆունկցիան quadratic ֆունկ֊ցիային է փոխանցում (a b c) ցուցակի տարրերը և ստանում է արմատների (r1 r2)

ցուցակը, իսկ որպես տեստի արդյունք վերադարձնում է (e1 = r1) ∧ (e2 = r2)

արտահայտության արժեքը։

1 (defun test-quadratic (abc ex)2 (let ((res (quadratic (first abc) (second abc) (third abc))))3 (and (= (first res) (first ex))4 (= (second res) (second ex)))))

first, second և third ֆունկցիաները վերադարձնում են ցուցակի համապա֊տասխանաբար առաջին և երկրորդ տարրերը։ Common Lisp լեզուն պարունակումէ նաև fourth , fifth, sixth, seventh, eighth, ninth և tenth ֆունկցիաները՝ցուցակի չորրորդ, հինգերորդ, վեցերորդ, յոթերորդ, իներորդ և տասերորդ տար֊րերը ստանալու համար։

21

Page 22: Common Lisp․ 12 խնդիր

Խնդիր 4

Մեծագٳյն ընդհանٳր բաժանարար

Երկու n ևmամբողջ թվերիմեծագٳյն ընդհանٳր բաժանարար (ՄԸԲ, GCD – great֊est common divisor) է կոչվում այն ամենամեծամբողջ d թիվը, որնառանցմնացորդիբաժանում է n և m թվերը։ (Եթե a-ն և b-ն ամբողջ թվեր են, ապա ասում ենք որ a-նբաժանٳմ է b-ին՝ a|b, եթե գոյություն ունի այնպիսի c ամբողջ թիվ, որ b = ac։)

Մեծագույն ընդհանուր բաժանարարի որոշման ամենահայտնի եղանակը Էվ֊կլիդեսի ալգորիթմն է [7, էջ 102], որը ծրագրավորման տեխնիկային կամ ալգո֊րիթմներին վերաբերող գրականությանմեջ հանդիպում է հիմնականում ռեկուրսիվիրականացման տեսքով.

gcd(n,m) =

n, եթե m = 0,

gcd(m,nmodm), եթե m ̸= 0,

(mod-ը բաժանման մնացորդը որոշող գործողությունն է)։Է. Դեյքստրան [3] աշխատանքում նկարագրել է երկու թվերի մեծագույն ընդ֊

հանուր բաժանարարը որոշելու մի ավելի պարզ ալգորիթմ, որտեղ մնացորդիորոշման գործողության փոխարեն օգտագործվում է հանման գործողությունը։ Այդալգորիթմի իտերատիվ նակրագրությունը այսպիսինն է. «եթե տրված են m և n

թվերը, ապա, քանի դեռ դրանք իրար հավասար չեն, նրանցիցմեծը փոխարինվումէ մեծի և փոքրի տարբերությամբ։ Այն պահին, երբ m-ը հավասարվում է n-ին, այդարժեքն էլ համարվում է նախապես տրված m և n թվերի մեծագույն ընդհանուրբաժանարար»։1: function Gcd(n, m)2: while n ̸= m do3: if n > m then4: n = n−m

5: else6: m = m− n

7: end if

22

Page 23: Common Lisp․ 12 խնդիր

8: end while9: return n

10: end functionՕգտագործված while կառուցվածքը հուշում է նախապայմանով ցիկլ կազմակեր֊պելու անհրաժեշտության մասին։ Common Lisp լեզվում ունի ցիկլերի նկարագրմանմի քանի եղանակներ, բայց դրանցից առավել արտահայտիչը loop մակրոսն է։ Այնծրագրավորողին տրամադրում է լեզվի ստանդարտում ներառված մի ենթալեզٳ,որի օգնությամբ հնարավոր է նկարագրել զանազան կրկնման գործողություններ[14, Գլուխ 26]։ Մասնավորապես, դասական նախապայմանով ցիկլեր կարող ենքստանալ loop մակրոսի while և until ենթակառուցվածքների միջոցով։

(loop while ⟨condition⟩ do ⟨form⟩+)(loop until ⟨condition⟩ do ⟨form⟩+)

Առաջին դեպքում ⟨form⟩+ արտահայտությունները հաշվարկվում են այնքանանգամ, քանի դեռ ճշմարիտ է while բառից հետո գրված պայմանը։ Երկրորդդեպքում, երբ օգտագործված է until ծառայողական բառը, do ծառայողականբառից հետո գրված արտահայտություների հաշվարկումը կրկնվում է այնքանանգամ, քանի դեռ կեղծ է ⟨condition⟩ պայմանը։ Օրինակ, ըստ ասվածի, հետևյալերկու արտահայտությունները համարժեք են.

(loop while (/= n m) do ... )(loop until (= n m) do ... )

Հիմա Gcd փսևդոկոդը ծրագրավորենք Common Lisp լեզվով.

1 (defun gcd (n m)2 (loop until (= n m)3 do (if (> n m) (decf n m) (decf m n))4 finally (return n)))

Ցիկլի մարմնում, do ծառայողական բառից հետո գրված պայմանի երկու ճյուղե֊րում, n և m փոփոխականների արժեքները նվազեցվում են decf մակրոսով։Այն իր առաջին արգումենտում տրված փոփոխականի արժեքը պակասեցնումէ երկրորդ արգումենտում տրված արժեքի չափով։ Նմանապես incf մակրոսընախատեսված է փոփոխականի արժեքը տրված չափով մեծացնելու համար։ Այսերկու մակրոսների համար էլ ⟨data⟩ արգումենտի լռելության արժեքը 1 է։

(incf ⟨place⟩ ⟨data⟩?) ⇒ ⟨place⟩ := ⟨place⟩ + ⟨data⟩(decf ⟨place⟩ ⟨data⟩?) ⇒ ⟨place⟩ := ⟨place⟩ - ⟨data⟩

loop մակրոսի finally ծառայողական բառից հետո գրված արտահայտութ֊յունը հաշվարկվում էայն դեպքում, երբ որևէպայմանովավարտվել է ցիկլի մարմնիհաշվարկումը։ Տվյալ դեպքում ցիկլն ընդհատվում է n = m պայմանով և returnմակրոսի օգնությամբ վերադարձվում է n-ը որպես loop կառուցվածքի արժեք։

23

Page 24: Common Lisp․ 12 խնդիր

Բայց gcd ֆունկցիայի այս բերված սահմանումը Common Lisp կատարման մի֊ջավայր ներմուծելը կարող է բերել ոչ ցանկալի հետևանքների։ Բաննայն է, որ Com֊mon Lisp ստանդարտում արդեն սահմանված է gcd ֆունկցիան, որը հաշվում է իրարգումենտների մեծագույն ընդհանուր բաժանարարը։ gcd ֆունկցիան սպասումէ զրո կամ ավելի արգումենտներ և եթե որևէ արգումենտ տրված չէ, ապա այնվերադարձնում է 0 արժեքը։ Օրինակ�

(gcd) ⇒ 0(gcd 24) ⇒ 24(gcd 15 21) ⇒ 3(gcd 9 27 54 95 96) ⇒ 1

Նմանապես սահմանված է նաև տրված թվերի փոքրագٳյն ընդհանٳր բազմա֊պատիկը (ՓԸԲ, LCM – Least common multiple) հաշվարկող lcm ֆունկցիան։ Այննույնպես սպասում է զրո կամ ավելի արգումենտներ և վերադարձնում է 1արժեքը,եթե որևէ արգումենտ տրված չէ։ Օրինակ�

(lcm) ⇒ 1(lcm 24) ⇒ 24(lcm 15 21) ⇒ 105(lcm 9 27 54 95 96) ⇒ 82080Մեր նոր սահմանմանը Common Lisp կատարման միջավայրը կարող է արձա֊

գանքել երկու եղանակով (կախված իրականացումից). կամ կզգուշացնի, որ այդանունով ֆունկցիա արդեն սահմանված է, կամ պարզապես ստանդարտով սահ֊մանված տարբերակը կփոխարինի նոր տրվածով։ Այս ոչ ցանկալի իրավիճակնե֊րից, որ հայտնի է որպես անٳնների կոնֆլիկտ, Common Lisp լեզվում հնարավոր էխուսափել՝ փաթեթի (package) միջոցով սահմանելով նոր անունների տիրույթ։ Եվայդ նորանուններիտիրույթում սահմանված սիմվոլներնարդեն հակասությանմեջչեն մտնի արդեն գոյություն ունեցող նույնանուն սիմվոլների հետ։

Փաթեթը սահմանվում է defpackage մակրոսով [12, Գլուխ 21], որի առաջինարգումենտը սահմանվող փաթեթի անունն է, իսկ հաջորդները՝ փաթեթի հատ֊կությունները։

Մեր gcd ֆունկցիան մեկուսացնելու համար սահմանենք new-gcd փաթեթը։Դրա համար ստեղծենք ex04.lisp անունով մի ֆայլ և այն սկսենք հետևյալսահմանմամբ։

1 (defpackage :new-gcd2 (:nicknames :ng)3 (:use :common-lisp)4 (:shadow :gcd)5 (:export :gcd))

Որտեղ defpackage մակրոսի առաջին արգումենտով տրված է սահմանվող փա֊թեթի անունը։ Հաջորդ արգումենտներով նշված է, որ new-gcd փաթեթի համար

24

Page 25: Common Lisp․ 12 խնդիր

համառոտ անուն (nicknames) է հանդիսանում ng սիմվոլը: Նոր սահմանվածփաթեթն օգտագործում է (use) common-lisp ստանդարտ փաթեթի սիմվոլները,ստվերում (ծածկում) է (shadow) արդեն գոյություն ունեցող gcd սիմվոլը և արտաքինաշխարհին է տրամադրում (export) իր gcd սիմվոլը։

Բայց սա դեռ չի նշանակում, որ այս սահմանումին հաջորդող սիմվոլներիսահմանումները պատկանելու են new-gcdփաթեթին։ Common Lisp համակարգումգոյություն ունի ընթացիկ փաթեթի հասկացություն, և նոր սահմանվող սիմվոլներըներառվում են ընթացիկ փաթեթում։ Տվյալ պահին ընթացիկ փաթեթի անունըկարող ենք ստանալ package-name ֆունկցիայի արգումենտին տալով *package*դինամիկ փոփոխականը, որը միշտ կապված է ընթացիկ փաթեթի հետ։

(package-name *package*) ⇒ "COMMON-LISP-USER"Այս COMMON-LISP-USER փաթեթը Common Lisp ստանդարտում նախապես սահ֊մանված երեք փաթեթներից մեկն է (մյուսներն են KEYWORDS և COMMON-LISPփաթեթները), որը ընթացիկ է դառնում համակարգի գործարկումից անմիջապեսհետո։

Որևէ մի այլ փաթեթ ընթացիկ դարձնելու համար է նախատեսված in-packageմակրոսը։ Օրինակ,մեր սահմանած new-gcdփաթեթը ընթացիկ դարձնելու համարex04.lisp ֆայլում պետք է գրել.

1 (in-package :new-gcd)

Այս կետից սկսած մինչև ֆայլի վերջը, քանի դեռ չի հանդիպել մեկ այլ in-package մակրոսի կիրառություն, բոլոր նոր սահմանվող սիմվոլները պատկանելուեն new-gcdփաթեթին։ Եվ հիմա, ֆայլը շարունակելով gcd ֆունկցիայի՝ 23-րդ էջումբերված սահմանմամբ, կարող ենք վստահ լինել, որ անունների կոնֆլիկտ չենքունենք։

Փորձելու համար գործարկենք Common Lisp կատարման միջավայրը և այնտեղբեռնենք ex04.lisp ֆայլի պարունակությունը։

1 (load "ex04.lisp" :print t)

Այս արտահայտության մեջ load ֆունկցիայի :print անվանված արգումենտըհնարավորություն է տալիս ֆայլի բեռնման ընթացքում տեսնել, թե ինչ նոր սիմվոլ֊ներ են ավելացել միջավայրում։

Ֆայլի բարեհաջող բեռնումից հետո կարող ենք list-all-packages ֆունկ֊ցիայի օգնությամբ ստանալ կատարման միջավայր բեռնված բոլոր փաթեթներիցուցակը և համոզվել, որ նրանց մեջ է նաև new-gcd փաթեթը։ Միջավայրումփաթեթի գոյությունը կարելի է պարզել նաև find-package ֆունկցիայով։ Այնստանում է փաթեթի անունը և վերադարձնում է փաթեթ օբյեկտը։ Եթե միջավայ֊րում տրված անունով փաթեթ չկա, վերադարձնում է NIL:

25

Page 26: Common Lisp․ 12 խնդիր

new-gcd փաթեթի հաջող բեռնվելուց հետո կարող ենք նրանում սահմանվածgcd ֆունկցիան օգտագործել new-gcd:gcd կամ, քանի որ new-gcdփաթեթի համարսահմանել էինք նաև ng համառոտ անունը, ng:gcd գրառմամբ։ Օրինակ, 259 և 763

թվերի ՄԸԲ-ն հաշվելու համար պետք է գրել.(new-gcd:gcd 259 763) ⇒ 7(ng:gcd 259 763) ⇒ 7Սակայն ⟨package⟩:⟨symbol⟩ գրառումը ճիշտ է միայն այն սիմվոլների համար,

որոնք փաթեթը սահմանելիս թվարկվել են :export հատկության ցուցակում։Բոլոր այն սիմվոլները, որոնք թվարկված չեն :export ցուցակում, մատչելի են⟨package⟩::⟨symbol⟩ գրառմամբ։

new-gcd փաթեթում սահմանված gcd ֆունկցիան ստուգելու համան common-lisp-user փաթեթում սահմանենք նաև մի ֆունկցիա, որը տրված երկու թվերիհամար ՄԸԲ-ն հաշվում է և՛ ստանդարտ gcd ֆունկցիայով, և՛ նոր սահմանվածֆունկցիայով, ապա համեմատում է այդ երկու արժեքները։

1 (in-package :common-lisp-user)2 (defun test-gcd (no ni)3 (let ((vo (new-gcd:gcd no ni))4 (vi (gcd no ni)))5 (format t "ng:gcd -> ~d, cl:gcd -> ~d, ~a~%"6 vo vi (if (= vo vi) 'PASS 'FAIL))))

Տեստավորող ֆունկցիայիմի քանիմի քանի կիրառությունները ցույց են տալիս,որ նոր սահմանված gcd ֆունկցիան այս դեպքերում ճիշտ է աշխատում։

(test-gcd 3 45) ⇒ ng:gcd -> 3, cl:gcd -> 3, PASS(test-gcd 123 456) ⇒ ng:gcd -> 3, cl:gcd -> 3, PASS(test-gcd 3462 9854) ⇒ ng:gcd -> 2, cl:gcd -> 2, PASS(test-gcd 432 9) ⇒ ng:gcd -> 9, cl:gcd -> 9, PASS

26

Page 27: Common Lisp․ 12 խնդիր

Խնդիր 5

Պատկերի մակերեսը

Դիցուք հարթության վրա տրված է մի P պատկեր և պահանջվում է հաշվել նրամակերեսը: Հայտնի է նաև, որ P պատկերի եզրագիծն այնպիսին է, որ հնարավորչէ տրոհել (հեշտ) ինտեգրվող կտորների, բայց տրված է մի f(x, y) ֆունկցիա, որըպարզում է հարթության տրված (x, y) կետի՝ P պատկերի ներսում ընկած լինելը:

Ընդգրկենք P պատկերն a կողմով A քառակուսու մեջ, որի կողմերը զուգահեռեն կոորդինատների առանցքներին, իսկ ներքև ձախ անկյունը համընկնում է (0, 0)կետի հետ (նկ. 5.1)։ Այնուհետև A քառակուսու մեջ ընտրենք N պատահականկետեր և n0-ով նշանակենք այն կետերի քանակը, որոնք ընկած են P պատկերիներսում։

O a

a A

P

Նկար 5.1: Մոնտե-Կառլոյի մեթոդի նկարագրությունը։

Ըստ մոտավոր ինտեգրման Մոնտե-Կառլոյի մեթոդի՝ P պատկերի մակերեսիհարաբերությունը A քառակուսու մակերեսին հավասար է պատկերի ներսում ըն֊կած կետերի քանակի հարաբերությանը բոլոր կետերի քանակին.

area(P )

area(A) =n0

N, area(P ) =

a2n0

N:

Ակնհայտ է, որարդյունքի ճշտությունը կախված էպատահական կետերի ընտրմանհավասարաչափությունից և կետերի քանակից։

27

Page 28: Common Lisp․ 12 խնդիր

Այս նկարագրվա մեթոդը կարելի է ծրագրավորել Common Lisp լեզվի loopմակրոսի մեկ կիրառմամբ.

1 (loop repeat n2 with no = 03 when (f (random (float a)) (random (float a)))4 do (incf no)5 finally (return (/ (float (* a a no)) n)))

Այն ասում է հետևյալը. «n անգամ կատարել (repeat) ցիկլի մարմինը, որիընթացում, երբ (when) f ֆունկցիային փոխանցում ենք [0, a) միջակայքի երկուպատահական (random) իրական (float) թվեր և ստանում ենք դրական (ոչ կեղծ)պատասխան, ապա մեկով ավելացնել (incf) n0 լոկալ փոփոխականի արժեքը։Ցիկլի ավարտին (finally) վերադարձնել (return) a2n0/n արտահայտության ար֊ժեքը»։

loop մակրոսի մարմնում լոկալ փոփոխականները հայտարարվում են withծառայողական բառով։ Մեր օրինակում «with no = 0» արտահայտությամբ հայ֊տարարվել է no փոփոխականը և նրան տրվել է 0 սկզբնական արժեքը։

Իսկ «(random (float a)))»արտահայտությունը fֆունկցիայիարգումենտումավելի համառոտ գրելու համար սահմանենք մի ֆունկցիա, որը վերադարձնում է[0; a)միջակայքի պատահական իրական թիվ.

1 (defun random-a (a)2 "[0;a) միջակայքի պատահական թիվ"3 (random (float a)))

Փսևդոպատահական թվեր գեներացնող random ֆունկցիայի արգումենտը կարողէ լինել ինչպես ամբողջ այնպես էլ իրական թիվ։ Առաջին դեպքում ֆունկցիանվերադարձնում է [0;n) միջակայքի պատահական ամբողջ թիվ, իսկ երկրորդդեպքում՝ իրական թիվ՝ հավասարաչափ բաշխումով։

Բերված loop ցիկլի պարամետրեր են հանդիսանում P պատկերը բնութագրողf(x, y) ֆունկցիա-պրեդիկատը, պատկերն ամբողջությամբ ընդգրկող քառակուսուa կողը, ինչպես նաևA քառակուսումեջ ընտրվող պատահական թվերիN քանակը։Մոնտե-Կառլոյիմեթոդովտրված P պատկերի մակերեսը հաշվելու համար սահմա֊նենք մի ֆունկցիա, որը ստանում է այս պարամետրերը, ապա հաշվարկում ևվերադարձնում է պատկերի մակերեսը։ Թող այդ ֆունկցիան կոչվի monte-carlo-method։

1 (defun monte-carlo-method (f a &optional (n 100))2 (loop repeat n3 with no = 04 when (funcall f (random-a a) (random-a a))5 do (incf no)6 finally (return (/ (float (* a a no)) n))))

28

Page 29: Common Lisp․ 12 խնդիր

Իսկ ինչպե՞ս է monte-carlo-method ֆունկցիան որպես իր առաջին արգու֊մենտ ստանալու f(x, y) ֆունկցիան։ Lisp ընտանիքի լեզուներում, ինչպես նաև«ֆունկցիոնալ» հատկությամբ օժտված այլ լեզուներում, դա իսկապես հնարավորէ։ Common Lisp լեզվում ֆունկցիան առաջին դասի օբյեկտ է՝ այն կարելի էփոխանցել որպես ֆունկցիայի արգումենտ և վերադարձնել որպես ֆունկցիայիարժեք։ defun մակրոսը ստեղծում է ֆٳնկցիա օբյեկտ և այն կապում է տրվածանունով սիմվոլի հետ։ Ֆունկցիա-օբյեկտը, կամ նրա հետ կապված սիմվոլըարգումենտների նկատմամբ կիրառվում է funcall ֆունկցիայով.

(funcall ⟨function-object⟩ ⟨argument⟩∗)

Իսկ function հատուկ կառուցվածքը ստանում է ֆունկցիայի սիմվոլիկ անունը(կամ անանուն ֆունկցիա) և վերադարձնում է ֆունկցիա-օբյեկտը։ Ֆունկցիա-օբ֊յեկտը կարելի է ստանալ նաև օգտագործելով function հատուկ կառուցվածքիհամառոտ գրառումը՝ #' (sharp-quote)։

Ֆունկցիայի արգումենտների ցուցակում &optional ծառայողական բառը ցույցէ տալիս, որ իրենից հետո թվարկված են այդ ֆունկցիայի ոչ պարտադիր արգու֊մենտները։ Օրինակ, եթե մի որևէ h ֆունկցիա սահմանված է ահա այս տեսքով.

1 (defun h (a &optional (b 2) c)2 (list a b c))

ապա ճիշտ են հետևյալները.(h 5 2 3) ⇒ (5 2 3)(h 4 2) ⇒ (4 2 NIL)(h 1) ⇒ (1 2 NIL)(h 1 2) ⇒ (1 2 NIL)(h 5 6 7) ⇒ (5 6 7)

Այս օրինակներից տեսնում ենք, որ եթե ոչ պարտադիր արգումենտին նախնականարժեք տրված չէ, ապա այն համարվում է NIL։ Տեսնում ենք նաև, որ 3-րդ և 4-րդօրինակներում h ֆունկցիայի երկու տարբեր կանչեր տվել են նույն արդյունքը։Ֆունկցիայի կանչի ժամանակ արգումենտի արժեքի բացահայտ տրված լինելըկարող ենք ստուգել, եթե h ֆունկցիայի արգումենտների թվարկման մեջ (b 2)արտահայտությունը փոխարինենք (b 2 b-supplied-p) արտահայտությամբ,

1 (defun h (a &optional (b 2 b-supplied-p) c)2 (list a (list b b-supplied-p) c))

Այս b-supplied-p փոփոխականը կստանա T արժեք այն դեպքում, երբ b փոփո֊խականի արժեքը բացահայտ տրված է։

(h 1 2 3) ⇒ (1 (2 T) 3)(h 1) ⇒ (1 (2 NIL) NIL)Ընդհանրացնելով նշենք, որ ֆունկցիայի ոչ պարտադիր արգումենտների հայ֊

տարարությունը կարող է ունենալ հետևյալ տեսքերից որևէ մեկը.

29

Page 30: Common Lisp․ 12 խնդիր

&optional ⟨var⟩ ...&optional (⟨var ⟩ ⟨init⟩) ...&optional (⟨var ⟩ ⟨init⟩ ⟨supplied-p⟩) ...

ՈրպեսՄոնտե-Կառլոյիմեթոդի կիրառություն դիտարկենք 1 շառավղով շրջանիմակերեսի հաշվման օրինակը: Տեղադրենք շրջանի կենտրոնը (1, 1) կետում, ևընդգրկող քառակուսու կողն ընդունենք 2։ ֆունկցիան, որը որոշում է (x, y) կոորդի֊նատներով կետի շրջանագծի ներսում ընկած լինելը, կարելի է սահմանել այսպես.

1 (defun is-in-circle (x y)2 (let ((a (1- x)) (b (1- y)))3 (<= (+ (* a a) (* b b)) 1)))

Այս սահմանումներով monte-carlo-method ֆունկցիայի կանչը գրենք n = 100, n =

2000 և n = 4000 դեպքերի համար (արդյունքների ցուցադրությունը 5.2 նկարում).

(monte-carlo-method (function is-in-circle) 2)(monte-carlo-method #'is-in-circle 2 2000)(monte-carlo-method #'is-in-circle 2 4000)

O

2

2

1

n = 100 O

2

2

1

n = 2000 O

2

2

1

n = 4000

Նկար 5.2: Շրջանի մակերեսի հաշվումը Մոնտե-Կառլոյի մեթոդով։

Քանի որ is-in-circle-ը defun մակրոսով սահմանված գլոբալ ֆունկցիայիսիմվոլիկ անուն է, նրա հետ կապված ֆունկցիա-օբյեկտը ստացել ենք functionհատուկ կառուցվածքով (նաև #' սիմվոլով) և փոխանցել monte-carlo-methodֆունկցիային։

30

Page 31: Common Lisp․ 12 խնդիր

Խնդիր 6

Պարզ և կատարյալ թվեր

Պարզ է կոչվում մեկից մեծ այն բնական թիվը, որը բացի իրենից և մեկից այլդրական բաժանարար չունի: Ակնհայտ է, որ զույգ թվերից պարզ է միայն 2-ը։Հայտնի է նաև, որ n թվի պարզ լինելը ցույց տալու համար բավական է համոզվել,որ այն [2, ⌈

√n⌉]միջակայքից բաժանարարներ չունի։

Եթե n ≥ 3 և այն կենտ է, ապա նրա պարզ լինելը կարելի է ստուգել loopմակրոսի for և never ենթակառուցվածների օգտագործմամբ կազմված հետևյալցիկի միջոցով.

1 (loop for i from 2 to (ceiling (sqrt n))2 never (zerop (mod n i)))

որտեղ loop-ի կիրառությունը կարելի է կարդալ այսպես. «i-ն սկսել (from) 2-իցև շարունակել մինչև (to) ⌈√n⌉ արժեքն այնպես, որ երբեք (never) տեղի չունենաi|n պայմանը»։ Եթե loop-ը կարողանում է մինչև վերջ կատարվել never-ից հետոգրված պայմանով, ապա նրա արդյունքը t է, հակառակ դեպքում՝ nil։

Այսպիսով, is-prime ֆունկցիայի սահմանումը Lisp լեզվով.

1 (defun is-prime (n)2 "Ստուգում է n թվի պարզ լինելը։"3 (cond ((<= n 1) nil)4 ((evenp n) (= n 2))5 (t (loop as i from 2 to (ceiling (sqrt n))6 never (zerop (mod n i))))))

evenp ֆունկցիան պարզում է իր արգումենտում տրված թվի զույգ լինելը, ceilingֆունկցիան կլորացնում է իր արգումենտը մինչև իրենից մեծ ամենափոքր ամբողջթիվը՝ ⌈n⌉ (⌊n⌋ տիպի կլորացում անելու համար պետք է օգտագործել floor ֆունկ֊ցիան), sqrt-ը վերադարձնում է արգումենտի երկրորդ աստիճանի արմատը։

Կատարյալ է կոչվում այն ամբողջ թիվը, որը հավասար է իր բաժանարաների(բացի իրենից) գումարին։ Օրինակ, առաջին կատարյալ թիվը 6-ն է՝ 6 = 1 + 2 + 3։

31

Page 32: Common Lisp․ 12 խնդիր

Փորձենք տրված m թվի բաժանարարների գումարը հաշվել իտերատիվ՝ loop-իկիրառմամբ, և ռեկուրսիվ եղանակներով։

1 (defun sum-of-divisors (m)2 (loop for i from 1 to (/ m 2)3 when (zerop (mod m i))4 sum i))

Այս օրինակում loop մակրոսը sum ծառայողական բառի միջոցով կուտակում էբոլոր այն i ∈ [1, m

2] արժեքների գումարը, որոնց համար i-ն բաժանում է m-ին՝ i|m։

Ռեկուրսիվ եղանակով բաժանարաների գումարը հաշվելու համար պետք էծրագրավորել հետևյալ բանաձևերը.

f(m, k) =

1, երբ k = 1,

k + f(m, k − 1), երբ k|m,

f(m, k − 1), մյուս դեպքերում։(6.1)

f(m, k) ֆունկցայի արգումենտի k թիվը m-ի բաժանարարի այն «թեկնածուն» է,որից ներքև իմաստ ունի որոնել բաժանարանները: Պարզագույն դեպքում կարելիէ վերցնել k = m− 1, բայց ռեկուրսիայի խորությունը նվազեցնելու համար հարմարէ k = ⌊m/2⌋ արժեքը։

Սահամանենք sum-of-divisors-rec ֆունկցիան, որի ներսում, լոկալ ֆունկ֊ցիայի տեսքով, իրականացված է (6.1) բանաձևը։

1 (defun sum-of-divisors-rec (m)2 (labels3 ((f (a k)4 (cond ((<= k 1) 1)5 ((= 0 (mod a k)) (+ k (f a (1- k))))6 (t (f a (1- k))))))7 (f m (floor (/ m 2)))))

labels հատուկ կառուցվածքը հնարավորություն է տալիս սահմանել լոկալ ֆունկ֊ցիաներ այնպես, ինչպես let և let* կառուցվածքները հնարավորություն էինտալիս սահմանել լոկալ փոփոխականներ։ Լոկալ ֆունկցիաներ կարելի է սահմա֊նել նաև flet կառուցվածքով։ labels-ի և flet-ի տարբերությունն այն է, որառաջինի միջոցով սահմանված ֆունկցիաները տեսանելի են միմյանց համար ևկարելի է սահմանել ռեկուրսիվ ֆունկցիաներ, իսկ երկրոդի միջոցով սահմանվածֆունկցիաները միմյանց մասին տեղեկություն չունեն (տես [14])։ Լոկալ ֆունկցիանսահմանվում է ճիշտ այնպես, ինչպես կսահմանվեր defun կառուցվածքի միջոցով,և նրա տեսանելության տիրույթը labels կամ flet կառուցվածքի մարմինն է։

Կարելի է իհարկե sum-of-divisors-rec ֆունկցիան իրականացնել առանցլոկալ ֆունկցիայի։ Ըստ էության դա կլինի (6.1) բանաձևի իրականացումը, որի k

32

Page 33: Common Lisp․ 12 խնդիր

արգումենտը հարմարության համար դարձրած է ոչ պարտադիր՝ ⌊m/2⌋ սկզբնա֊կան արժեքով։

1 (defun sum-of-divisors-rec-2 (m &optional (k (floor (/ m 2))))2 (cond3 ((<= k 1) 1)4 ((zerop (mod m k)) (+ k (sum-of-divisors-rec-2 m (1- k))))5 (t (sum-of-divisors-rec-2 m (1- k)))))

Եվ վերջապես, m թվի կատարյալ լինելը պարզելու համար սահմանենք is-perfect պրեդիկատ ֆունկցիան.

1 (defun is-perfect (m)2 "Ստուգում է m թվի կատարյալ լինելը։"3 (= m (sum-of-divisors m)))

Այս ֆունկցիայի սահմանման մեջ միևնույն հաջողությամբ կարելի է կիրառել նաևվերը սահմանված sum-of-divisors-rec և sum-of-divisors-rec-2 ֆունկցիա֊ները:

* * *Տրված n բնական թվից փոքր պարզ թվերի հաշվարկման մի հետաքրքիր ու

արդյունավետ եղանակ է Էրատոսթենեսի մաղը [15, գլ� 2]։ Գաղափարը հետևյալնէ� նախ՝ շարք է կազմվում [3;n]միջակայքի կենտ բնական թվերից (քանի որ զույգթվերը պարզ չեն կարող լինել), ապա՝ «մաղելով» հեռացվում են այն թվերը, որոնքիրենց նախորդներից որևէ մեկի պատիկն են։ Պրոցեսի ավարտին «մաղի» վրամնում են պարզ թվերը� նրանք, որոնք որոնք որևէ այլ թվի պատիկ չեն։ Օրինակ,նկ� 6.1֊ում պատկերված են Էրատոսթենեսի մաղի առաջին երկու քայլերը՝ n = 51

դեպքի համար։

3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51

3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51

3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51

Նկար 6.1: Էրատոսթենեսի մաղի առաջին երկու քայլերը։

Կենտ թվերի սկզբնական ցուցակը կառուցելը հեշտ է։ Դա կարելի է անել,օրինակ, list-of-odds ռեկուրսիվ ֆունկցիայի օգնությամբ�

1 (defun list-of-odds (n)2 (labels3 ((odds-aux (m r)4 (if (< m 3)5 r6 (odds-aux (- m 2) (cons m r)))))7 (odds-aux (if (oddp n) n (1- n)) '())))

33

Page 34: Common Lisp․ 12 խնդիր

Ներդրված odds-aux ֆունկցիայի առաջին արգումենտը շարքի վերին սահմանըորոշող կենտթիվնէ, իսկ երկրորդարգումենտը՝ կուտակվողարդյունքը։ Երբ list-of-odds ֆունկցիան կանչում է odds-aux֊ը, «(if (oddp n) n (1- n))» արտա֊հայտությամբ ապահովում է, որ առաջին արգումենտում տրվի կենտ թիվ։

Սակայն, այս խճճված ֆունկցիայի փոխարեն կարող ենք օգտագործել loopցիկլը՝ i պարամետրը սկսելով 3֊ից և 2 քայլով շարունակելով մինչև n֊ից փոքրկամ հավասար ամենամեծ կենտ թիվը։

1 (defun list-of-odds (n)2 (loop for i from 3 upto n by 2 collect i))

Թվերի ցուցակը «մաղելու» պրոցեսը ալգորիթմի վերածելու համար առանձ֊նացնենք դրա մեկ քայլը, այն է՝ ցուցակից ընտրված տարրին պատիկ բոլորմյուս տարրերի հեռացնելը։ Ամենապարզ իրականացում կարող է լինել, օրինակ,հետևյալ remove-multiplies ռեկուրսիվ ֆունկցիան։ Այն ունի երկու պարամետր՝e ընտրված տարրը և xs ցուցակը։ Եթե ցուցակը դատարկ է, ապա ֆունկցիայիարդյունքը նույնպես դատարկ ցուցակ է։ Եթե ընտրված տարրը բաժանում էցուցակի առաջին տարրին՝ (zerop (mod (car xs) e)), ապա վերջինս անտես֊վում է, և ֆունկցիայի ռեկուրսիվ կանչ է կիրառվում xs ցուցակի պոչի նկատմամբ։Եթե ցուցակի առաջին տարրը պատիկ չէ e֊ին, ապա այդ տարրը կցվում է նշվածռեկուրսիվ կանչի արդյունքում ստացված ցուցակի սկզբից։

1 (defun remove-multiplies(e xs)2 (cond3 ((endp xs)4 '())5 ((zerop (mod (car xs) e))6 (remove-multiplies e (cdr xs)))7 (t8 (cons (car xs) (remove-multiplies e (cdr xs))))))

Այսպիսի ֆունկցիային ընդունված է անվանել ֆիլտրող ֆունկցիա, կամ պարզա֊պես ֆիլտր։ Common Lisp լեզվում ֆիլտրերից մեկի՝ remove-if֊ի մասին մանրա֊մասն կխոսենք 9֊րդ օրինակում (էջ 54)։ Այստեղ միայն ցույց տանք, թե ինչպեսկարելի է remove-multiplies ֆունկցիան իրականացնել դրա օգնությամբ�

1 (defun remove-multiplies (e xs)2 (remove-if #'(lambda (x) (zerop (rem x e))) xs))

Էրատոսթենեսի մաղի մի քայլն արդեն ունենք։ Հիմա սահմանենք esieveֆունկցիան, որը տրված ցուցակից առանձնացնում է պոչը և դրանից հեռացնում էցուցակի առաջին տարրի պատիկները։ Այնուհետև այդ ստացված արդյունքի վրանորից ռեկուրսիվ կիրառվում է esieve ֆունկցիան՝ արդյունքին կցելով սկզբնականցուցակից առանձնացված առաջին տարրը։

34

Page 35: Common Lisp․ 12 խնդիր

1 (defun esieve (es)2 (if (endp es)3 '()4 (let ((a (car es))5 (d (cdr es)))6 (cons a (esieve (remove-multiplies a d))))))

Մնում է սահմանել primes-less-than ֆունկցիան, որը esieve և list-of-oddsֆունկցիաների համադրմամբ կառուցում է տրված n բնական թվից փոքր (կամհավասար) պարզ թվերի ցուցակը։

1 (defun primes-less-than (n)2 (esieve (list-of-odds n)))

Վերջպես տեսնենք, թե ինչ արդյունք է տալիս Էրատոսթենեսի մաղը 500֊իցփոքր թվերի համար�

* (primes-thes-than 500)(3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 7983 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163167 173 179 181 191 193 197 199 211 223 227 229 233 239 241251 257 263 269 271 277 281 283 293 307 311 313 317 331 337347 349 353 359 367 373 379 383 389 397 401 409 419 421 431433 439 443 449 457 461 463 467 479 487 491 499)

35

Page 36: Common Lisp․ 12 խնդիր

Խնդիր 7

Թվերի վերածٳմը բառերի

Հաճախ բանկային կտրոնների կամ վճարային անդորագրերի վրա հանդիպումենք որևէ թվի, մեծամասամբ դրամական գումարի արտահայտությունը բացիթվայինից նաև բառային տեսքով։ Օրինակ, «1258 (հազար երկու հարյուր հիսուն֊ութ)»։ Իրականացնենք ամբողջ թվի թվանշանային ներկայացումից բառային ներ֊կայացումը կատարող թարգմանիչը՝ սահմանափակելով թվի երկարությունը վեցնիշով։

Եթե վերլուծենք վեցանիշ թվերի տեքստային արտահայտությունները, ապակնկատենք, որ դրանք բոլորը կազմված են միավորների անուններից («մեկ»,«երկու» և այլն), տասնավորների անուններից («տաս», «քսան» և այլն), ինչպեսնաև «հարյուր» և «հազար» բառերից։ Միավորների ու տասնավորների անուններիցուցակները կսահմանենք որպես գլոբալ հաստտատուններ, իսկ «հարյուր» և«հազար» բառերը կօգտագործենք թարգմանիչի մեջ։

Ծրագրում գլոբալ հաստատունները սահմանվում են defconstant մակրոսով։Այն ստանում է սահմանվող հաստատունի անունը, արժեքը և նկարագրությանտող, որը կարելի է բաց թողնել։

(defconstant ⟨name⟩ ⟨initial-value⟩ ⟨documentation⟩?)

Ծրագրի տեքստում հաստատուններն այլ օբյեկտներից հեշտ տարբերելու համարընդունված է դրանց անունները սկսել և ավարտել + սիմվոլով։

1 (defconstant +units+ '("" "մեկ" "երկու" "երեք" "չորս" "հինգ"2 "վեց" "յոթ" "ութ" "ինը")3 "Միավորների անունները")4 (defconstant +tens+ '("" "տաս" "քսան" "երեսուն" "քառասուն"5 "հիսուն" "վաթսուն" "յոթանասուն" "ութսուն" "իննսուն")6 "Տասնավորների անունները")

Այս երկու ցուցակներն օգտագործելու ենք թիվը թվանշանային տեսքից բառայինտեսքի ձևափոխող T ընթացակարգի նկարագրության և իրականացման մեջ։

36

Page 37: Common Lisp․ 12 խնդիր

Այժմ դիցուք T-ն ձևափոխության ընթացակարգն է։ n ∈ [0, 106) թվի տեքստայինարտահայտությունը T-ի օգնությամբ ստանալու համար կիրառենք հետևյալ կա֊նոնները (որտեղ + նիշն օգտագործված է որպես տողերի կցման գործողություն).

T1: Եթե n ∈ [0, 9], ապա T (n)-իարդյունքը +units+ ցուցակի n-րդտարրնէ՝ T (n) = units[n]:

T2: Եթե n ∈ [10, 99] և n = n0 × 10 + n1, ապա T (n)-ի արդյունքը +tens+ցուցակի n0-րդ տարրն է, որին աջից կցված է +units+ ցուցակիn1-րդ տարրը՝ T (n) = tens[n0] + tens[n1]։

T3: Եթե n = n0 × 100 + n1, ապա T (n) = T (n0)+ “ հարյուր ” +T (n1):

T4: Եթե n = n0 × 1000 + n1, ապա T (n) = T (n0)+ “ հազար ” +T (n1):

Ցուցակի n-րդ տարրը վերցնելու համար Common Lisp ստանդարտում նախա֊տեսված է nth ֆունկցիան։ Բայց, եթե նույնիսկ այն սահմանված էլ չլիներ, ապակարելի էր հեշտությամբ իրականացնել, օրինակ, հետևյալ կերպ.

1 (defun our-nth (index data)2 (if (zerop index)3 (car data)4 (our-nth (1- index) (cdr data))))

Որպեսզի տասնավորների անունների +tens+ ցուցակից վերցնենք, օրինակ, 40արժեքի անունը, ապա պետք է որպես ինդեքս օգտագործել 40/10 քանորդը։

(nth (truncate 40 10) +tens+) ⇒ "քառասուն"truncate ֆունկցիան ստանում է երկու արգումենտ՝ d0 և d1, ապա հաշվարկում ևվերադարձնում է d0-ն d1-ի վրա բաժանելու քանորդն ու մնացորդը։

Եթե ֆունկցիան վերադարձնում էմեկից ավելի արժեքներ, ապա դրանք կարելիէ կոնկրետ անունների հետ կապել multiple-value-bind մակրոսի օգնությամբ։

(multiple-value-bind (⟨var⟩∗) ⟨values-form⟩ ⟨body-form⟩∗)

Այստեղ ⟨var⟩∗ լեքսիկական փոփոխականներին կապվում են ⟨values-form⟩ արտա֊հայտության հաշվարկումից վերադարձված արժեքները, որոնք տեսանելի են մակ֊րոսի ⟨body-form⟩ մարմնում։ Օրինակ, հետևյալ կոդը կարտածի «18|15»արդյունքը։

1 (multiple-value-bind (qu re)2 (truncate 321 17)3 (format "~d|~d>" qu re))

Ֆունկցիայից մի քանի արժեքներ վերադարձվում են values ֆունկցիայով։Ստորև բերված է մի օրինակ, որտեղ ֆունկցիան հաշվում է տրված α անկյանսինուսը և կոսինուսը, ապա դրանք դուրս են բերվում արտածման հոսքին.

37

Page 38: Common Lisp․ 12 խնդիր

1 (defun sin-cos (alpha)2 (values (sin alpha) (cos alpha)))3

4 (multiple-value-bind (si co) (sin-cos (/ pi 6))5 (princ si) (princ co) (terpri))

sin-ը և cos-ը ստանդարտ ֆունկցիաներ են, որոնք արգումենտները պետք է տալռադիաններով։ pi-ն գլոբալ հաստատուն է, որի արժեքը π թիվն է։ princ ֆունկ֊ցիան իր արգումենտում տրված տողն արտածում է մարդու համար ընթեռնելիտեսքով, իսկ terpri ֆունկցիան արտածում է նոր տողի անցման սիմվոլը։

Հիմա սահմանենք num-to-words ֆունկցիան որպես T ընթացակարգի իրա֊կանացում, իսկ T1–T4 կանոնները ծրագրավորենք որպես cond կառուցվածքիճյուղեր։

1 (defun number-to-words (num)2 (cond3 ((<= 0 num 9)4 (nth num +units+))5 ((<= 10 num 99)6 (multiple-value-bind (a b) (truncate num 10)7 (concatenate 'string (nth a +tens+)8 (if (and (= a 1) (/= b 0)) "ն" "ը")9 (nth b +units+))))10 ((<= 100 num 999)11 (multiple-value-bind (a b) (truncate num 100)12 (concatenate 'string (if (= a 1) "" (nth a +units+))13 " հարյուր "14 (number-to-words b))))15 ((<= 1000 num 999999)16 (multiple-value-bind (a b) (truncate num 1000)17 (concatenate 'string (number-to-words a)18 " հազար "19 (number-to-words b))))))

Հաջորդականությունները (sequence), իսկ Common Lisp լեզվում տողերը հա֊ջորդականություններ են, իրար կարող են կցվել concatenate ֆունկցիայով.

(concatenate ⟨result-type⟩ ⟨sequence⟩∗)

number-to-words ֆունկցիայում concatenate ֆունկցիան օգտագործվում է հատ֊ված առ հատված կառուցված արտահայտությունը ամբողջացնելու համար։

number-to-words ֆունկցիան տեստավորելու համար սահմանենք մի ֆունկ֊ցիա, որը արգումենտում ստանում է երկու տարրերից բաղկացած ցուցակ։ Այդցուցակի առաջին տարրը թվի թվանշանային ներկայացումն է, իսկ երկրորդը՝ այդ

38

Page 39: Common Lisp․ 12 խնդիր

նույն թվիակնկալվող բառային ներկայացումը, ապա number-to-words ֆունկցիա֊յով թվանշանային տեսքից ստանում է բառային տեսքը և համեմատում նախապեստրված տարբերակի հետ։

1 (defun test-number-to-words (nw)2 (let* ((num (first nw))3 (exp (second nw))4 (res (string= (number-to-words num) exp)))5 (format t "~A: ~6D ~A~%" (if res 'pass 'fail) num exp)))

Որևէ s1 և s2 տողերի այբբենական համեմատման համար են նախատեսված7.1 աղյուսակում թվարկված stringφ տեսքի ֆունկցիաները։ Այդ ֆունկցիաներիերկրորդ խումբն առաջինից տաբերվում է նրանով, որ անտեսում է մեծատառերիու փոքրատառերի տարբերությունը։

s1 ∼ s2 Խٳմբ I Խٳմբ IIs1 = s2 string= string-equals1 ̸= s2 string/= string-not-equals1 > s2 string> string-greaterps1 ≥ s2 string>= string-not-greaterps1 < s2 string< string-lessps1 ≤ s2 string<= string-not-lessp

Աղյուսակ 7.1: Տողերի այբբենական համեմատման ֆունկցիաները։

Կազմենք նաև տեստավորման տարբերակներ (test cases)։ Սահմանենք մի նորգլոբալ ցուցակ, որը կպարունակի test-number-to-words ֆունկցիային տալուհամար նախատեսվածմիքանի երկու տարրանոց ցուցակներ (պարզ է, որտեստա֊յին տարբերակների քանակը կարող է լինել անսահմանափակ)։

1 (defconstant +test-cases+2 '((5 "հինգ")3 (23 "քսաներեք")4 (456 "սխալ տարբերակ")5 (7890 "յոթ հազար ութ հարյուր իննսուն")6 (12345 "տասներկու հազար երեք հարյուր քառասունհինգ")))

Տեստավորումն իրականացանելու համար պետք է անցնել այս ցուցակի վրայովև ամեն մի տարրի վրա կիրառել test-number-to-words ֆունկցիան։ Ցուցակիտարրերով իտերացիա կազմակերպելու համար է dolist մակրոսը.

(dolist (⟨var⟩ ⟨list⟩ ⟨result⟩?)⟨body-form⟩∗)

39

Page 40: Common Lisp․ 12 խնդիր

⟨var ⟩ անունը հաջորդաբար կապվում է ⟨list⟩ ցուցակի հերթական տարրին ևհաշվարկվում են ⟨body-form⟩ արտահայտությունները։ Ցիկլի ավարտին վերա֊դարձվում է ⟨result⟩ արժեքը. եթե այն տրված չէ, ապա՝ nil։

1 (dolist (em +test-cases+)2 (test-number-to-words em))

Այս ցիկլի կատարման արդյունքները բերված են ստորև, որտեղ երևում է, որտեստային տարբերակներից երրորդը չի համապատասխանում թվին և այդ տար֊բերակի դիմաց տպվել է «FAIL»։

PASS: 5 հինգPASS: 23 քսաներեքFAIL: 456 սխալ տարբերակPASS: 7890 յոթ հազար ութ հարյուր իննսունPASS: 12345 տասներկու հազար երեք հարյուր քառասունհինգ

Բայց dolist մակրոսը ցուցակի տարրերի վրա տրված ֆունկցիան կիրառելումիակ միջոցը չէ։ Common Lisp լեզուն ունի մի ավելի գեղեցիկ հնարավորություն՝mapc ֆունկցիան։

(mapc ⟨function⟩ ⟨list⟩+)

Նրաառաջինարգումենտըֆունկցիա է, որը կիրառվում է ⟨list⟩ ցուցակի հերթականէլեմենտի վրա։ Եթե ⟨function⟩ ֆունկցիան սպասում է k արգումենտներ, ապա ⟨list⟩ցուցակները նույնպես պետք է լինեն k հատ։ Որպես արդյունք այս ֆունկցիանմիշտվերադարձնում է իր երկրորդարգումենտը։ mapcֆունկցիայիմիջոցով +test-cases+ ցուցակի վրա test-number-to-words ֆունկցիան կիրառելու համարպետքէ գրել.

(mapcar (function test-number-to-words) +test-cases+)

որը հաշվարկումից հետո կարտածի վերը բերված արդյունքը։

40

Page 41: Common Lisp․ 12 խնդիր

Խնդիր 8

Կառٳցել Մորզեի այբٳբեն

Ենթադրենք հանձնարարված էանգլերենտեքստերի կոդավորման համար կազմելՄորզեի այբուբենին նման կոդավորման այբուբեն։ Հիշենք, որ Մորզեի կոդավոր֊ման սխեմայում լեզվի այբուբենի ամենի մի տառին համապատասխանեցվում էկետերի և գծիկների հաջորդականություն։ Կոդավորման սխեման կազմվում էայնպես, որ ծածկագրված տեքստի ծավալը լինի հնարավորինս փոքր։ Պարզ է,որ այդ նպատակին կարելի է մոտենալ, եթե կոդավորվող լեզվում ավելի հաճախհանդիպող տառերին տառերին համապատասխանեցվեն ավելի կարճ կոդեր։

ՈՒրեմն, նախ՝ վիճակագրական եղանակով հաշվենք, թե անգլերեն այբուբենի26 տառերից յուրաքանչյուրն ի՛նչ հաճախությամբ է հանդիպում տեքստերում։ Դրահամար ընտրենք մի որևէ տեքստ1, և այն սիմվոլ առ սիմվոլ կարդալով կազմենքայբուբենի տառերի հաճախության աղյուսակը։ Քանի որ հաղորդագրություններըկոդավորվում ենմիատեսակտառերով,մենքմեծատառերն ուփոքրատառերը չենքտարբերի, իսկ տեքստում հանդիպող բոլոր մյուս նշանները կանտեսենք։

Տառերի հաճախության աղյուսակը մոդելավորենք 26 տարրերից բաղկացածվեկտորով, որի առաջին տարրը ցույց է տալիս «a» տառի հանդիպելու հաճախութ֊յունը, երկրորդը՝ «b» տառինը, և այդպես շարունակ մինչև «z» տառը։ Զանգված֊ները Common Lisp լեզվում ստեղծվում են make-array ֆունկցիայով [14, Գլուխ 17],որի առաջին արգումենտը չափողականությունների ցուցակն է։ Միչափանի զանգ֊ված (վեկտոր) ստեղծելիս չափողականությունների ցուցակի փոխարեն պարզա֊պես կարելի է տալ տարրերի քանակը։ Օրինակ, 26 երկարությամբ միաչափզանգված ստեղծելու համար պետք է գրել.

(make-array 26)1http://www.gutenberg.org/ ռեսուրսը պարունակում է ազատորեն տարածվող

տեքստերի մի հսկայական բազմություն։ Այս օրինակը նկարագրելու և ստուգելու համարօգտագործվել է Մարկ Տվենի «Թոմ Սոյերի արկածները» գիրքը, որը պարունակում է մոտ400 հազար նիշ։

41

Page 42: Common Lisp․ 12 խնդիր

Եթե հարկավոր է, որ զանգվածը ստեղծելիս տարրերին տրվի որևէ նախնականարժեք, պետք է օգտագործել :initial-element անվանված արգումենտը։ Ահաայսպես.

(make-array 26 :initial-element 0)

Այս կոդը կստեղծի 26 տարրերի վեկտոր և բոլոր տարրերին կվերագրի 0 արժեքը։Մեկ այլ՝ :initial-contents անվանված արգումենտով տրվում է զանգվածիսկզբնական պարունակությունը։ Օրինակ, (1

324) մատրիցը կառուցելու և my-matrix

սիմվոլին կապելու համար պետք է գրել հետևյալը.

(setq mt-matrix (make-array '(2 2):initial-contents '((1 2) (3 4)))

setq հատուկ կառուցվածքը Common Lisp լեզվի վերագրման պրիմիտիվն է [18,Գլուխ 2.4].

(setq ⟨var ⟩1 ⟨expr ⟩1 ⟨var⟩2 ⟨expr⟩2 ...)

Այն հաշվում է հերթական ⟨expr⟩i արտահայտությունը և կապում համապատախան⟨var ⟩iփոփոխականին։ Վերագրումները կատարվում են հաջորդաբար՝ ձախիցաջ։⟨var ⟩i սիմվոլները չեն հաշվարկվում։ Եթե հարկավոր են զուգահեռ վերագրումներ,ապա պետք է օգտագործել psetq մակրոսը։

Զանգվածի տարրերին հասանելիությունն ապահովում է aref ֆունկցիան,որի առաջին արգումենտը զանգվածն է, իսկ մյուսները՝ տարրի ինդեքսները։Այն օգտագործվում է ինչպես տարրի արժեքը կարդալու, այնպես էլ նոր արժեքվերագրելու համար։ Օրինակ, my-matrix մատրիցում 3արժեքը 5-ով փոխարինելուև ապա այն արտածելու համար պետք է գրել.

(setf (aref my-matrix 1 0) 5)(print (aref my-matrix 1 0))

Վերագրման համար այստեղ օգտագործված է setf մակրոսը, քանի որ պետք էհաշվարկել ոչ միայն արժեքը, այլ նաև այն տեղը, որին արժեք է վերագրվելու։Այս մակրոսը նույնպես վերագրումները կատարում է հաջորդաբար, և զուգահեռվերագրումների համար պետք է օգտագործել psetf մակրոսը։

Տրված անունով ֆայլը բացելու և այն ֆայլային հոսքի հետ կապելու համար էնախատեսված with-open-file մակրոսը.

(with-open-file (⟨stream⟩ ⟨filename⟩ ⟨option⟩∗)⟨body-form⟩∗)

Եթե ⟨option⟩ արգումենտներով ոչինչ տրված չէ, ապա with-open-file մակրոսը⟨filename⟩ անունով ֆայլը բացում է կարդալու համար և այն կապում է ⟨stream⟩

42

Page 43: Common Lisp․ 12 խնդիր

անունով հոսքի հետ։ Օրինակ, եթե filenameփոփոխականով տրված է որևէ ֆայլիանուն, ապա հետևյալ կոդը բացում է այդ ֆայլը կարդալու համար և այն կապում էinp փոփոխականի (ֆայլային հոսք) հետ.

(with-open-file (inp filename) ...)

Ստեղծված ֆայլային հոսքի տեսանելիության տիրույթը with-open-file մակրոսիմարմինն է։

Ընդհանուր դեպքում Common Lisp լեզուն ֆայլերը բացելու համար նախատե֊սում է open, իսկ փակելու համար՝ close ֆունկցիաները [14, Գլուխ 23]։

(open ⟨filename⟩ ⟨option⟩∗)(close ⟨stream⟩)

open ֆունկցիայի նույն ⟨option⟩ անվանված արգումենտներն են օգտագործվումորպես with-open-file մակրոսի արգումենտներ։ Մասնավորապես, :directionարգումենտով կարելի է ցույց տալ հոսքի ուղղությունը։ Այս արգումենտիարժեքներկարող են լինել :input, որը լռելության արժեքն է և ցույց է տալիս, որ ստեղծվելու էընթերցելու հոսք, :output, որի դեպքում ստեղծվում է արտածման հոսք, և :io, որիդեպքում էլ ստեղծվում է երկուղղված՝ միաժամանակ և՛ ընթերցման և՛ արտածմանհոսք։

Հերթական սիմվոլը ֆայլից կարդացվում է read-char ֆունկցիայով, որի առա֊ջին արգումենտը ընթերցվող հոսքն է, իսկ երկրորդ՝ eof-error-p, արգումենտըցույց է տալիս, թե ինչ պետք է անել, երբ կարդացող ֆունկցիան հասել է հոսքիվերջին: Եթե eof-error-p-իարժեքըտարբեր է nil-ից, ապագեներացվում է սխալ,եթե nil է, ապա read-char ֆունկցիան վերադարձնում է իր երրորդ արգումենտի՝eof-value, արժեքը։ Օրինակ�

(read-char inp nil nil)

արտահայտությամբ inp ֆայլային հոսքից կարդում ենք մեկ սիմվոլ։

Ֆայլի պարունակությունը սիմվոլ առ սիմվոլ կարդալու համար կիրառենք Com֊mon Lisp լեզվում ցիկլերի կազմակերպման հարավորություններից ևս մեկը՝ doմակրոսը։ Նրա պարզեցված տեսքը հետևյալն է.

(do ((⟨var ⟩ ⟨init-form⟩ ⟨step-form⟩)) (⟨end-test⟩)⟨body-form⟩∗)

Ցիկլի առաջին իտերացիան կատարվում է ⟨var⟩ փոփոխականի ⟨init-form⟩ արժե֊քով։ Հաջորդ իտերացիաներում ⟨var⟩ փոփոխականին կապվում է ⟨step-form⟩-իհաշվարկված արժեքը։ Ցիկլը շարունակում է կատարվել քանի դեռ կեղծ է ⟨end-test⟩պայմանը։ Այլ կերպ ասած, do մակրոսի այս բերված տեսքը համապատասխանումէ հետևյալ փոևդոկոդին.

43

Page 44: Common Lisp․ 12 խնդիր

1: var := init-form2: repeat3: body-form*4: var := step-form5: until end-testՕրինակ, 0-ից 10թվերի քառակուսիներըտպելու համար do մակրոսով գրված ցիկլնայսպիսին է.

1 (do ((i 0 (1+ i))) ((> i 10))2 (princ (* i i))3 (terpri))

Մեր խնդրի համար do մակրոսի կիրառումը կարող է ունենալ հետևյալ տեսքը.

1 (do ((ch #\Space (read-chr inp nil)))2 ((null ch))3 ...)

Հերթական սիմվոլը կարդալուց հետո պետք է տարբերել, թե արդյո՞ք այնայբուբենի 26 տառերից որևէ մեկն է, ապա որոշել նրա հերթական համարը ևտառերի հաճախությունների աղյուսակումմեկով ավելացնել տվյալ տառի հանդի֊պելու քանակը։ alpha-char-p պրեդիկատ ֆունկցիան վերադարձնում է T, եթեարգումենտը այբուբենի տառ է՝ մեծատառ կամ փոքրատառ։

Տառի հերթական համարը որոշելու համար սահմանենք char-index ֆունկ֊ցիան, որը ստանում է տառը, դարձնում է այն մեծատառ՝ char-upcase ֆունկցիա֊յով, ապա char-code ֆունկցիայով ստանում է կոդը ASCII աղյուսակում, որից էլհանելով 65՝ «A» տառի կոդը, ստացվում է տառի ինդեքսը հաճախություններիաղյուսակում։

1 (defun char-index (ch)2 "Հաշվել տրված տառի ինդեքսը։"3 (- (char-code (char-upcase ch)) 65))

Սահմանենքմիֆունկցիա, որն արգումենտում ստանում է վերլուծության ենթա֊կա ֆայլի անունը, և վերադարձնում է այդ ֆայլում հանդիպողտառերի քանակներիվեկտորը։ Թող այդ ֆունկցիան կոչվի count-letters-of-file։

1 (defun count-letters-of-file (filename)2 (let ((result (make-array 26 :initial-element 0)))3 (with-open-file (inp filename)4 (do ((ch #\Space (read-char inp nil)))5 ((null ch))6 (when (alpha-char-p ch)7 (incf (aref result (char-index ch))))))8 result))

44

Page 45: Common Lisp․ 12 խնդիր

Որպեսզի կարողանանք տառերը դասավորել ըստ իրենց հաճախություններինվազման, նախ սահմանենք vector-to-pair-list ֆունկցիան, որը հաճախութ֊յունների վեկտորից կառուցում է (⟨տառ⟩ ⟨քանակ⟩) զույգերի ցուցակը։

1 (defun vector-to-pair-list (vec)2 (loop for e across vec3 for c from 654 collect (list (code-char c) e)))

loop մակրոսի for–across ենթակառուցվածքը թույլ է տալիս vec վեկտորիհերթական տարրը կապել ցիկլի eպարամետրի հետ։ collect ենթակառուցվածքըցուցակ է կազմում ցիկլի ամեն իտերացիայում իր հաշվարկած արժեքներից, և այդցուցակը վերադարձնում է որպես արժեք։ Օրինակ, [3; 10]միջակայքի բոլոր թվերիցուցակը 0.5 քայլով կազմելու համար կարելի է գրել.

(loop for k from 3 to 10 by 0.5collect k)

Հետո սահմանենք sorted-list-of-letters ֆունկցիան, որը կարգավորում էvector-to-pair-list ֆունկցիայի վերադարձրած զույգերի ցուցակը՝ ըստ զույ֊գերի երկրորդ տարրերի նվազման։

1 (defun sorted-list-of-letters (palis)2 (flet ((pair-greater (a b)3 (> (second a) (second b))))4 (mapcar #'first (sort palis #'pair-greater))))

Զույգերի միջև «մեծ է» հարաբերությունը սահմանվում է pair-greater լոկալֆունկցիայով, որը տրվում է sort ֆունկցիային որպես կարգավորման պրեդիկատ։Զույգերի ցուցակը կարգավորելուց հետո mapcar ֆունկցիայի օգնությամբ բոլորզույգերիցառանձնացնում ենքառաջինտարրերը և դրանցից կազմում նոր ցուցակ։

Այս mapcar ֆունկցիան շատ նման է նախորդ օրինակում (էջ 40) օգտագործվածmapc ֆունկցիային։

(mapcar ⟨function⟩ ⟨list⟩+)

Այն իր առաջին արգումենտում տրված k-տեղանի ⟨function⟩ ֆունկցիան հերթա֊կանորեն կիրառում է ⟨list⟩ ցուցակների տարրերի նկատմամբ և վերադարձնում էստացված արդյունքների ցուցակը։ Օրինակ�

(mapcar #'1+ '(0 1 2 3 4)) ⇒ (1 2 3 4 5)(mapcar #'list '(0 1 2) '(a b c)) ⇒ ((0 A) (1 B) (2 C))

Այս պահին ունենք այն ամենը, որ count-letters-of-file, vector-to-pair-list և sorted-list-of-letters ֆունկցիաների հաջորդական կիրառմամբ որևէֆայլի համար ստանանք նրա տառերի ցուցակը՝ ըստ տեքստում հանդիպելու

45

Page 46: Common Lisp․ 12 խնդիր

հաճախության։ Օրինակ, եթե sawyer.txt ֆայլը պարունակում է «Թոմ Սոյերիարկածները» գրքի անգլերեն տարբերակի տեքստը, ապա

(sorted-list-of-letters(vector-to-pair-list

(count-letters-of-file "sawyer.txt")))

արտահայտության հաշվարկը վերադարձնում է հետևյալ ցուցակը.

(#\E #\T #\O #\A #\N #\H #\I #\S #\R #\D #\L #\U #\W#\M #\C #\Y #\G #\F #\B #\P #\K #\V #\J #\X #\Q #\Z)

Եթե ֆայլում տառի հանդիպելու հաճախությունը դիտարկենք որպես «նախա֊պատվություն», ապա հեշտ է տեսնել, որ տառերի ստացված ցուցակը նախապատ֊վٳթյٳններով հերթ է։ Այդ հերթում տարրերը ձախից աջ հերթագայում են ըստնախապատվության աճման։ Հիմա նախապատվություններով հերթը նկարենքբինար ծառի (բինար բուրգի) տեսքով, ապա նրա դեպի ձախ գնացող կողերինվերագրենք «.» պիտակը, իսկ դեպի աջ գնացողներին՝ «-» պիտակը։ Այնուհետևնրա հանգույցները, արմատից բացի, մակարդակ առ մակարդակ լրացնենք մերստացած ցուցակի տառերով։

E T

O A N H

I S R D L U W M

C Y G F B P K V J X Q Z

*0

1 2

3 4 5 6

7 8 9 10 11 12 13 14

15 16 17 18 19 20 21 22 23 24 25 26

. -

. - . -

. - . - . - . -

. - . - . - . - . - . -

Նկար 8.1: Տառերի դասավորությունը բինար բուրգում։

Ըստ 8.1 նկարի, այբուբենի յուրաքանչյուր տառին համապատասխանեցնենքայն կոդը, որը ստացվում է ծառի արմատից դեպի այդ տառը տանող կողերիպիտակների թվարկումից։ Օրինակ, «D» տառի կոդը «-.--» է, իսկ «Q» տառինը՝«-.-.»։ Այդ կոդերի կառուցումը ծրագրագորելու համար, իհարկե, կարող ենքնկարում բերված ծառի տեսքով, ապա, նրա հանգույցներով անցնելով, ստանալտառերի կոդերը։ Բայց հիշենք աղյուսակով (վեկտորով) ներկայացված բինարբուրգիմիհատկություն։ Այն է՝ եթեաղյուսակում հանգույցի ինդեքսը k է, ապա նրաձախ հանգույցի ինդեքսը 2k + 1 է, իսկ աջ հանգույցինը՝ 2k + 2։ Ըստ այսմ՝ որևէ kինդեքսով հանգույցի ծնողի ինդեքսը կորոշվի ⌈k/2⌉ − 1 բանաձևով։ 8.1 նկարումայդ ինդեքսները գրված են հանգույցների տակ։

46

Page 47: Common Lisp․ 12 խնդիր

Սահմանենք morse-code-of ֆունկցիան, որ արգումենտում ստանում է բինարբուրգի (նախապատվություններով հերթի) հանգույցի ինդեքսը և վերադարձնում էնրան համապատասխանող կոդը։ Կառուցված կոդը ստացվելու է նիշերի ցուցակիտեսքով։ Եթե ինդեքսը զրո է, ապա ֆունկցիան վերադարձնում է դատարկ ցուցակ,հակառակ դեպքում կառուցում է տրված հանգույցի ծնող հանգույցի կոդը և, եթեհանգույցի ինդեքսը կենտ է (այն գտնվում է ծնողի ձախ կողմում), ապա նրա պոչիցկցում է «.» նիշը, եթե զույգ է՝ «-» նիշը։

1 (defun morse-code-of (k)2 (labels3 ((morse-code-aux (h r)4 (if (zerop h)5 r6 (morse-code-aux (1- (ceiling h 2))7 (cons (if (oddp h) #\. #\-) r)))))8 (morse-code-aux k '())))

Այնուհետև սահմանենք build-morse-code ֆունկցիան, որը ստանում է երկուֆայլի անուն։ Առաջինն այն ֆայլի անունն է, որը պարունակում է վերլուծությանենթակատեքստը, իսկ երկրորդնայն ֆայլի անունն է, որիմեջ գրվելու է կառուցվածկոդավորման սխեման։

1 (defun build-morse-code (infile outfile)2 (let ((sortedl (sorted-list-of-letters3 (vector-to-pair-list4 (count-letters-of-file infile)))))5 (with-open-file (sout outfile :direction :output6 :if-exists :supersede)7 (loop for c in sortedl8 for i from 19 do (princ c sout) (princ " " sout)10 (mapc #'(lambda (e) (princ e sout))11 (morse-code-of i))12 (terpri sout)))))

Եթե ֆայլային հոսքը ստեղծված է արտածման համար, ապա open ֆունկցիայի(ինչպես նաև with-open-file մակրոսի) :if-exists անվանված արգումենտիարժեքով ցույց է տրվում այն գործողությունը, որը պետք է կատարել, երբ տրվածանունով ֆայլ արդեն գոյություն ունի։ Այս դեպքում տրված :supersede արժեքըստիպում է ջնջել հին ֆայլը և դրա փոխարեն ստեղծել նորը։ Եթե պետք լիներարտածվող տվյալերը կցել արդեն գոյություն ունեցող ֆայլի վերջից, ապա պետք էօգտագործվեր :append արժեքը։

loop մակրոսի օգնությամբ այստեղ կազմակերպվել է երկու զուգահեռ հաշվիչ֊ներով ցիկլ։ c հաշվիչն անցնում է տառերի ցուցակով, i հաշվիչը, որ ցույց է

47

Page 48: Common Lisp․ 12 խնդիր

տալիս ցուցակում տառի ինդեքսը, սկսում է 1-ից և հասնում է 26-ի։ Ցիկլի doծառայողական բառից հետո գրված արտահայտությունները princ ֆունկցիայիմիջոցով sout հոսքին են դուրս բերում տառը, բացատ, տառի կոդը և, terpriֆունկցիայի միջոցով, նոր տողի անցման նիշը։

Եվ վերջապես, «Թոմ Սոյերի արկածները» գրքի տեքստի հիման վրա Մորզեիկոդավորման սխեմային նման սխեմա կառուցելու համար հաշվարկենք

(build-morse-code "sawyer.txt" "morsecode.txt")

արտահայտությունը, որը morsecode.txt ֆայլում կգեներացնի հետևյալ կոդա֊վորման սխեման (հարմարության համար այստեղ բերված է սյունակներով).

E . H -- L -.. G ...- K .--.T - I ... U -.- C ..-. V .---A .. S ..- W --. F ..-- J -...O .- R .-. M --- B .-.. X -..-N -. D .-- Y ... P .-.- Q -.-..

Z -.--

* * *

Հետաքրքիր վարժություն կլինի Մորզեի կոդ կառուցել տարբեր հեղինակներիու տարբեր չափերի տեքստերի համար և համեմատել արդյունքները։ Ինչպես նաևայդ արդյունքները համեմատել Մորզեի ստանդարտ կոդի հետ ու տեսնել, թեինչպիսիք են շեղումները։

48

Page 49: Common Lisp․ 12 խնդիր

Խնդիր 9

Տեքստի բառապաշարի

վերլٳծٳթյٳն

Ենթադրենք տրված է ինչ-որ տեքստ, օրինակ գրական ստեղծագործություն, ևպահանջվում է վիճակագրական տվյալներ հավաքել այդ տեքստի բառապաշարիմասին։ Վիճակագրական պահանջները կարող են լինել բազմազան, բայց այսօրինակում կդիտարկենք հետևյալները. 1. Հաշվել բառապաշարի հարստությանցուցանիշը, որը տեքստի իրարից տարբեր բառերի քանակի հարաբերությունն էտեքստի բոլոր բառերի քանակին։ 2.Գտնել տեքստումմիայն kանգամ օգտագործ֊ված բառերը։ 3. Գտնել ամենաշատ օգտագործված k բառերը։

Այս երեք ենթախնդիրները լուծելու համար հարկավոր է նախ՝ տրվածտեքստա֊յին ֆայլը տրոհել բառերի և կազմել բառարան, որտեղ ամեն բառի հետ պահվածէ նաև տեքստում նրա հանդիպելու քանակը, այնուհետև՝ վերլուծել բառարանիպարունակությունը և կազմել անհրաժեշտ վիճակագրությունը։

Մեր խնդրի համատեքստում բառ կանվանենք այբուբենի իրար հաջորդողտառերի անընդհատ շարքը։ Բոլոր սիմվոլներից այբուբենի տառերը տարբերելուհամար Common Lisp լեզվում նախատեսված է alpha-char-p պրեդիկատը, այնստանում է նիշ (character) և վերադարձնում է T, եթե այդ նիշն այբուբենի տառ է,և NIL՝ հակառակ դեպքում։ Բոլոր նիշերը, որոնց համար այս պրեդիկատը տալիս էբացասական պատասխան, կհամարենք ավելորդ և դեն կնետենք։

Սահմանենք մի ֆունկցիա, որը տրված մուտքային հոսքից կարդում է տրվածպրեդիկատին բավարարող նիշերի անընդհատ շարք և վերադարձնում է դրանցցուցակը։ Այս ֆունկցիայի հաջորդական կանչերով կարող ենք հոսքից հեռացնելավելորդ նիշերը, իսկ բառ կազմող նիշերի ցուցակից կազմել բառեր։

1 (defun scan-sequence (pred strm)2 (if (peek-char t strm nil nil)3 (loop for ch = (read-char strm nil)4 until (null ch)

49

Page 50: Common Lisp․ 12 խնդիր

5 while (funcall pred ch)6 collect (char-downcase ch)7 finally (when ch (unread-char ch strm)))8 '(#\@ #\@ #\@)))

Ներմուծման հոսքի վիճակը, թե հնարավո՞ր է արդյոք այնտեղից ինչ-որ բանկարդալ, ստուգվում է peek-char ֆունկցիայով, որը վերադարձնում է հերթականսիմվոլը, բայց այն չի հեռացնում հոսքից։

(peek-char ⟨peek-type⟩? ⟨stream⟩? ⟨eof-error-p⟩? ⟨eof-value⟩?)

Եթե ⟨peek-type⟩ արգումենտը t է, ապա peek-char ֆունկցիան անտեսում է բացա֊տանիշերը և վերադարձնում է դրանց հաջորդող սիմվոլը։ Եթե ⟨eof-error-p⟩ արգու֊մենտը nil է, ապա հոսքի ավարտին հասնելիս peek-char ֆունկցիան սխալ չիգեներացնում և վերադարձնում է ⟨eof-value⟩ արգումենտի արժեքը։ Մեր դեպքում,եթե peek-char ֆունկցիան վերադարձրել է nil, ապա դա նշանակում է, որհոսքի ավարտն է, և այս դեպքում scan-sequence ֆունկցիան վերադարձնում է«(#\@ #\@ #\@)» պայմանական ցուցակը։ Իսկ այս ցուցակից կազմված «@@@»բառն էլ հենց հանդիսանում է ֆայլը կարդալու ավարտի ազդանշան։

Հոսքից նիշերի հաջորդականությունը կարդում է loop ցիկլը, որը հերթականիտերացիայի ժամանակ read-char ֆունկցիայով կարդում էմեկ նիշ։ Ցիկլի ավար֊տը որոշվում է երկու պայմաններով՝ «until (null ch)», այն է՝ հենց որ հանդիպիnil, և «while (funcall pred ch)»,այն է՝ քանի դեռ կարդացված նիշը բավարարٳմէ predպրեդիկատին։ collect ծառայողական բառը ցուցակ է կազմում հերթականch նիշերից՝ նախապես դրանք դարձնելովփոքրատառ։Այդ ցուցակը վերադարձվումէ որպես loop ցիկլի արժեք։ Քանի որ այս ցիկլում նախ կարդում ենք նիշը և հետոորոշում կայացնում դրա մասին, ապա պրեդիկատին չբավարարող նիշը պետք էհետ գրել հոսքի մեջ։ finally ծառայողական բառից հետո գրված unread-charֆունկցիան տրված նիշը հետ է վերադարձնում տրված հոսքին։

Սահմանված scan-sequance ֆունկցիան վերադարձնում է նիշերի ցուցակ։ Այդցուցակը տողի՝ string տիպի օբյեկտի վերածելու համար սահմանենք հետևյալֆունկցիան.

1 (defun character-list-to-string (cl)2 (loop with word = (make-string (list-length cl))3 for c in cl4 for i from 05 do (setf (char word i) c)6 finally (return word)))

loop ցիկլի մարմնում with ծառայողական բառի օգնությամբ հայտարարվել էword լոկալ փոփոխականը և նրան որպես սկզբնական արժեք վերագրվել է make-string ֆունկցիայով կառուցված տողը։ make-string ֆունկցիան string տիպի

50

Page 51: Common Lisp․ 12 խնդիր

կոնստրուկտորն է [14, 18.3], այն արգումենտում սպասում է կառուցվելիք տողիերկարությունը, որ տվյալ դեպքում պետք է հավասար լինի նիշերի ցուցակիերկարությանը։ Նիշերի ցուցակի հերթական տարրը word տողի համապատաս֊խան դիրքին վերագրելու համար loop ցիկլում նախատեսված են երկու պարա֊մետրեր՝ c և i։ «for c in cl» գրառումը նշանակում է, որ c պարամետրերը ցիկլիկատարման ընթացքում «վազում է» cl ցուցակով, իսկ «for i from 0» գրառումընշանակում է, որ i պարամետրը նախապես ստանում է 0 արժեքը և ցիկլի ամենմի իտերացիայում ավելանում է մեկով։ c պարամետրի արժեքը word բառի i-րդդիրքում գրելու համար char ֆունկցիայով դիմում ենք այդ դիրքին և setf ֆունկ֊ցիայով կատարում վերագրումը։ Ցիկլի ավարտին վերադարձնում ենք word լոկալփոփոխականը որպես ֆունկցիայի արժեք։

Այսպիսի character-list-to-string ֆունկցիա սահմանելը արդարացվածկլիներ, եթե Common Lisp լեզվում հաջորդականությունների համար նախատեսվածչլիներ map ֆունկցիան։

(map ⟨result-type⟩ ⟨function⟩ ⟨sequence⟩+)

Այս ֆունկցիան տրված ⟨function⟩-ը կիրառում է ⟨sequence⟩+ հաջորդականություն֊ների հերթական տարրերի նկատմամբ և կառուցում է ⟨result-type⟩ տիպի նորհաջորդականություն։ Ճիշտ այնպես, ինչպես mapcar ֆունկցիան (տես էջ 45) իրառաջին արգումետում տրված ֆունկցիան կիրառում էր հաջորդ արգումենտներումտրված ցուցակների հերթական տարրերի վրա և կառուցում էր նոր ցուցակ։Օրինակ�

(map 'list #'oddp '(0 1 2 3 4 5)) ⇒ (NIL T NIL T NIL T)(mapcar #'oddp '(0 1 2 3 4 5)) ⇒ (NIL T NIL T NIL T)Նիշերի ցուցակից string տիպի տող ստանալու համար նույնպես կարելի է

կիրառել map ֆունկցիան՝ նկատի ունենալով այն փաստը, որ Common Lisp լեզվումstringտիպը sequenceտիպի ենթատիպ է։ Դրանում կարելի է համոզվել, օրինակ,հաշվարկելով հետևյալ արտահայտությունը, որի վերադարձրած T արժեքն ասումէ, որ տողը նույնպես հաջորդականություն է։

(typep (string 'abc) 'sequence) ⇒ TՀետևաբար կարելի է սահմանել մի նոր ֆունկցիա, որը ստանում է նիշերի

ցուցակ և map ֆունկցիայով կառուցում է տողը։

1 (defun chars-to-string (cl)2 (map 'string #'identity cl))

identityֆունկցիանառանցփոփոխության վերադարձնում է իրարգումենտը։ Այնհամարժեք է «(lambda (x) x)» ֆունկցիային։

Ֆայլային հոսքից մեկ բառ կարդալու համար սահմանենք read-one-wordֆունկցիան, որը հերթական բառը կարդում է երկու գործողությամբ: Առաջինանգամ scan-sequenceֆունկցիայի կանչով կարդում ու դեն է նետում բառ չկազմող

51

Page 52: Common Lisp․ 12 խնդիր

նիշերը՝ այն սիմվոլները, որոնք չեն բավարարում alpha-char-p պրեդիկատին։Իսկ երկրորդ անգամ արդեն scan-sequence ֆունկցիայի վերադարձրած նիշերիցուցակից կազմում է մեզ հետաքրքող բառը։

1 (defun read-one-word (strm)2 (scan-sequence (complement #'alpha-char-p) strm)3 (chars-to-string (scan-sequence #'alpha-char-p strm)))

Օգտագործված complement ֆունկցիան վերցնում է f ֆունկցիան և վերադարձ֊նում է մի նոր f ′ ֆունկցիա, որի արգումենտների ցուցակը նույնն է ինչ f -ինը,իսկ վերադարձրած արժեքը f -ի արժեքի ժխտումն է։ Տվյալ դեպքում (complement#'alpha-char-p) արտահայտությամբ ստացել ենք մի ֆունկցիա, որը T արժեքկվերադարձնի այբուբենի տառերից բացի բոլոր այլ նիշերի համար։

Տեքստիցառանձնացված բառերը ևամեն բառի հետտեքստում դրա հանդիպե֊լու քանակը պահելու համար կօգտագործենք Common Lisp լեզվի հեշ-աղյٳսակը։make-hash-table ֆունկցիան ստեղծում է նոր հեշ-աղյուսակ՝ hash-table տիպիօբյեկտ, որի բանալիներ կարող են ծառայել այնպիսի օբյեկտները, որոնց հավա֊սարությունը հնարավոր է ստուգել eq, eql, equal և equalpպրեդիկատներով։ Նորհեշ-աղյուսակ ստեղծելիս բանալիները համեմատող պրեդիկատը տրվում է make-hash-table ֆունկցիայի :test անվանված արգումենտով, որի լռելության արժեքըeql ֆունկցիան է։

Բառերը ֆայլից կարդալու և բառարան կազմելու համար սահմանենք read-words-from-file ֆունկցիան և նրա մարմնում սահմանենք words հեշ-աղյուսակը։

1 (defun read-words-from-file (filename)2 (let ((words (make-hash-table :test #'equal)))3 (with-open-file (fin filename)4 (loop for wd = (read-one-word fin)5 until (equal "@@@" wd)6 do (incf (gethash wd words 0))))7 words))

Քանի որ այս խնդրում հեշ-աղյուսակի բանալիները տողեր են, ապա որպեսհամեմատման ֆունկցիա ընտրվել է equal պրեդիկատը (տես [18, էջ 89])։

loop ցիկլի հերթական իտերացիայում read-one-word ֆունկցիան fin հոսքիցկարդում էմեկ բառ: Եթե կարդացածը «@@@» բառն է, ապա դա համարվում է ֆայլիընթերցման ավարտ։ do ծառայողական բառից հետո գրված արտահայտությունըմեկով ավելացնում է wordsաղյուսակում գրանցված wd բառին համապատասխանթիվը։ Եթե բառը դեռ աղյուսակում չկա, ապա այն ավելացվում է։ gethash ֆունկ֊ցիան հեշ-աղյուսակում որոնում է տրված բանալիով գրառումը և վերադարձնում էնրան համապատասխանեցված օբյեկտը։

(gethash ⟨key⟩ ⟨hash-table⟩ ⟨default⟩?)

52

Page 53: Common Lisp․ 12 խնդիր

Եթե ⟨key⟩ բանալիով գրառումը ⟨hash-table⟩ աղյուսակում չկա, ապա ֆունկցիանվերադարձնում է ⟨default⟩-ը, իսկ եթե ⟨default⟩-ը տրված չէ, ապա՝ NIL։

Տեքստը տրոհելու և բառերի հաճախությունների բառարան կազմելու համարհեշ-աղյուսակը լավագույն տվյալների կառուցվածքն է։ Բայց բառարանին հար֊ցումներ անելու և հարկավոր վիճակագրություն կազմելու համար Common Lispստանդարտը հեշ-աղյուսակների համար չի նախատեսում բավարար ճկուն ևբազմազան ֆունկցիաներ։ Այդ պատճառով էլ հեշ-աղյուսակից կկառուցենք

((⟨word⟩1 . ⟨count⟩1) (⟨word⟩2 . ⟨count⟩2) ...)

տեսքի ցուցակ, որի ամեն մի տարրը բառարանի բառը և այդ բառի՝ տքեստումհանդիպելու քանակը պարունակող կետով զույգ (dotted pair) է։ Այս տեսքի ցուցակըCommon Lisp լեզվում կոչվում է զٳգորդման ցٳցակ (association list)։

Կետով զٳյգը (cons ⟨car ⟩ ⟨cdr⟩) գործողության միջոցով կառուցված (⟨car ⟩ .⟨cdr⟩) տեսքի մի օբյեկտ է, որի երկրորդ տարրը ցուցակ չէ։ Օրինակ�

(cons 'a 1) ⇒ (A . 1)(cons '(a b) 1) ⇒ ((A B) . 1)

Եթե cons ֆունկցիայի ⟨cdr⟩ արգումենտը ցուցակ է, ապա, ինչպեստեսանք 12 էջում,ստեղծվում է նոր ցուցակ՝ ⟨car ⟩ գլխով և ⟨cdr ⟩ պոչով։ Բանն այն է, որ Lisp լեզվիսովորական ցուցակները ⟨cdr⟩ դաշտով իրար կապված կետով զույգեր են, որոնցիցվերջինի ⟨cdr⟩ դաշտը կապված է nil օբյեկտի հետ։ Ահա, օրինակ, (a . b), (a) և(a b) օբյեկտների գրաֆիկական տեսքը։

a b a nil a b nil

Այսպիսով՝ հեշ-աղյուսակից պետք է ստանալ կետով զույգերի ցուցակ։ maphashֆունկցիան ստանում էմի երկտեղանի ֆունկցիա և հեշ-աղյուսակ, ապա, անցնելովհեշ-աղյուսակիտարրերով, ֆունկցիային էփոխանցում հերթական բանալի-արժեքզույգերը։ Օրինակ, ստանդարտ արտածման հոսքին հեշ-աղյուսակն արտածելուհամար կարող ենք գրել հետևյալ ֆունկցիան.

1 (defun print-hash-table (table)2 (maphash #'(lambda (k v)3 (format t "~a --> ~d~%" k v))4 table))

Նման ձևով սահմանենք մի ֆունկցիա, որն արգումենտում ստանում է հեշ-աղ֊յուսակ և վերադարձնում է զույգերի ցուցակ. պարզապես պետք է ցուցակի մեջհավաքել բանալիից և արժեքից cons ֆունկցիայով կառուցված կետով զույգերը։(Հետագա բոլոր վերլուծություններում «բառերի ցուցակ» ասելով կհասկանանք(բառ . քանակ) զույգերի ցուցակը։)

53

Page 54: Common Lisp․ 12 խնդիր

1 (defun hash-table-to-list (table)2 (let ((result '()))3 (maphash #'(lambda (k v)4 (push (cons k v) result))5 table)6 result))

Առաջին վերլուծական արդյունքը՝ բառապաշարի հարստության գործակիցը,հաշվելու համար պետք է իրարից տարբեր բառերի քանակը բաժանել բոլորբառերի քանակին։ Իրարից տարբեր բառերի քանակը բառերի ցուցակի երկա֊րությունն է, որ կստանանք list-length ֆունկցիայով։ Բոլոր բառերի քանակըհաշվելու համար պետք է իրար գումարել բառերի ցուցակի զույգերի երկրորդտարրը։ Դա կարելի է անել, օրինակ, loop մակրոսի sum կառուցվածքի օգնությամբ։Եթե words-ը բառերի ցուցակն է, ապա

1 (loop for k in words sum (cdr k))

արտահայտությունը վերադարձնում է բոլոր բառերի քանակը։vocabulary-factor ֆունկցիան արգումենտում ստանում է բառերի ցուցակը և

վերադարձնում է բառարանի հարստության գործակիցը։

1 (defun vocabulary-factor (words)2 (let ((unique (list-length words))3 (all (loop for k in words sum (cdr k))))4 (/ unique all)))

Միայն k անգամ օգտագործված բառերը գտնելու համար պետք է բառերիցուցակից հեռացնել այն զույգերը, որոնց երկրորդ բաղարդիչը հավասար չէ k-ի,ապա վերադարձնել մնացած զույգերի առաջին տարրերի ցուցակը։

Հաջորդականությունից մի որևէ պայմանի բավարարող տարրերը հեռացնելուև նոր հաջորդականություն կազմելու համար նախատեսված է remove-if ֆունկ֊ցիան։ Այն վերադարձնում է ⟨sequence⟩ հաջորդականության պատճենը՝ առանց⟨function⟩ պրեդիկատին բավարարող տարրերի։

(remove-if ⟨function⟩ ⟨sequence⟩)

Եթե words-ը բառերի ցուցակն է, ապա միայն k անգամ օգտագործված բառերըկարող ենք «զտել» հետևյալ արտահայտությամբ։

1 (remove-if #'(lambda (p) (/= (cdr p) k)) words)

Այստեղ օգտագործված λp . car(p) ̸= k պրեդիկատը ցուցակի հերթական զույգի⟨cdr ⟩ բաղադրիչը համեմատում է k թվի հետ։ Բայց կարող ենք պրեդիկատը գրելλx . x ̸= k տեսքով, իսկ remove-if ֆունկցիայի :key անվանված արգումենտովնշել, որ պրեդիկատին պետք է փոխանցել զույգի ⟨cdr⟩ բաղադրիչը։

54

Page 55: Common Lisp․ 12 խնդիր

1 (remove-if #'(lambda (x) (/= x k) words :key #'cdr)

Սահմանենք only-k-times-used-words ֆունկցիան, որ ստանում է բառերիցուցակը և k թիվը, ապա վերադարձնում է այն բառերի ցուցակը, որոնց հանդիպե֊լու քանակը k է։ Բառերի ցուցակից հարկավոր զույգերը զտվում են remove-if ֆունկցիայով, ապա mapcar ֆունկցիայով առանձնացվում են զույգերի ⟨car ⟩բաղադրիչները և կառուցվում է դրանց ցուցակը։

1 (defun only-k-times-used-words (words k)2 (mapcar #'car3 (remove-if #'(lambda (x) (/= x k))4 words :key #'cdr)))

Բառերից ցուցակից k ամենաշատ օգտագործված բառերը գտնելու խնդիրը միքիչ խճճվում է այն պատճառով, որ մի քանի բառեր կարող են տեքսում հանդիպելնույն քանակով։ Դրա համար էլ, եթե words-ը բառերի ցուցակն է, ապա պետք է.ա) mapcar ֆունկցիայի օգնությամբ ստանալ բառերի քանակների ցուցակը, բ) այդցուցակից remove-duplicates ֆունկցիայի օգնությամբ հեռացնել կրկնություն֊ները, գ) sort ֆունկցիայով կարգավորել թվերն ըստ նվազման, դ) nth ֆունկցիա֊յով ընտել կարգավորված թվերից k-րդը։ Այնուհետև բառերի ցուցակից ընտրել այնբառերը, որնց քանակը փոքր է կամ հավասար ընտրված k-րդ թվից։

1 (defun most-used-k-words (words k)2 (let* ((cl (mapcar #'cdr words))3 (ul (remove-duplicates cl))4 (n (nth k (sort ul #'>))))5 (mapcar #'car6 (remove-if #'(lambda (x) (<= x n))7 words :key #'cdr))))

remove-duplicates ֆունկցիան վերադարձնում է արգումենտում տրված հաջոր֊դականության պատճենը՝ առանց կրկնվող տարրերի։ sort ֆունկցիան հաջորդա֊կանությունը կարգավորում է ըստ տրված համեմատող ֆունկցիայի։ Օրինակ�

(sort '(8 6 5 9 8 3 2 1) #'<) ⇒ (1 2 3 5 6 8 8 9)(sort '(8 6 5 9 8 3 2 1) #'>) ⇒ (9 8 8 6 5 3 2 1)

sort ֆունկցիայի :key անվանված արգումենտը հնարավորություն է տալիս տար֊րերը համեմատելուց առաջ դրանք ձևափոխել տրված ֆունկցիայով։ Օրինակ, եթեհարկ լինի words բառերի ցուցակը կարգավորել այբբենական կարգով, կարող ենքգրել.

1 (sort words #'string< :key #'car)

կամ, օրինակ, նույն բառերի ցուցակը ըստ բառերի երկարությունների աճմանկարգավորելու համար կարող ենք գրել.

55

Page 56: Common Lisp․ 12 խնդիր

1 (sort words #'< :key #'(lambda (w) (length (car w))))

Պետք է հաշվի առնել նաև այն, որ sort ֆունկցիան ունի դեստրٳկտիվ վարք,այսինքն՝ ձևափոխում է արգումենտը, և այն դեպքերում, երբ արգումենտի փոփո֊խումը ցանկալի չէ, sort ֆունկցիային պետք է փոխանցել պատճենված օբյեկտը։Ցուցակների դեպքում պատճենը կարելի է ստանալ copy-list ֆունկցիայով։

Այս օրինակում կառուցած ծրագիրը փորձարկելու համար նորից կարող ենքօգտագործել «Թոմ Սոյերի արկածները» գրքի անգլերեն տարբերակի տեքստը, ուտեսնել թե որքան բառեր ու ինչպիսի հաճախությաբ է օգտագործել Մարկ Տվենը։

* * *

Այս օրինակի հիմնական նպատակն, իհարկե, hash-table օբյեկտի հետաշխա֊տանքի ցուցադրությունն է։ Բայց պակաս հետաքրքիր չէ նաև նույն խնդրի լուծմանկազմակերպումն առանց հեշ-աղյուսակի օգտագործման՝ այսպես ասած, ավելիֆٳնկցիոնալ լուծումը։ Դրա համար պետք է նախ հոսքից կարդալ բոլոր բառերըև կազմել դրանց ցուցակը, այնուհետև այդ ցուցակը կարգավորել բառարանայինկարգով, որպեսզի իրար մոտ հավաքվեն կրկնվող բառերը, և վերջում, բառ-քանակզուգորդման ցուցակը կազմելու համար, կրկնվող բառերի ամենմիխումբը փոխա֊րինել (⟨word⟩ . ⟨count⟩) զույգով։

Նախ ձևափոխենք scan-sequence ֆունկցիան այնպես, որ չօգտագործվի loopմակրոսը։ Այս նոր scan-sequence-rec ֆունկցիան աշխատում է նույն տրամաբա֊նությամբ, բայց հոսքից պրեդիկատին բավարարող նիշերի ցուցակը կազմելուհամար օգտագործում է ռեկուրսիվ մոտեցում։

1 (defun scan-sequence-rec (pr sm)2 (let ((c (read-char sm nil)))3 (cond4 ((null c) '())5 ((funcall pr c) (cons c (scan-sequence-rec pr sm)))6 (t (unread-char c sm) '()))))

Ֆունկցիան դատարկ ցուցակ կվերադարձնի այն դեպքում, երբ հոսքում էլ կար֊դալու բան չկա, կամ եղած նիշերից ոչ մեկը չի բավարարում պրեդիկատին։

Տառերի ցուցակից բառ ստանալու համար այս օրինակում արդեն քննարկվեցchars-to-string ֆունկցիայի երկու տարբերակ։ Ժամանակն է արդեն ցույց տալ,թե ինչպես կարելի է նույն նպատակին հասնել format ֆունկցիայի օգնությամբ։Ահա այդ նոր սահմանումը.

1 (defun chars-to-string (cl)2 (format nil "~(~{~C~}~)" cl))

56

Page 57: Common Lisp․ 12 խնդիր

Ֆորմատավորման տողում օգտագործված «~(⟨text⟩~)» կառուցվածքն իր արգու֊մենտում տրված տողի բոլոր տառերը փոխարինում է դրանց փոքրատառերով։Այս նույն հրահանգի «~:(⟨text⟩~)» տարբերակն իր արգումենտում տրված բոլորբառերի առաջին տառերը դարձնում է մեծատառ, իսկ մյուսները՝ փոքրատառ։«~@(⟨text⟩~)» տարբերակը մեծատառ է դարձնում միայն տողի առաջին տառը,իսկ մյուստառերը դարձնում էփոքրատառ։ Իսկ «~:@(⟨text⟩~)»տարբերակըտողիբոլոր տառերը դարձնում է մեծատառ։

Ֆորմատավորմանտողի «~{⟨f ⟩~}» կառուցվածքը սպասում է, որ formatֆունկ֊ցիայի հերթական արգումենտը ցուցակ է և այդ ցուցակի ամենմի տարրը ձևափո֊խում է ըստ ⟨f ⟩ ֆորմատավորմանտողի։ Տվյալ դեպքում «~C» հրահանգը նույնութ֊յամբ վերադարձնում է արգումենտում տրված նիշը։

Հիմա սահմանենք read-all-words ֆունկցիան, որը վերադարձնում է տրվածհոսքից կարդացած բոլոր բառերի ցուցակը։ Սա նույնպես ռեկուրսիվ ֆունկցիաէ, որի համար ռեկուրսիայի ավարտի ազդանշան է հանդիսանում read-one-wordֆունկցիայի վերադարձրած դատարկ տողը։

1 (defun read-all-words (sm rs)2 (let ((w (read-one-word sm)))3 (if (equal w "")4 rs5 (read-all-words sm (cons w rs)))))

Ձևափոխվում և ավելի պարզ է դառնում նաև read-words-from-file ֆունկ֊ցիան, որը պարզապես տրված անունով ֆայլից ստեղծում է ընթերցելու հոսք և այնփոխանցում է read-all-words ֆունկցիային։

1 (defun read-words-from-file-1 (filename)2 (with-open-file (inp filename)3 (read-all-words inp '())))

Այսպիսով, read-words-from-file ֆունկցիային տալով հետազոտվող ֆայլիանունը, ստանում ենք այդ ֆայլում հանդիպող բոլոր բառերի ցուցակը՝ ըստիրենց հանդիպելու հաջորդականության և բոլոր տառերը դարձրած փոքրատառ։Արդեն գիտենք, որ այս ցուցակի բառերը բառարանային կարդով դասավորելուհամար բավական է կիրառել sortֆունկցիան՝ դրա երկրորդարգումենտումտալովstring-lessp պրեդիկատը։ Մնում է տեսնենք, թե ինչպես ենք խմբավորելու այդկարգավորված ցուցակի նույնարժեք բառերը և կառուցելու զուգորդման ցուցակը։

Պայմանակնորեն այդ գործողությունն անվանենք ցուցակի սեղմٳմ (compress )։Եթե դիտարկվող ցուցակը դատարկ է, ապա ֆունկցիան նույնպես վերադարձնումէ դատարկ ցուցակ. սա ռեկուրսիայի ավարտն է։ Եթե ցուցակի առաջին տարրըկետով զույգ չէ, դա ստուգվում է atom պրեդիկատով, ապա այն փոխարինումենք երկրորդ տարրը 1 արժեքով կետով զույգով, իսկ հետո compress ֆունկցիայի

57

Page 58: Common Lisp․ 12 խնդիր

ռեկուրսիվ կանչ կիրառում այդ ձևափոխված ցուցակի նկատմամբ։ Եթե ցուցակիառաջինտարրը կետով զույգ է, ապաայդ զույգի առաջինտարրը համեմատում ենքցուցակի երկրորդ տարրի հետ և, հավասար լինելու դեպքում, մեկով ավելացնումենք զույգի երկրորդ տարրը՝ դեն նետելով ցուցակի երկրորդ տարրը։ Կարճ ասած՝ցուցակի հետ կատարվում է հետևյալ ձևափոխությունը.

(("a" . k) "a" "a" "b" "c") ⇒ (("a" . k + 1) "a" "b" "c")Այս ձևափոխությունից ցուցակի նկատմամբ նորից կիրառվում է compress ֆունկ֊ցիայի ռեկուրսիվ կանչ։ Եվ այն դեպքում, երբ ցուցակի առաջին տարրը կետովզույգ է, բայց այդ զույգի առաջին տարրը հավասար չէ ցուցակի երկրորդ տարրին,compress ֆունկցիայի ռեկուրիվ կանչ է կիրառվում ցուցակի պոչի նկատմամբ ևստացված արդյունքի սկզբից կցվում է նախնական ցուցակի առաջին տարրը։

Ահա compress ֆունկցիան ամբողջությամբ.

1 (defun compress (sl)2 (let ((h (car sl))3 (r (cdr sl)))4 (cond5 ((endp sl)6 '())7 ((atom h)8 (compress (cons (cons h 1) r)))9 ((string-equal (car h) (car r))10 (compress (cons (cons (car h) (1+ (cdr h))) (cdr r))))11 (t12 (cons h (compress r))))))

Թող որ tom-sawyer.txt ֆայլում պարունակվում է «Թոմ Սոյերի արկածները»վեպի տեքստը։ Այդ տեքստի բոլոր բառերի և դրանց հանդիպելու քանակներիզուգորդությունների ցուցակը կարող ենք կառուցել հետևյալ արտահայտությամբ.

1 (compress (sort (read-words-from-file-1 "tom-sawyer.txt") #'string<))

58

Page 59: Common Lisp․ 12 խնդիր

Խնդիր 10

Տարեվերջի քննٳթյٳններ

Նախագծենք մի համակարգ, որը պետք է օգտագործվի դպրոցականների տարե֊վերջյան քննությունների արդյունքների կուտակման, պահպանման ու վերլուծութ֊յան համար։ Համակարգը պետք է հնարավորություն տա պահել սովորողներիանձնականտվյալները և քննություններից ստացված գնահատականները։ Ինչպեսնաև՝ համակարգի օգտագործողը պետք է հնարավորություն ունենա դուրս բերելգնահատակաների հետ կապված վիճակագրական տվյալներ (միջին գնահատա֊կան, գերազանցիկների ցուցակ և այլն)։

Common Lisp լեզվի defstruct մակրոսով սահմանենք student կառուցված֊քը, որի name սլոտը (դաշտը) նախատեսված է սովորողի ազգանունն ու անունըպահելու համար, year սլոտը՝ դասարանի համար, grades սլոտը՝ գնահատական֊ների ցուցակի համար։

Երբ defstruct մակրոսով սահմանվում է ⟨name⟩ անունով նոր կառուցվածք՝

(defstruct ⟨name⟩ ⟨slot⟩∗)

ապա համակարգում գեներացվում են նաև make-⟨name⟩ կոնստրուկտոր ֆունկ֊ցիան, copy-⟨name⟩ պատճենող ֆունկցիան, ⟨name⟩-p պրեդիկատը, ինչպես նաևամենմի սլոտի համար ⟨name⟩-⟨slot⟩ դիմման ֆունկցիաները։

Ամենապարզ դեպքում student կառուցվածքի սահմանումը կարող է ունենալայսպիսի տեսք.

1 (defstruct student2 name year grades)

Այս հայտարարությունը միջավայրում ավելացնում է student անունով նորտիպ, որի նմուշները կարելի է ստեղծել make-student կոնստրուկտոր ֆունկցիա֊յով։ Օրինակ, եթե Պողոսյան Մեսրոպը սովորում է հինգերոդ դասարանում, ապանրա տվյալները պարունակող student օբյեկտ կարող ենք ստեղծել հետևյալարտահայտությամբ.

59

Page 60: Common Lisp․ 12 խնդիր

1 (setq mesrop (make-student :name "Պողոսյան Մեսրոպ"2 :year 5 :grades '()))

Համոզվելու համար, որ իսկապես mesrop սիմվոլին կապված է student տիպիօբյեկտ, կարող ենք օգտագործել student-p պրեդիկատը.

(student-p mesrop) ⇒ TԲացի ամեն մի կառուցվածքի համար գեներացված ⟨name⟩-p պրեդիկատից,

Common Lisp լեզուն պարունակում է նաև օբյեկտների տիպը որոշող type-of ևtypep ֆունկցիաները։ Օրինակ.

(type-of mesrop) ⇒ STUDENT(typep mesrop 'student) ⇒ T

defstruct մակրոսը հնարավորություն է տալիս սահմանվող կառուցվածքիսլոտներն օժտել հատկություններով, ինչպիսիք են լռելٳթյան (default) արժեքը,տիպը (:type) և հաստատٳնٳթյան հայտանիշը (:read-only)։ Բերենք studentկառուցվածքի մի նոր սահմանում, որտեղ name սլոտի տիպը նշված է որպես տող,year սլոտինը՝ թիվ, իսկ grades սլոտինը՝ ցուցակ։ Ինչպես նաև նշված է, որ nameև year սլոտները ստեղծումից հետո չեն կարող փոփոխվել (օժտված են read-onlyհատկությամբ)։

1 (defstruct student2 (name "" :type string :read-only t)3 (year 0 :read-only t :type integer)4 (grades '()))

Այս սահմանման մեջ name սլոտի լռելության արժեքը սահմանված է որպես դա֊տարկ տող, year սլոտինը՝ 0, իսկ grades սլոտինը՝ դատարկ ցուցակ։

Եթե փորձենք, օրինակ, print ֆունկցիայով արտածել mesrop օբյեկտը, ապակտեսնենք հետևյալը

#S(STUDENT :NAME "Պողոսյան Մեսրոպ" :YEAR 5 :GRADES NIL)

Սա Common Lisp լեզվում կառուցվածքների նմուշների ներկայացման ձևաչափն է։Կարող ենք ընդհանրացնել և ասել, որ եթե ⟨name⟩ կառուցվածքն ունի ⟨slot⟩0, ⟨slot⟩1և այլ սլոտները՝ իրենց ⟨val⟩0, ⟨val⟩1 և այլ համապատասխան արժեքներով, ապաայդ ⟨name⟩ կառուցվածքիամենմինմուշը համակարգում ներկայացվում է հետևյալձևաչափով.

#S(⟨name⟩ :⟨slot⟩0 ⟨val⟩0 :⟨slot⟩1 ⟨val⟩1 . . .)

Մինչև գնահատականների ցուցակի կազմակերպմանն անցնելը սահմանենք+courses+ գլոբալ հաստատունը, որտեղ զուգորդությունների ցուցակի միջոցովքննվող առարկաների անուններին համապատասխանեցված են պայմանականբանալի-բառեր։

60

Page 61: Common Lisp․ 12 խնդիր

1 (defconstant +courses+2 '(("Հայոց լեզու" . :arm) ("Մաթեմատիկա" . :mat)3 ("Պատմություն" . :his) ("Ինֆորմատիկա" . :inf)))

Զուգորդությունների ցուցակում որոնումը կատարվում է assoc ֆունկցիայով.

(assoc ⟨value⟩ ⟨alist⟩)

որը վերադարձնում է ⟨alist⟩ ցուցակի ⟨value⟩ արժեքով car ունեցող զույգը։ Արժեք֊ները համեմատվում են eql ֆունկցիայով։ Այլ համեմատող ֆունկցիա տալու հա֊մար պետք է օգտագործել :test անվանված արգումենտը։ Օրինակ, +courses+ցուցակում «Պատմություն» արժեքին համապատասխանեցված արժեքը ստանալուհամար պետք է գրել.

(car (assoc "Պատմություն" +courses+ :test #'equal)) ⇒ :hisԲայց բավականին անհարմար է ամեն անգամ զուգորդությունների ցուցակում

հարկավոր բանալի-բառը որոնելու համար գրել այս երկար արտահայտությունը։Common Lisp լեզվում նմանատիպ խնդիրները լուծվում են նոր մակրոսներ սահմա֊նելով։ Կարելի է ասել, որ մակրոսը ծրագրի մեջ ծրագիր է գրում։

Մակրոսի սահմանումը շատ նման է ֆունկցիայի սահմանմանը. defmacro մակ֊րոսը ստանում է սահմանվող մակրոսի անունը, արգումենտների ցուցակը, ևմարմինը կազմող արտահայտությունների հաջորդականությունը։

(defmacro ⟨name⟩ (⟨argument⟩∗) ⟨body-form⟩∗)

Սակայն, եթե ֆունկցիայի կիրառությունը հաշվարկٳմ և վերադարձնում է արժեք,ապա մակրոսի կիրառությունը ընդլայնվٳմ և գեներացնում է կոդ։ Օրինակ,սահմանենք course-id մակրոսը, որը +courses+ ցուցակից ընտրում է տրվածառարկային համապատասխանեցված բանալի-բառը։

1 (defmacro course-id (course)2 `(cdr (assoc ,course +courses+ :test #'equal)))

Երբ ծրագրում կհանդիպի այս մակրոսի որևէ կիրառություն, օրինակ,

(course-id "Մաթեմատիկա")

ապա այն հաշվարկումից առաջ կձևափոխվի (կընդլայնվի) հետևյալ տեսքի.

(cdr (assoc "Մաթեմատիկա" +courses+ :test #'equal))

որն էլ կհաշվարկվի և կվերադարձնի :mat արժեքը։Մակրոսի ընդլայնման արդյունքը կարելի է ստանալ macroexpand-1 ֆունկ֊

ցիայի օգնությամբ։ Օրինակ.(macroexpand-1 '(course-id "Մաթեմատիկա")) ⇒

(CDR (ASSOC "Մաթեմատիկա" +COURSES+ :TEST #'EQUAL))

61

Page 62: Common Lisp․ 12 խնդիր

Մակրոսի մարմնի նկարագրության մեջ օգտագործված «`» (հակառակ ապա֊թարց) սիմվոլի նպատակը նույնպես արտահայտությունների պաշտպանություննէ հաշվարկումից։ Բայց, ի տարբերություն «'» (ապաթարց) սիմվոլի, այն թույլէ տալիս իր «պաշտպանած» արտահայտության մեջ «,» (ստորակետ) և «,@»սիմվոլներով «անջատել» պաշտպանությունը։ Այլ կերպ ասած՝ չհաշվարկվող ար֊տահայտությունների մեջ ներդնել հաշվարկվող արտահայտություններ։

Պայմանավորվենք ամենմիաշակերտի ստացած գնահատականների ցուցակըներկայացնել որպես հատկٳթյٳնների ցٳցակ (property list կամ plist) [14, Գլուխ 10]։Դա մի արտապատկերումների ցուցակ է, որի զույգ ինդեքսով դիրքերում բանալի֊ներն են, իսկ դրանց հաջորդող կենտ ինդեքսով դիրքերում՝ այդ բանալիներինհամապատասխան արժեքները։ Օրինակ, եթե Մեսրոպը հայոց լեզվից ստացել է5, իսկ ինֆորմատիկայից՝ 3 գնահատական, ապա գնահատականները կարող ենքmesrop սիմվոլին կապված օբյեկտի grades սլոտում գրանցել հետևյալ կերպ.

(setf (getf (student-grades mesrop) :arm) 5)(setf (getf (student-grades mesrop) :inf) 3)

Վերը նշեցինք, որ ամեն defstruct մակրոսը գեներացնում է նաև սահմանվողկառուցվածքի սլոտներին դիմող ֆունկցիաներ։ Այս student-grades ֆունկցիանհենց student կառուցվածքի grades սլոտին դիմելու ֆունկցիան է։ getf ֆունկ֊ցիան իր առաջին արգումենտում տրված հատկությունների ցուցակում որոնումէ երկրորդ արգումենտով տրված բանալին և վերադարձնում է այդ բանալունհամապատասխանեցված արժեքը։ Եթե նշված բանալին չի գտնվել, ապա վերա֊դարձնում է NIL։ Օրինակ.

(getf (student-grades mesrop) :arm) ⇒ 5(getf (student-grades mesrop) :his) ⇒ NIL

Երկրորդ օրինակից երևում է, որ Մեսրոպի՝ պատմություն առարկայից ստացածգնահատականը դեռևս գրանցված չէ գնահատականների ցուցակում։

Այժմ, նորից հաշվելով (pprint mesrop)արտահայտությունը, կարող ենքտեսնել,թե ինչ փոփոխություններ են կատարվել գնահատականների ցուցակում։

#S(STUDENT :NAME "Պողոսյան Մեսրոպ" :YEAR 5:GRADES (:inf 3 :arm 5))

Սահմանենք set-grade մակրոսը, որը տրված student օբյեկտի գնահատա֊կանների ցուցակը համալրում է (կամ փոփոխում է) նշված առարկայից ստացածգնահատականով։

1 (defmacro set-grade (stud cour gra)2 `(setf (getf (student-grades ,stud) ,cour) ,gra))

62

Page 63: Common Lisp․ 12 խնդիր

Առայժմ մի կողմ թողնենք student կառուցվածքն ու գնահատականների ցու֊ցակը և քննարկենք աշակերտների ցուցակի կազմակերպումը՝ դրանում նոր աշա֊կերտ ավելացնելու և աշակերտին ըստ անունի որոնելու գործողություններով։

Սահմանենք *students-db* դինամիկ (գլոբալ) սիմվոլը և նրան վերագրենք '()(դատարկ ցուցակ) սկզբնական արժեքը։

1 (defparameter *students-db* '()2 "Աշակերտների տվյալները պարունակող ցուցակ")

Դինամիկ սիմվոլների սահմանման defparameter մակրոսըմիջավայրում ավե֊լացնում է իր առաջին արգումենում տրված ⟨var-name⟩ անունով նոր սիմվոլ և նրանէ կապում երկրորդ արգումենտում տրված ⟨value⟩ արժեքը։

(defparameter ⟨var-name⟩ ⟨value⟩ ⟨document⟩?)

Եթե միջավայրում արդեն սահմանված է ⟨var-name⟩ անունով սիմվոլ, ապա նրանպարզապես վերագրվում է նոր արժեք։ Նաև նշենք, որ ընդունված է Common Lispլեզվում սահմանվող գլոբալ սիմվոլների անունների սկզբից ու վերջից կցել «*»նիշը։

Գլոբալ փոփոխական կարելի է սահմանել նաև defvar մակրոսով։ Եթե սահ֊մանվող ⟨var-name⟩ փոփոխականի համար սկզբնական ⟨value⟩ արժեքը տրվածչէ, ապա defvar մակրոսը միջավայրում ստեղծում է չկապված (unboud) սիմվոլ։Գլոբալ գլոբալ փոփոխկանի կապված լինելը կարելի է ստուգել boundp պրեդիկա֊տով։ Օրինակ.

(progn (defvar *a*) (boundp '*a*)) ⇒ NIL(progn (defvar *b* 7) (boundp '*b*)) ⇒ T

Իսկ եթե ⟨var-name⟩ սիմվոլն արդեն առկա է միջավայրում և նրան արժեք էկապված, ապա defvar մակրոսի կիրառությունը նրան նոր արժեք չի վերագրի։Օրինակ.

(progn (defvar *b* 8) (print *b*)) ⇒ 7

Այսպիսով, *students-db* գլոբալ ցուցակը սահմանեցինք որպես սովորող֊ների տվյալները պարունակող տվյալների բազա։ Այդ տվյալների բազայում նորգրառում ավելացնելու համար պետք է ստեղծել նոր student օբյեկտ, լրացնելնրա դաշտերը աշակերտի տվյալներով, և այդ օբյեկտն ավելացնել *students-db*ցուցակին։ Քանի որ մեզ համար կարևոր չէ տվյալների բազայի տարրերի կարգը,նոր գրառումը cons ֆուկցիայի օգնությամբ կկցենք ցուցակի սկզբից: Օրինակ,եթե mesrop-ը նոր ստեղծված գրառումն է, ապա հետևյալ արտահայտությունն այնկավելացնի *students-db* ցուցակի սկզբից։

(setq *students-db* (cons mesrop *students-db*))

Այս նույն արդյունքը կարելի է ստանալ push մակրոսի միջոցով, որը տրվածօբյեկտը կցում է տրված ցուցակի սկզբից։

63

Page 64: Common Lisp․ 12 խնդիր

(push mesrop *students-db*)

Համոզվելու համար, որ վերջին երկու արտահայտությունները համարժեք են,կարող ենք macroexpand-1 (կամ macroexpand) ֆունկցիայով ստանալ push մակ֊րոսի կիրառության բացված տեսքը.

(macroexpand-1 '(push mesrop *students-db*)) ⇒(SETQ *STUDENTS-DB* (CONS MESROP *STUDENTS-DB*))

Բայց push մակրոսի կիրառմամբ հնարավոր չէ ապահովել բազայի տարրերիեզակիությունը։ Որպեսզի թույլ չտանք *students-db* ցուցակում նույն աշակեր֊տի տվյալներն ավելացնելմեկից ավելի անգամներ, ավելի հարմար է օգտագործելpushnew մակրոսը։

(pushnew ⟨object⟩ ⟨place⟩)

Այն ⟨place⟩ ցուցակում ⟨object⟩-ը կավելացնի միայն վերջինիս բացակայությանդեպքում։ Լռելությամբ ⟨object⟩-ը ⟨place⟩-ի տարրերի հետ համեմատվում է eqlֆունկցիայով, բայց :testանվանված արգումենտով կարելի է տալմեզ հարկավորհամեմատող ֆունկցիան։

Ընդհանրացնենք վերջին պարբերություններում քննարկվածն ու սահմանենքnew-student ֆունկցիան, որն արգումենտում ստանում է աշակերտի անունն ուդասարանը ևտվյալների բազայումավելացնում է համապատասխան նոր գրառում։

1 (defun new-student (name year)2 (flet ((student= (so si)3 (and (string= (student-name so)4 (student-name si))5 (= (student-year so)6 (student-year si)))))7 (pushnew (make-student :name name :year year)8 *students-db* :test #'student=)))

Դե, եթե արդեն կարողանում ենք բազայում նոր գրառումներ ավելացնել, ապապետք է կարողանանք նաև այնտեղ որոնել մեզ հետաքրքրող տվյալները։ Սահմա֊նենք search-studentֆունկցիան, որնարգումենտում ստանում է սովորողիանուննու դասարանը, իսկ վերադարձնում է *students-db* ցուցակի համապատասխանօբյեկտը։

1 (defun search-student (name year)2 (find-if #'(lambda (s)3 (and (string= (student-name s) name)4 (= (student-year s) year)))5 *students-db*))

64

Page 65: Common Lisp․ 12 խնդիր

find-if ֆունկցիան ստանում է պրեդիկատ և հաջորդականություն, իսկ վերա֊դարձնում է հաջորդականության այն առաջին տարրը, որը բավարարում է պրեդի֊կատին։ Եթե հաջորդականության մեջ պրեդիկատին բավարարող օբյեկտ չկա,find-if ֆունկցիան վերադարձնում է NIL։

*student-db* ցուցակը տվյալների բազայի մոդելն է հիշողության մեջ։ Այդբազայի հետ բնական աշխատանքն ապահովելու համար կարևոր է ունենալմիջոցներ, որոնք պետք եղած դեպքում *student-db* ցուցակի պարունակությունըկպահպանեն ֆայլի մեջ և հետ կկարդան այնտեղից։

Նախ սահմանենք database-store ֆունկցիան, որը *student-db* ցուցակըգրում է տրված անունով ֆայլի մեջ։

1 (defun database-store (file-name)2 (with-open-file (dout file-name :direction :output3 :if-exists :supersede)4 (print *students-db* dout)))

Տվյալների բազան ֆայլից կարդալու համար էլ սահմանենք database-loadֆունկցիան, որը read ֆունկցիայով կարդում է ֆայլի ամբողջ պարունակությունըև այն վերագրում է *students-db* փոփոխականին։

1 (defun database-load (file-name)2 (with-open-file (din file-name :direction :input)3 (setq *students-db* (read din))))

Այս պահին ունենք մի համակարգ, որը հնարավորություն է տալիս ստեղծելսովորողների տվյալների բազա, այդ բազայում ավելացնել և որոնել գրառումներ։Բայց, օրինակ, ինչպե՞ս բազայում ավելացնել ութերորդ դասարանում սովորողՊետրոսյան Հայկուշի գնահատականները։ Շատ լավ կլիներ, եթե դա կարողանա֊յինք անել հետևյալ տեսքի արտահայտություններով.

(update-grade "Պետրոսյան Հայկուշ" 8 "Հայոց լեզու" 4)(update-grade "Պետրոսյան Հայկուշ" 8 "Մաթեմատիկա" 5)

Արդեն սահմանված set-grade և course-id մակրոսներն օգտագործելով սահ֊մանենք update-grade ֆունկցիան, որը պետք է ստուգի, որ ուսանողի մասինգրառումնառկա էտվյալների բազայում, ապագրանցիտրվածառարկայի գնահա֊տականը։

1 (defun update-grade (name year course grade)2 (let ((stud (search-student name year)))3 (when stud4 (set-grade stud (course-id course) grade))))

65

Page 66: Common Lisp․ 12 խնդիր

Տվյալների բազայում աշակերտների գնահատականները գրանցելուց հետոհետաքրքիր կլիներ իմանալ ամեն մի աշակերտի գնահատականների թվաբանա֊կան միջինը։ Դա հաշվելու համար պետք է վերցնել գնահատականների ցուցակը,իրար գումարել կենտ ինդեքսներով տարրերը և բաժանել գնահատականներիքանակի վրա։

Նախ սահմանենք odd-elements ֆունկցիան, որը վերադարձնում է տրվածցուցակի կենտ ինդեքսով տարրերի ցուցակը։

1 (defun odd-elements (ls)2 (if ls3 (cons (cadr ls) (odd-elements (cddr ls)))4 '()))

Եվ սահմանենք average-grade ֆունկկցիան, որը վերադարձնում է տրված աշա֊կերտի գնահատականների թվաբանականմիջինը՝ եթեմիայն գնահատականներիցուցակը դատարկ չէ։

1 (defun average-grade (stu)2 (let ((gs (odd-elements (student-grades stu))))3 (/ (loop for k in gs sum k) (list-length gs) 1.0)))

Այսպիսով, այս օրինակում կառուցեցինք պարզագույն և շատ բաներում ան֊կատարմիհամակարգ, որը հնարավորություն էտալիս ստեղծել տվյալների բազա,պահպանել այն ֆայլի մեջ ու կարդալ այնտեղից, ավելացնել նոր գրառումներ ուկատարել պարզագույն հարցում։

66

Page 67: Common Lisp․ 12 խնդիր

Խնդիր 11

Կրկնվող ֆայլերի որոնٳմ

Ենթադրենք ունենք էլեկտրոնային գրքերի մեծ գրադարան, որտեղ գրքերն ըստթեմաների բաշխված են տարբեր պանակներում, և պետք է գտնել ու ցուցակագրելբոլոր կրկնվող գրքերը։ Այս խնդիրը համեմատաբար հեշտ կլուծվեր, եթե նույնգրքի ֆայլը պարզապես նույն անունով հանդիպեր գրադարանի տարբեր պանակ֊ներում։ Բայց ավելի հաճախ հանդիպում է այն իրավիճակը, երբ նույն գիրքըգրադարանի պանակներում մի քանի անգամ հանդիպում է ֆայլի տարբեր անուն֊ներով։ Սա մեզ ստիպում է որոնել ֆայլերի նույնականացման մի եղանակ, որումչի մասնակցի ֆայլի անունը։ Այդպիսի հայտանիշ կարող է հանդիսանալ (իհարկե,որոշ վերապահումներով) ֆայլի հեշ-կոդը (MD5, SHA-2 և այլն)։ Կհամարենք, որերկու ֆայլեր նույնական են, եթե միաժամանակ համընկնում են նրանց և՛ չափերը,և՛ MD5 հեշ կոդերը։

Նախապես պայմանավորվենք նաև, որ որոնմանը մասնակցելու են միայն այնֆայլերը, որոնց ընդլայնումները թվարկված են +book-extensions+ հաստատու֊նով: Օրինակ, այսպես.

1 (defconstant +book-extensions+2 '("pdf" "ps" "djvu" "djv" "chm"))

Մեզ հետաքրքրող ֆայլերի ցուցակը կազմելուց առաջ նախ ծանոթանանք, թեֆայլային համակարգի ճանապարհների հետաշխատելու համար ի՛նչ կառուցվածք֊ներ և ֆունկցիաներ է առաջարկում Common Lisp լեզուն1։

Սկսենք ճանապարհների ներկայացումից։ Ֆայլային համակարգի ճանապարհըներկայացված է pathnameտիպի օբյեկտով։ Այդ օբյեկտի համար լեզվի ստանդար֊տը նախատեսում է վեց բաղադրիչ. host (հոսթ, մեքենա), device (սարք), di-rectory (պանակ), name (անուն), type (տիպ, ընդլայնում), version (տարբերակ)։

1Կարելի է ասել, որ այս օրինակի մեկնաբանության որոշ քայլեր ազդված են «Practical Com֊mon Lisp» [12] գրքի 15-րդ գլուխում շարադրված նյութից, որտեղ հեղինակը կառուցում է լեզվիիրականացումից անկախ ֆայլային ճանապարհների գրադարան։

67

Page 68: Common Lisp․ 12 խնդիր

Եթե pathname օբյեկտը կառուցվում է make-pathname ֆունկցիայով, ապա այսբաղադրիչներն արժեքավորվում են համապատասխան անվանված արգումենտ֊ներով։

(make-pathname ⟨:host⟩? ⟨:device⟩? ⟨:directory⟩? ⟨:name⟩?

⟨:type⟩? ⟨:version⟩? ⟨:defaults⟩? ⟨:case⟩?)

Օրինակ, «/dev/null» ֆայլի ճանապարհի pathname օբյեկտը կառուցելու համարպետք է :directory արգումենտին տալ «dev» արժեքը, իսկ :name արգումենտին՝«null» արժեքը.

1 (make-pathname :directory '(:absolute "dev") :name "null")

:absolute սիմվոլը, նշում է, որ ցուցակի մյուս տարրերից կառուցվելու է բացար֊ձակ ճանապարհ։ Իսկ հարաբերական ճանապարհ ներկայացնելու համար նրափոխարեն պետք է տալ :relative սիմվոլը։

Եթե պետք է, որ նոր ստեղծվող pathname օբյեկտի բաղադրիչները լռելությանարժեք ստանանմեկ այլ նմանատիպ օբյեկտից, ապա make-pathname ֆունկցիայի:defaults անվանված արգումենտը նախատեսված է հենց այդ դեպքի համար։

Սահմանենք +library-path+ հաստատունը և նրան տանք այն գրադարանիճանապարհը, որում որոնելու ենք կրկնվող ֆայլերը (թող այս դեպքում այդ ճանա֊պարհը լինի «/home/armenba/Library/»)։

1 (defconstant +library-path+2 (make-pathname :directory '(:absolute "home" "armenba" "Library")))

Ֆայլային ճանպարհ ներկայացնող բաղադրյալ օբյեկտ կարելի է կառուցելնաև parse-namestring ֆունկցիայով։ Այն վերլուծում է տողի տեսքով տրվածճանապարհը և կառուցում է pathname օբյեկտ։ +library-path+ հաստատունըկարող ենք սահմանել նաև այսպես.

1 (defconstant +library-path+2 (parse-namestring "/home/armenba/Library/"))

pathname տիպի օբյեկտների վերը թվարկված վեց բաղադրիչները կարելիէ ստանալ pathanme-β տեսքի ֆունկցիաներով, որտեղ β-ն վերը հիշատակվածբաղադրիչներից որևէ մեկի անունն է։ Օրինակ, հետևյալ արտահայտության հաշ֊վարկը կվերադարձնի +library-path+ հաստատունի բաղադրիչների ցուցակը.

1 (mapcar #'(lambda (f) (funcall f +library-path+))2 (list #'pathname-host #'pathname-device3 #'pathname-directory #'pathname-name4 #'pathname-type #'pathname-version))

68

Page 69: Common Lisp․ 12 խնդիր

Սա ֆայլային ճանապարհների օբյեկտային ներկայացման մասին։ Հիմա փոր֊ձենք ստանալ տրված պանակում գտնվող բոլոր ֆայլերի ու ենթապանակներիֆայլային ճանապարհները։ Common Lisp լեզվի directory ֆունկցիան է, որ նա֊խատեսված է այս խնդրի լուծման համար։ Այն իր արգումենտում ստանում էորոնվող ֆայլային ճանապարհների շաբլոնը և վերադաձնում է այդ շաբլոնինհամապատասխանող pathname օբյեկտների ցուցակը։ Օրինակ, եթե հարկավորէ ստանալ «/L̃ibrary» պանակի բոլոր PDF ֆայլերի ցուցակը, ապա directoryֆունկցիայի արգումենտը պետք է լինի «(pathname "/L̃ibrary/*.pdf")»։ Այստեղօգտագործված pathname ֆունկցիան իր արգումենտը ձևափոխում է pathnameտիպի օբյեկտի։

Ֆայլային ճանապարհի շաբլոնը make-pathname ֆունկցիայով կառուցելու հա֊մար պարզապես պետք է «*» նիշով տրված բաղադրիչին համապատասխանարգումենտին փոխանցել :wild (խմբային նիշ, wildcard) սիմվոլը։ Օրինակ, մեր+library-path+ պանակի բոլոր PDF ֆայլերը բնութագրող շաբլոնը կառուցելուհամար կարող ենք գրել.

1 (make-pathname :name :wild :type "pdf" :defaults +library-path+)

Հիմա սահմանենք մի մակրոս, որն արգումենտում ստանում է պանակի ճանա֊պարհ և վերադարձնում է այդ պանակում գտնվող բոլոր ֆայլերի ու ենթապանակ֊ների ճանապարհները։ Դրա համար directory ֆունկցիան կիրառենք տրվածպանակի ճանապարհից կառուցված շաբլոնի նկատմամբ, որում name և typeբաղադրիչներին տրված է :wild արժեքը2։

1 (defmacro directory-content (d)2 `(directory (make-pathname :type :wild :name :wild :defaults ,d)))

Քանի որ directory ֆունկցիան չի անցնում ներդրված պանակների բոլորմակարդակներով, այս մակրոսը կվերադարձնի գրադարանի միայն տրված մա֊կարդակի պարունակությունը։ Գրադարանն ամբողջ խորությամբ անցնելու և մեզհետաքրքրող ֆայլերն ընտրելու համար սահմանենք deep-select-files ֆունկ֊ցիան։

1 (defun deep-select-files (dir)2 (let* ((items (directory-content dir))3 (only-files (select-only-files items))4 (only-dirs (select-only-directories items)))5 (dolist (d only-dirs)6 (nconc only-files (deep-select-files d)))7 only-files))

2Որպեսզի Clozure CL իրականացման directory ֆունկցիան մեկ ցուցակում վերադարձնիտրված պանակի և՛ ֆայլերը, և՛ պանակները, պետք է տալ նաև նրա :directories անվանվածարգումենտը՝ t արժեքով։

69

Page 70: Common Lisp․ 12 խնդիր

let* կառուցվածքի հայտարարած լեքսիկական փոփոխականներից առաջինում՝items, ընտրում ենք տրված պանակի բոլոր տարրերը։ Ապա, select-only-filesև select-only-directories մակրոսներով, որոնք կսահմանենք ստորև, այդցուցակը տրոհում ենք ֆայլերի ճանապարհների only-files ու պանակների ճա֊նապարհների only-dirs առանձին ցուցակների։ Հիմա, վերջնական արդյունքստանալու համար, մնում է նույն այս deep-select-files ֆունկցիան ռեկուրսիվկիրառել only-dirs ցուցակի բոլոր տարրերի նկատմամբ և ստացված ցուցակներըկցել only-files ցուցակի պոչից։ Վերջում՝ վերադարձնել only-files ցուցակըորպես արժեք։

nconcֆունկցիան իրար է կցումարգումենտումտրված ցուցակները։ Այն լրացու֊ցիչ հիշողություն չի պահանջում, որովհետև փոփոխությունները կատարում էհենց արգումենտների հետ (արդյունքը կառուցելու համար արգումենտները չենպատճենվում)։ Օրինակ, եթե հաշվարկվեն հետևյալ արտահայտությունները.

(setq a '(1 2 3) b '(4 5 6) c '(7 8 9))(nconc a b c)(format t "a = ~a, b = ~a, c = ~a~%" a b c)

ապա որպես պատասխան կարտածվի

a = (1 2 3 4 5 6 7 8 9), b = (4 5 6 7 8 9), c = (7 8 9)

Այն ֆունկցիաները, որոնք, ինչպես nconc ֆունկցիան, փոփոխում են իրենցարգումենտները, Common Lisp լեզվում կոչվում են դեստրٳկտիվ ֆունկցիաներ։Ցուցակներն իրար կցող ոչ դեստրուկտիվ ֆունկցիան կոչվում է append։ Այնպատճենում է արգումենտում տրված ցուցակները և վերադարձնում է այդ պատ֊ճենների կոնկատենացիան։

select-only-files մակրոսը ֆայլային ճանապարհների ցուցակից զտում ևվերադարձնում է միայն այն ֆայլերի ցուցակը, որոնց ընդյալնումները թվարկվածեն +book-extensions+ հաստատունի արժեքում։

1 (defmacro select-only-files (ites)2 `(remove-if-not #'(lambda (e)3 (member (pathname-type e)4 +book-extensions+5 :test #'string-equal))6 ,ites))

member ֆունկցիան տրված տարրը որոնում է տրված ցուցակում։ Այն վերադարձ֊նում է NIL, եթե որոնվածը չի գտնվել, հակառակ դեպքում վերադարձնում էցուցակի այն մասը, որ սկսվում է որոնվող տարրով։ Տրված տարրի որոնմանժամանակ համեմատումը կատարվում է member ֆունկցիայի :test անվանված

70

Page 71: Common Lisp․ 12 խնդիր

արգումենտով տրված պրեդիկատով, որի լռելության արժեքը eql ֆունկցիան է։Ահա մի քանի օրինակներ.

(member 2 '(1 2 3 4 )) ⇒ (2 3 4)(member "c" '("a" "b" "c" "d") :test #'equal) ⇒ ("c" "d")(member 6 '((1 2) (3 4) (5 6)) :key #'second) ⇒ ((5 6))

:test անվանված արգումենտին տալով string-equal ֆունկցիան, հնարավո֊րություն ենք ստանում ֆայլերի վերջավորությունները համեմատելիս անտեսելմեծատառերի ու փոքրատառերի տարբերությունը։

Իսկ select-only-directories մակրոսը ֆայլային ճանապարհների ցուցա֊կից առանձնացնում և վերադարձնում է միայն պանակների ճանապարհները։ Այսդեպքում համարում ենք, որ p օբյեկտը պանակի ճանապարհ է, եթե նրանումմիաժամանակ բացակայում են type և name բաղադրիչները:

1 (defmacro select-only-directories (ites)2 `(remove-if #'(lambda (p)3 (or (pathname-type p) (pathname-name p)))4 ,ites))

Եվ այսպես, ունենք deep-select-files ֆունկցիան, որը ստանում է մի որևէպանակի ճանապարհ և, ռեկուրսիվ անցնելով նրա բոլոր ենթապանակներով,վերադարձնում է +book-extensions+ ցուցակում թվարկված ընդյալնում ունեցողբոլոր ֆայլերի ճանապարհները։

Այժմ բանաձևերի տեսքով սահմանենք այն գործողությունները, որոնց միջոցովֆայլերի ճանապարհների ցուցակից պետք է առանձնացնենք կրկնությունները։

Դիցքուք Gp(L) գործողությունը վերադարձնում է L ցուցակի բոլոր այն Li (i ∈ N)ենթացուցակները, որտեղ |Li| > 1 և ցանկացած երկու x, y ∈ Li տարրերի համար p

գործողությունը վերադարձնում է նույն արժեքը։

Gp(L) ={Li

∣∣∣ i ∈ N; |Li| > 1; ∀x, y ∈ Li, p(x) ≡ p(y)}: (11.1)

Պարզ է, որ եթե որպես p գործողություն ընտրենք ֆայլի չափը վերադարձնողֆունկցիան, իսկ որպեսL ցուցակ՝մեր սահմանած deep-select-filesֆունկցիայիվերադարձրած ֆայլային ճանապարհների ցուցակը, ապաGsize(L) գործողությունըկվերադարձնի մի L′ ցուցակների ցուցակ, որի ամեն մի Li ∈ L′ ենթացուցակումհավաքված են նույն չափն ունեցող ֆայլերի ճանապարհները։

Gp(L) գործողությունը ծրագրավորենք հետևյալ եղանակով։ Նախ՝ ըստ p հատ֊կության խմբավորում կատարելու համար վերցնենք H հեշ աղյուսակը։ Ապա՝անցնենք բոլոր e ∈ L տարրերով և ամեն մի e-ի համար հաշվենք k = p(e) ար֊ժեքը։ Եթե H հեշ աղյուսակն արդեն պարունակում է k բանալիով արժեք, ապաայդ արժեքին կցենք e տարրը, հակառակ դեպքում H աղյուսակում ավելացնենք

71

Page 72: Common Lisp․ 12 խնդիր

նոր (k, {e}) զույգը։ Վերջում ցուցակ կազմենք H հեշ աղյուսակի այն արժեքներից,որոնցում առկա են մեկից ավելի տարրեր։

Ստորև բերված group-by-property ֆունկցիան Gp(L) գործողության իրակա֊նացումն է.

1 (defun group-by-property (paths prop)2 (let ((temp (make-hash-table :test #'equal)))3 (dolist (e paths)4 (push e (gethash (funcall prop e) temp nil)))5 (loop for k being the hash-value of temp6 when (> (list-length k) 1)7 collect k)))

Այստեղ մեզ անծանոթ է միայն loop մակրոսի being ենթակառուցվածքը։ Այնհնարավորություն է տալիս ցիկլ կազմակերպել hash-table օբյեկտի բանալինե֊րով կամ արժեքներով (կամ երկուսով էլ միաժամանակ)։ Այս դեպքում, օրինակ,«the hash-value of» արտահայտությունը ցույց է տալիս, որ ցիկլի կատարմանժամանակ k փոփոխականը հաջորդաբար անցնում է temp հեշ աղյուսակի բոլորարժեքներով։

«each hash-keys in»արտահայտության օգնությամբ being ենթակառուցվածքըհնարավորություն է տալիս անցում կատարել նաև հեշ աղյուսակի բանալիներով։Օրինակ, եթե temp-ը հեշ աղյուսակ է, ապա նրա բանալիների ցուցակը կարելի էստանալ հետևյալ արտահայտությամբ.

1 (loop as k being each hash-keys in temp2 collect k)

Միաժամանակ հեշ աղյուսակի բանալիներով ու նրանց համապատասխանեցվածարժեքներով անցնելու համար նախատեսված է loop մակրոսի using ծառայողա֊կան բառը։ Օրինակ, temp հեշ աղյուսակից նրա բանալի-արժեք ցույգերի ցուցակըստանալու համար բավական է հաշվարկել այսպիսի մի արտահայտություն.

1 (loop as k being each hash-keys in temp using (hash-value v)2 collect (list k v))

Ֆայլի երկարությունը հաշվելու համար Common Lisp լեզվում նախատեսվածէ file-length ֆունկցիան։ Բայց այն պահանջում է, որ իր արգումենտում տրվիֆայլից վրա բացված ընթերցման հոսք։ Սահմանեք size-of-file ֆունկցիան, որըwith-open-file մակրոսով ստեղծում է ֆայլային հոսք, ապա հաշվում և վերա֊դարձնում է այդ հոսքին կապված ֆայլի երկարությունը։

1 (defun size-of-file (path)2 (with-open-file (s path)3 (file-length s)))

72

Page 73: Common Lisp․ 12 խնդիր

Ֆայլերի ճանապարհներն ըստ MD5 կոդի խմբավորելու համար, size-of-fileֆունկցիայի նմանությամբ, սահմանենք ֆայլի պարունակությանMD5 կոդը հաշվողmd5-of-file ֆունկցիան։ Դրա համար Quicklisp գրադարանների ղեկավարմանհամակարգի օգնությամբ բեռնենք md5 գրադարանը.

1 (ql:quickload "md5")

Այն տրամադրում է md5sum-file ֆունկցիան, որն արգումենտում ստանում էֆայլի ճանապարհը և վերադարձնում է ֆայլի պարունակության MD5 հեշ կոդը՝ 32ամբողջ թվերից բաղկացած զանգվածի տեսքով։ Օրինակ, միայն «A» տառը և նորտողի սիմվոլը պարունակող ֆայլի համար md5sum-file ֆունկցիան վերադարձ֊նում է հետևյալ վեկտորը.

#(191 7 46 145 25 7 123 78 118 67 122 147 152 103 135 239)

Բայց ավելի հաճախ ընդունված է MD5 հեշ կոդը ներկայացնել այս թվերի տասն֊վեցական ներկայացումներից կազմված տողի տեսքով։ Բերված օրինակի համարայն ունի «BF072E9119077B4E76437A93986787EF» տեսքը։

1 (defun md5-of-file (path)2 (apply #'concatenate 'string3 (map 'list #'(lambda (s) (format nil "~2,'0X" s))4 (md5:md5sum-file path))))

Հեշ-կոդի վեկտորային տեսքի թվերից յուրաքանչյուրը տասնվեցական ներկայաց֊ման է ձևափոխվում format ֆունկցիայի "~2,'0X" արգումենտով։ map ֆունկ֊ցիայով տասական թվերի ցուցակից կառուցվում է թվի տասնվեցական տեսքիտողերի ցուցակ։ Վերջում concatenate ֆունկցիան իրար է կցում ցուցակի անդամ֊ները և ստանում մեկ տող։

Common Lisp լեզվում ֆունկցիան արգումենտների նկատմամբ կիրառելու մի֊ջոցներից մեկն էլ apply ֆունկցիան է։ Այն իր առաջին արգումենտում ստանում է⟨func⟩ ֆունկցիա օբյեկտը, հաջորդող արգումենտներով՝ ⟨func⟩ ֆունկցիայի արգու֊մենտները, որոնցից վերջինն անպայման պետք է լինի ցուցակ։

(apply ⟨func⟩ ⟨arg⟩+)

Օրինակ, [0; 1000] միջակայքի հիսուն պատահական թվեր գեներացնելու և դրանցգումարը հաշվելու համար կարող ենք գրել հետևյալը.

1 (apply #'+ (loop repeat 50 collect (random 1000)))

Սահմանենք նաև Dp(Y ) գործողությունը, որը տրված Y ցուցակների ցուցակիբոլոր Li ∈ Y տարրերի նկատմամբ կիրառում է p գործողությունը և արդյունքներըմիավորում է մեկ ցուցակում։

73

Page 74: Common Lisp․ 12 խնդիր

Dp(Y ) =∪

Li∈Yp(Li) : (11.2)

Dhash գործողությունը կիրառելով L′ = Gsize(L) արդյունքի նկատմամբ, կստանանքմի նոր L′′ ցուցակների ցուցակ, որի ենթացուցակներում խմբավորված են նույնչափն ու հեշ-կոդն ունեցող ֆայլերի ճանապարհները։

L′′ = Dhash(Gsize(L)) =∪

Li∈Gsize(L)

Ghash(Li) : (11.3)

Այս L′′ ցուցակն էլ հենց մեր խնդրի որոնելի լուծումն է։(11.3) բանաձևը ծրագրավորենք detect-duplicates ֆունկցիայի տեսքով։ Այն

արգումենտում ստանում է ֆայլային ճանապարհների ցուցակ և վերադարձնում էնույն չափն ու հեշ-կոդն ունեցող ֆայլերի ճանապարհների խմբեր։

1 (defun detect-duplicates (paths)2 (mapcan #'(lambda (g) (group-by-property g #'md5-of-file))3 (group-by-property paths #'size-fo-file)))

Այս mapcan դեստրուկտիվ ֆունկցիան իր առաջին արգումենտում տրված ֆունկ֊ցիան կիրառում է կիրառում է հաջորդ արգումենտներով տրված ցուցակներիհերթական տարրերի նկատմամբ և արդյունքները միավորում է nconc ֆունկցիա֊յով։ Կարելի է ասել, որ այն համարժեք է հետևյալ ֆունկցիային [6].

1 (defun our-mapcan (fn &rest lsts)2 (apply #'nconc (apply #'mapcar fn lsts)))

ՈՒրեմն, detect-duplicatesֆունկցիան վերադարձնում է ցուցակների ցուցակ,որի ամենմի ենթացուցակը պարունակում է նույնական ֆայլերի ճանապարհները։Եթե անհրաժեշտություն լինի, օրինակ, տպել այդ ճանապարհները, ապա դակարող ենք անել հետևյալ ֆունկցիայի կիրառությամբ.

1 (lambda (l) (mapc #'pprint (mapcar #'identity l)))

Կամ, եթե կուզենանք կրկնվող ֆայլերից թողնել միայն մեկ նմուշ, օրինակ,ենթացուցակներում ջնջելով առաջինից բացի մյուս ֆայլերը, ապա կարող ենքկիրառել հետևյալ ֆունկցիան.

1 (lambda (l) (mapc #'delete-file (mapcar #'cdr l)))

Իսկ ինչպե՞ս կարելի է հաշվել այն տարածքը, որ սկավառակի վրա զբաղեցնումեն կրկնվող ֆայլերի ավելորդ պատճենները։

74

Page 75: Common Lisp․ 12 խնդիր

Խնդիր 12

Վեկտորային գրաֆիկա

Common Lisp լեզվի՝ օբյեկտներին կողմնորոշված ծրագրավորմանն աջակցող մի֊ջոցները ցուցադրելու համար կառուցենք vector-graphics փաթեթը, որը թույլէ տալիս հատված, էլիպս և նٳղղանկյٳ երկրաչափական պատկերների օգտա֊գործմամբ կառուցել պարզագույն գծապատկերներ նկարներ և դրանք պահպանելSVG (Scalable Vector Graphics) ֆորմատի ֆայլերում [4]։

1 (in-package :vector-graphics)

Եթե խոսենք օբյեկտներին կողմնորոշված ծրագրավորման տերմինաբանութ֊յամբ, ապա կարող ենք ասել, որ հատվածը, էլիպսը և ուղղանկյունը նույն պատկերվերացական հասկացության կոնկրետացումներն են։ Եվ բնական կլինի նախսահմանել shape (պատկեր) աբստրակտ դասը, ապա այն ընդլայնել segment(հատված), ellipse (էլիպս) և rectangle (ուղղանկյուն) կոնկրետ դասերով։

(xb, yb)

(xe, ye)

(xc, yc)

rhrv

(xo, yo)

(xc, yc)

Նկար 12.1: Հատված, էլիպս և ուղղանկյուն

Բայց, մինչև առաջ անցնելը, քանի որ բոլոր երկրաչափական պատկերներըմոդելավորելիս օգտագործելու ենք հարթության կետը, սահմանենքայն մոդելավո֊րող point կառուցվածքը և (0; 0) կետը ներկայացնող +zero+ հաստատունը։

1 (defstruct point x y)2 (defconstant +zero+ (make-point :x 0 :y 0))

75

Page 76: Common Lisp․ 12 խնդիր

Օբյեկտներին կողմնորոշված ծրագրավորման համար Common Lisp լեզվումստանդարտացված է CLOS (Common Lisp Object System) ընդլայնումը [8]։ Դասերըսահմանվում են նրա տրամադրած defclass մակրոսով։ Այն ստանում է սահման֊վող դասի անունը՝ ⟨name⟩, բազային դասերի անունների ցուցակը՝ ⟨base-classes⟩,սլոտների նկարագրությունների ցուցակը՝ ⟨slots⟩, և դասի հատկությունների հա֊ջորդականությունը՝ ⟨class-options⟩։ Թվարկվածներից պարտադիր է միայն դասիանունը։

(defclass ⟨name⟩ (⟨base-classes⟩∗) (⟨slots⟩∗) ⟨class-options⟩∗)

Քանի որ shape աբստրակտ դասը չունի բազային դաս և չունի սլոտներ, այնսահմանելու համար պարզապես պետք է գրել.

1 (defclass shape () ())

Որևէ դասի նմուշի հետ կատարվող գործողություններ ծրագրավորելու համարCommon Lisp լեզվում կիրառվում է ընդհանրացված (generic) ֆունկցիաներիմեխա֊նիզմը [9]։ Ընդհանրացված ֆունկցիան սահմանվում է defgeneric մակրոսով ևորոշում է գործողության անունն ու պարամետրերի ցուցակը։ Ենթադրենք արդենսահմանել ենք shape դասի segment, ellipse և rectangle ընդլայնում դասերը ևուզում ենք սահմանել մի ֆունկցիա, որը ամեն մի տիպի համար վերադարձնումէ նրա անունը՝ տողի տեսքով։ Նախ սահմանում ենք name-of ընդհանրացվածֆունկցիան (կարելիս է ասել, որ սա գործողության ինտերֆեյսն է).

1 (defgeneric name-of (arg))

Այնուհետև defmethod մակրոսով սահմանում ենք ընդհանրացված ֆունկցիայիմեթոդները (գործողության ինտերֆեյսի իրականացումները) երեք տարբեր տիպե֊րի համար։

1 (defmethod name-of ((se segment))2 "Segment")3 (defmethod name-of ((el ellipse))4 "Ellipse")5 (defmethod name-of ((re rectangle))6 "Rectangle")

Այժմ, եթե se, el և re օբյեկտների համար ճիշտ են «(typep se 'segment)»,«(typep el 'ellipse)» և «(typep re 'rectangle)» արտահայտությունները,ապա ճիշտ են նաև name-of ֆունկցիայի հետևյալ կիրառությունները.

(name-of se) ⇒ "Segment"(name-of el) ⇒ "Ellipse"(name-of re) ⇒ "Rectangle"

76

Page 77: Common Lisp․ 12 խնդիր

Հիմա սահմանենք svg-view ընդհանրացված ֆունկցիան, որն ինտերֆեյս էնախապատրաստում հատվածները, էլիպսները և ուղղանկյունները SVG լեզվովներկայացնելու համար։

1 (defgeneric svg-view (obj))

segment, ellipse և rectangle դասերի համար սահմանելու ենք svg-view ֆունկ֊ցիայի մեթոդները, որոնք արգումենտում ստանում են համապատասխան տիպիօբյեկտ և տողի տեսքով վերադարձնում են նրա SVG ներկայացումը։

Որպես shape աբստարակտ դասի առաջին ընդյանում սահմանենք segmentդասը՝ point տիպի begin (սկզբնակետ) և end (վերջնակետ) սլոտներով (նկ. 12.1)։

1 (defclass segment (shape)2 ((begin :type point3 :initform +zero+4 :initarg :begin)5 (end :type point6 :initform +zero+7 :initarg :end)))

Սլոտների նկարագրությունն այստեղ լրացված է հատկություններով։ :type հատ֊կությամբ որոշվում է սլոտի տիպը. ինչպես արդեն նշվեց, begin և end սլոտներըկետեր են։ :initform հատկությամբ տրվում է սլոտի սկզբնականարժեքը։ Այստեղհատվածի սկզբնակետի ու վերջնակետի լռելության արժեք է ընտրված (0; 0) կետը։:initarg հատկությամբ տրվում է այն անվանված արգումենտը, որի միջոցովդասի նմուշ ստեղծելիս արժեք է տրվելու սլոտին։

CLOS համակարգում սահմանված դասի նմուշները՝ օբյեկտները, ստեղծվում եննախապես սահմանված make-instance ընդհանրացված ֆունկցիայով։

(make-instance ⟨class⟩ ⟨:initarg⟩ ⟨value⟩ ⟨:initarg⟩ ⟨value⟩ ...)

Այստեղ ⟨:initarg⟩-ը հենց սլոտի նկարագրության ժամանակ :initarg հատկութ֊յամբտրված սիմվոլն է։ Օրինակ, հարթության (10; 10) և (120; 180) կետերըմիացնողհատվածը ներկայացնող օբյեկտը ստեղծելու համար պետք է գրել.

1 (make-instance 'segment2 :begin (make-point :x 10 :y 10)3 :end (make-point :x 120 :y 180)))

Երբեմնայս եղանակով օբյեկտ ստեղծելիս ստիպված ենք լինում աշխատել ուռճա֊ցած ու անհարմար գրառումների հետ։ Դրանից խուսափելու համար կարող ենքսահմանել segment դասի կոնստրուկտոր։ Ահա այսպես.

77

Page 78: Common Lisp․ 12 խնդիր

1 (defun make-segment (xb yb xe ye)2 (make-instance 'segment3 :begin (make-point :x xb :y yb)4 :end (make-point :x xe :y ye)))

Հիմա արդեն վերը հիշատակված հատվածը ստեղծելու և seg սիմվոլին կապելուհամար կարող ենք գրել ահա այսպիսի պարզ արտահայտություն.

1 (defvar seg (make-segment 10 10 120 180))

SVG լեզվում հատվածները պատկերվում են <line> թեգով, որի x1 և y1ատրիբուտները ցույց են տալիս հատվածի սկզբնակետը, իսկ x2 և y2 ատրիբուտ֊ները՝ հատվածի վերջնակետը։ segment դասի համար svg-view ընդհանրացվածֆունկցիայի մեթոդը սահմանելիս պետք է ձևավորենք այդ հենց <line> թեգը։

1 (defmethod svg-view ((se segment))2 (let ((b (slot-value se 'begin))3 (e (slot-value se 'end)))4 (format nil "<line x1='~d' y1='~d' x2='~d' y2='~d' />"5 (point-x b) (point-y b) (point-x e) (point-y e))))

slot-value ֆունկցիան նախատեսված է օբյեկտի սլոտներին դիմելու, դրանցարժեքները վերցնելու և նոր արժեք վերագրելու համար։ Այս դեպքում նրա օգնութ֊յամբ ստացել ենք se օբյեկտի begin և end սլոտների արժեքները։ Իսկ եթե պետքլինի հատվածի սկզբնակետը բերել (0; 0) կետին, ապա պետք է գրել.

1 (setf (slot-value se 'begin) +zero+)

Հարթության վրա էլիպսը ներկայացնենք իր կենտրոնի (center) կոորդինատ֊ներով և հորիզոնական (h-radius) ու ուղղահայաց (v-radius) շառավիղներով(նկ. 12.1)։

1 (defclass ellipse (shape)2 ((center :type point3 :initform +zero+4 :initarg :center5 :accessor ellipse-center)6 (h-radius :type intege7 :initform 08 :initarg :h-radius9 :accessor ellipse-h-radius)10 (v-radius :type integer11 :initform 012 :initarg :v-radius13 :accessor ellipse-v-radius)))

78

Page 79: Common Lisp․ 12 խնդիր

Այս սահմանումը շատ նման է segment դասի սահմանմանը։ Մի տարբերությունմիայն. սլոտի :accessor հատկությամբ տրված է ֆունկցիայի անուն, որն օգտա֊գործվելու է ինչպես սլոտի արժեքը կարդալու, այնպես էլ նրա արժեքը փոխելուհամար։ Օրինակ, եթե e0-ն ellipse դասի օբյեկտ է, ապա նրա հորիզոնականշառավղին կարելի է 34 արժեքը վերագրել հետևյալ արտահայտությամբ.

1 (setf (ellipse-h-radius e0) 34)

Նորից գրառումների համառոտության համար սահմանենք ellipse դասիկոնստրուկտոր ֆունկցիա.

1 (defun make-ellipse (xc yc rh rv)2 (make-instance 'ellipse3 :center (make-point :x xc :y yc)4 :h-radius rh :v-radius rv))

SVG լեզվի <ellipse> թեգը պատկերում է էլիպս։ Նրա cx և cy ատրիբուտ֊ներով որոշվում են կենտրոնի կոորդինատները, իսկ rx և ry ատրիբուտներով հա֊մապատաասխանաբար հորիզոնական և ուղղահայաց շառավիղները։ svg-viewընդհանրացված ֆունկցիայի մեթոդը այսպիսինն է.

1 (defmethod svg-view ((el ellipse))2 (let ((c (ellipse-center el))3 (h (ellipse-h-radius el))4 (v (ellipse-v-radius el)))5 (format nil "<ellipse cx='~d' cy='~d' rx='~d' ry='~d' />"6 (point-x c) (point-y c) h v)))

Այստեղ let կառուցվածքի մեջ տեսնում ենք, թե ինչպես են օգտագործվել սլոտինկարագրության ժամանակ :accessor հատկությամբ տրված ֆունկցիաները։

Որպես ուղղանկյուններ դիտարկելու ենք այնպիսինները, որոնց կողմերը զու֊գահեռ են կոորդինատական առանցքներին։ Այդպիսի ուղղանկյունները կարելի էմիարժեքորեն որոշել նրա վերին ձախ (origin) և ստորին աջ (corner) անկյուններիկոորդինատներով (նկ. 12.1)։

rectangle դասի սահմանումը նույնպես շատ պարզ է։ Բայց այս անգամ սլոտ֊ների արժեքը ստանալու և նրան նոր արժեք վերագրելու համար նախատեսենքտարբեր անուններով ֆունկցիաներ։

1 (defclass rectangle (shape)2 ((origin :type point3 :initarg :origin4 :initform +zero+5 :reader get-origin6 :writer set-origin)

79

Page 80: Common Lisp․ 12 խնդիր

7 (corner :type point8 :initarg :corner9 :initform +zero+10 :reader get-corner11 :writer set-corner)))12

13 (defun make-rectangle (xo yo xc yc)14 (make-instance 'rectangle15 :origin (make-point :x xo :y yo)16 :corner (make-point :x xc :y yc)))

Այստեղ :reader հատկությամբ տրված է այն ֆունկցիայի անունը, որը վերադարձ֊նում է օբյեկտի տվյալ սլոտի արժեքը։ Օրինակ, եթե s0-ն segment դասի նմուշէ, ապա նրա begin սլոտի արժեքը կարող ենք ստանալ «(segment-begin s0)»արտահայտությամբ։ Իսկ եթե պետք է s0 ուղղանկյան corner սլոտին տալ (23; 97)կետին համապատասխան արժեքը, ապա դա կարելի է անել :writer հատկութ֊յամբ տրված ֆունկցիայի միջոցով.

1 (set-corner (make-point :x 23 :y 97) s0)

SVG լեզվի <rect> թեգի օգտագործմամբ ուղղանկյուններ նկարելու համարպետք է նրա x և y ատրիբուտներով տալ վերին ձախ անկյան (origin) կոորդի֊նատները, իսկ width և height ատրիբուտներով՝ համապատասխանաբար ուղ֊ղանկյան լայնությունն ու բարձրությունը։ let* կառուցվածքի մեջ նախ հաշվենքայդ ատրիբուտների արժեքները, ապա դրանք տեղադրենք <rect> թեգի շաբլոնիմեջ.

1 (defmethod svg-view ((re rectangle))2 (let* ((o (rectangle-origin re))3 (c (rectangle-corner re))4 (w (- (point-x c) (point-x o)))5 (h (- (point-y c) (point-y o))))6 (format nil "<rect x='~d' y='~d' width='~d' height='~d' />"7 (point-x o) (point-y o) w h)))

Մինչև այս պահը սահմանեցինք միայն երեք երկրաչափական պատկերներմոդելավորող դասերը։ Իսկ ամբողջական նկարները մոդելավորելու համար սահ֊մանենք picture կառուցվածքը։ Այն ունի երեք սլոտ. shapes, որը նկարը կազմողտարրական պատկերների ցուցակն է, colour, որը պատկերների եզրագծի գույննէ և scale, որը նկարի մասշտաբն է։

1 (defstruct picture2 (shapes '() :type list)3 (colour "black" :type string :read-only t)4 (scale 1.0 :type real))

80

Page 81: Common Lisp․ 12 խնդիր

Այս picture տիպի օբյեկտին նոր պատկերներ ավելացնելու համար սահմա֊նենք add-shapes ֆունկցիան, որն իր արգումենտում տրված shape տիպի օբ֊յեկտներն ավելացնում է picture օբյեկտի shapes ցուցակին։

1 (defun add-shapes (pic &rest shas)2 (setf (picture-shapes pic) (append (picture-shapes pic) shas)))

Երբ գլոբալ ֆունկցիաները սահմանելիս արգումենտների ցուցակում մի որևէֆորմալ արգումենտի նախորդում է &rest ծառայողական բառը, ապա դա նշանա֊կում է, որ ֆունկցիայի կիրառման ժամանակ հաջորդող արգումենտները ֆունկ֊ցիային են փոխանցվելու որպես մեկ ցուցակ։ Մեր add-shapes ֆունկցիայի այս֊պիսի սահմանումը հնարավորություն է տալիս ֆունկցիայի մեկ կանչով տրվածpicture օբյեկտին ավելացնել մի քանի (կամ անորոշ քանակի) shape օբյեկտներ։

save-as-svg ֆունկցիան արգումենտում ստանում է picture տիպի օբյեկտ ևֆայլի անուն, ապա գեներացնում է picture օբյեկտի պարունակությանը համա֊պատասխան SVG ֆայլ։

1 (defun save-as-svg (pic file-name)2 (with-open-file (so file-name :direction :output3 :if-exists :supersede)4 (format so "<svg xmlns='http://www.w3.org/2000/svg'>~%")5 (format so "<g stroke='~a' fill='white' transform='scale(~f)'>~%"6 (picture-colour pic) (picture-scale pic))7 (dolist (p (picture-shapes pic))8 (princ (svg-view p) so)9 (terpri so))10 (princ "</g></svg>" so)))

Կարծես թեամեն ինչ պատրաստ է։ Մինչև այս պահը թվարկված բոլոր դասերը,կառուցվածքները, մեթոդներն ու ֆունկցիաները գրառենք vectorgraphics.lispֆայլում, ըստ նրանց սահմանման հաջորդականության։ Բացի այդ ֆայլի սկզբումավելացնենք vector-graphics փաթեթի սահմանումը՝ թվարկելով արտաքին մի֊ջավայրին տրամադրվող (exported) սիմվոլները։

1 (in-package :common-lisp-user)2 (defpackage :vector-graphics3 (:nicknames :vg)4 (:use :common-lisp)5 (:export :make-segment6 :make-ellipse7 :make-rectangle8 :make-picture9 :add-shapes10 :save-as-svg))

81

Page 82: Common Lisp․ 12 խնդիր

Նկար 12.2: picture և shape դասերի կիրառության օրինակ

Հիմա տեսնենք, թե ինչպես պետք է օգտագործել սահմանված փաթեթեը։Օրինակ, 12.2 նկարի ավտոմեքենան մեր գրադարանի օգնությամբ նկարելու հա֊մար ստեղծենք automobile.lisp ֆայլը՝ հետևյալ պարունակությամբ.

1 (load "vector-graphics.lisp")2

3 (defparameter *automobile*4 (vg:make-picture :colour "black" :scale 2))5 (vg:add-shapes *automobile*6 (vg:make-rectangle 0 20 50 50)7 (vg:make-rectangle 50 0 70 50)8 (vg:make-rectangle 53 3 67 25)9 (vg:make-rectangle 70 25 100 50)10 (vg:make-segment 90 25 90 10)11 (vg:make-ellipse 20 50 10 10)12 (vg:make-ellipse 80 50 10 10))13 (vg:save-as-svg *automobile* "automobile.svg")14

15 (terpri)(quit)

Ֆայլի առաջին load արտահայտությամբ բեռնում ենք vector-graphics փաթեթիսահմանումը պարունակող ֆայլը։ Հետո *automobile* օբյեկտը սահմանում ենքորպես սև եզրագծերով և 2 մասշտաբով պատկեր, ու նրա մեջ ենք լցնում չորսուղղանկյուն, մեկ հատված և երկու էլիպս։ Եվ վերջում նկարը պահպանում ենքautomobile.svg ֆայլում։

82

Page 83: Common Lisp․ 12 խնդիր

ՀավելվածA

Lisp ընտանիքի լեզٳներ

Ծնվելով 1950֊ականներին՝ Ջոն ՄաքՔարթիի (John MacCarthy), նրա աշակերտ֊ների և գործընկերների ստեղծած LISP լեզուն այսօր վերածվել էմիմեծ ընտանիքի։Այդ նախնական LISP֊ից են ծնունդ առել այնպիսի հռչակավոր ներկայացուցիչներ,ինչպիսիք են Common Lisp֊ը, Scheme-ը, Clojure-ը։ Այս հավելվածում մի քանիտարրական տեղեկություններ են այդ լեզուների մասին։

Common Lisp

Common Lisp բազմանպատակ ծրագրավորման լեզուն ունի իր ստանդարտի բազ֊մաթիվ իրականացումներ։ Այդ իրականացումների մեջ կան այնպիսիները, որոնքտարածվում են բաց կոդով կամ անվճար են։ Այս գրքի բոլոր օրինակները կառու֊ցելիս ու փորձարկելիս հիմնականում օգտագործվել է Common Lisp լեզվի ստան֊դարտի Steel Bank Common Lisp (http://www.sbcl.org/ ) կոմպիլյատորը։

Բացի SBCL-ից, տարբեր ակտիվությամբ օգտագործվել են նաև հետևյալները.

• Armed Bear Common Lisp – Ամբողջովին գրված է Java լեզվով։ Շատ հարմարէ JVM լեզուներում որպես սկրիպտային լեզու օգտագործելու համար (ունիJSR-223 ինտերֆեյսը)։ http://common-lisp.net/project/armedbear/

• Carnegie Mellon University Common Lisp – Ստանդարտի ամենահին իրականա֊ցումներից է, սրանից են ճյուղավորվել SBCL-ը և կոմերցիոն Scieneer CommonLisp-ը։ http://www.cons.org/cmucl/

• Clozure Common Lisp – Բավականին արագագործ ինտերպրետատոր և կոմ֊պիլյատոր է։ http://ccl.clozure.com/

• Embeddable Common Lisp – Շատ հարմար է C և C++ լեզուներով գրվածծրագրերում որպես ընդլայնում կամ սկրիպտային լեզու ներդնելու համար։http://ecls.sourceforge.net/

83

Page 84: Common Lisp․ 12 խնդիր

Common Lisp իրականացման անունն ու տարբերակը կարող ենք ստանալlisp-implementation-type և lisp-implementation-version ֆունկցիաներով։Օրինակ, եթե աշխատում ենք SBCL-ով, ապա.

(lisp-implementaion-type) ⇒ "SBCL"(lisp-implementaion-version) ⇒ "1.0.55.0.debian"Օպերացիոն համակարգի ընդհանուր անունը վերադարձնում է software-type

ֆունկցիան։ Օրինակ, եթե աշխատում ենք Linux համակարգում, ապա(software-type) ⇒ "Linux"

իսկ եթե Windows համակարգում, ապա(software-type) ⇒ "Microsoft Windows"Իսկ software-version ֆունկցիան վերադարձնում է օպերացիոն համակարգի

կոնկրետ տարբերակը։ Linux համակարգերում դա միջուկի տարբերակն է.(software-version) ⇒ "3.8.0-38-generic"

Ինչպես արդեն հիշատակվել է ավելի վաղ (էջ 16), ծրագրերը Common Lispկատարմանմիջավայր են բեռնվում load հրամանով։ Իսկ ինտերպրետատորի հետաշխատանքն ավարտվում է quit ֆունկցիայով։

Scheme

Scheme ալգորիթմական լեզٳն ստեղծվել է աշխարհի լավագույն ուսումնականհաստատություններից մեկում՝ Մասաչուսեթսի տեխնոլոգիական ինստիտուտում(MIT)։ Հենց այս լեզուն է երկար տարիներ ՄՏԻ֊ում օգտագործվել որպես ինֆոր֊մատիկայի ներածական դասընթացի լեզու։ Այսօր արդեն Scheme լեզուն ստան֊դարտացված է (R7RS) և հայտնի են դրա մի քանի հաջող իրականացումներ։

• Racket – Scheme֊ի ամենահարուստ և ամենագեղեցիկ իրականացումներիցէ։ Այստեղ ոչ միայն իրականացված է ստանդարտ Scheme լեզուն, այլևայն ընդլայնված ու լրացված է բազմաթիվ օգտակար գրադարաններով ուգործիքներով։ Հատկապես ուշադրության է արժեքնի աշխատանքի ինտեգ֊րացված միջավայրը (IDE)։ http://racket-lang.org/

• MIT/GNU Scheme – MIT֊ի իրականացումն է, որ ներառում է հարուստ գրադա֊րաններ, ծրագրերի շտկման համակարգ (debugger) և կոմպիլյատոր։ Այսիրականացումն է օգտագործվել MIT֊ի հանրահայտ [1] և [16] գրքերում։https://www.gnu.org/sostware/mit-scheme/

• GNU Kawa – Իրականացված է Java լեզվով և JVM֊ի համար։ Kawa֊ի կոմպիլ֊յատորը Scheme լեզվով գրված ծրագրերը թարգմանում է JVM֊ի բայթ֊կոդի։Շատ հարմար է օգտագործել ոչ միայն ինչպես ինքնուրույն ծրագրավորման

84

Page 85: Common Lisp․ 12 խնդիր

լեզուն, այլև որպես սկրիպտային լեզու՝ JMV֊ի համար գրված համակարգե֊րում։ http://www.gnu.org/sostware/kawa/

• GNUGuile – GNU նախագծի ծրագրերի ընդլայնման լեզուն է՝ գրված C լեզվով։Հատուկ հարմարեցված C API֊ն թույլ է տալիս ոչ միայն Guile֊ը օգտագործելորպես ներդրվող գործիք, այլև հենց իրեն՝ Guile֊ին ընդլայնել C լեզվով գրվածնոր մոդուլներով։ http://www.gnu.org/sostware/guile/

Պարզապես ծանոթանալու համար բերենք Scheme լեզվով գրված մի քանիօրինակներ։ Առաջինը թող լինի ցուցակը շրջող reverse-list ֆունկցիան՝ գրվածtail-recursive տարբերակով։

1 (define (reverse-list ls)2 (define (reverse-help l r)3 (if (null? l)4 r5 (reverse-help (cdr l) (cons (car l) r))))6 (reverse-help ls '()))

define կառուցվածքով Scheme կատարմանմիջավայր է ներմուծվում նոր սահմա֊նում։ Տվյալ դեպքում սահմանվել է reverse-list ֆունկցիան, իսկ նրա մարմնում՝reverse-help օգտնական ֆունկցիան, որը կիրառելի է միայն իրեն պարփակողֆունկցիայի մարմնում։ Ի տարբերություն Common Lisp֊ի Scheme֊ում ֆունկցիայիանունն ու պարամետրերը տրվում են մեկ ցուցակով. «(reverse-help l r)»ցուցակում «l»֊ը և «r»֊ը «reverse-help» ֆունկցիայի պարամետրերն են։

Ֆունկցիա կարելի է սահմանել նաև մեկ այլ համարժեք եղանակով՝ defineգործողությամբ սիմվոլին վերագրելով լամբդա արտահայտություն։ Օրինակ, սահ֊մանենք filter-list ֆունկցիան, որը տրված ցուցակից հեռացնում է տրվածպրեդիկատին չբավարարող տարրերը։

1 (define filter-list2 (lambda (pr ls)3 (cond4 ((null? ls)5 '())6 ((pr (car ls))7 (cons (car ls) (filter-list pr (cdr ls))))8 (else9 (filter-list pr (cdr ls))))))

Լեզվի քերականությունն ընդլանելու համար Scheme լեզուն առաջարկում էdefine-syntax կառուցվածքը։ Օրինակ, սահմանենք «եթե» պայմանի կառուց֊վածքը, որը կարելի է օգտագործել հետևյալ երկու տեսքերով.

85

Page 86: Common Lisp․ 12 խնդիր

(եթե ⟨պայման⟩ ապա ⟨արտահայտٳթյٳն⟩)(եթե ⟨պայման⟩ ապա ⟨արտահայտٳթյٳն⟩1 այլապես ⟨արտահայտٳթյٳն⟩2)

Սա Scheme լեզվի if կառուցվածքի համարժեքն է՝ հայերեն ծառայողական բառերիօգտագործմամբ։

1 (define-syntax եթե2 (syntax-rules (ապա այլապես)3 ((եթե condition ապա then-part)4 (if condition then-part nil))5 ((եթե condition ապա then-part այլապես else-part)6 (if condition then-part else-part))))

Clojure

Մեծ տարածում է գտել նաև Lisp ընտանիքի՝ վերջին տարիներին ստեղծված ևսմի ներկայացուցիչ՝ Clojure լեզուն (http://clojure.org/ )։ Clojure֊ը (ինչպես ABCL֊ն ուKawa-ն) գրված է Java լեզվով և JVM վիտուալ մեքենայի համար։ Այսպիսով, Clo֊jure լեզվով ծրագրավորողի տրամադրության տակ են Java-աշխարհի հարուստգրադարանները և Lisp֊աշխարհի ֆունկցիոնալ հնարավորությունները։

Սահմանենք join-with ֆունկցիան, որը տողերի strlist ցուցակն իրար էկցում՝ որպես բաժանիչ օգտագործելով separ տողը։

1 (defn join-with2 [strlist separ]3 (reduce (fn [x y] (str x separ y)) strlist))

Այստեղ օգտագործված reduce ֆունցիան իր առաջին արգումենտում տրվածանանուն ֆունկցիան, սահմանված fn օպերատորով, կիրառում է երկրորդ արգու֊մենտում տրված հաջորդականության տարրերն իրար կցելու համար։ Օրինակ,«("a" "b" "c" "d")» ցուցակից «"a, b, c, d"» տողը ստանալու համարպետք է գրել։

(join-with ("a" "b" "c" "d") ", ") ⇒ "a, b, c, d"

86

Page 87: Common Lisp․ 12 խնդիր

Հավելված B

Quicklisp

Quicklisp-ը Common Lisp ծրագրավորման լեզվի գրադարանների ղեկավարմանհամակարգ է։ Աշխատելով ներկայումս հայտնի բոլոր Common Lisp իրականացում֊ների հետ, այն հնարավորություն է տալիս ցանցից բեռնել, տեղադրել և օգտագոր֊ծել շուրջ 1000 գրադարաններ։

Quicklisp համակարգը տեղադրելը շատ հեշտ է։ Դրա համարպետք է ներբեռնելhttp://beta.quicklisp.org/quicklisp.lisp ֆայլը, ապա load ֆունկցիայով այն բեռնել Com֊mon Lisp կատարման միջավայր։

Բեռնելուց հետո պետք է հաշվարկել (quicklisp-quickstart:install) ար֊տահայտությունը, որի արդյունքում էլ $HOME պանակում ստեղծվում է quicklispպանակը, և ցանցից այնտեղ են բեռնվում անհրաժեշտ ֆայլերը։ Եթե պետք էQuicklisp-ը տեղադրել մի այլ տեղ, ապա install ֆունկցիայի :path անվանվածարգումենտով կարելի է տալ այդ տեղը։

Որպեսզի Lisp համակարգը գործարկելիս ավտոմատ բեռնվի Quicklisp համա֊կարգը, նրա տեղադրումից հետո պետք է կանչել ql:add-to-init-file ֆունկ֊ցիան։ Այս ֆունկցիան համապատասխան Lisp-իրականացման սկզբնական արժե֊քավորման ֆայլում (initialization file) գրում է Quicklisp-ը բեռնող ֆունկցիայի կանչը։

ql:system-apropos ֆունկցիան Quicklisp-ին հարցնում է նկարագրության կամանվան մեջ տրված տեքստը պարունակող գրադարանի գոյության մասին։ Եթեհետաքրքրում է, օրինակ, Common Lisp լեզվի համար ստեղծված alexandriaգրադարանը, ապա պետք է գրել.

(ql:system-apropos "alexandria")

Եթե Quicklisp-ը արտածում է գտված գրադարանի նկարագրությունը, ապա այնկարելի է կատարման միջավայր բեռնել ql:quickload ֆունկցիայով։ Օրինակ.

(ql:quickload "alexandria")

87

Page 88: Common Lisp․ 12 խնդիր

Եթե հարցվող գրադարանը բացակայում է լոկալ շտեմարանում, ապա այնավտոմոտ ներբեռնվում է Quicklisp համակարգի ցանցային պահոցից, իսկ հետոբեռնվում է Common Lisp կատարմանմիջավայր։ Հաջորդ անգամ նույն գրադարա֊նին դիմելիս այն արդեն կբեռնվի լոկալ շտեմարանից։

Եթե ինչ֊որ պատճառովանհրաժեշտ է լոկալ շտեմարանից հեռացնել ցանցայինպահոցից բեռնված գրադարանը, ապապետք է օգտագործել ql:uninstallֆունկ֊ցիան։

(ql:uninstall "alexandria")

Լոկալ շտեմարանի բոլոր գրադարանները ցանցային պահոցի ավելի թարմտարբերակներով (եթե այդպիսիք կան) փոխարինելու համար նախատեսված էql:update-all-dists ֆունկցիան։

(ql:update-all-dists)

Բեռնված գրադարանի արտաքին (exported) սիմվոլների ցուցակը ստանալուհամար կարող ենք օգտագործել do-external-symbols իտերատորը, նրան տալովfind-package ֆունկցիայի վերադարձրածփաթեթ֊օբյեկտը։ Օրինակ, alexandriaգրադարանի արտաքին անունների ցուցակը կարող ենք ստանան, հաշվարկելովհետևյալ արտահայտությունը�

(do-external-symbols (di (find-package 'alexandria))(print di))

88

Page 89: Common Lisp․ 12 խնդիր

Առարկայական ցանկ

Symbols*package* 25*query-io* 14*standard-output* 13, 62&key 20&optional 29&rest 811+ 101- 10

Aalpha-char-p 44, 49, 52append 70apply 73aref 42assoc 61atom 6, 57

Bboundp 63

Ccar 11cdr 11ceiling 31char 51char-code 44char-upcase 44close 43complement 52concatenate 38, 73

cond 16, 18, 38cons 12, 53, 63copy-list 56cos 38

Ddecf 23defclass 76

:accessor 79:initarg 77:initform 77:reader 80:type 77:writer 80

defconstant 36, 68defgeneric 76defmacro 61defmethod 76defpackage 24

:export 25:nicknames 25:shadow 25:use 25

defparameter 63defstruct 59

:read-only 60:type 60

defun 8, 20, 30, 32defvar 63directory 69do 43

89

Page 90: Common Lisp․ 12 խնդիր

do-external-symbols 88documentation 20dolist 39

Eeighth 21endp 12eq 52eql 52, 61, 71equal 12, 52equalp 52evenp 31

Ffifth 21file-length 72find-if 65find-package 25, 88first 21flet 32, 45float 28floor 31force-output 14format 13, 56, 73fourth 21funcall 29function 29

Ggcd 24getf 62gethash 52

Hhash-table 52

Iidentity 51if 9, 16, 18in-package 25, 75incf 23, 28, 45, 52

Llabels 32

lcm 24let 15, 19, 32, 79let* 19, 32, 70, 80lisp-implementation-type 84lisp-implementation-version 84list 7, 20list-all-packages 25list-length 8, 54listp 6load 16, 82, 84

:print 25loop 23, 28, 31, 32, 45, 47, 50, 52, 54, 72

across 45as 72being 72by 45collect 45, 50do 23, 47each 72finally 23, 28, 50for 31, 45from 31, 45hash-keys 72hash-value 72in 51, 72never 31of 72repeat 28sum 32, 54the 72to 31until 23, 50upto 34using 72when 28while 23, 50with 28, 50

Mmacroexpand 64macroexpand-1 61, 64make-array 41

90

Page 91: Common Lisp․ 12 խնդիր

:initial-contents 42:initial-element 42

make-hash-table 52:test 52

make-instance 77make-pathname 68

:absolute 68:case 68:defaults 68:device 68:directory 68:host 68:name 68:relative 68:type 68:version 68:wild 69

make-string 50map 51, 73mapc 40mapcan 74mapcar 45, 55maphash 53member 70minusp 19multiple-value-bind 37

Nnconc 70, 74nil 7, 31, 40ninth 21nth 37, 55

Oopen 43

:append 47:direction 43:if-exists 47:input 43:io 43:output 43:supersede 47

or 71

Ppackage-name 25parse-namestring 68pathname 67, 69

device 67directory 67host 67name 67type 67version 67

pathname-device 69pathname-directory 69pathname-host 69pathname-name 69pathname-type 69pathname-version 69peek-char 50pi 8, 38plusp 19princ 38, 47progn 19, 63psetf 42psetq 42push 63pushnew 64

:test 64

Qquit 17, 84quote 7

Rrandom 28read 65read-char 43, 50read-line 15remove-duplicates 55remove-if 54, 71

:key 54remove-if-not 70rest 11return 23, 28round 15

91

Page 92: Common Lisp․ 12 խնդիր

Ssecond 21, 45setf 42, 51setq 42, 70seventh 21sin 38sixth 21sleep 14slot-value 78software-type 84software-version 84sort 45, 55

:key 55sqrt 31string 50string-equal 16, 39, 71string-greaterp 39string-lessp 39string-not-equal 39string-not-greaterp 39string-notlessp 39string/= 39string< 39string<= 39string= 39string> 39string>= 39

Tt 7, 31tenth 21terpri 38, 48third 21truncate 37type-of 60typep 60, 76

Uunless 18unread-char 50

Vvalues 37

Wwhen 18, 45with-open-file 42, 72

Zzerop 19

92

Page 93: Common Lisp․ 12 խնդիր

Գրականٳթյٳն

[1] Harold Abelson, Gerald Sussman, and Julie Sussman. Structure and Interpretation of ComputerPrograms. McGraw-Hill, Inc., New York, NY, USA, 2 edition, 1997.

[2] Conrad Barski. Land of Lisp: Learn to Program in Lisp, One Game at a Time! No Starch PressSeries. No Starch Press, 2010.

[3] Edsger W. Dijkstra. A short introduction to the art of programming. circulated privately,August 1971.

[4] David J. Eisenberg. SVG Essentials. O’Reilly & Associates, Inc., Sebastopol, CA, USA, 1edition, 2002.

[5] Paul Graham. On LISP: Advanced Techniques for Common LISP. Prentice-Hall, Inc., UpperSaddle River, NJ, USA, 1993.

[6] Paul Graham. ANSI Common Lisp. Prentice Hall Press, 1996.

[7] Ronald L. Graham, Donald E. Knuth, and Oren Patashnik. Concrete Mathematics: A Founda֊tion for Computer Science. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA,2nd edition, 1994.

[8] Sonya E. Keene. Object-oriented programming in Common LISP: a programmer’s guide toCLOS. Addison-Wesley, 1989.

[9] Gregor Kiczales, Jim Des Rivieres, and Daniel G. Bobrow. The Art of the Metaobject Protocol.MIT Press, Cambridge, MA, USA, 1991.

[10] John McCarthy. Recursive functions of symbolic expressions and their computation by ma֊chine, Part I. Commun. ACM, 3(4):184–195, 1960.

[11] Peter Norvig. Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp.Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 1st edition, 1992.

[12] Peter Seibel. Practical Common Lisp. APress, 2004.

93

Page 94: Common Lisp․ 12 խնդիր

[13] Michael Sperber, R. kent Dybvig, Matthew Flatt, Anton Van Straaten, Robby Findler, andJacob Matthews. Revised6 report on the algorithmic language scheme. J. Funct. Program.,19(S1):1–301, 2009.

[14] Guy L. Steele, Jr. Common LISP: the language (2nd ed.). Digital Press, Newton, MA, USA,1990.

[15] Alexander A. Stepanov and Daniel E. Rose. From Mathematics to Generic Programming. Ad֊dison-Wesley Professional, 2014.

[16] Gerald Jay Sussman, Jack Wisdom, and Meinhard E. Mayer. Structure and interpretation ofclassical mechanics. MIT Press, 2001.

[17] А.Г. Курош. Курс высшей алгебры. Наука, 1968.

[18] Хювёнен Э. and Сеппянен И. Мир Лиспа. Том 1. Введение в язык Лисп и функциональноепрограммирование. Мир, 1990.

[19] Хювёнен Э. and Сеппянен И. Мир Лиспа. Том 2. Методы и системы программирования.Мир, 1990.

94

Page 95: Common Lisp․ 12 խնդիր

Արմեն Բադալյան

Common Lisp։ 12 խնդիր

Armen Badalian

Common Lisp: 12 tasks

Տեքստը պատրաստվել է LATEX համակարգով։Տպատառերը՝ GHEAMariam, GHEAGrapalat, DejaVu Sans Mono

Page 96: Common Lisp․ 12 խնդիր