Funkcje z klawiatury, czyli słów parę o parserze


[Komentarz LeMUra]
[Komentarz Kaczusia]


Czasami niektóre z osób używających komputery nie tylko do grania, korzystają z faktu, że na komputerze można policzyć parę różnych rzeczy. Zadziwają was zapewne (przynajmniej niektórych) programy, które liczą, zamieniają wpisane przez nas definicje funkcji na ich pochodne, itp. Pytanie - jak to robi taki program, my wpisujemy mu instrukcje, a on je wykonuje? To zasługa parserów.


Napisanie parsera dla przeciętnego programisty nie jest trudne, lecz w niektórych przypadkach dość pracochłonne. Ponieważ miałem na swoim koncie parę pomniejszych parserów, to postanowiłem pewnego dnia, że napiszę procedurę zamieniającą funkcję wpisaną z klawiatury na język zrozumiały dla komputera. Projekt powstał w mig, ale po dość długiej pracy parser mój nie doczekał się nawet pierwszej kompilacji... (zostawiłem gdzieś wersję źródłową, może kiedyś, gdy będę miał więcej czasu dokończę). Dobra ale nie o tym chciałem pisać. Parę lat temu wpadł mi w ręce parser stworzony przez Niemca - Jensa Gelhara. Nie jest on zbyt uniwersalny, ale łatwo go poprawić i przystosować do własnych potrzeb. Ma on pewną wadę (albo zaletę, zależy od punktu widzenia) napisany jest w C obiektowym, ale nie przejmujcie się, to co Wam będzie potrzebne postaram się przedstawić.

Podstawowa rzecz, to deklaracja wskaźnika, którego będziemy używać do operacji na naszej funkcji.

Func *funkcja;

Koniecznie musi być wskaźnik - o tym dlaczego napiszę w 5 części kursu programowania obiektowego - wiąże się to z dość wygodną z punktu widzenia programisty sztuczką którą ułatwia obiektowość.


Następnie należy pobrać do tablicy znakowej postać funkcji i wywołać funkcję Parse, która utworzy nam obiekt, na który wskazywać będzie nasz wskaźnik:

funkcja = Parse(tablica);

Argumentem funkcji jest wskaźnik na tablicę znakową z postacią funkcji. Oczywiście nie muszę przypominać, że nazwa tablicy jest zarazem wskazaniem na jej początek.


Teraz używać możemy jednej z dwu zdefiniowanych przez autora parsera funkcji: print i eval. print - to wypisanie funkcji po przekształceniu przez parser:

funkcja->print(buf);

Argumentem jest taki sam wskaźnik, i tu sądzę należy wprowadzić pierwszą zmianę. Nie wiem, co planował Autor, ale równie dobrze możemy podać tu wskaźnik na string "pocałuj mnie w..." [w co, Kaczuś? - red.] i też będzie dobrze - funkcja zadziała prawidłowo. Więc należy wprowadzić zmiany. We wszystkich deklaracjach (w pliku fclass.h), w każdym z obiektów (czyli klasie) należy znaleźć deklaracje:

virtual void print(char *) = 0;

i

void print(char *);

(pierwsza wystąpi raz, druga 5 razy) i zmienić odpowiednio na:

virtual void print() = 0;

i

void print();

Podobnie w pliku fclass.c (jeżeli kompilujecie Sasem, należy zmienić nazwę na fclass.cpp) i plot.c (tu również należy zmienić nazwę pliku). Należy w tych plikach znaleźć nagłówki definicji funkcji:

Nazwa_Klasy::print(char *str)

oraz wywołania funkcji

Obiekt->printf(str);

i pozmieniać odpowiednio na:

Nazwa_Klasy::print()

i

Obiekt->print();

eval - to funkcja licząca wartości funkcji matematycznej wprowadzonej przez nas w punkcie x (argumentem jest zmienna typu double - czyli rzeczywista podwójnej precyzji):

funkcja->eval(x);

Funkcja ta zwraca wartość typu double.


Należy również pamiętać o zwalnianiu pamięci - niestety, ze względu że jest to robione przez wskaźnik - pamięć nie zostanie zwolniona automatycznie. Tak przy wyjściu, jak i zmianie funkcji (ponownym wywołaniu funkcji Parse i zwróceniu wartości pobraną przez ten sam wskaźnik), należy zwolnić pamięć, na której początek wskazuje nasz wskaźnik - jak to zrobić?

delete funkcja;

Niedogodności parsera:

ale, jak już napisałem, nie ma się czemu dziwić - ot po prostu to jest program szkoleniowy, lecz na tyle dobrze napisany, że dość łatwo można te niedociągnięcia naprawić.


O pochodnych [może, a raczej wątpię] napiszę do kolejnej Izviestii (potrzebne będą wiadomości opisane w kolejnym odcinku kursu), natomiast jak uwzględnić obsługę błędów np. przy dzieleniu? Mamy funkcję:

