Showing posts with label Scheme. Show all posts
Showing posts with label Scheme. Show all posts

Saturday, December 12, 2015

Միակապ ցուցակի շրջելը ռեուրսիվ եղանակով

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

(define (reverse-it li)
    (define (reverse-it-rec l r)
        (if (null? l)
            r
            (reverse-it-rec (cdr l) (cons (car l) r))))
    (reverse-it-rec li '()))

Այստեղ reverse-it պրոցեդուրայի մարմնում սահմանված է վերջին կանչի ռեկուրսիա (tail recursive) ունեցող reverse-it-rec պրոցոդուրան, որում էլ հենց կտարվում է տրված ցուցակի շրջելը։ reverse-it-rec֊ն ուն երկու պարամետր՝ ցուցակի չշրջված մասը և արդեն շրջված մասը։ Պարզ է, որ reverse-it֊ում նրան կանչելիս առաջին արգումենտը պետք է լինի շրջվելիք ցուցակը, իսկ երկրորդը՝ դարարկ ցուցակ։ Եթե l֊ը դատարկ է, ապա համարվում է, որ r-ը արդեն շրջված ցուցակն է, և այն վերադարձվում է որպես արդյունք։ Հակառակ դեպքում l֊ի առաջին տարրը կցվում է r-ի սկզբից, և reverse-it-rec ի ռեկուրսիվ կանչը կիրառվում է l֊ի պոչի և այդ նոր r֊ի նկատմամբ։

* * *

Բայց Լիլիթն ուզում էր, որ ես սա գրեմ C++ լեզվով, որից ես շատ քիչ բան եմ հասկանում, և այդ պատճառով էլ որոշեցի գրել C լեզվով։ Սակայն այս դեպքում իրավիճակը բոլորովին այլ է․ C լեզվում չկան ո՛չ ներդրված ցուցակը, ո՛չ էլ դրա հետ աշխատող ֆունկցիաները։ Ես պետք է սկսեմ սկզբից՝ սահմանելով նախ՝ ցուցակը, ապա՝ այն շրջող ֆունկցիան։

Եվ այսպես, սահմանում եմ միակապ ցուցակի մեկ հանգույցը ներկայացնող node ստրուկտուրան։ Այն ունի երկու երկու դաշտ՝ մեկը ինֆորմացիայի համար, մյուսը՝ հաջորդ հանգույցին կապելու։ Պարզության համար ինֆորմացիայի տիպն ընտրել եմ double։

strcut node {
    double data;
    struct node* next;
};

Հանգույցներ կառուցելու համար ինձ պետ է նաև create_node ֆունկցիան, այն ստանում է double թիվ և վերադարձնում է այդ թիվը պարունակող նոր ստեղծված հանգույցի ցուցիչը։

struct node* create_node( double d )
{
    struct node res = malloc(sizeof(struct node));
    res->data = d; res->next = NULL;
    return res;
}

Աշխատանիքի միջանկյալ ու վերջնական արդյունքները տեսնելու համար պետք է գալու նաև ցուցակն արտածող print_list ֆունկցիան։ Դա էլ սահմանեմ․

void print_list_rec( struct node* list )
{
    if( list == NULL ) return;
    printf("%lf ", list->data);
    print_list_rec( list->next );
}

void print_list( struct node* list )
{
    printf("{ ");
    print_list_rec( list );
    printf("}\n");
}

Հիմա ամենահետաքրքիր պահն է։ Ես հանմանում եմ reverse_it և reverse_it_rec ֆունկցիաները՝ փորձելով վերարտադրել վերը բերված Scheme պրոցեդուրայի վարքը։

struct node* reverse_it_rec( struct node* l, struct node* r )
{
    if( l == NULL )  /* երբ ցուցակը դատարկ է */
        return r;    /* արդյունքը կապված է r ցուցիչին */

    struct node* h = l;  /* h֊ը ցուցակի գլուխն է */
    l = l->next; ․       /* l֊ը հիմա ցուցակի պոչն է, դեռ չշրջված */
    հ->next = r;         /* սկզբնական l֊ի առաջին տարրը կապել r֊ին */
    return reverse_it_rec( l, h );
}

Դե իսկ reverse_it ֆունկցաին պարզապես կանչելու է reverse_it_rec֊ը՝ առաջին արգումենտում տալով շրջվելիք ցուցակը, իսկ երկրորդում՝ NULL։

struct node* revers_it( struct node* list )
{
    return reverse_it_rec( list, NULL );
}

Այսքանը։ Հիմա կարող եմ վերը ներկայացված կոդը գրել ֆայլի մեջ, կցել stdio.h և stdlib.h ֆայլերը, օրինակ պատրաստել main ֆունկցիայում և տեսնել, թե ինչպես է աշխատում իմ գրած ֆունկցիան։

Օրինակը շատ պարզ է․ կառուցում եմ ցուցակի հինգ հանգույցներ՝ օգտագործելով create_node ֆունկցիան, ապա դրանք իրար եմ կապում next ցուցիչի օգնությամբ։

struct node* n0 = create_node(1);
struct node* n1 = create_node(2); n0->next = n1;
struct node* n2 = create_node(3); n1->next = n2;
struct node* n3 = create_node(4); n2->next = n3;
struct node* n4 = create_node(5); n3->next = n4;

Ցուցակը տպում եմ, որպեսզի տեսնեմ տարրերի սկզբնական հաջորդականությունը։ Այնուհետև այն շրջում եմ reverse_it ֆուկցիայի օգնությամբ, և նորից տպում եմ ստացված ցուցակը։

print_list(n0);
struct node* rl = reverse_list(n0);
print_list(rl);

Ահա արդյունքը․

{ 1.000000 2.000000 3.000000 4.000000 5.000000 }
{ 5.000000 4.000000 3.000000 2.000000 1.000000 }

Տեսնելու համար, թե ինչ տեսք ունեն l և r ցուցակները ռեկուրսիայի ամեն մի կանչի ժամանակ, կարելի է reverse_it_rec ֆունկցիայի սկզբում ավելացնել print_list(l) և print_list(r) արտահայտությունները։

Monday, March 31, 2014

Java և Kawa: Տեքստի տրոհումը Scanner դասի օգնությամբ

Շարունակելով բլոգիս «Tcl: Բառարանների օգտագործումը», «Python: Բառարանի օգտագործումը» և «C++11: Տեքստի տրոհումը բառերի՝ istream-ի միջոցով» գրառումների թեման, ես ուզում էի այս գրառմանս մեջ պատմել Java լեզվի միջոցներով տեքստը բառերի տրոհելու և բառերի հաճախությունը հաշվելու մասին։ Ինձ գայթակղեց Java-ի Scvanner դասը, որը կարելի է կանոնավոր արտահայտությունների միջոցով կարգավորել տեքստից բառեր կարդալու համար։ Բայց այս գրառման մեջ ուզում եմ նաև Java-ի HashMap բառարանի օգտագործումը համեմատ ել JVM վիրտուալ մեքենայով աշխատող GNU Kawa լեզվի (Scheme լեզվի իրականացում) hashtable բառարանի օգտագործման հետ։

Նախ ներկայացնեմ Java տարբերակը (օգտագործված է JDK 8-ը)։
package wordcount;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Scanner;

/**/
public class WordCount {
  /**/
  private static void readFile( String name, HashMap<String,Integer> words )
  {
    // տրված անունով ֆայլի համար ստեղծել Scanner օբյեկտ
    try( Scanner scan = new Scanner(new File(name)) ) {
      // բացի մեծատառերից ու փոքրատառերից ամեն ինչ համարել բաժանիչ
      scan.useDelimiter( "[^A-Za-z]+" );
      // քանի դեռ Scanner-ից կարելի է կարդալ
      while( scan.hasNext() ) {
        // կարդալ հերթական բառը
        String wo = scan.next().toLowerCase();
        // գտնել կարդացած բառի արտապատկերումը բառարանում
        Integer co = words.get(wo);
        // եթե այդ բառը բառարանում չկա, ավելացնել այն՝ 1 քանակով, 
        // իսկ եթե կա՝ քանակն ավելացնել 1-ով
        words.put(wo, co == null ? 1 : co + 1);
      }
    }
    catch( FileNotFoundException ex ) {
      System.err.println(ex.getMessage());
    }
  }
    
  /**/
  public static void main(String[] args) 
  {
    // ստեղծել String->Integer արտապատկերում
    HashMap<String,Integer> words = new HashMap<>();
    // կարդալ ֆայլի պարունակությունը words բառարանի մեջ
    readFile("~/Projects/martineden.txt", words);
    // բառ-քանակ զույգերն արտածել ստանդարտ արտածման հոսքին
    words.forEach( (k,v) -> System.out.printf("%s\t%s\n", k, v) );
  }
}
* * *
Kawa լեզուն JVM վիրտուալ մեքենայի համար գրված բազմաթիվ լեզուներից մեկն է։ Բայց ինձ համար հետաքրքիր է նրանով, որ այն Lisp լեզվի Scheme դիալեկտի իրականացում է Java լեզվով։ Ամբողջովին գրված լինելով Java լեզվով՝ այն ա) պլատֆորմից անկախ է, և բ) հնարավորություն ունի օգտագործել Java լեզվի ստանդարտ գրադարանը՝ այն ամենը, ինչ մատչելի է JVM կատարման միջավայրում։

