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, March 21, 2014

Java: Անանուն ֆունկցիաները Java 8 լեզվում

Պատահաբար հանդիպեցի Java 8 լեզվում անանուն ֆունկցիաների իրականացման մասին Oracle ֆիրմայի մի հաղորդագրության։ Այդտեղ օրինակներ էր բերված, թե ինչ նպատակների համար են նախատեսված Java 8 լեզվի լյամբդա ֆունկցիաները, և թե ինչպես կարելի է օգտագործել դրանք։
     Իմ բլոգի «Բարձր կարգի ֆունկցիաներ և անանուն ֆունկցիաներ» գրառման մեջ ես ֆունկցիայի ինտեգրալի թվային հաշվման օրինակով համեմատել էի Common Lisp և C++11 լեզվի անանուն ֆունկցիաները։ Այս գրառման մեջ ուզում եմ նույն այդ օրինակը ցույց տալ Java 8 լեզվի անանուն ֆունկցիաների օգտագործմամբ։

Մաթեմատիկական ֆունկցիան, որ կարելի է ինտեգրել, իրականացնում է MathFunc ինտերֆեյսը։ Այն սահմանված է MathFunc.java ֆայլում։
package integral;

public interface MathFunc {
 public double apply( double x );
}
Թվային ինտեգրման մեթոդն իրականացնում է Method ինտերֆեյսը, որը սահմանված է Method.java ֆայլում՝ հետևյալ կերպ։
package integral;

public interface Method {
 public double apply( MathFunc f, double a, double b );
}
Սահմանեմ Integral դասը և սահմանեմ այդ դասի evaluate մեթոդը, որն արգումենտում սպասում է ինտեգրման մեթոդը, ինտեգրվող ֆունկցիան, ինտեգրման միջակայքը և միջակայքի՝ հատվածների տրոհման գործակիցը։ Հաշվելու եղանակը պարզագույնն է. meth օբյեկտի apply մեթոդին են փոխանցվում ինտեգրվող ֆունկցիան և այն փոքր հատվածը, որի վրա նշված թվային մեթոդով հաշվվում է ինտեգրալը։
package integral;

public class Integral {
    public static double evaluate(Method meth, MathFunc func, 
                                double left, double right, double epsilon)
    {
        double result = 0.0;
        for( double x = left; x < right; x += epsilon )
            result += meth.apply(func, x, x + epsilon);
        return result;
    }
}
Որպեսզի, օրինակ, Integral դասի evaluate ստատիկ մեթոդի օգնությամբ հաշվեմ \(f(x)=3x^2-2x+1\) ֆունկցիայի ինտեգրալը, պետք է նախ՝ \(f(x)\) ֆունկցիան սահմանեմ որպես MathFunc ինտերֆեյս իրականացնող օբյեկտ, իսկ ինտեգրման մեթոդը, օրինակ սեղանների կանոնը, սահմանեմ որպես Method ինտերֆեյսն իրականացնող օբյեկտ։
     Սահմանեմ Calc դասը՝ main մուտքի կետով։
package integral;

public class Calc {
    /**/ 
    public static void main( String[] args )
    {
        // ինտեգրման մեթոդ
        Method Simple = new Method() {
            @Override
            public double apply( MathFunc f, double a, double b )
            { return (b - a) * f.apply((b + a) / 2); }
        };

        // ինտեգրվող ֆունկցիա 
        MathFunc Sin = new MathFunc() {
            @Override
            public double apply( double x )
            { return 3*x*x - 2*x + 1; }
        };

        // ինտեգրալի հաշվումն ու արտածումը
        double r0 = Integral.evaluate(Simple, Sin, 0, 1, .0001);
        System.out.println( r0 );
    }
}
Բայց սա հնացած եղանակն է։ Անանուն ֆունկցիաների օգտագործմամբ գրվում է շատ ավելի համառոտ, գեղեցիկ ու հասկանալի կոդ։ Ստորև բերված main ֆունկցիայում ինտեգրման մեթոդն ու ինտեգրվող ֆունկցիան evaluate ֆունկցիային փոխանցված են որպես անանուն ֆունկցիաներ։
package integral;

public class Calc {
    /**/ 
    public static void main( String[] args )
    {
        double r1 = Integral.evaluate( (f,a,b)->(b-a)*f.apply((b+a)/2), 
                                       (x)-> 3*x*x - 2*x + 1, 0, 1, .0001 );
        System.out.println( r1 );
    }
}
* * *
Java 8 լեզվում անանուն (լյամբդա) ֆունկցիան սահմանվում է հետևյալ քերականությամբ․

(արգումենտների ցուցակ) -> ֆունկցիայի մարմին

