Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Piszemy (trochę ?) mniej złożony kalkulator

O czym będzie ten wpis

Pomysł na wpis zrodził się, gdy zaszła koniunkcja dwóch warunków:
  • Przeglądałem kategorię "programowanie" w celu odnalezienie jakiś ciekawych wpisów
  • W sumie udało mi się takowe znaleźć z okolic czerwca ubiegłego roku, tak więc pozwolę je sobie przytoczyć:

    Linki te zawierają kolejno:

    • stworzenie bazowej klasy wyrażeń
    • obliczanie wyrażenia w postaci postfiksowej z wykorzystaniem stosu
    • oraz w końcu zamiana wyrażenia infiksowego na postfiksowe przy wykorzystaniu algorytmu wymyślonego przez prof. Dijkstrę, czyli Shunting-yard algorithm

    Pomysł całkiem niezły. Kalkulator "funkcjonalniejszy" od pierwowzoru, ale zawsze musi być jakieś "ale"... inaczej nie powstałby ten wpis. Gdyby przez złożoność rozumieć nie funkcjonalność, a złożoność projektową to jest tragedia. Nie liczyłem linii, ale na oko z 300 do tak prostego zadania. Dodatkowo ilość warunków i pętli mnie przeraża.

  • Udzieliłem odpowiedź jednemu użytkownikowi forum, odnośnie jego postu
  • I tu można dostrzec paradoks sam zaleciłem powyższy algorytm (podły ze mnie człowiek, wiem). Do tego pisanie tego w PHP to musi skończyć się apopleksją piszącego, bez dwóch zdań. Mówiąc poważnie, ten algorytm nie jest zły. Jego zaletą jest prostota zależności, o których musimy wiedzieć (o kolejce i stosie chyba każdy słyszał). Nie mniej dostajemy coś mało funkcjonalnego. Głównie dla tego, że algorytm ma pewne ograniczenia. Zastosować go można tylko i wyłącznie do podzbioru języków z gramatyką LR, gdzie dwa symbole nieterminalne nie pojawiają się po prawej stronie żadnej z reguł gramatyki. Oczywiście do prostego kalkulatora to wystarczy, ale warto zdawać sobie sprawę z ograniczeń.

    Ja w tym wpisie chcę zaproponować alternatywne podejście do tworzenia kalkulatora, tylko zastanawiam się, czy nie będzie ono trudniejsze w zrozumieniu. Mimo wszystko spróbuję, gdyż uważam, że nie ma rzeczy trudnych, tylko źle wytłumaczone. W trakcie wpisu omówię bądź, co bądź, pobieżnie informacje na temat gramatyk bezkontekstowych oraz przejdziemy proces tworzenia najprostszego kalkulatora. Napiszemy go w Scali.

    Dlaczego Scala?

    Można powiedzieć, że jestem częściowym Java Refugee (z ang. uchodźca). Czemu częściowym? W biznesie jak to w biznesie JEE trzyma się całkiem dobrze, a jak coś działa to się tego nie zmienia. Jak widać komitet wydający kolejne JSR utworzył z tego hasła motyw przewodni, no i mamy, to co mamy.

    A Scala? Scala jest przyjemna, jest tym, czym Java nigdy nie chciała zostać, jest stabilna i najważniejsze jest skalowalna. Nie zamierzam się teraz nad tym rozwodzić (może kiedyś w osobnym wpisie), dlaczego z języków dostępnych na JVM najlepiej wybrać Scalę, ale zapewne będzie to tylko opinia, i tak znajdzie się ktoś, kto powie że banany są lepsze od jabłek. Oczywiście na tym prościutkim przykładzie nic ze skalowalności nie zobaczymy, ale cukier syntaktyczny będzie się lał.

    A więc do dzieła...

    Zacznijmy od podstaw. Chciałbym mieć czas na omówienie wszystkiego dogłębnie, ale niestety go nie znajdę w tym momencie. Dużo może ułatwić poznanie na własną rękę jakiejś serii wykładów na temat automatów i gramatyk. Wprowadźmy szybko cztery pojęcia. Pierwsze z nich to Abstrakcyjne Drzewo Syntaktyczne (AST). Jest to struktura przechowująca budowę pewnego dokumentu w oparciu o zdefiniowaną gramatykę, pozwala stwierdzić jak należy interpretować dany dokument, czy nie ma w nim błędów, etc.. Gramatyka (my rozważamy tylko te bezkontekstowe) czyli formalny zbiór definicji, dzięki którym można tworzyć wyrażenia w danym języku. Terminal, czyli węzeł AST, który jest liściem (kończy rekursywne wywołanie budowy AST), oraz węzeł nie terminalny, czyli dopełnienie zbioru (wszystko poza liśćmi).

    Tak więc zdefiniujmy naszą klasę bazową dla Terminali oraz obiekty pochodne (czyli również będące Terminalami), dziedziczące z niej, mające podstawową implementację z wykorzystaniem mechanizmu property.

    abstract class Terminal case class Number(value: Double) extends Terminal case class BinaryOp(operator: String, left: Terminal, right: Terminal) extends Terminal

    Mamy zatem definicję dwóch terminali, są to kolejno liczby oraz operatory dwuargumentowe.
    Dla liczb przechowujemy double (warto zwrócić na to uwagę, ponieważ wyniki są tylko pewnym przybliżeniem, nie mniej ta kwestia nas nie obchodzi, nie o tym jest wpis).

    Dalej zdefiniujmy klasę Kalkulatora, która będzie właściwie robić całą magię: class Calculator { ... }

    Zacznij implementację powyższej klasy od funkcji, która dla otrzymanego terminalu obliczy go. Taki mechanizm z angielskiego nazywamy resolve: def resolve(terminal: Terminal): Double = { terminal match { case Number(x) => x case BinaryOp("^", x1, x2) => (math.pow(resolve(x1), resolve(x2))) case BinaryOp("+", x1, x2) => (resolve(x1) + resolve(x2)) case BinaryOp("-", x1, x2) => (resolve(x1) - resolve(x2)) case BinaryOp("*", x1, x2) => (resolve(x1) * resolve(x2)) case BinaryOp("/", x1, x2) => (resolve(x1) / resolve(x2)) } }

    Jak widzimy, jest to dość proste. Funkcja otrzyma obiekt będący terminalem, a następnie tworząc prototyp dopasuje (match) odpowiedni prototyp do obiektu i wykona kod zdefiniowany w domknięciu.

    Teraz przejdźmy do parsowania wyrażenia. Użyję tu małego cheatu. Jako, że Scala ma podstawowy parser (a właściwie parser połączony z domyślnym lekserem), to postaramy się go wykorzystać. Profesjonalnie byłoby użyć jakiejś uznanej biblioteki do tworzenia lekserów/parserów (np. dla C będą to odpowiednio Flex/Bison, dla Javy JFlex/JCup, etc.)
    dla danego języka. Jednak projekt jest na tyle prosty i "naklepany" w ciągu 7 minut, że nie ma to większego sensu.

    Zdefiniujmy obiekt o nazwie ExpressionParser w następujący sposób: object ExpressionParser extends JavaTokenParsers with PackratParsers { ... }

    Dzięki temu uzyskamy dostęp do leksera z klasy JavaTokenParsers, a przyjęcie cech PackratParser umożliwi nam definiowanie gramatyki LR.

    Teraz przejdziemy do w sumie najciekawszej części, czyli definiowania context-free grammar, w celu utworzenia AST. Przejrzystość Scali pod tym względem jest niesamowita i łatwo przyswajalna: lazy val expression: PackratParser[Terminal] = (expression <~ "+") ~ expr1 ^^ { case lhs~rhs => BinaryOp("+", lhs, rhs) } | (expression <~ "-") ~ expr1 ^^ { case lhs~rhs => BinaryOp("-", lhs, rhs) } | expr1 lazy val expr1: PackratParser[Terminal] = (expr1 <~ "*") ~ expr2 ^^ { case lhs~rhs => BinaryOp("*", lhs, rhs) } | (expr1 <~ "/") ~ expr2 ^^ { case lhs~rhs => BinaryOp("/", lhs, rhs) } | expr2 lazy val expr2 : PackratParser[Terminal] = (expr3 <~ "^") ~ expr2 ^^ { case lhs~rhs => BinaryOp("^", lhs, rhs) } | expr3 lazy val expr3 : PackratParser[Terminal] = "(" ~> expression <~ ")" | floatingPointNumber ^^ { x => Number(x.toDouble) } def parse(text : String) = parseAll(expression, text)

    Teoretycznie, gdyby użyć porządnej biblioteki generowania parserów, to można by to jeszcze uprościć. Głównie za sprawą tego, że expr1, expr2 i expr3 są redundantne. Tak naprawdę użyłem ich do "poziomowania" ze względu na brak możliwości zdefiniowania priorytetu operatorów (co jest możliwe w np. Bisonie).

    No więc, co tu się dzieje, dla tych co Scalę widzą pierwszy raz na oczy: Początek (tak dla zmyły) umieściłem na końcu. Obiekt klasy Kalkulator wywoła (dzięki oddelegowaniu metody) metodę parseAll z klasy parsera. Rozpocznie się parsowanie zmiennej text (w której znajdzie się wyrażenie arytmetyczne) przyjmując za korzeń tego drzewa definicję wyrażenia. Następnie rozwinięte zostaną węzły mające dodawanie i odejmowanie, potem mnożenie i dzielenie, kolejna będzie potęga, a na samym końcu to co znajduje się w nawiasie. No własnie a co tam się znajduje? Znowu wyrażenie, zatem nawias wymusi kolejność działań, ale wewnątrz nawiasu odnowa kolejność zostanie zachowana (aż do napotkania kolejnego nawiasu - prosta rekursja).
    Takie rozwijanie będzie miało miejsce aż do rozwinięcia wszystkiego do poziomu symboli terminalnych. Wtedy to, od dołu, zaczniemy obliczać wyrażenia. Najpierw najbardziej zagnieżdżony nawias, a wewnątrz każdego nawiasu kolejno potęga, mnożenie i dzielenie, dodawanie i odejmowanie (kolejność działań została zachowana).

    Warto jeszcze zwrócić uwagę na jedną rzecz. Podczas definiowania gramatyki, expression i expr1 są rozwijane lewostronnie (rekursja po lhs). Natomiast dla expr2 jest po prawej. Pytanie dlaczego tak to umieściłem? Odpowiedź jest dość prosta: cztery podstawowe działania liczymy od lewej do prawej zatem musimy rozwijać je w takiej kolejności inaczej moglibyśmy otrzymać coś takiego:

    BinOp / | \ 1 - BinOp / | \ 2 - 3

    licząc więc od dołu zostanie wykonane 2 - 3 czyli -1, a potem 1-(-1) czyli 2. To byłoby skrajnie głupie w obliczu algebry, której używa większość ludzi. Tak samo sprawa wygląda z potęgowaniem, które jest łączne prawostronnie. Więcej w przykładach testowych w ostatnim kodzie.

    Za obliczanie będą odpowiedzialne następujące metody klasy Kalkulator def parse(text: String) = ExpressionParser.parse(text).get def evaluate(text: String): Double = resolve(parse(text)) Tak więc, parse oddelegowuje wykonanie do metody parse z klasy ExpressionParser czyli tak naprawdę do parseAll (o czym już mówiłem), natomiast evaluate wykorzystuje parse i resolve do podstawienia pod węzły wartości, które można obliczyć. I to właściwie już wszystko. Dołączyłem jeszcze obiekt testujący, ale to już wraz z całym kodem (56 linii):

    abstract class Terminal case class Number(value: Double) extends Terminal case class BinaryOp(operator: String, left: Terminal, right: Terminal) extends Terminal class Calculator { def resolve(terminal: Terminal): Double = { terminal match { case Number(x) => x case BinaryOp("^", x1, x2) => (math.pow(resolve(x1), resolve(x2))) case BinaryOp("+", x1, x2) => (resolve(x1) + resolve(x2)) case BinaryOp("-", x1, x2) => (resolve(x1) - resolve(x2)) case BinaryOp("*", x1, x2) => (resolve(x1) * resolve(x2)) case BinaryOp("/", x1, x2) => (resolve(x1) / resolve(x2)) } } import scala.util.parsing.combinator._ object ExpressionParser extends JavaTokenParsers with PackratParsers { lazy val expression: PackratParser[Terminal] = (expression <~ "+") ~ expr1 ^^ { case lhs~rhs => BinaryOp("+", lhs, rhs) } | (expression <~ "-") ~ expr1 ^^ { case lhs~rhs => BinaryOp("-", lhs, rhs) } | expr1 lazy val expr1: PackratParser[Terminal] = (expr1 <~ "*") ~ expr2 ^^ { case lhs~rhs => BinaryOp("*", lhs, rhs) } | (expr1 <~ "/") ~ expr2 ^^ { case lhs~rhs => BinaryOp("/", lhs, rhs) } | expr2 lazy val expr2 : PackratParser[Terminal] = (expr3 <~ "^") ~ expr2 ^^ { case lhs~rhs => BinaryOp("^", lhs, rhs) } | expr3 lazy val expr3 : PackratParser[Terminal] = "(" ~> expression <~ ")" | floatingPointNumber ^^ { x => Number(x.toDouble) } def parse(text : String) = parseAll(expression, text) } def parse(text: String) = ExpressionParser.parse(text).get def evaluate(text: String): Double = resolve(parse(text)) } object Main extends App { val calc = new Calculator println(calc.evaluate("10+10")) // prosty example println(calc.evaluate("2+2*2")) // haczyk z podstawówki mnożenie jest najpierw println(calc.evaluate("(2+2)*2")) // wymuśmy działanie println(calc.evaluate("2^3^4")) // raczej oczywisty wynik println(calc.evaluate("2^-3^4")) // też chyba spodziewany, potęga jest łączna prawostronnie println(calc.evaluate("(2^-3)^4")) // a tu wymuszenie 2^-3 == (1/2)^3 stąd ułamek println(calc.evaluate("-7+6*2/3+9^2-1")) // -7+6*2/3+81-1 <=> -7+12/3+81-1 <=> -7+4+81-1 <=> 77 // coś "skomplikowanego" println(calc.parse("-7+5*3/3+9^2-1")) // obejrzyjmy sobie drzewko (niestety bez wcięć) }

    oraz wyniki:

    20.0 6.0 8.0 2.4178516392292583E24 2.4178516392292583E24 2.44140625E-4 77.0 BinaryOp(-,BinaryOp(+,BinaryOp(+,Number(-7.0),BinaryOp(/,BinaryOp(*,Number(5.0),Number(3.0)),Number(3.0))),BinaryOp(^,Number(9.0),Number(2.0))),Number(1.0))

    Kod został napisany tak, aby działał na ideonie, dzięki czemu można go testować, bez instalowania Scali.

    O czym jeszcze chciałem powiedzieć

    Ostatni przykład pokazuje "drzewo". Należy je czytać, tak jakby to były funkcje. Najbardziej zagnieżdżone wykonanie to korzeń, każdy kolejny zredukowany poziom zagnieżdżenia to dzieci, aż w końcu wszystkie terminale to liście.

    Dodatkowo jeszcze raz przypominam, użyty został typ double, który nie przechowuje wartości dokładnie, tylko z pewnym przybliżeniem.

    Mała dygresja (ale w temacie) o demagogii programistycznej

    Od czasu do czasu, tu i tam spotykam się z mała wiedzą programistyczną. Dziś opowiem o:
    Oblicz tą stałą (w postaci wyrażenia), to będzie szybciej program działał
    Zapewne przy takim kodzie: int time = 1 * 60 * 60; spotkaliście się z:
    OMG!!! Oblicz to nobie, program spowalniasz !!!!!111oneone
    a co to się nie dzieje, gdy zrobisz taki hopsztos: while (...) { int time = 1 * 60 * 60; } Wiesz co się dzieje? Aż by się chciało rzucić mięsem, no ... nic. Praktycznie każdy wysoko poziomowy kompilator działa właśnie w oparcie o budowę takiego drzewa AST, z tym, że poza arytmetyką są tam inne konstrukcje, if, while, for, etc. I jak widziałeś mój program zwinął to wyrażenie do jednej liczby. Taki zapis wpływa tylko i wyłącznie na czas budowy AST (więcej węzłów) czyli w czasie kompilacji. To samo dzieje się w drugim przypadku, z tym, że dodatkowo zmienne nieulegające modyfikacji kompilatory wynoszą poza pętle.

    Aby uściślić temat, są języki (skryptowe/interpretowane : np. PHP), gdzie ma to znaczenie. Taki plik nie jest kompilowany, ale interpreter i tak musi zbudować dla niego AST, po prostu robi to za każdym razem. O ile w przypadku aplikacji desktopowych uruchamianych raz na godzinę, to bez znaczenia o tyle np. w technologiach webowych ma to duży wpływ (budowane co request).

    Króciutkie podsumowanie

    Zacząłem temat w sumie o kompilatorach, bo jakby nie patrzeć to co napisałem to prosty arithmetic resolver. Jednak nie jestem pewny czy będę go kontynuował (czas, czas, czas). Początkowo, czyli jakiś rok temu chciałem napisać serię dotyczącą automatów i gramatyk (głównie bezkontekstowych), ale znowu czas, czas, czas, więc zobaczymy jak to będzie. Zdaję sobie sprawę, że dla osoby zupełnie zielonej, może być to ciężko przetrawić, bez tego "wprowadzenia" z czym co się je. Ale chyba nie muszę powtarzać co mnie ogranicza.

    Z takich ciekawostek warto zobaczyć np. formalny opis gramatyki Javy. Dostępny jest pod tym linkiem.

    Jeśli macie jakieś pytania, to śmiało, postaram się odpowiedzieć, tylko możecie się spodziewać jakiegoś odnośnika do materiałów, jeśli w wytłumaczeniu będzie potrzebna teoria, odnośnie automatów skończonych, gramatyk, etc. W przeciwnym razie postaram się po prostu odpowiedzieć (jeśli nie zajmie to dużo czasu). 

    programowanie

    Komentarze

    0 nowych
    Stalker11517   2 #1 05.02.2014 20:42

    OMG ja zaczynam w C++ ale takich cudów to ja jeszcze nie widziałem :D

    matrix012345   4 #2 05.02.2014 20:53

    kostek135 odpowiedział na wpis, który był odpowiedzią na mój wpis, który był odpowiedzią na wpis Description_1. Pytanie kto odpowie na wpis kostka? ;)

    Tormiasz   6 #3 05.02.2014 22:00

    Super! :) Gdyby ktoś zaczął pisać blogowy poradnik o C++, byłbym bardziej niż szczęśliwy :)

    GBM MODERATOR BLOGA  19 #4 05.02.2014 22:54

    @Tormiasz: Przecież artykułów o C++ masz w Internetach od ciula... Nie mówiąc o książkach (e-bookach dostępnych przy dobrym googlowaniu, lub na Helionie), albo dokumentacji... ;-)

    Nie rozumiem, po co powielać treść i... dlaczego C++ ? Zainteresuj się Pythonem (to jest przyszłość ;-)

    Pzdr,
    GBM

    Jim1961   7 #5 06.02.2014 00:25

    @kostek135
    Wiedzę masz konkretną - pozazdrościć - z małą uwagą: mając Opcache pliki php nie są interpretowane za każdym razem; Python i Ruby również lecą z bytecodu ... nie znam implementacji, ale strzelam że powyższe "hopsztosy" ze stałymi zostały przewidziane.

    IMHO, jeżeli ktoś się zastanawia, czy kilka wyrażeń z kilku stałych (no bo ile takich może być w całej aplikacji) opóźni wykonywanie programu, to powinien zmienić inicjały na Adolf H. ... w 100% wąskie gardła znajdują się gdzie indziej i to nimi powinno się zająć ... bez względu czy język jest kompilowany czy interpretowany. Od razu im się przypomina pewna polemika w światku php: echo czy print :]

    mleczyk   2 #6 06.02.2014 07:45

    Automaty skończone... ehhh pamiętam te godziny spędzone nad projektem zaliczeniowym :P Poza tym fajny w[is, miło się mi go czytało.

    mikolaj_s   13 #7 06.02.2014 16:04

    Jakoś to bardzo podobne do przykładu z "Programming Scala" by Odersky ;)

    kostek135   8 #8 06.02.2014 17:30

    @Jim1961
    Zgadzam się z tobą w 100%. O opcache coś słyszałem. O kompilacji Pythona i Rubiego nie. Jak rozumiem to nie działa domyślnie? W sensie utworze sobie plik *.rb i podam go do wykonania w konsoli, to on zostanie tylko zinterpretowany (ewentualnie zostanie utworzony bytecode i wykonany, zamiast przechodzenia po AST, ale plik pośredni nie zostanie nigdzie zapisany, więc tak naprawdę AST i tak będzie każdorazowo rozwijane)? Czy aby uniknąć tego o czym mówiłem nawiasem muszę wpierw podać go do kompilatora, który utworzy bytecode i zapisze plik pośredni?

    Tak naprawdę na dzień dzisiejszy, trudno powiedzieć w programowaniu, że coś jest takie, albo inne bo to może być kwestia dni, kiedy przestanie przynależeć do danej szufladki. Scalę, też możemy wykonać jako skrypt, bez tworzenia pliku pośredniego. Po prostu zamiast zapisywać ten plik, bytecode jest dostarczany do class loadera i uruchomiony. Uruchamiając ten skrypt po raz drugi, znowu wykonana zostanie kompilacja w locie. Można też zastanowić się czy opcache dla PHP nie robi z niego języka kompilowanego. Mówię, trudno jednoznacznie coś zaszufladkować.

    Wracając do PHP, ile tak naprawdę osób programując w nim wie o opcache, wie jak co działa pod spodem, jak działają kompilatory, maszyny wirtualne, interpretery? Z PHP jest ten problem, że większość "programistów" pisze byle co, byle jak.

    Poza tym jestem hejterem PHP z przyczyn subiektywnych:
    1. Popularność, czyli masa niskiej jakości tutoriali oraz powielanie głupot. Boom PHP przypadł na jego wersję 4 w sumie. Mimo, że to już przeszłość, mogę się założyć że w niedługim czasie na tutejszym forum powstanie wpis: Pomóżcie z zapytaniem mysql_query("...")
    Z jednej strony, trudno się temu dziwić, jak tutoriali, które tak radzą jest cała masa. Przez co ten język przegrał w moim odczuciu. Sytuację można porównać do tego co się dzieje w sklepach aplikacji mobilnych. Programowanie stało się tak proste, że każdy może wydać bubel. Jedyną sensowną opcją na chwilę wyjścia PHP 5 byłoby odcięcie się twórców od tego bagna.

    2. Obecnie moja praca, to głównie poprawianie systemów, które nie osiągają założonej wydajności. Jak można się domyśleć większość z nich napisana jest w PHP. Nadmienię, że bardzo często nie jest to wina samego PHP, ale "programisty", który tworzył ten system. Nieadekwatne struktury danych, naiwne algorytmy, błędy projektowe, stare technologie, etc. W sumie z tego co pamiętam podjęliśmy się poprawy dwóch w PHP, były nawet całkiem, całkiem i rokowały jakieś nadzieje. Z drugiej strony była niezliczona ilość projektów przepisanych na Javę, C#, etc. Gdyby dać wymagania niezależnemu analitykowi lekko można zacząć wycenę takiego projektu (infrastruktura i inne też się w to wliczają) na od 80 do 120 kawałków.
    Trzeba wykonać audyt na temat obecnego rozwiązania:
    - Czemu się do niczego nie nadaje.
    - Proponowane nowe rozwiązanie (technologie, etc.)
    - Jakieś dogadanie terminów wstępnie
    - No i najlepsze, cena. I czego tu można się spodziewać. Wielkich oczu, zdziwienia, czasem drwin, że jak to, że aż tyle, że poprzednie kosztowało przysłowiowe 500 zł. No i często podejrzewam, że tak było beznadziejny PHP (ze składnia w wersji 4) wspierany równie beznadziejnym mysql, na jakimś lipnym hostingu napisany przez studenta pierwszego roku, a oczekiwania odnośnie czasu jak w przetwarzaniu Google'a. Tacy pseudo-programiści tak niewyobrażalnie psują rynek, że ciężko to opisać. Poza tym wychodzi też polska mentalność przedsiębiorców, gdzie celem nadrzędnym jest przyoszczędzić na wszystkim i wszędzie.

    Podsumowując, jestem skrzywdzony przez PHP i go nie lubię. Subiektywnie.

    Nie jestem też pewny, czy zrozumiałeś mój zamysł odnośnie zdementowania powszechnej głupoty.
    Przykład z pętlą miał pokazać, że dużo ludzi wysunie wniosek, że co każdy obieg pętli będzie wykonywane mnożenie. To jest oczywista głupota, bo takie wyrażenie zostanie zwinięte do jednej liczby w trakcie ewaluacji AST. Ba, sam kiedyś uważałem, że mnożenie będzie wykonywane! Przez głupi internet właśnie... Ten fragment miał raczej pełnić rolę wniosków, po co to wszystko o czym pisałem, może się przydać - bo przecież nie do pisania kalkulatora to raczej należy traktować jako skutek uboczny :)

    Kończę ten komentarz, bo zaraz osiągnie długość wpisu.

    @All
    Dziękuję za dobre słowa.

    kostek135   8 #9 06.02.2014 17:33

    @mikolaj_s
    Trudno mi się ustosunkować, bo nie miałem tej książki w ręku. Wpis powstał o część wykładów na temat gramatyk z Uniwersytetu Warszawskiego (gramatyki były zapisane w sposób formalny, abstrahując od konkretnego języka programowania) oraz wiedzy o Scali zdobytej na przestrzeni czasu.

    kostek135   8 #10 06.02.2014 17:35

    Wpis powstał w oparciu o*

    Jim1961   7 #11 06.02.2014 19:57

    @kostek135
    Twój zamysł zrozumiałem (choć o tym nie wiedziałem), z tym że ja nigdy się nie zastanawiałem, czy taki hopsztos ma znaczenie ... do czego się odniosłem. Na pewno nie będę ganiał jakiegoś newbie za to że sobie bardziej po ludzku zapisze 10 milionów w kodzie -> 10*1000*1000.

    Co do hejtowania php - się rozpisałeś :P - nie będę przeczył, bo z większością argumentów się zgadzam.

    Co do "kompilacji" Pythona i Rubiego, nie mam z tym kontaktu, ale z sieci wiem że w zastosowaniach produkcyjnych i profesjonalnych, oba korzystają z maszyn wirtualnych do których wczytywane są pliki z bytecode'm - takie odpowiedniki skompilowanej Javy w plikach z rozszerzeniem "class". Odpalając .rb chy .py w konsoli, tak drzewa budowane są w locie, a .pyc czy .rbc (chyba) nigdzie nie zapisywane. Najlepiej jak się jakiś "specjalista" wypowie.