Я пишу переводчика для небольшого языка. Этот язык поддерживает мутацию, поэтому его оценщик отслеживает Store
для всех переменных (где type Store = Map.Map Address Value
, type Address = Int
и data Value
- это специфический для языка ADT).Как я могу работать во вложенных монадах чисто?
Также возможно, чтобы вычисления были сбой (например, деление на ноль), поэтому результат должен быть Either String Value
.
Тип моего переводчика, то есть
eval :: Environment -> Expression -> State Store (Either String Value)
где type Environment = Map.Map Identifier Address
отслеживает локальных привязок.
Например, интерпретации постоянного Литерал не нужно трогать магазин, и результат всегда удается, так
eval _ (LiteralExpression v) = return $ Right v
Но когда мы применяем бинарную операцию, нам необходимо рассмотреть магазин. Например, если пользователь производит вычисления (+ (x <- (+ x 1)) (x <- (+ x 1)))
и x
изначально 0
, тогда конечный результат должен быть 3
, а x
должен быть 2
в полученном магазине. Это приводит к случаю
eval env (BinaryOperator op l r) = do
lval <- eval env l
rval <- eval env r
return $ join $ liftM2 (applyBinop op) lval rval
Обратите внимание, что do
-notation работает в State Store
монады. Кроме того, использование return
мономорфно в State Store
, а использование join
и liftM2
мономорфно в монаде Either String
. То есть, здесь мы используем
(return . join) :: Either String (Either String Value) -> State Store (Either String Value)
и return . join
не является не-ор.
(Как видно, applyBinop :: Identifier -> Value -> Value -> Either String Value
.)
Это кажется запутанным, в лучшем случае, и это относительно простой случай. Случай приложения функции, например, значительно сложнее.
Какие полезные рекомендации следует знать о том, чтобы мой код был удобочитаемым и доступным для записи?
EDIT: Вот более типичный пример, который лучше демонстрирует уродство. Вариант NewArrayC
имеет параметры length :: Expression
и element :: Expression
(он создает массив заданной длины со всеми элементами, инициализированными константой). Простым примером является (newArray 3 "foo")
, который дает ["foo", "foo", "foo"]
, но мы также можем написать (newArray (+ 1 2) (concat "fo" "oo"))
, потому что мы можем иметь произвольные выражения в NewArrayC
. Но когда мы на самом деле назвать
allocateMany :: Int -> Value -> State Store Address,
, который принимает количество элементов для распределения и значение для каждого слота, и возвращает начальный адрес, мы должны распаковать эти значения. В логике ниже вы можете увидеть, что я дублирую кучу логики, которая должна быть встроена в монаду Either
. Все case
s должны просто связываться.
eval env (NewArrayC len el) = do
lenVal <- eval env len
elVal <- eval env el
case lenVal of
Right (NumV lenNum) -> case elVal of
Right val -> do
addr <- allocateMany lenNum val
return $ Right $ ArrayV addr lenNum -- result data type
left -> return left
Right _ -> return $ Left "expected number in new-array length"
left -> return left
Это замечательно. Спасибо. – wchargin
Вы правы, что я «хочу, чтобы бит состояния был самым внешним» (в частности, это связано с тем, что поиск идентификатора может быть неудачным в зависимости от состояния), но я не понимаю, как использовать 'ExceptT String (State Store)' вместо ' StateT Store (любая строка) 'выполняет это. Фактически, [этот пост, кажется, предлагает иначе] (http://stackoverflow.com/a/5076096/732016). Не могли бы вы объяснить? – wchargin
Просто продолжайте расширять новые типы, пока не получите что-то у основания. Вы увидите разницу довольно быстро: 'ExceptT String (State Store) a' расширяется до' Store -> (Store, Any String a) 'while' StateT Store (кроме String) a' расширяется до 'Store -> Либо строка (Store, a) '. Конечно, ты хочешь, конечно. –