double BinOpN::eval (double x)
{
   double lv = l->eval(x), rv = r->eval(x);
   switch(oper)
   {
     case Op_add:  return lv+rv;
     case Op_sub:  return lv-rv;
     case Op_mult: return lv*rv;
     case Op_div:  return lv/rv;
     case Op_pot:  if (r->isconst() && floor(rv)==rv)
                     return intpotz(lv, int(rv));
                   else if (lv > 0) return exp(rv*log(lv));
                                    return 0;
     default: exit(EPANIC);
   }
}  

Najlepiej zamiast wartości byłoby zwracać jakiś obiekt (strukturę lub klasę), w którym przekazalibyśmy zwracaną wartość i flagę błędu, lecz gdy nie ma, to by nie komplikować należy się zastanowić jak to przedstawić - ja proponuje zwrócić charakterystyczną wartość. Pytanie brzmi: jaką? Wszyscy zaraz powiedzą - FALSE, czyli 0, ale to byłby błąd - ponieważ bardzo często jest to wartość przez nas poszukiwana. Innym wyjściem jest zwrócenie odpowiednio dużej wartości ze znakiem - będziemy dzięki temu mieli nieskończoność... Problem - co zrobić z (x/x^2) - w takich przypadkach powinniśmy dokładniej wszystko przeanalizować, ale to nie czas i miejsce na to, dla uproszczenia przyjmijmy "plus nieskończoność". Tak więc nasza funkcja będzie wyglądać:

double BinOpN::eval (double x)
{
   double lv = l->eval(x), rv = r->eval(x);
   switch(oper)
   {
     case Op_add:  return lv+rv;
     case Op_sub:  return lv-rv;
     case Op_mult: return lv*rv;
     case Op_div:  if(rv) 
                   {
                     if(lv) 
                        return lv/rv;
                     else
                        return 100000;
                   }
                   else
                   {
                     if(lv<0)
                        return -100000;
                     else
                        return 100000;
     case Op_pot:  if (r->isconst() && floor(rv)==rv)
                     return intpotz(lv, int(rv));
                   else if (lv > 0) return exp(rv*log(lv));
                                    return 0;
     default: exit(EPANIC);
   }
} 

Kolejny problem, to tworzenie nowych funkcji. Wprowadźmy dodatkową funkcję "tan". Najpierw należy dodać deklaracje flagi do:

enum UnOps { Op_neg, Op_sqr, Op_sqrt, Op_sin, Op_cos, Op_exp, Op_ln};

i dopisać:

enum UnOps { Op_neg, Op_sqr, Op_sqrt, Op_sin, Op_cos, Op_exp, Op_ln, Op_tan};

A następnie dopisać fragmenty funkcji obsługujących ten nowy element. w tym wypadku będziemy musieli wnieść poprawki w funkcji:

void UnOpN::print(char* str)

dołożyć linijkę:

case Op_tan: printf("tan "); break;

podobnie w funkcji:

   double UnOpN::eval (double x)
   {...
      case Op_tan: return tan(y);
   ...}  

No i na koniec, rozpoznanie funkcji za pomocą funkcji Parser(), odpowiednie zmiany mamy wprowadzić w funkcji Func *Factor()
Myślę, że z tym nie będzie kłopotów? :))



Szczegółowy opis występujących w bibliotece funkcji pojawi się w kolejnej Izviestii, gdy osoby nieznające obiektowości będą mogły zapoznać się z dodatkowymi, nie prezentowanymi do tej pory przeze mnie na łamach Izviestii elementami programowania obiektowego.


Teraz parę słów na temat użycia biblioteki. Przykładowy - mało rozbudowany przykład zamieścił sam autor. Jest to program fplot - rysujący wykres wprowadzonej przez nas funkcji. Jest on na prawdę bardzo prosty i niestety z opisem po niemiecku. Ale sądzę, że to powinno w zupełności wystarczyć. Jeśli chodzi o użycie, to interesują nas tylko trzy funkcje, o których napisałem wcześniej.

Jest jeszcze kwestia i pytanie, czy używać cudzych funkcji (fragmentów kodu źródłowego) we własnych programach, czy nie. Myślę, że to zależy od tego co to za program (przede wszystkim jaką rolę odgrywa cudza funkcja) i czy mamy zgodę autora na użycie danej funkcji. Jeśli chodzi o bibliotekę fclass, to otrzymałem zgodę na opis i rozpowszechnianie tych funkcji. Myślę, że autor nie będzie miał nic przeciwko umieszczeniu jego funkcji w waszych programach. Być może (przynajmniej byłoby to w dobrym guście) należałoby umieścić informacje o użyciu danej funkcji w programie, jak również - o czym chyba nie muszę przypominać - zapytać autora o pozwolenie użycia jego funkcji (przede wszystkim w programach, za które pobieracie jakieś opłaty). Dlatego na koniec podaje adres E-mailowy do autora (Jensa Gelhara): himpel@pearl.in_ulm.de.

Na tym kończę


Kaczuś of BlaBla & AUG-Lodz


P.S.

Teoretycznie powinienem wprowadzić sam te zmiany (na własne potrzeby wprowadziłem) ale nie moim zadaniem jest poprawiać autora. Poza tym wprowadzając takie zmiany można czegoś jeszcze się nauczyć.

Powrót do Menu