Включаем все предупреждения компилятора
{-# OPTIONS_GHC -Wall -Wextra #-}
Объявляем имя модуля
module SimpleInterpreter where
Импортируем модуль RecursiveParser, поскольку интерпретировать будем деревья выражений, определённые в этом модуле.
import RecursiveParser
Дополнительно импортируем функцию liftA2 из стандартного модуля Control.Applicative.
import Control.Applicative (liftA2)
Основная точка входа: функция interpret. Принимает словарь переменных (в виде списка пар ключ-значение), и дерево выражения для интерпретации.
Возвращает либо сообщение об ошибке, либо значение типа Double – результат вычисления выражения.
Здесь, чтобы не таскать всюду словарь переменных, мы объявляем функцию go, которая принимает дерево выражения и возвращает результат вычисления этого дерева, а словарь переменных остаётся на верхнем уровне.
interpret :: [(String, Double)] -> Expr -> Either String Double
= go
interpret vars where
BinOp op e1 e2) = liftA2 (opFunc op) (go e1) (go e2)
go (Identifier str) =
go (maybe (Left $ "Undefined variable " <> str) Right $ lookup str vars
ENumber val) = Right val
go (UnaryMinus e1) = negate <$> go e1 go (
Функция liftA2 работает аналогично функции fmap, но для функций двух аргументов. При этом аргументы вычисляются слева направо. Для случая (Either a b), конструкция liftA2 f x y может быть эквивалентно записана в виде
= case x of
liftA2 f x y Right x' -> case y of
Right y' -> Right (f x' y')
Left err -> Left err
Left err -> Left err
Функция opFunc принимает название оператора и возвращает бинарную функцию, соответствующую этому оператору:
opFunc :: Operator -> Double -> Double -> Double
OpPlus = (+)
opFunc OpMinus = (-)
opFunc OpMult = (*)
opFunc OpDiv = (/)
opFunc OpExp = (**) opFunc
Операнды вычисляются рекурсивным вызовом функции go.
“Вычисление” идентификатора сводится к поиску его в словаре переменных. Функция lookup ищет в словаре, переданным вторым аргументом, первую запись, совпадающую с ключом, переданным первым аргументом, и возвращает значение, обёрнутое в тип Maybe. Если соответствующая запись не найдена, то результат – Nothing.
lookup :: k -> [(k, v)] -> v
Функция maybe возвращает первый аргумент, если третий аргумент – Nothing, и применяет к значению в третьем аргументе функцию, переданную вторым аргументом в противном случае.
maybe :: b -> (a -> b) -> Maybe a -> b
Поведение maybe эквивалентно следующей конструкции:
maybe def f val = case val of
Nothing -> def
Just x -> f x
“Вычисление” числового литерала – это просто возврат его значения.
Наконец, применение унарного минуса изменяет знак, как функция negate. Здесь снова используется оператор (<$>), позволяющий применить функцию прямо к значению (Double), если оно есть, несмотря на то, что функция go возвращает (Either String Double)