[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.
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:
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:
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:
i
(pierwsza wystąpi raz, druga 5 razy) i zmienić odpowiednio na:
i
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:
oraz wywołania funkcji
i pozmieniać odpowiednio na:
i
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 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ć?
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:
i dopisać:
A następnie dopisać fragmenty funkcji obsługujących ten nowy element. w tym wypadku będziemy musieli wnieść poprawki w funkcji:
dołożyć linijkę:
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.
Kaczuś of BlaBla & AUG-Lodz
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ć.