суббота, 25 апреля 2015 г.

Продолжаю осваивать Haskell

Решил ненадолго прервать копание Хаскелля в глубину (это занятие, как вы знаете, может длиться бесконечно) и немножко попрактиковаться в кодировании: написал простой текстовый калькулятор с функциями и переменными. Parsec я использовать не стал, т.к. хотелось написать свой парсер для лучшего понимания. Получилось 200 строк кода – я считаю, что это неплохо для такого объёма функциональности. Вряд ли на каком-нибудь статическом языке получилось бы короче. В частности, таблица всех функций уложилась всего в четыре строчки:
functions :: (Floating a) => [(String, a -> a)]
functions = [("exp", exp), ("sqrt", sqrt), ("log", log), ("sin", sin), ("tan", tan), ("cos", cos),
  ("asin", asin), ("atan", atan), ("acos", acos), ("sinh", sinh), ("tanh", tanh), ("cosh", cosh),
  ("asinh", asinh), ("atanh", atanh), ("acosh", acosh), ("abs", abs)]
Очень красиво, я считаю. Никакого лишнего мусора.

Таблица операторов уложилась в 10 строчек:
divide expr loc l r | (r == 0.0) = throwError CalculatorException "Evaluation error. Division by zero" expr loc
divide _ _ l r = l / r

const2 = const . const

binaryOperators :: (Eq a, Floating a) => [BinaryOperatorInfo a]
binaryOperators = [
  BinaryOperatorInfo Assign '=' 0 undefined,
  BinaryOperatorInfo Plus '+' 1 (const2 (+)),
  BinaryOperatorInfo Minus '-' 1 (const2 (-)),
  BinaryOperatorInfo Multiply '*' 2 (const2 (*)),
  BinaryOperatorInfo Divide '/' 2 divide,
  BinaryOperatorInfo Power '^' 3 (const2 (**))]
Заметьте, в таблице указаны приоритеты операторов, которые учитывает парсер. При добавлении нового оператора код парсера менять не нужно – он универсален.

Ну и таблица констант. 2 строчки:
constants :: (Floating a) => [(String, a)]
constants = [("pi", pi), ("e", exp 1)]

Как видите, я в коде решил попробовать исключения. Впечатление двоякое: с одной стороны, он делает код простым и избавляет от необходимости протаскивать значение наверх по стеку функций, с другой – исключения в чистом языке являются несколько чужеродными. Кроме того, возникли проблемы с ленивостью, точнее непонимания мною ленивых вычислений, в результате чего мой первоначальный код не ловил исключения, несмотря на то что был обёрнут в catch-блок. Проблема решилась заменой функции return на функцию evaluate. В общем, вывод таков – исключения лучше использовать только при работе с IO. В чистые функции их не стоит пихать.

Вопрос читателям: нормально ли оформлен код? Я что-то всё никак не могу понять, как лучше всего вставлять код в посты с красивой подсветкой и рамкой.