ԽԱՆՈՒԹԸ ՓԱԿ Է
Գրառումները շարունակում եմ Օրագրում։
Տե՛ս նաև իմ GitHub էջը։
Այս կիրակի ես վերջապես որոշեցի սկսել ծանոթությունը Haskell լեզվի հետ։ Haskel-ը ֆունկցիանալ լեզու է. երբեմն ասում են, որ այն ֆունկցիոնալների մեջ ամենաֆունկցիոնալն է։ Ես, ինչ-որ տարրական պատկերացում ունենալով ֆունկցիոնալ ծրագրավորման մասին, ինձ համար սահմանեցի հետևյալ առաջին խնդիրը.
Ֆակտորիալ։ Գրել ծրագիր, որ հրամանային տողից ստանում է որևէ դրական ամբողջ թիվ, ապա հաշվարկում և արտածում է այդ թվի ֆակտորիալը։
Բայց, մինչև խնդրի լուծմանն անցնելը, ես պիտի պատրաստեմ Haskell լեզվի միջավայրը, որում աշխատեցնելու եմ իմ գրած ծրագրերը։ Կարելի է, իհարկե, օգտագործել որևէ առցանց ծառայություն, ինչպիսիք են, օրինակ, www.tutorialspoint.com-ը կամ https://repl.it-ը, բայց ես նախընտրում եմ ամեն ինչ ունենալ ձեռքի տակ՝ իմ մեքենայի վրա։
Իսկ իմ մեքենան Raspberry Pi է` Debian-ի հիման վրա կառուցված օպերացիոն համակարգով։ Haskell Platform-ի էջից գտա, թե ինչպես է պետք տեղադրել Haskell-ի կոմպիլյատորն ու ինտերպրետատորը.
Haskel Platform-ի կոմպիլյատորի և ինտերպրետատորի հաջող տեղադրված լինելը ստուգելու համար նախ հրամանային տողից աշխատեցեմ ghci
ինտերպրետատորը.
Հրավերքի տողում Prelude
ցույց է տալիս, որ ինտերպրետատորը գործարկվել է և ակտիվ է Prelude փաթեթը։ Խնդրեմ Հասկելին ցույց տալ π թվի արժեքը.
Կարծես թե աշխատում է։ Փորձեմ հենց այստեղ սահմանել ֆակտորիալը հաշվող ֆունկցիան՝ ամենապարզ մոտեցմամբ.
Prelude> factorial n = if n == 1 then 1 else n * factorial (n - 1)
Մի քանի օրինակներով համոզվեմ, որ սահմանած ֆունկցիան աշխատում է.
Prelude> factorial 1
1
Prelude> factorial 5
120
Prelude> factorial 10
3628800
Prelude> factorial 100
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Հիմա այս ֆունկցիան գրեմ մի ֆայլի մեջ, օրինակ, ex0.hs
անունով, ու փորձեմ այդ ֆայլը թարգմանել Հասկելի կոմպիլյատորով։
-- Իմ առաջին ծրագիրը
factorial :: Integer -> Integer
factorial n = if n == 1 then 1 else n * factorial (n - 1)
Այստեղ ֆունկցիայի սահմանումից առաջ ավելացրել եմ նաև դրա վերնագիրը (կամ նկարագրությունը)։ Այդ նկարագրությամբ տրվում է ֆունկցիայի տիպը. ::
սիմվոլից ձախ գրված է ֆունկցիայի անունը՝ factorial
, իսկ աջ կողմում՝ արգումենտի ու վերադարձվող արժեքի տիպերը։ Այսինքն՝ ֆունկցիան ստանում է Integer
տիպի արգումենտ և վերադարձնում է Integer
տիպի արժեք։ Երկրորդ տողում հենց ֆունկցիայի սահմանումն է. =
սիմվոլից ձախ ֆունկցիայի անունն ու արգումենտն է, իսկ աջ կողմում՝ մարմինը, որը տվյալ դեպքում պարզ ճյուղավորման արտահայտություն է։
Haskell Platform-ում կոմպիլյատորը ghc
—ն է։ Աշխատեցնում եմ՝ մուտքին տալով ex0.hs
ֆայլը.
$ ghc ex0.hs
[1 of 1] Compiling Main ( ex0.hs, ex0.o )
ex0.hs:1:1: error:
The IO action ‘main’ is not defined in module ‘Main’
|
1 |
| ^
Սխալի հաղորդագրությունն ասում է, որ Main
մոդուլում սահմանված չէ main
գործողությունը։ Բանից պարզվում է, որ Հասկելի կոմպիլյատորը նույնպես (ինչպես, օրինակ, Սի լեզվի կոմպիլյատորը) որպես մուտքի կետ է համարում main
գործողությունը։ Հիմա ex0.hs
ֆայլում ավելացնում եմ main
գործողությունն այնպես, որ այն արտածի 12
-ի ֆակտորիալը.
-- Իմ առաջին ծրագիրը
factorial n = if n == 1 then 1 else n * factorial (n - 1)
-- Մուտքի կետը
main =
print (factorial 12)
Նորից փորձեմ թարգմանել։ Ի դեպ, Հասկել լզվում --
սիմվոլով սկսվում են մեկնաբանությունները։
Արդեն ամեն ինչ լավ է։ Իմ գրած ծրագիրը թարգմանվեց (compile), կապակցվեց (link), և հիմա կարող եմ աշխատեցնել ու տեսնել արդյունքը.
Բայց այս ծրագիրը կարողանում է հաշվել ու տպել միայն 12
-ի ֆակտորիալը։ Իսկ ես ուզում եմ, որ այն կարողանա հաշվել հրամանային տողում տրված թվի ֆակտորիալը։ ՄԻ քիչ քչփորելուց հետո պարզեցի, որ Հասկել ծրագրում գրամանային տողի պարամետրերը կարելի է վերցնել System.Environment
մոդուլի getArgs
գործողությամբ։ Օրինակ, հետևյալ ծրագիրը (գրառված ex1.hs
ֆայլում) արտածում է հրամանային տողում տրված պարամետրերի ցուցակը.
-- Հրամանային տողի պարամետրերի ցուցադրություն
import System.Environment
main = do
args <- getArgs
print args
Ահա թարգմանության ու կատարման մի քանի օրինակ.
$ ./ex1
[]
$ ./ex1 a
["a"]
$ ./ex1 a bb
["a","bb"]
$ ./ex1 a bb ccc
["a","bb","ccc"]
$ ./ex1 1 22 333
["1","22","333"]
Այստեղից երևում է, որ հրամանային տողի պարամետրերը ծրագրում երևում են տեքստային արժեքների ցուցակի տեսքով։ Ես պետք է թվի տեքստային ներկայացումից ստանամ դրա թվային արժեքը, ապա այդ արժեքի նկատմամբ կիրառեմ factorial
ֆունկցիան:
Հասկելի read
ֆուկցիան տեքսից «կարդում» է որևէ տիպի արժեք։ Այդ տիպը տրվում է ֆունկցիայի կանչի հետ՝ ::
սիմվոլոլից հետո։ Օրինակ, «read "12" :: Int
» արտահայտությունը "12"
տողից կարդում է Int
տիպի 12
արժեքը։ «read "12" :: Float
» արտահայտությունը նույն տողից կարդում է 12.0
արժեքը՝ Float
տիպի։
Այսպիսով, ես պետք է վերցնեմ հրամանային տողի պարամետրերի ցուցակի առաջին տարրը (head
ֆունկցիայիով), դրա նկատմամբ կիրառեմ read
ֆունկցիան՝ Integer
տիպի համար, ստացված արժեքի նկատմամբ կիրառեմ factorial
-ը ու տպեմ ստացված արժեքը։ Ահա այսպիսի մի արտահայտություն main
ֆունկցիայում.
Ձևափոխված ex0.hs
ծրագիրը կունենա հետևյալ վերջնական տեսքը.
import System.Environment
-- Ֆակտորիալի հաշվարկը
factorial :: Integer -> Integer
factorial n =
if n == 1
then 1
else n * factorial (n - 1)
-- Մուտքի կետ
main :: IO ()
main = do
args <- getArgs
print (factorial (read (head args) :: Integer))
Լավ. տեսնենք, թե սա ինչպես է աշխատում։
$ ghc ex0.hs
[1 of 1] Compiling Main ( ex0.hs, ex0.o )
Linking ex0 ...
$ ./ex0 2
2
$ ./ex0 12
479001600
$ ./ex0 20
2432902008176640000
$ ./ex0 40
815915283247897734345611269596115894272000000000
Լավ էլ աշխատում է։ Բայց, իհարկե, թերություններ կան։ Առաջին թերությունը տեխնիկական է. դիտարկված չէ այն դեպքը, երբ հրամանային տողում ոչինչ տրված չէ։ Օրինակ, եթե աշխատեցնեմ ծրագիրը՝ հրամանային տողում ոչինչ չտալով, ապա կստանամ հաղորդագրություն այն մասին, որ head
ֆունկցիային տրված է դատարկ ցուցակ.
Սա պետք է ուղղել՝ main
գործողության մեջ պայման գրելով։ Այսպես.
main = do
args <- getArgs
if not (null args)
then print (factorial (read (head args) :: Integer))
else putStrLn "Ոչինչ տրված չէ։"
Հիմա եթե ծրագիրն աշխատեցնեմ դատարկ հրամանային տողով, ապա որպես պատասխան կստանամ «Ոչինչ տրված չէ։
»։
Հաջորդիվ. թերևս Հասկել լեզվով գրող ոչ մի ծրագրավորող թվի ֆակտորիալը հաշվող ֆունկցիան չի գրի այնպես, ինչպես ես գրել եմ։ Վարպետ Հասկել-ծրագրավորողը պարզապես կգրի.
Եվ վերջ։ Այստեղ գրված է ֆակտորիալի բառացի սահմանումը՝ այն 1
-ից n
թվերի ([1 .. n]
) արտադրյալն է (product
):
digraph
դասի կաղապար։ Այս կաղապարի պարամետրն այն տիպն է, որ նմուշը գրառվելու
է գրավի գագաթում։ Այդ տիպի վրա դրված մակ պահանջն այն է, որ նրա համար սահմանված
լինի հավասարության ստուգման ==
գործողությունը։ Այդ գործողությունն օգտագործելու
եմ տրված տվյալը պարունակող գագաթի ինդեքսը որոնելու համար։
template<typename Y> class digraph { /// ... };Գրաֆի ներքին ներկայացման համար ընտրել եմ գագաթների հարևանության ցուցակների եղանակը։ Գագաթները հերթականությամբ ավելացնում եմ միաչափ զանգվածում (vector): Իսկ ամեն մի գագաթի (vertex) համապատասխանեցնում եմ նրան հարևան գագաթների ինդեքսների ցուցակը։ Քանի որ որոշել եմ գագաթում գրառել կաղապարի պարամետրով տրված ցանկացած տիպի օբյեկտ, մի գագաթի նկարագրությունը սահմանել եմ
vertex
տիպով.
using index = int; using index_list = std::vector<index>; using vertex = std::pair<Y,index_list>;Դե իսկ գրաֆի գագաթների ցուցակն էլ արդեն կսահմանեմ հետևյալ կերպ.
std::vector<vertex> _vertices;Գրաֆը կառուցելու եմ կողերը հաջորդաբար ավելացնելով։
add_edge
մեթոդը ստանում է
կաղապարի Y
տիպի երկու արժեք և գրաֆում կոդ է ավելացնում այդ արժեքները պարունակող
գագաթների միջև։ Եթե այդպիսի գագաթներ դեռ չկան, ապա գրաֆում ավելացնում է նաև
գագաթները։
void add_vertex(const Y& u, const Y& v) { auto ui = add_vertex(u); auto vi = add_vertex(v); _vertices[ui].second.push_back(vi); }Այստեղ օգտագործված
add_vertex
մեթոդը գրաֆում ավելացնում է տրված արժեքը պարունակող
գագաթ։ Եթե այդպիսին արդեն կա, ապա վերադարձնում է դրա ինդեքսը։
index add_vertex(const Y& v) { // որոնել for( index ix = 0; ix < _vertices.size(); ++ix ) if( _vertices[ix].first == v ) return ix; // ավելացնել _vertices.push_back(v, {}); return _vertices.size() - 1; }Այսքանը լրիվ հերիք է գրաֆի կառուցման համար։ Թերևս կարելի է ավելացնել
to_string
մեթոդը, որը կվերադարձնի գրաֆի ինչ-որ տեքստային ներկայացում. պարզապես շտկումների
համար։
std::string to_string() { std::ostringstream oss; for( auto& e : _vertices ) { oss << e.first << " -> { "; for( auto& i : e.second ) oss << _vertices[i].first << ' '; oss << '}' << std::endl; } return oss.str(); }Այս մեթոդը պահանջում է, որ կաղապարի
Y
տիպի համար սահմանված լինի արտածման հոսքի մեջ
ուղարկելու գործողությունը՝ operator<<
։
Անցնեմ գրաֆի գագաթների տոպոլոգիական թվարկման իրականացմանը։ Տոպոլոգիական կարգավորման
մասին բան չեմ գրի. կարելի է կարդալ, օրինակ, Ուիքիփեդիայի
Topological Sorting էջը։ Միայն
ասեմ, որ իրականացման համար ընտրել եմ Kahn-ի ալգորիթմը։ Մի քանի բառով դրա էությունը
հետևյալն է. ամենասկզբում ստեկի մեջ են ավելացնում (push) բոլոր այն գագաթները, որոնք
նախորդող չունեն (աղեղները դրանցից միայն դուրս են գալիս)։ Այնուհետև, քանի դեռ ստեկը
դատարկ չէ, ստեկից հանում ենք u գագաթը, ապա գրաֆից հեռացնում ենք (pop) u գագաթից
դուրս եկող բոլոր կողերը։ Եթե այս գործողության արդյունքում հայտնվում է գագաթ, որը դեպի
իրեն եկող կող չունի, ապա այդ գագաթն ավելացնում ենք (push) ստեկում։ Քանի որ գրաֆից
կող հեռացնելը կվնասեր մեր սկզբնական գրաֆը, այդ «կող հեռացնել» գործողությունը
մոդելավորվում է մի վեկտորով, որում ամեն մի գագաթի համար պահվում է դեպի իրեն եկող
կողերի քանակը։
Գրաֆի գագաթների՝ տոպոլոգիական կարգով թվարկումը իրականացրել եմ enumerate
մեթոդում,
որի պարամետրը ֆունկցիա է։ Այս ֆունկցիայով տրվում է այն գործողությունը, որը պետք է
կատարվի գրաֆի թվարկվող հերթական գագաթի հետ։ Ավելի մանրամասն՝ մեկնաբանություններում.
void enumerate(std::functionՓորձարկենք ալգորիթմը ստորև բերված գրաֆի վրա՝ որպես թվարկման գործողություն տալով պարզապես գագաթի պարունակությունը տպող որևէ ֆունկցիա։f) { // գրաֆի գագաթների քանակը const std::size_t sz = _vertices.size(); // degrees զանգվածում գրաֆի ամեն մի գագաթի համար // հաշվում ենք դեպի այն եկող կողերի քանակը std::vector<std::size_t> degrees(sz); for( auto& e : _vertices ) for( auto& n : e.second ) degrees[n] += 1; // նախապես S ստեկում ավելացնում ենք այն գագաթները, // որոնք նախորդող չունեն std::stack<index> S; for( std::size_t i = 0; i < degrees.size(); ++i ) if( degrees[i] == 0 ) S.push(i); // իտերացիա գագաթներով while( !S.empty() ) { // քանի դեռ ստեկը դատարկ չէ // վերցնել գագաթի տարրը index ix = S.top(); S.pop(); // տրված գործողությունը կատարել գագաթում գրառված // տվյալների հետ f(_vertices[ix].first); // դիտարկվող գագաթի բոլոր հարևանների համար... for( auto& vi : _vertices[ix].second ) { // ... եթե այդ հարևանը նախորդոնղեր չունի, ապա // այն արդեն մասնակցել է թվարկմանը, ... if( degrees[vi] == 0 ) continue; // «կողը հեռացնելու» գործողության մոդելը. // պակասեցնում ենք հաշվիչը, ... degrees[vi] -= 1; // ... եթե հաշվիչը դառնում է զրո, ապա այդ գագաթը // ավելացնում ենք ստեկում if( degrees[vi] == 0 ) S.push(vi); } } }
to_string
մեթոդով տպում եմ գրաֆի տեքստային ներկայացումը։ Թեսթավորման համար պարզապես
ուզում եմ տպել գագաթները տոպոլոգիական կարգով։ Դրա համար enumerate
մեթոդի
արգումենտում տալիս եմ լամբդա-ֆունկցիա, որը պարզապես արտածում է իր արգումենտը։
int main() { graphs::digraph<std::string> g0; g0.add_edge("a", "q0"); g0.add_edge("b", "q0"); g0.add_edge("b", "q1"); g0.add_edge("c", "q1"); g0.add_edge("q0", "x"); g0.add_edge("q1", "x"); g0.add_edge("q0", "y"); g0.add_edge("q1", "y"); std::cout << "Graph: " << std::endl << g0.to_string() << std::endl; std::cout << "Topological order of vertices: " << std::endl; g0.enumerate([&](auto& s) {std::cout << s << ' '; }); }Կատարման արդյունքում արտածվում է հետևյալը.
Graph: a -> { q0 } q0 -> { x y } b -> { q0 q1 } q1 -> { x y } c -> { q1 } x -> { } y -> { } Topological order of vertices: c b q1 a q0 y xԼավ է։ Բայց ես ուզում եմ գրաֆի համար իրականացնել նրա գանաթերը տոպոլոգիական կարգով թվարկող իտերատոր. այնպիսին, ինչպիսիք սահմանված են STL-ում։ Այդ տիպի իտերատորի առկայության դեպքում կարող եմ գրել այսպիսի կոդ.
for( auto& v : g0 ) std::cout << v << ' ';Պարզ է, որ այդ իտերատորի մեթոդների իրականացումը պետք է արտացոլի նույն
enumerate
մեթոդի վարքը։ digraph
դասում սահմանում եմ ներդրված iterator
դասը։ range-for-ի աշխատանքն ապահովելու համար իտերատորում պետք է իրականացնել
operator*
, operator++
և operator!=
գործողությունները։ Իտերատորի
կոնստրուկտորում հաշվարկվում են բոլոր գագաթների մուտքային կիսաաստիճանները՝
տվյալ գագաթին ուղղված կողերի քանակը, ապա ստեկի մեջ են ավելացվում այն գագաթների
ինդեքսները, դեպի որոնց եկող կողեր չկան։ operator*
-ը վերադարձնում է հղում
հերթական գագաթում գրառված տվյալին։ operator++
-ը իտերատորը փոխանցում է հաջորդ
գագաթին։ operator!=
-ը համեմատում է երկու իտերատորների հերթական գագաթները։
Նորից՝ մանրամասները կոդի մեկնաբանություններում։
templateՄնում է հիմաclass digraph { // ... public: class iterator { public: using iterator_category = std::forward_iterator_tag; using value_type = Y; // iterator(const digraph& g, bool init) : _ref{g} { if( init ) { // հաշվարկվում են մուտքային կիսաաստիճանները _degrees.resize(_ref._vertices.size()); for( auto& e : _ref._vertices ) for( auto& n : e.second ) _degrees[n] += 1; // ստեկում ավելացնել բոլոր «սկզբնական» գագաթների ինդեքսները for( std::size_t i = 0; i < _degrees.size(); ++i ) if( _degrees[i] == 0 ) _S.push(i); // կատարել առաջին քայլը՝ արժեքավորել դիտարկվող գագաթի ինդեքսը this->operator++(); } } // iterator& operator++() { // եթե ստեկը դատարկ չէ, ապա անցնել հերթական գագաթին, հակառակ // դեպքում հերթական գագաթի ինդեքսին վերագրել -1 if( !_S.empty() ) { _current = _S.top(); _S.pop(); for( auto& vi : _ref._vertices[_current].second ) { if( _degrees[vi] == 0 ) continue; _degrees[vi] -= 1; if( _degrees[vi] == 0 ) _S.push(vi); } } else _current = -1; return *this; } // bool operator!=(const iterator& other) { // համեմատել երկու իտերատորներում ընթացիկ գագաթի ինդեքսը return _current != other._current; } // const value_type& operator*() const { // վերադարձնել ընթացիկ գագաթում պահվող տվյալը return _ref._vertices[_current].first; } private: const digraph& _ref; // հղում գրաֆին index _current = -1; // հերթական դիտարկվող գագաթի ինդեքսը std::vector<std::size_t> _degrees; // գագափթների մուտքային կիսաաստիճանները std::stack<index> _S; // ինդեքսների ստեկը }; };
digraph
դասի համար իրականացնել իտերատոր վերադարձնող begin
և end
մեթոդները։
iterator begin() { return iterator(*this, true); } iterator end() { return iterator(*this, false); }Իտերատորի բերված իրականացումը և
digraph
դասի begin
և end
մեթոդներն այն նվազագույնն են, որոնք ապահովում են range-for տիպի ցիկլի աշխատանքը։
— Ուսուցի՛չ,— ասաց Ուաո Գոն,— ինչպե՞ս կարելի է գեներացնել տողի բոլոր տեղադրությունները (permutations):
Կոնֆուցիոսը մի քիչ մտածեց ու հիշեց, որ այդ մասին կարդացել է Կնուտի «Ծրագրավորման արվեսստը» գրքի չորրորդ հատորում (Donald Knuth, «The Art of Computer Programming», vol. 4A)։ Այդ գրքի 7.2.1.2 Generating all permutations բաժնում պատմվում է տեղադրությունները գեներացնելու զանազան ալգորիթմների մասին. ինչպես միշտ՝ Կնուտն իր բարձունքի վրա է։
— Չգիտեմ։ Կարդացեք դասականներին,— ասաց Կոնֆուցիոսը մի քիչ էլ մտածելուց հետո։
Հաջորդ օրը Կոնֆուցիոսը տաճար մտավ ու տեսավ Ուաո Գոին աղոթելիս։ Կանչեց նրան իր սեղանի մոտ ու ցույց տվեց տախտակների վրա գրված այս տեքստը.
(defun insert-at (e l i) (if (zerop i) (cons e l) (cons (car l) (insert-at e (cdr l) (1- i))))) (defun range (e r) (if (= 0 e) (cons 0 r) (range (1- e) (cons e r)))) (defun insert-at-all (e l) (mapcar #'(lambda (i) (insert-at e l i)) (range (length l) '()))) (defun insert-to-all-items (e ls) (apply #'append (mapcar #'(lambda (q) (insert-at-all e q)) ls))) (defun permutations-of (l) (if (null (cdr l)) (list l) (insert-to-all-items (car l) (permutations-of (cdr l)))))
— Ի՞նչ է սա, ուսուցի՛չ,— հարցրեց Ուաո Գոն։
— permutations-of-string
-ը գեներացնում է տրված տողի բոլոր տեղադրությունները։
— Ինչպե՞ս։
— Տե՛ս։ Մի որևէ հաջորդականության բոլոր տեղադրությունները ստանալու համար կարելի է առանձնացնել դրա տարրերից մեկը, օրինակ առաջինը, ապա, ռեկուրսիվ եղանակով, հաշվել մյուս տարրերի հաջորդականության բոլոր տեղադրությունները և վերջում առանձնացված տարրը «խցկել» կառուցված տեղադրությունների բոլոր հնարավոր դիրքերում՝ ամեն մի «խցկելու» գործողությամբ գեներացնելով նոր տեղադրություն։ Պա՞րզ է։
— Լավ կլիներ օրինակ բերեիք, ուսուցի՛չ։
— Լավ։ Վերցնենք {a, b, c}
։ Առաձնացնենք դրա առաջին տարրը՝ a
-ն, մնացած տարրերի հաջորդականությունը կլինի {b, c}
: Այս վերջինիս բոլոր հնարավոր տեղադրություններն են.
{(b, c), (c, b)}
Հիմա առանձնացված a
տարրը տեղադրենք այս բազմության բոլոր տարրերի բոլոր դիրքերում։ (b, c)
-ի համար կստանանք.
(a, b, c), (b, a, c), (b, c, a)
իսկ (c, b)
-ի համար էլ.
(a, c, b), (c, a, b), (c, b, a)
Ահա սրանց միավորումն էլ հենց {a, b, c}
հաջորդականության բոլոր տեղադրություններն են.
{(a, b, c), (b, a, c), (b, c, a), (a, c, b), (c, a, b), (c, b, a)}
— Ուսուցի՛չ, ո՞րն է ռեկուրսիայի տարրական դեպքը։
— Դա միայն մեկ տարր ունեցող հաջորդականությունն է։ Օրինակ, {a}
-ի բոլոր տեղադրությունների բազմությունն է. {(a)}
։
— Իսկ ի՞նչ հեզվով են գրված ձեր տախտակները, ուսուցի՛չ։
— Օ՜, դա Լիսպն է, լեզուների մեջ վեհագույնը։
— Թույլ տվեք մի անգամ էլ նայել տախտակներին,— խնդրեց Ուաո Գոն։
— Ահա՛։
— Թարգմանեք, խնդրում եմ, շատ հետաքրքիր է։
— Լավ։ Սկսենք առաջին տախտակից՝ ամենապարզից։ Ինչպես տեսար, տեղադրություններ կառուցելու տարրական գործողությունը տրված հաորդականության տրված դիրքում մի որևէ տարր խցկելն է։ Օրինակ, եթե հաջորդականությունն ունի երեք տարր՝ abc
, ապա գոյություն ունեն նոր տարրը խցկելու 4 հնարավոր դիրքեր՝ ₀a₁b₂c₃
։ insert-at
գործողությունը e
տարրը տեղադրում է l
ցուցակի i
-րդ դիրքում։
(defun insert-at (e l i) (if (zerop i) (cons e l) (cons (car l) (insert-at e (cdr l) (1- i)))))
Հաջորդ տախտակի վրա գրված insert-at-all
գործողությունը e
տարրը խցկում է l
ցուցակի բոլոր թույլատրելի դիրքերում (դրանք |l|+1
հատ են), և վերադարձնում է խցկելու յուրաքանչյուր գործողությունից «ծնված» ցուցակների ցուցակը։ range
օժանդակ ֆունկցիան պարզապես կառուցում է տարրը տեղադրելու ինդեքսների ցուցակը։
(defun range (e r) (if (= 0 e) (cons 0 r) (range (1- e) (cons e r)))) (defun insert-at-all (e l) (mapcar #'(lambda (i) (insert-at e l i)) (range (length l) '())))
Երրորդ տախտակի insert-to-all-items
գործողությունը insert-at-all
ֆունկցիան կիրառում է ls
ցուցակի բոլոր տարրերի նկատմամբ, և այդ կիրառություններից ստացված բոլոր ցուցակները միավորում է մի ընդհանուրի մեջ։
(defun insert-to-all-items (e ls) (apply #'append (mapcar #'(lambda (q) (insert-at-all e q)) ls)))
Դե, իսկ չորրորդ տախտակին գրված permutations-of
գործողությունը հենց խնդրի բուն լուծումն է՝ ռեկուրսիայի կազմակերպմամբ։ Եթե տրված l
ցուցակը միայն մի տարր ունի, ապա պատասխանը հենց այդ ցուցակը պարունակող ցուցակն է։ Ռեկուրսիայի քայլում կառուցվում է ցուցակի պոչի տեղադրությունների բազմությունը, ապա՝ insert-to-all-items
նախնական ցուցակի առաջին տարրը ավելացվում է բոլոր այդ տեղադրություններին։
(defun permutations-of (l) (if (null (cdr l)) (list l) (insert-to-all-items (car l) (permutations-of (cdr l)))))
— Իսկ ինչպե՞ս ենք կառուցելու _տողի_ բոլոր տեղադրությունների բազմությունը, ուսուցի՛չ։
— Դրա համար պետք է տողից կառուցենք նրա տառերի ցուցակը, կառուցենք այդ ցուցակի բոլոր տեղադրությունների բազմությունը, ապա ամեն մի տեղադրությունից ստանանք նոր տող։ Տո՛ւր ինձ մի մաքուր տախտակ։
Կոնֆուցիոսը վերցրեց Ուաո Գոի մեկնած տախտակն ու դրա վրա գրեց.
(defun permutations-of-string (s) (mapcar #'(lambda (e) (format nil "~(~{~C~}~)" e)) (permutations-of (coerce s 'list))))
— Հիմա, Ուաո Գո՛, գնա ու շարունակիր աղոթքդ,— ասաց Կոնֆուցիոսը։
Ուաո Գոն խոնարհվեց ուսոցչին ու խնդրեց.
— Թույլ տուր մի անգամ էլ նայեմ տախտակներին։
Կարգավորման ալգորիթմներից գեղեցկագույնի՝ QuickSort-ի մասին գրված է ծրագրավորման ալգորիթմներին վերաբերող համարյա բոլոր գրքերում։ Բայց արժե առանձնացնել հատկապես Robert Sedgewick, Kevin Wayne, «Algorithms, 4th Edition» գրքի իլյուստրացիան, որից օգտվելով էլ (ինչպես նաև Go լեզվի sort փաթեթի կոդից) կառուցել եմ ստորև բերվող իրականացումը։ Սակայն իմ նպատակը QuickSort-ի վերլուծությունը չէ. ես ուզում եմ դրա օրինակով ներկայացնել, թե ինչպես կարելի է Go լեզվով իրականացնել ընդհանրացված (generic) ալգորիթմ։
Սկսեմ պարզ դեպքից։ Ենթադրենք գրել եմ quick
փաթեթը, որի Sort
ֆունկցիան կարգավորում է ամբողջ թվերի զանգվածը.
package quick // Sort ֆունկցիան կարգավորում է ամբողջ թվերի տրված զանգվածը func Sort(arr []int) { quickSort(arr, 0, len(arr)-1) } func quickSort(arr []int, low, high int) { if low < high { m := partition(arr, low, high) quickSort(arr, low, m-1) quickSort(arr, m+1, high) } } func partition(arr []int, low, high int) int { p := low i, j := low+1, high for { for i != high && arr[i] < arr[p] { i++ } for arr[p] < arr[j] { j-- } if i >= j { break } arr[i], arr[j] = arr[j], arr[i] } arr[p], arr[j] = arr[j], arr[p] return j }
Իմ նպատակն է նույն այս իրականացումն օգտագործել int
-երից բացի այլ տիպերի համար։ Այսինքն՝ Sort
ֆունկցիան պետք է սահմանել այնպիսի պարամետրով, որ հնարավոր լինի այն կիրառել կամայական տիպի տարրերի զանգվածի նկատմամբ։ Ուշադիր նայելով []int
տիպի համար իրականացմանը, տեսնում եմ, որ զանգվածի հետ կատարվում են երեք գործողություններ. ա) ստանալ զանգվածի չափը՝ len(arr)
, բ) համեմատել զանգվածի տարրերը՝ arr[i] < arr[p]
, գ) մեկը մյուսով փոխարինել զանգվածի տարրերը՝ arr[i], arr[j] = arr[j], arr[i]
։ Սահմանեմ (quick
փաթեթում) Sortable
ինտերֆեյսը՝ այս երեք գործողություններտ ներկայացնող մեթոդներով.
package quick // Sortable ինտերֆեյսով որոշվում է «կարգավորելի» զանգվածը type Sortable interface { Size() int // զանգվածի չափը Less(i, j int) bool // a[i] < a[j] համեմատումը Swap(i, j int) // a[i] <-> a[j] փոխատեղումը }
Հիմա արդեն կարող եմ հերթով ձևափոխել Sort
, quickSort
և `partition` ֆունկցիաները։ Առաջինում պետք է փոխել պարամետրի տիպը և len(arr)
-ը փոխարինել arr.Size()
-ով։
// Sort ֆունկցիան կարգավորում է ամբողջ թվերի տրված զանգվածը func Sort(arr Sortable) { quickSort(arr, 0, arr.Size()-1) }
Պարզվում է, որ quickSort
ֆունկցիայում փոխելու բան չկա։
partition
ֆունկցիայում տարրերի համեմատությունները պետք է փոխարինել Less
մեթոդի կիրառությամբ, իսկ տարրերի փոխատեղման վերագրումները՝ Swap
մեթոդի կիրառությամբ։
func partition(arr Sortable, low, high int) int { pv := low i, j := low+1, high for { for i != high && arr.Less(i, pv) { i++ } for arr.Less(pv, j) { j-- } if i >= j { break } arr.Swap(i, j) } arr.Swap(pv, j) return j }
Պատրաստ է։ Հիմա տեսնենք, թե ինչպես է այս նոր իրականացումն օգտագործվելու։ Սկսենք արդեն աշխատող տարբերակից. ենթադրենք ուզում եմ կարգավորել int
-երի զանգված։ Պետք է իրականացնեմ Sortable
ինտերֆեյսը.
type integers []int func (a integers) Size() int { return len(a) } func (a integers) Less(i, j int) bool { return a[i] < a[j] } func (a integers) Swap(i, j) { a[i], a[j] = a[j], a[i] }
Հետո արդեն կարող եմ Sort
ֆունկցիան կիրառել `integers` զանգվածի նկատմամբ։
// ... a0 := integers{4, 1, 9, 2, 3, 8, 5, 6} quick.Sort(a0) fmt.Println(a0) // ...
Հարց։ Եթե մի որևէ ֆունկցիա վերադարձնում է[]int
զանգված, ապա դրա արդյունքի վրա ո՞նց է կիրառվելուSort
ֆունկցիան։
Ինչ-որ ժամանակ առաջ պիտի գնայինք գյուղ և Յերեվանի տանը մի քանի օր մարդ չէր լինելու։ Մտքովս անցավ մտածել մի սարքավորում, որը հնարավորություն կտա գյուղից, ինչ-որ եղանակով միացնել տան լույսերը (կամ մի այլ սարք)։ Ինտերնետում բավականին քչփորելով գտա մի քանի եղանակներ, որոնք օգտագործում էին օժանդակ ցանցային ծառայություններ։ Վերջապես, համադրելով մի քանի գաղափարներ, կառուցեցի ստորև նկարագրված սարքա-ծրագրային համակարգը։
Աշխատանքի մեխանիզմն այսպիսինն է. Յերեվանի տանը դրած համակարգիչը, օգտագործում եմ Raspberry Pi 1 Model B Rev. 2, ամեն երկու (կամ 1, կամ 5 և այլն) րոպեն մեկ ստոգում է հատուկ այդ նպատակի համար ստեղծված էլ-փոստը։ Հենց որ ստացվում է նոր նամակ՝ ստուգում նամակի վերնագիրը, որում գրված է կոնկրետ առաջադրանքը, օրինակ, «LIGHT ON» կամ «LIGHT OFF»։ Որպեսզի որևէ օտար մարդ չկարողանա համակարգչին առաջադրանք տալ (նամակ ուղարկել), ստուգվում է նաև ուղարկողի հասցեն (միամիտ ու անհուսալի պաշտպանություն է. կարելի է ու պետք է կատարելագործել)։ Եթե ամեն ինչ սպասվածի պես է, ապա համակարգչի միացված ռելեյի միջոցով, օգտագործում եմ KY-019 մոդուլը, միացվում կամ անջատվում է էլեկտրական սարքը։
Էլ֊փոստը կարդալու համար օգտագործում եմ getmail֊ը, իսկ cron֊ը օգտագործում եմ getmail֊ը երկու րոպեն մեկ աշխատեցնելու համար։ getmail֊ը տեղադրել եմ սովորական եղանակով․
$ sudo apt install -y getmail4
Տեղադրելուց հետո այն պետք է կարգավորել այնպես, որ կարդա իմ էլ֊փոստը։ Դրա համար $HOME
պանակում ստեղծում եմ .getmail
պանակը, իսկ դրա մեջ էլ getmailrc
ֆայլը։ Վերջինս էլ հենց getmail֊ի կարգավորումների ֆայլն է։ Ինձ մոտ այն հետևյալ տեքսի է․
[retriever] type = SimpleIMAPSSLRetriever server = imap.yandex.com port = 993 username = __իմ էլ֊փոստի անունը__ password = __իմ էլ֊փոստի գաղտնաբառը__ [options] read_all = false delivered_to = false received = false [destination] type = MDA_external path = ~/Projects/a5/readanddo.sh
retriever
բլոկում getmail֊ը կարգավորվում է կոնկրետ փոստարկղի համար։ Կարծում եմ, որ այդ բլոկի պարամետրերը բացատրելու կարիք չկա․ դրանց անուններն ամեն ինչ ասում են իրենց մասին։
options
բլոկի read_all = false
պարամետրը նշանակում է, որ պետք չէ ամեն անգամ սերվերից կարդալ բոլոր նամակները, այլ կարդալ միայն նորերը։ Եթե delivered_to
և received
պարամետրերը դրված են true
, ապա ստացված նամակի վերնագրին (header) ավելացվում են համապատասխանաբար «Delivered To:» և «Received:» դաշտերը (սրանց իմաստը չեմ հասկանում, պարզապես false
եմ դրել ավելորդություններից խուսափելու համար)։
Ամենակարևորն իմ աշխատանքում destination
բլոկն է։ Սրա պարամետրերով են որոշվում, թե ինչ պետք է անել փոստարկղի սերվերից ներբեռնված նամակների հետ։ Իմ դեպքում type = MDA_external
պարամետրն ասում է, որ նամակները պետք է մշակվեն արտաքին (ոչ ներդրված) MDA ― mail delivery application ծրագրով։ Ամեն անգամ, հենց որ getmail֊ը սերվերից նոր նամակ է կարդում, այն ուղղարկում է path
պարամետրով տրված ծրագր (կամ սկրիպտի) ստանդարտ ներմուծման հոսքին։
Ես գրել եմ readanddo.sh
սկրիպտը, որը ստուգում է նամակի «From:» և «Subject:» դաշտերը։ Եթե դրանցում գրված են սպավող արժեքները՝ «From:» դաշտում հրամաններ ուղարկող էլ֊փոստի հասցեն, իսկ «Subject:» դաշտում՝ կոնկրետ հրամանը, ապա Raspberry Pi֊ի GPIO֊ին ուղղարկվում է համապատասխան ազդանշանը։
#!/bin/bash operation='' commander='' while read line do if [[ ${line} =~ ^Subject: ]] then if [[ ${line} =~ DO:LIGHT:ON ]] then operation="LIGHT:ON" elif [[ ${line} =~ DO:LIGHT:OFF ]] then operation="LIGHT:OFF" fi fi if [[ ${line} =~ ^From: ]] then if [[ ${line} =~ __իմ էլ֊փոստի հասցեն__ ]] then commander=${line} fi fi done if [ -z ${commander} ] then exit 0 fi if [ -z ${operation} ] then exit 0 fi gpio -g mode 4 out if [ ${operation} = "LIGHT:ON" ] then gpio -g write 4 1 exit 0 elif [ ${operation} = "LIGHT:OFF" ] then gpio -g write 4 0 exit 0 fi
Ռելեյի KY-019 մոդուլն ունի երեք մուտքային ոտիկներ․ «+
», «-
» և «S
»։ «+
»-ը միացնում եմ Raspberry Pi-ի 2֊րդ GPIO֊ին՝ 5v, «-
»-ը միացնում եմ 6֊րդ GPIO֊ին՝ GND, իսկ «S
»֊ը, որը ղեկավարող ազդանշանն է, միացնում եմ 7-րդ GPIO֊ին (ֆիզիկական համարակալմամբ 7֊րդը BCM համարակալմամբ 4֊րդն է)։
Երբ readanddo.sh
սկրիպտը համոզվում է, որ հրամանն ուղարկվել է նախապես որոշված հասցեից, և հրամանի ֆորմատն էլ նախապես որոշվածներից մեկն է, RPi֊ի 4֊րդ GPIO֊ի (BCM համարակալմամբ) ուղղությունը դարձնում է «out
».
gpio -g mode 4 outև այդ GPIO֊ի արժեքը դնում է
0
կամ 1
.
gpio -g write 4 1 gpio -g write 4 0
Մնում է միայն սահմանել cron֊ի առաջադրանք, որը երկու րոպեն մեկ կգործարկի getmail
ծրագիրը։
Դժվար թե սա կիրառելի լինի իրական կյանքում։ Կարծում եմ, որ կան տանը մարդու ներկայության իմիտացիայի ավելի լավ միջոցներ։
// // Ֆայլի անունը. mapcar.js // // // Ֆունկցիայի անունը որոշեցի թողնել նույնը, ինչ որ // Common Lisp լեզվում է՝ mapcar։ // // mapcar ֆունկցիան սպասում է մեկ և ավելի արգումենտներ։ // Դրանցից առաջինը կիրառվող ֆունկցիան է, մյուսները՝ վեկտորներ են։ // var mapcar = function( func, ...args ) { // համոզվել, որ առաջին արգումենտը ֆունկցիա է if( 'function' !== typeof func ) { throw 'mapcar-ի առաջին արգումենտը ֆունկցիա չէ։' } // համոզվել, որ երկրորդ և հաջորդ արգումենտներում վեկտորներ են. if( !args.every(Array.isArray) ) { throw 'Ոչ բոլոր արգումենտներն են վեկտոր տիպի։' } // համոզվել, որ ֆունկցիայի պարամետրերի քանակն ու mapcar-ին // տրված արգումենտների քանակները նույնն են if( func.length != args.length ) { throw 'Ֆունկցիայի պարամետրերի քանակն ու վեկտորների քանակը տարբեր են։' } // mapcar ֆունկցիայի կիրառման արդյունքը վեկտոր է let result = [] // եթե վեկտորների երկարությունները տարբեր են, ապա mapcar—ի // արդյունքը ստացվելու է դրանցից ամենակարճի չափով const lengths = args.map((e) => e.length) const reslen = Math.min.apply(null, lengths) // ցիկլը կատարելով վեկտորներից ամենակարճի տարրերի քանակով... for( let i = 0; i < reslen; ++i ) { // վերցնել բոլոր վեկտորների i-րդ տարրերը, ... const atu = args.map((ev) => ev[i]) // ֆունկցիան կիրառել դրանց նկատմամբ, ... const ri = func.apply(null, atu) // արդյունքն ավելացնել result վեկտորում result.push(ri) } // վերադարձնել կառուցված արդյունքը return result } // տրամադրել այս ֆունկցիան արտաքին աշխարհին module.exports.mapcar = mapcar
true
է վերադարձնում միայն այն դեպքում, երբ զանգվածի բոլոր տարրերը բավարարում են տրված պրեդիկատին։var f = function(x, y, z) { ... }
, ապա f.apply(null, [1, 2, 3])
։ Հարմար է այն դեպքում, երբ կանչի արգումենտները դինամիկ են ձևավորվում։mapcar
-ն ինձ պետք էր zip
-ի նման մի ֆունկցիա իրականացնելու համար։var zip = function(x, y) { return mapcar((a, b) => [a, b], x, y) }