SimpleInterpreter

Включаем все предупреждения компилятора

{-# 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
interpret vars = go
  where
  go (BinOp op e1 e2) = liftA2 (opFunc op) (go e1) (go e2)
  go (Identifier str) =
    maybe (Left $ "Undefined variable " <> str) Right $ lookup str vars
  go (ENumber val) = Right val
  go (UnaryMinus e1) = negate <$> go e1

Функция liftA2 работает аналогично функции fmap, но для функций двух аргументов. При этом аргументы вычисляются слева направо. Для случая (Either a b), конструкция liftA2 f x y может быть эквивалентно записана в виде

liftA2 f x y = case x of
  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
opFunc OpPlus = (+)
opFunc OpMinus = (-)
opFunc OpMult = (*)
opFunc OpDiv = (/)
opFunc OpExp = (**)

Операнды вычисляются рекурсивным вызовом функции 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)