Հիմա ցույց տամ, թե ինչպես եմ Kawa լեզվի միջոցներով կարդում ֆայլի բառերի հաջորդականությունը և կազմում դրանց հաճախությունների բառարանը։ Նախ սահմանեմ read-all-words ֆունկցիան, որն արգումենտում ստանում է Java լեզվի Scanner օբյեկտը և վերագարձնում է տեքստի բառերի հաճախությունների բառարանը՝ որպես Scheme լեզվի hashtable օբյեկտ։
(define (read-all-words sca :: Scanner)
  (define (read-all-words-rec ht)
    (when (invoke sca 'hasNext)
      (add-to-table (string-downcase (invoke sca 'next)) ht)
      (read-all-words-rec ht)))
  (let ((words (make-hashtable string-hash string=?)))
    (read-all-words-rec words)
    words))
read-all-words ֆունկցիայի համար սահմանված է read-all-words-rec լոկալ ռեկուրսիվ ֆունկցիան, որը Scanner օբյեկտից կարդում է մեկ բառ, այդ բառի բոլոր մեծատառերը դարձնում է փոքրատառ՝ string-downcase ֆունկցիայով, ապա ավելացնում է արգումենտում տրված բառարանում։ Բառարանը ստեղծվում է որպես read-all-words ֆունկցիայի լոկալ օբյեկտ՝ make-hashtable ֆունկցիայով։ Բառը բառարանում ավելացնող add-to-table ֆունկցիան սահմանված է հետևյալ կերպ․
(define (add-to-table w table)
  (let ((co (hashtable-ref table w 0)))
    (hashtable-set! table w (+ co 1))))
read-file ֆունկցիան, որ կսահմանեմ ստորև, տրված ֆայլի անունի համար ստեղծում է մի Scanner օբյեկտ և "[^A-Za-z]+" արտահայտությունը սահմանում է որպես դրա բաժանիչ։ Հետո read-all-words ֆունկցիայով ստանում է բառերի հաճախությունների բառարանը, այդ բառարանից կազմում է կետով զույգերի (dotted pair) ցուցակ, և վերադարձնում է այդ վերջին ցուցակն՝ ըստ բառերի այբբենական կարգի կարգավորած։
(define (read-file name :: )
  (let ((sca (Scanner:new (File:new name))))
    (invoke sca 'useDelimiter "[^A-Za-z]+")
    (let-values (((ks vs) (hashtable-entries (read-all-words sca))))
      (invoke sca 'close)
      (list-sort (lambda (a b) (stringlist ks) (vector->list vs))))))
* * *
Ինձ դուր է գալիս Lisp լեզվով ծրագրավորումը։ Բայց ինձ դուր է գալիս նաև Java լեզվի գրադարանները։ Kawa իրականացումը հնարավորություն է տալիս մեկտեղել երկու դուրեկան բան :)

Friday, September 20, 2013

GNU/bytecode գրադարանի օգտագործման օրինակ

Ծրագրավորման լեզուների կոմպիլյացիայի հերցերն ուսումնասիրելիս ես տևական ժամանակ փնտրում էի մի գրադարան, որը հարավորություն կտար առանց մանրամասնությունների մեջ մտնելու գեներացնել Java վիրտուալ մեքենայի class ֆայլեր։ Առաջին որոնումները բերեցին ASM և BCEL գրադարաններին և Jasmin «Java ասեմբլերին»։ Վերջինս հնարավորություն է տալիս մնեմենիկ հրամաններով հրագրավորել ալգորիթմը, ապա այն «ասեմբլացնել» ու ստանալ վիրտուալ մեքենայի բայթ-կոդ՝ class ֆայլ։

Բայց, տարբեր պատճառներով, այդ գրադարաններից ոչ մեկը ես հարմար չգտա սովորելու համար։ Հետագա որոնումների ընթացքում հանդիպեցի GNU/bytecode գրադարանին, որ Scheme ծրագրավորման լեզվի Kawa իրականացման մաս է կազմում։

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

Նախ սկսեմ Java տարբերակից.

public class Algorithms {
  public static int factorial( int n )
  {
    if( n == 1 ) return 1;
    return n * factorial( n - 1 );
  }

  public static int gcd( int n, int m )
  {
    while( n != m )
      if( n > m )
        n -= m;
      else
        m -= n;
    return n;
  }
}

Այս դասի նկարագրությունը Algorithms.java ֆայլի մեջ գրառելուց և javac կոմպիլյատորով թարգմանելուց հետո ստացվում է Algorithms.class ֆայլը։ class ֆայլը կարելի է javap դիզասեմբլերով հետ թարգմանել ու տեսնել թե Java լեզվի կոմպիլյատորն ինչ հրամաններ է գեներացրել։ Այդ բոլոր հրամանների նկարագրությունը կարելի է գտնել Java վիրտուալ մեքենայի նկարագրության մեջ։

Compiled from "Algorithms.java"
public class Algorithms {
  public Algorithms();
    Code:
       0: aload_0
       1: invokespecial #1    // Method java/lang/Object."<init>":()V
       4: return 

  public static int factorial(int);
    Code:
       0: iload_0
       1: iconst_1 
       2: if_icmpne     7
       5: iconst_1 
       6: ireturn 
       7: iload_0 
       8: iload_0 
       9: iconst_1 
      10: isub 
      11: invokestatic  #2    // Method factorial:(I)I
      14: imul 
      15: ireturn 

  public static int gcd(int, int);
    Code:
       0: iload_0
       1: iload_1 
       2: if_icmpeq     24
       5: iload_0 
       6: iload_1 
       7: if_icmple     17
      10: iload_0 
      11: iload_1 
      12: isub 
      13: istore_0 
      14: goto          0
      17: iload_1 
      18: iload_0 
      19: isub 
      20: istore_1 
      21: goto          0
      24: iload_0 
      25: ireturn 
}

Այս արտածումը տրված է JVM-ի հրամաններ մնեմոնիկ ներկայացմամբ, որոնց բացատրությունները բերված են JVM Specification էջում։

 

Հիմա ցույց տամ, թե ինչպես եմ gnu.bytecode գրադարանի օգտագործմամբ ստեղծում factorial և gcd ալգորիթմները պարունակող class ֆայլը։ Արդեն նշեցի, որ gnu.bytecode գրադարանը Kawa լեզվի իրականացման մաս է։ Չնայած որ կարելի է կոդից առանձնացնել միայն gnu.bytecode-ն և կառուցել առանձին *.jar ֆայլ, ես կօգտագործեմ հենց kawa-1.13.jar ֆայլը։

ՈՒրեմն, նախ ներմուծում եմ gnu.bytecode գրադարանը․ import gnu.bytecode.*;

Հետո սահմանում եմ AlgoEx դասն իր main մեթոդով․

public class AlgoEx {
    public static void main(String[] args) throws Exception
    {

Հետո ստեղծում եմ ClassType դասի օբյեկտ, որը ներկայացնում է Java վիրտուալ մեքենայի դասը։ Այնուհետև դասի համար որպես ծնող նշում եմ java.lang.Object դասը, իսկ որպես տեսանելիության մոդիֆիկատոր նշում եմ public - Access.PUBLIC հոստատունի օգնությամբ։

        // class `Algorithms'
        ClassType clo = new ClassType("Algorithms");
        clo.setSuper("java.lang.Object");
        clo.setModifiers(Access.PUBLIC);

Քանի որ և՛ factroial, և՛ gcd մեդոդները սահմանելու եմ public ու static մոդիֆիկատորներով, այստեղ սահմանել եմ pubstat հաստատունը․

        final int pubstat = Access.PUBLIC | Access.STATIC;

Հետո clo դասում ավելացնում եմ նոր մեթոդ՝ "factorial" անունով և "(I)I" սիգնատուրայով։ addMethod մեթոդի վերադարձրած հղումև վերագրում եմ Method տիպի mfac փոփոխականին։

        // method `factorial'
        Method mfac = clo.addMethod("factorial", "(I)I", pubstat);

CodeAttr դասը նախատեսված է բուն կոդի գեներացիայի համար։ Ստորև բերված բլոկում կառուցված է ամբողջ թվի ֆակտորիալը հաշվող ֆունկցիայի կոդի գեներացիան․

        CodeAttr code = mfac.startCode();
        code.pushScope();
        code.emitLoad(code.getArg(0));
        code.emitPushInt(1);
        code.emitIfEq();
        code.emitPushInt(1);
        code.emitReturn();
        code.emitElse();
        code.emitLoad(code.getArg(0));
        code.emitDup();
        code.emitPushInt(1);
        code.emitSub(Type.intType);
        code.emitInvoke(mfac);
        code.emitMul();
        code.emitReturn();
        code.emitFi();
        code.popScope();

Նույն կերպ կառուցված է gcd մեթոդի կոդի գեներացիան։

        // method `gcd'
        Method mgcd = clo.addMethod("gcd", "(II)I", pubstat);
        Label bw = new Label(code);
        Label ew = new Label(code);
        code = mgcd.startCode();
        code.pushScope();
        bw.define(code);
        Variable n = code.getArg(0);
        Variable m = code.getArg(1);
        code.emitLoad(n);
        code.emitLoad(m);
        code.emitGotoIfEq(ew);
        code.emitLoad(n);
        code.emitLoad(m);
        code.emitIfGt();
        code.emitLoad(n);
        code.emitLoad(m);
        code.emitSub(Type.intType);
        code.emitStore(n);
        code.emitElse();
        code.emitLoad(m);
        code.emitLoad(n);
        code.emitSub(Type.intType);
        code.emitStore(m);
        code.emitFi();
        code.emitGoto(bw);
        ew.define(code);
        code.emitLoad(n);
        code.emitReturn();
        code.popScope();
 

Եվ վերջում writeToFile մեթոդով կառուցված դասի պարունակությունը գրվում է class ֆայլի մեջ։

        // write class file
        clo.writeToFile();
    }
}

Հիմա թարգմանեմ այս ֆայլը Java կոմպիլյատորով և գործարկեմ այն, որպեսզի ստանամ Algorithms.class ֆայլը։

$ javac -classpath .:kawa-1.13.jar AlgoEx.java 
$ java -classpath .:kawa-1.13.jar AlgoEx

Այս երկու հրամանների կատարումից հետո գեներացվում է Algorithms.class ֆայլը։ Կարելի է javap դիզասեմբլերով տեսնել այս ֆայլի պարունակությունը։ Բայց ես կգրեմ մի տեստային ֆայլ, որը կօգտագործի Algwrithms դասի factorial և gcd ֆունկցիաները։

public class Test {
    public static void main(String[] args)
    {
        System.out.println(Algorithms.factorial(10));
        System.out.println(Algorithms.gcd(123,31));
    }
}

Այս դասի թարգմանությունն ու կատարումը ցույց է տալիս, որ gnu.bytecode գրադարանի օգնությամբ կառուցված class ֆայլը ճիշտ է․

$ javac Test.java
$ java Test
3628800
1

Monday, June 24, 2013

Scheme պրոցեդուրաները

Scheme ծրագրավորման լեզվում պրոցեդուրաները սահմանվում են lambda ծառայողական բառով, որին հետևում են արգումենտների ցուցակը և պրոցեդուրայի մարմինը կազմող արտահայտությունները։ Օրինակ, կարող եմ սահմանել տրված երկու թվերի քառակուսիների գումարը հաշվող պրոցեդուրա․

(lambda (x y) (+ (* x x) (* y y)))

Սա x և y ֆորմալ արգումենտներով պրոցեդուրա է։ Եթե ուզենամ այն կիրառել 3 կամ 4 արգումենտների նկատմամբ, ապա պետք է գրեմ․

((lambda (x y) (+ (* x x) (* y y))) 3 4)  ; => 25

Այս անհարմար գրառումից խուսափելու համար կարող եմ պրոցեդուրային անուն տալ՝ define ծառայողական բառի օգնությամբ այն կապելով որևէ սիմվոլի հետ.

(define sum-squares
    (lambda (x y) (+ (* x x) (* y y))))

Եվ արդեն կարող եմ պրոցեդուրան արգումենտների նկատմամբ կիրառել sum-squares անունով։

(sum-squares 3 4)  ; => 25

Պրոցեդուրայի սահմանումն ավելի համառոտ կարելի է գրել առանց lambda ծառայողական բառի օգտագործման՝ define սահմանման առաջին արգումենտը դարձնելով ցուցակ, որի առաջին տարրը սահմանվող պրոցեդուրայի անունն է, իսկ հաջորդողները՝ ֆորմալ արգումենտները։ Օրինակ,

(define (sum-squares x y)
    (+ (* x x) (* y y)))

Ես նախնտրում եմ lambda-ի օգտատգործմամբ տարբերակը։ Կարծում եմ, որ դա ավելի համահունչ է define բառի հետ։

Հիմա, ենթադրենք, պետք է սահմանել պրոցեդուրա, որը իրար է գումարում տրված թվերը։ Առաջին միտքն այն է, որ սհամանել մեկ արգումենտով պրեցեդուրա և նրան փոխանցել թվերի ցուցակը։ Օրինակ այսպես.

(define sum-list
    (lambda (l)
        (if (null? l)
            0
            (+ (car l) (sum-list (cdr l))))))

Սա ռեկուրսիվ պրոցեդուրա է, որ 0 է վերադարձնում դատարկ ցուցակի դեպքում, իսկ ոչդատարկ ցուցակների համար ցուցակի առաջին տարրը գումարում է պոչի տարրերի գումարին։ Այս պրոցեդուրան պետք է կիրառել հետևյալ կերպ.

(sum-list '())         ; => 0
(sum-list '(1))        ; => 1
(sum-list '(1 2 3 4))  ; => 10

Բայց ավելի բնական կլիներ, եթե ես կարողանայի գրել այսպիսի արտահայտություններ.

(sum)          ; => 0
(sum 1)        ; => 1
(sum 1 2 3 4)  ; => 10

Այլ կերպ ասած՝ ունենալ պրոցեդուրա, որի արգումենտների քանակը ֆիքսված չէ. դրանք կարող են կա՛մ բացակայել, կա՛մ լինել անորոշ քանակի։

Երբ lambda կառուցվածքի առաջին արգումենտը տրված է որպես ատոմ (այլ ոչ թե որպես ցուցակ), ապա պրոցեդուրայի արգումենտը համարվում է անորոշ քանակի։ Օրինակ, հիշատակված sum պրոցեդուրան, օգտագործելով sum-list պրոցեդուրան, կարելի է սահմանել հետևյալ կերպ.

(define sum
    (lambda nums
        (sum-list nums)))

Երբ այս կերպ սահմանված պրոցեդուրան կիրառվում է արգումենտների նկատմամբ, այդ արգումենտներից կազմվում է ցուցակ և փոխանցվում է պրոցեդուրային։ Բնականաբար, պրոցեդուրայի մարմնում պետք է ֆորմալ արգումետի հետ աշխատել որպես ֆունկցիա։ Օրինակ, եթե sum պրոցեդուրան սահմանելու լինեի առանց sum-list պրոցեդուրայի օգտագործման, ապա պետք է գրեի.

(define sum
    (lambda nums
        (if (null? nums)
            0
            (+ (car nums) (apply sum (cdr nums))))))

apply հրամանն օգտագործված է այն պատճառով, որ sum պրոցեդուրայի ռեկուրսիվ կանչի ժամանակ (cdr nums) ցուցակը նրան փոխանցվելու է որպես նոր ցուցակի միակ տարր։

Wednesday, January 30, 2013

Scheme: Չփոփոխվող բինար ծառերի մասին

Շարունակելով իմ նախորդ գրառման բինար որոնման ծառերի թեման, ուզում եմ նույն այդ օրինակով ցույց տալ, թե ինչպես կարելի է ծրագրեր գրել օգտագործելով միայն չփոփոխվող (immutable) տվյալների կառուցվածքներ։ Այս անգամ բինար որոնման ծառերի վարքը ծրագրավորել եմ Scheme լեզվով (այն Lisp ընտանիքի թերևս ամենահայտնի ներկայացուցիչն է)։ Ծառը ներկայացված է ցուցակի տեսքով, որի առաջին տարրը արմատի արժեքն է, երկրորդը՝ ձախ ենթածառն է, իսկ երրորդը՝ աջ ենթածառը։ Տերևներն իրենց աջ ու ձախ ենթածառերի փոխարեն պարունակում են #f արժեքը։ Օրինակ, 23 արժեքով տերևը ներկայանում է (23 #f #f) ցուցակով։ Իսկ ստորև բերված նկարում պատկերված ծառը․
        10
       /  \
      6    20
     / \    \
    4   8    28
            /  \
          23    30
            \
             26
ցուցակային ներկայացմամբ կունենա ահա այսպիսի տեսք․
(10 (6 (4 #f #f) (8 #f #f)) (20 #f (28 (23 #f (26 #f #f)) (30 #f #f))))
(Իհարկե կարելի է տերևները ներկայացնել կա՛մ ատոմներով, կա՛մ մեկ տարր պարունակող ցուցակներով։ Բայց ես ընտրել եմ այս ներկայացումը։)

Նախ ծառում արժեք ավելացնելու օրինակով ցույց տամ փոփոխվող (mutable) և չփոփոխվող (immutable) ծրագրավորելու տարբերությունները։ Ենթադրենք C լեզվով որպես բինար որոնման ծառի հանգույց սահմանված է _node ստրուկտուրան, իր _new_node կոնստրուկտորով.
typedef struct _node {
  int data;
  struct _node* left;
  struct _node* right;
} node;
node* _new_node( int value, node* l, node* r )
{
  node* result = (node*)malloc(sizeof(node));
  result->data = value;
  result->left = l;
  result->right = r;
  return result;
}
Այս տիպի հանգույցներով ծառում արժեք ավելացնելու համար սահմանված է _add_value ռեկուրսիվ ֆունկցիան, որն արգումենտում ստանում է ծառի արմատի ցուցիչի ցուցիչը և ավելացվող արժեքը:
void _add_value( node** tree, int value )
{
  if( *tree == NULL )
    *tree = _new_node( value, NULL, NULL );
  if( value < (*tree)->data )
    _add_value( &((*tree)->left), value );
  if( value > (*tree)->data )
    _add_value( &((*tree)->right), value );
}
Այս ֆունկցիայի համար ծառը փոփոխվող օբյեկտ է։ Այն դեպքում, երբ *tree ցուցիչի արժեքը NULL է, ստեղծվում է նոր հանգույց, և այդ նոր հանգույցի ցուցչիը վերագրվում է *tree ցուցիչին։ Քանի որ tree փոփոխականը ֆունկցիայի արգումենտում հայտարարված է struct _node** tree տեսքով, ապա փոփոխությունը կատարվում է հենց ծառի մեջ։

Չփոփոխվող տվյալներով տարբերակում նոր ստեղծված հանգույցն ավելացվում է ոչ թե ծառի մեջ, այլ տրոհվում է ծառը, ավելացվում է հանգույցը պետք եղած տեղում, ապա ստեղծվում է նոր ծառ։
node* _add_value_i( node* tree, int value )
{
  // եթե ծառը դատարկ է, ապա ստեղծել ու վերադարձնել նոր հանգույց
  if( tree == NULL )
    return _new_node( value, NULL, NULL );

  // տրոհել ծառը արմատի արժեքի, ձախ ու աջ ենթածառերի
  int d = tree->data;
  node* l = tree->left;
  node* r = tree->right;
  // ազատել հանգույցի զբաղեցրած հիշողությունը
  free(tree);
  
  // եթե տրված արժեքը փոքր է արմատի արժեքից, ապա 
  // ապա այն ավելացնել ձախ ենթածառում
  if( value < d )
    l = _add_value_i( l, value );
  // եթե տրված արժեքը մեծ է արմատի արժեքից, ապա 
  // ապա այն ավելացնել աջ ենթածառում
  else if( value > d )
    r = _add_value_i( r, value );
  // ստեղծել նոր արմատ ու վերադարձնել նրա ցուցիչը
  return _new_node( d, l, r );
}
(Պարզ է, որ եթե տրված արժեքը հավասար է արմատի արժեքին, ապա հանգույցը քանդելու կարիք չկա, այլ պետք է այն նույնությամբ վերադարձնել։)

Ծառում արժեք ավելացնող add-value պրոցեդուրայի սահմանումը Scheme լեզվով բերված է ստորև։ Այն համարյա նույնությամբ կրկնում է C լեզվով գրված տարբերակը։
(define add-value
  (lambda (tree val)
    (if tree
        (let ([d (car tree)] [l (cadr tree)] [r (caddr tree)])
          (cond
             [(< val d) (list d (add-value l val) r)]
             [(> val d) (list d l (add-value r val))]
             [else (list d l r)]))
        (list val #f #f))))
Քանի որ Scheme լեզվում ներդրված է աղբի հավաքման մեխանիզմը, այս պրոցեդուրայում կարիք չկա C լեզվի free() ֆունկցիային համարժեք գործողություններ կատարելու։
Ֆունկցիաների սահմանման համար Scheme լեզվում նախաատեսված է նաև (define (function-mane arguments) ...) գրելաձևը։ Բայց ինձ ավելի է դուր գալիս lambda արտահայտությամբ սահմանված (define function-name (lambda (arguments) ...) տարբերակը։ Հաջորդ բոլոր ֆունկցիաները սահմանելիս նույնպես օգտագործել եմ այս վերջին գրելաձևը։
Նաև սահմանեմ մի պրոցեդուրա, որը ծառի մեջ ավելացնում է ոչ միայն մեկ տրված արժեքը, այլ արժեքների ցուցակը։ Սա հնարավորություն կտա հեշտ ու ակնառու եղանակով կառուցել մեծ ծառեր։
(define add-to-tree
  (lambda (tree values)
    (if (empty? values)
        tree
        (add-to-tree (add-value tree (car values)) 
                     (cdr values)))))
Օրինակ, վերևի նկարում պատկերված ծառը կարելի է սահմանել (և սահմանածում համոզվելու համար արտածել) հետևյալ արտահայտություններով.
(define *tr* (add-to-tree #f '(10 6 20 4 8 28 23 30 26)))
(displayln *tr*)
Ծառից որևէ տրված արժեքը պարունակող հանգույցը հեռացնելու համար նույնպես կիրառված է նույն մոտեցումը՝ տրոհվում է ծառը, ապա հետ է հավաքվում, բայց արդեն առանց հեռացվող արժեքի։ Այն դեպքում, երբ ծառը դատարկ չէ, արմատը տրոհվում է d, l և r բաղադրիչների։ Երբ որոնվող արժեքը հավասար է d-ին, դիտարկվում են հետևյալ դեպքերը.
  1. Եթե \(r=\varnothing \;\wedge\; l=\varnothing\), ապա վերադարձվում է #f։
  2. Եթե \(r=\varnothing \;\vee\; l=\varnothing\), ապա վերադարձվում է համապատասխանաբար ձախ կամ աջ ենթածառը։
  3. Եթե \(r\ne\varnothing \;\wedge\; l\ne\varnothing\), ապա հեռացվում է աջ ենթածառի ամենափոքր արժեքը պարունակող հանգույցը, իսկ նրա արժեքը գրվում է արմատի արժեքի փոխարեն։
(define remove-value
  (lambda (tree val)
    (if tree
      (let ([d (car tree)] [l (cadr tree)] [r (caddr tree)])
        (cond
          [(< val d)
           (list d (remove-value l val) r)]
          [(> val d)
           (list d l (remove-value r val))]
          [else
           (cond
             [(and (not l) (not r)) #f]
             [(and l (not r)) l]
             [(and (not l) r) r]
             [else
              (let* ([h (minimum-value r)]
                     [t (remove-value r h)])
                (list h l t))])]))
      #f)))
minimum-value պրոցեդուրան, որ օգտագործված է remove-value պրոցեդուրայում, վերադարձնում է իրեն տրված ծառի ամենափոքր արժեքը, կամ, այլ կերպ ասած, ծառի ամենաձախ հանգույցի արժեքը։
(define minimum-value 
  (lambda (tree)
    (let ([l (cadr tree)])
      (if (not l)
          (car tree)
          (minimum-value l)))))

* * *
ՈՒ, պարզապես հետաքրքրության համար սահմանեմ նաև ծառի հետ կատարվող մի քանի գործողություններ։ Առաջինը թող լինի մի պրոցեդուրա, որ պարզում է տրված արժեքի առկայությունը ծառում։
(define contains-value 
  (lambda (tree val)
    (if tree
        (let ([d (car tree)])
          (cond 
            [(= val d) #t]
            [(< val d) (contains-value (cadr tree) val)]
            [(> val d) (contains-value (caddr tree) val)]))
        #f)))
Իսկ inorder-traverse պրոցեդուրան կատարում է ծառի ձախ-արմատ-աջ անցում՝ արմատի արժեքի վրա կիրառելով տրված պրոցեդուրան։
(define inorder-traverse 
  (lambda (tree func)
    (when tree
      (inorder-traverse (cadr tree) func)
      (func (car tree))
      (inorder-traverse (caddr tree) func))))
inorder-modify պրոցեդուրան տրված ծառից կառուցում ու վերադարձնում է մի նոր ծառ, որի հանգույցների արժեքները ձևափոխվել են ըստ տրված գործողության։
(define inorder-modify
  (lambda (tree func)
    (if tree
        (let* ([l (inorder-modify (cadr tree) func)]
               [d (func (car tree))]
               [r (inorder-modify (caddr tree) func)])
          (list d l r))
        #f)))
Այս պրոցեդուրայի աշխատանքի արդյունքը ցուցադրելու համար, օրինակ, տրված ծառից կառուցենք և արտածենք մի նոր ծառ, որի հանգույցների \(x\) արքժեքները փոխարինված են \((x\; \sqrt{x})\) տեսքի զույգերով։
(define *tr* (add-to-tree #f '(10 6 20 4 8 28 23 30 26)))
(displayln *tr*)
(displayln (inorder-modify *tr* (lambda (e) (list e (sqrt e)))))
Ահա նաև արդյունքը.
(10 (6 (4 #f #f) (8 #f #f)) (20 #f (28 (23 #f (26 #f #f)) (30 #f #f))))
((10 3.1622776601683795) ((6 2.449489742783178) ((4 2) #f #f) ((8 2.8284271247461903) #f #f)) ((20 4.47213595499958) #f ((28 5.291502622129181) ((23 4.795831523312719) #f ((26 5.0990195135927845) #f #f)) ((30 5.477225575051661) #f #f))))