Калькуляторы - это сложно.
Нет, правда, это самый настоящий пример кроличьей норы. При том она идёт не просто вглубь, но ещё и разветвляется. Одна часть уходит в теорию компиляторов, вторая - в лямбда-исчисления, логику, теорию типов и в целом - математический базис информатики. А третья - это трудоёмкая кооперативная работа лидеров индустрии.
Но всё всегда начинается с азов. Ещё 7 лет назад я начинал писать калькулятор, и всё ещё не кончил. Первый раз всегда плох.
Сперва идут три инпута - левый операнд, оператор, правый операнд. Если появится мысль считать многочлены в одной строке - ты попался.
Сначала ты как-то умудряешься доставать одночлены из входной строки с наибольшим приоритетом - и подставлять вместо них результат. Скорее всего - прям в строку.
Происходит эпоха Просвящения - ты узнал про обратную польскую нотацию. Но парсишь всё так же плохо.
Если умный, то быстро узнаешь про алгоритм сортировочной станции, чтобы твои выражения парсить. Вкратце - алгоритм с вхардкоженной грамматикой, зато и вызовы функций умеет, и приоритеты правильно распределяет, да и выхлоп сразу в ОПН. Блажь.
Добавляешь переменные и объявления функций. Простенькая стэковая машинка начинает разлагаться на плесень и на липовый мёд.
Открываешь для себя полноценные парсеры с настоящим AST, как у взрослых дядь. Теперь считать чуть сложнее, но ты справляешься - достаточно тот AST рекурсивно сколлапсировать до единственного узла - результата. Появляются неймспейсы.
На этом моменте, твой калькулятор уже практически полный по Тьюрингу, остаётся только добавить ветвления и векторы. Появление нового типа заставляет тебя задуматься, как складывать векторы с функциями. Это чеховское ружьё.
Рано или поздно, ты всё-таки внимаешь драгонбуку, основе основ всея разработки компиляторов. Узнаёшь про лексический анализ, грамматики, виды парсеров и прочие приятные штуки. Но всё равно ещё долго будешь держаться за привычный recursive top-down, потому что он словно подол материнской юбки посреди толкущейся толпы.
Дальнейшая история здесь расходится на два возможных пути (отступничество не рассматривается):
1. Твой интерпретатор постепенно становится всё более серьёзной штукой с более внятной системой типов (тут-то ружьё и стреляет), со своим байткодом и потенциально IR, и вероятно - со временем даже превратится в компилятор. Тогда у тебя не остаётся вариантов, как вводить строгую типизацию, потому что так банально проще. Ты теперь разрабатываешь полномасштабный императивный язык.
2. Случайно решаешь посмотреть, кто такие лямбда исчисления и кого они считают. Оказалось, твои извилины. Потом будет логика, теория типов и неизбежный пиздинг из Хаскелла, как наиболее продвинутого прикладного типизированного лямбда-калькулюса. Ты теперь разрабатываешь функциональный язык, соболезную.
То есть, из абсолютно тривиальной задачи можно прийти к поистине монструозным результатам. Я себе как-то раз хотел научный калькулятор заделать, чтобы вместо питона его использовать, так столкнулся с контекстно-зависимой грамматикой - в зависимости от типа переменной, одно и то же выражение могло бы означать как применение функции, так и обычное неявное умножение (у которого, фанфакт, приоритет выше обычного умножения и даже деления). Забросил:(
А ведь это всё ещё (практически буквально) детские игрушки. Google в 2014 нанял Hans-J. Boehm, тот, который свой эффективный сборщик мусора написал. И поручили они ему калькулятор для Андроида сделать. И это оказалось настолько сложным, что он попросил помощи у своих коллег. Если вкратце, то от представления рациональных парой числителя и знаменателя он дошёл до представления полиномами, а для трансцедентных (как вот e и pi) использовал Real Recursive Arithmetic; но только там, где нельзя применить обычные символьные вычисления, потому что RRA - на то и рекурсивная, что никогда не остановится даже для абсолютного нуля. А это не очень прикольно для той же тригонометрии с её sin(pi).
Крайне рекомендую почитать полную статью, она довольно короткая, но исключительно занимательная: https://chadnauseam.com/coding/random/calculator-app
Post #618
262