Օրինակ, թվային մոտավոր ինտեգրման սեղանների կանոնի բանաձևը վերը բերված օրինակում սահմանված է ահա այսպես․
(f, a, b) -> (b - a) * f.apply((b + a) / 2)
Կարելի է անանուն ֆունկցիան կապել որևէ փոփոխականի հետ, օրինակ, թվային մոտավոր ինտեգրման Սիմպսոնի կանոնի բանաձևը սահմանված է որպես անանուն ֆունկցիա և վերագրված է simpson փոփոխականին․
public static Method simpson = 
   (f, a, b) -> ((b - a) / 6) * (f.apply(a) + f.apply(b) + 4*f.apply((a+b)/2));
Ինտեգրալը հաշվելիս կարելի է evaluate ֆունկցիային փոխանցել simpson փոփոխականը․
double r2 = Integral.evaluate( simpson, (x)-> 3*x*x - 2*x + 1, 0, 1, .0001 );

Առայժմ այսքանը։ Ես դեռ նոր եմ ուսումնասիրում Java 8-ի անանուն ֆունկցիաները։ Հետքգայում միգուցե նորից անդրադառնամ այս թեմային ու ներկայացնեմ ավելի հետաքրքիր օրինակներ։

Thursday, March 20, 2014

C++11: Տեքստի տրոհումը բառերի՝ istream-ի միջոցով

Իմ բլոգի գրառումներում արդեն երկու անգամ անդրադարձել եմ տրված տեքստի բառերի հաճախության աղյուսակի կառուցման խնդրին։ Մի անգամ Tcl լեզվով և մի անգամ էլ Python լեզվով։ Այս անգամ որոշել էի նույն խնդիրը գրել C++11 լեզվով՝ այդ լեզվի հնարավորություններն ուսումնասիրելու համար։

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

Ֆայլից սիմվոլ առ սիմվոլ տեքստը կարդալու և բառեր կազմելու հավես ես չունեի։ C լեզվի գրադարանային strtok ֆունկցիան էլ տարբեր պատճառներով չէի ուզում օգտագործել։ Նպատակ ունեի օգտագործել C++ լեզվի istream դասը և այդ դասի համար սահմանված operator>> գործողությունը։ Ավելի պարզ ասած, ուզում էի, որ հետևյալ read_file ֆունկցիան filename ֆայլից կկարդա բառերը և կլցնի words վեկտորի մեջ.
void read_file( char* filename, std::vector<std::string>& words )
{
  std::string w{ "" };
  std::ifstream fin{ filename };
  while( !fin.eof() ) {
    fin >> w;
    words.push_back( w );
  }    
  fin.close();
}
Բայց պարզ է, որ words վեկտորը պարզապես լցվելու է տեքստի՝ բացատներով բաժանված հատվածներով, որովհետև լռելությամբ operator>> գործողությունը բաժանիչ (delimiter) է համարում միայն բացատները։
     Իմ խնդրի համար բաժանիչ պետք է համարել այբուբենի մեծատառերից ու փոքրատառերից տարբերովող բոլոր նիշերը։ Եվ istream դասի օբյեկտը պետք է մի որևէ եղանակով կարգավորել այնպես, որ բաժանիչ համարվեն և անտեսվեն բոլոր ոչ պետքական նիշերը։      Ինտերնետում քչփորելուց հետո հասկացա, որ իմ ուզած բաժանիչները սահմանելու համար պետք է ստեղծեմ նոր locale օբյեկտ։ Հետո այդ locale-ի համար էլ սահմանեմ այնպիսի ctype ֆասետ (facet - սրա անունը այդպես էլ չհասկացա), որում արդեն այբուբենի տառերից տարբերվող նիշերին տրված է space դիմակը (mask)։ Ահա այդ նոր սահմանված դասը, որ ժառանգած է std::ctype<char>-ից։
class word_ctype : public std::ctype<char> {
private:
  static const mask* custom_table()
  {
    mask* wcs = new mask[table_size];
    std::copy_n(classic_table(), table_size, wcs);
    for( int c = 32; c < table_size; ++c ) {
      if( isalpha(c) ) continue;
      wcs[c] = (mask)space;
    }
    return wcs;
  }
public:
  word_ctype( std::size_t refs = 0 )
    : ctype(custom_table(), true, refs)
  {}
};
Հետո արդեն ավելի հետաքրքիր մասն է։ Սահմանեցի create_dictionary ֆունկցիան, որի առաջին արգումենտը վերլուծվող ֆայլի անունն է, իսկ երկրորդը՝ կառուցվելիք բառարանի ֆայլի անունն է։ Ստացվեց համարյա ֆունկցիոնալ կոդ։
void create_dictionary( char* infile, char* outfile )
{
  // բառարան է, որը հաշվում է ամեն մի բառի քանակը
  std::map<std::string,int> dict;
  // ընթերցման հոսքի ստեղծում՝ տրված ֆայլի անունով
  std::ifstream fin{infile};
  // ընթերցման հոսքում ներդնել նոր locale օբյեկտ՝ վերը սահմանված facet-ով
  fin.imbue(std::locale{std::locale::classic(), new word_ctype});
  // հոսքից կարդալու երկու իտերատորներ
  std::istream_iterator sbegin(fin), send;
  // կարդալ հոսքը սկզբից մինչև վերջ և բառերն ավելացնել բառարանում
  std::for_each( sbegin, send, [&dict](std::string w){ ++dict[downcase(w)]; } );
  fin.close();
  
  // ստեղծել արտածման հոսք՝ բառարանը գրելու համար
  std::ofstream fout{outfile};
  // բառարանի ամեն մի գրառման համար ...
  for( auto w : dict )
    // ֆայլում գրել բառը և նրա քանակը
    fout << w.first << ',' << w.second << std::endl;
  fout.close();
}
Այս ֆունկցիայում հոսքից կարդացած բառի բոլոր տառերը փոքրատառ դարձնելու համար օգտագործված downcase ֆունկցիան սահմանված է հետևյալ կերպ։
std::string downcase( std::string sr )
{
  std::transform( sr.begin(), sr.end(), sr.begin(), ::tolower );
  return sr;
}

Thursday, March 13, 2014

Հայերեն LaTeX

Այս գրառման մեջ ուզում եմ մի քանի խոսքով պատմել այն մասին, թե ինչպես եմ ես LATEX համակարգի օգտագործմամբ պատրաստում հայերեն փաստաթղթեր։

Որպես LATEX իրականացում օգտագործում եմ xetex իրականացման xelatex ծրագիրը։ Այդ իրականացումը նախագծված է Unicode տեքստերի հետ աշխատելու համար և օգտագործում է համակարգում տեղադրված Unicode տառատեսակները։

LATEX-ն օգտագործում է երեք տեսքի տառատեսակներ՝ serif, sans և mono։ Լռելությամբ օգտագործվում են Computer Modern ընտանիքի տառերը, որոնցում չկան հայերենի այբուբենի տառերի պատկերները։ Համակարգում տեղադրված տառատեսակներն օգտագործելու համար նախ՝ հարկավոր է «ձեռագրի» ֆայլում հայտարարել, որ օգտագործվելու է fontspec փաթեթը։
\usepackage{fontspec}
Այնուհետև \setmainfont, \setsansfont և \setmonofont հրամաններով ընտրել serif, sans և mono տեսքերի համար օգտագործվող տառատեսակները։ Օրինակ, DejaVu ընտանիքի տառատեսակների հայերեն Unicode հատվածում կան հայերեն տառերի պատկերները։ Դա նշանակում է, որ հայերեն տեքստեր պատրաստելու համար կարելի է գրել հետևյալը.
\setmainfont{DejaVu Serif}
\setsansfont{DejaVu Sans}
\setmonofont{DejaVu Sans Mono}
Եվ վերջ (բայց սա դեռ ամենը չէ՛)։ Հիմա արդեն կարելի է փաստաթղթի մարմնում գրել հայերեն Unicode տեքստ և xelatex ծրագրով այն բարեհաջող կթարգմանվի։ Ահա մի օրինակ.
\documentclass[12pt,a4paper,draft]{article}
\usepackage{fontspec}

\setmainfont{DejaVu Serif}
\setsansfont{DejaVu Sans}
\setmonofont{DejaVu Sans Mono}

\begin{document}
\chapter{Առակներ}

Ճանաչել իմաստութիւնն ու խրատը, իմանալ հանճարի խօսքերը, 
ընկալել խօսքի բարդ դարձուածքները, հասկանալ ճշմարիտ 
արդարութիւնը եւ ուղղել իրաւունքը՝ պարզամիտներին 
խորագիտութիւն եւ երիտասարդներին միտք ու հանճար տալու 
համար, որպէսզի, դրանք լսելով, իմաստունն աւելի իմաստուն 
լինի, իսկ հանճարեղը առաջնորդութիւն ստանայ, թափանցի 
առակների եւ խորին ասոյթների մէջ, իմաստունների ճառերի 
եւ նրանց այլաբանութիւնների մէջ։ 
\end{document}
Այս «ձեռագրի» համար ստացված PDF-ը ունի ստորև բերված նկարի տեսքը։
Հենց առաջին հայացքից երևում են մի քանի խնդիրներ. ա) LATEX-ի \chapter հրամանը տեքստի գլուխը սկսել է «Chapter» անգլերեն բառով, շատ լավ կլինի, որ գրվի հայերեն «Գլուխ» բառը, բ) հայերեն տեքստերի համար չկան տողադարձի կանոններ և երկար բառերը դուրս են եկել տեքստի համար նախատեսված սահմաններից, գ) քանի որ հայերեն տառերից շատերն ունեն տողից կախված մաս, դրանք լցնում են ստանդարտ միջտողային տարածությունը և տեքստը դառնում է աչքի համար նվազ հաճելի։

Առաջին խնդիրը կարելի է լուծել ձեռագրի հայտարարությունների մասում (\documentclass և \begin{document} արանքում) \renewcommand հրամանով վերասահմանելով latex փաստաթղթի տրամաբանական մասերի գլխագրերը.
\renewcommand{\chaptername}{Գլուխ} % book, report դասերի համար
\renewcommand{\contentsname}{Բովանդակություն}
\renewcommand{\figurename}{Նկար}
\renewcommand{\tablename}{Աղյուսակ}
\renewcommand{\appendixname}{Հավելված}
\renewcommand{\bibname}{Գրականություն} % book, report դասերի համար
%\renewcommand{\refname}{Գրականություն} % article դասի համար
\renewcommand{\indexname}{Առարկայական ցանկ}
Երկրորդ՝ տողադարձերի հետ կապված, խնդիրը լուծելու երկու եղանակ կա։ Կամ հայտարարությունների մասում ավելացնել \sloppy հրամանը, որը երկար բառը կտեղափոխի հաջորդ տողը և ամեն մի տողի միջբառային տարածությունները կավելացնի այնպես, որ տեքստը հավասարեցվի երկու կողմերից։ Ահա այսպես.
Այս եղանակին կարելի է դիմել էյն դեպքում, երբ պատրաստվող փաստաթուղթը գեղագիտական նշանակություն չի ունենալու այլ ծառայելու է միայն որպես տեղեկատվության փոխանցման միջոց։
    Երբ LATEX-ն օգտագործվում է, օրինակ, հրատարակության պատրաստվող գրքի տեքստի ձևավորման համար, իմ կարծիքով, ավելի ճիշտ է անցնել տեքստի վրայով և կատարել տողադարձերը։ Տողադարձը կատարելու համար ձեռագրում բառը պետք է վանկատել «\-» նիշով, իսկ LATEX-ի ֆորմատավորման ալգորիթմը այդ վանկատումից օգտվելու կկատարի տողադարձերը։ Ահա տեքստը, որտեղ որոշ բառերում նշված է տողադարձի տեղը.
Ճանաչել իմաստությունն ու խրատը, իմանալ հանճարի խոս\-քերը, 
ընկալել խոսքի բարդ դարձվածքները, հասկանալ ճշմա\-րիտ 
արդարությունը եւ ուղղել իրավունքը՝ պարզամիտներին 
խորագիտություն եւ երիտասարդներին միտք ու հանճար տա\-լու 
համար, որպեսզի, դրանք լսելով, իմաստունն ավելի իմաս\-տուն 
լինի, իսկ հանճարեղը առաջնորդություն ստանա, թա\-փան\-ցի 
առակների եւ խորին ասույթների մեջ, իմաստունների ճառերի 
և նրանց այլաբանությունների մեջ։ 
Իսկ այս տեքստից ստացվել է հետևյալ ֆորմատավորված փաստաթուղթը (ձեռագրից հեռացված է \sloppy հրամանը).
Ամեն ինչ, կարծես թե, լավ է։ Բայց տողադարձի ժամանակ LATEX-ն օգտագործում է սովորոկան hyphen գծիկը։ Հայերեն կետադրական կանոնները պահանջում են տողադարձի ժամանակ (և միայն տողադարձի ժամանակ) օգտագործել «֊» (ենթամնա) նիշը։ Դրա համար \setmainfont և \setsansfont հրամանները պետք է համալրել HyphenChar="058A ոչ պարտադիր արգումենտով.
\setmainfont[HyphenChar="058A]{GHEAMariam}
Ստորև բերված նկարներում տողադարձերը կատարված են ենթամնա նիշի օգտագործմամբ (ձախ նկարը) և սովորական hyphen նիշի օգտագործմամբ (աջ նկարը).
Եվ վերջապես, միջտողային տարածությունը մեծացնելու համար պետք է օգտագործել \linespread հրամանը՝ արգումենտում տալով ընդլայնման գործակիցը։
\linespread{1.2}
Հիմա արդեն վերջ։ LATEX ձեռագրում կատարելով վերը բերված փոփոխությունները՝ վստահորեն կարելի է պատրաստել բարձրորակ հայերեն փաստաթղթեր։