2015-11-14 5 views
13

私は小さな言語のための通訳を書いています。 この言語は突然変異をサポートしているため、評価者はすべての変数(type Store = Map.Map Address Value,type Address = Int、およびdata Valueが言語固有のADTである)のStoreを追跡します。ネストされたモナドではどうすればきれいに作業できますか?

計算が失敗する可能性もあります(ゼロで割るなど)。結果はEither String Valueである必要があります。

私の通訳の種類は、その後、type Environment = Map.Map Identifier Addressはローカルな束縛を追跡

eval :: Environment -> Expression -> State Store (Either String Value) 

です。例えば

、定数リテラルを解釈することは店をタッチする必要があり、結果は常に成功し、そう

eval _ (LiteralExpression v) = return $ Right v 

しかし、我々は二項演算子を適用すると、私たちは店を検討する必要もありません。 たとえば、ユーザーが(+ (x <- (+ x 1)) (x <- (+ x 1)))およびxを最初に評価する場合、最終結果は3であり、結果のストアではxはである必要があります。 これはdo -notationがState Storeモナド内で作業されている場合

eval env (BinaryOperator op l r) = do 
    lval <- eval env l 
    rval <- eval env r 
    return $ join $ liftM2 (applyBinop op) lval rval 

注意につながります。 さらに、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 :: Expressionelement :: Expression(すべての要素を定数に初期化して所定の長さの配列を作成します)を持ちます。 単純な例はで、["foo", "foo", "foo"]が得られますが、NewArrayCに任意の式を入れることができるので、(newArray (+ 1 2) (concat "fo" "oo"))と書くこともできます。 しかし、我々は実際に割り当てる要素の数と各スロットの値をとり、開始アドレスを返し

allocateMany :: Int -> Value -> State Store Address, 

を呼び出すとき、我々は、これらの値を展開する必要があります。 以下のロジックでは、Eitherモナドに組み込む必要があるロジックの束を複製していることがわかります。 caseはすべてバインドする必要があります。

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 

答えて

12

これはモナドトランスのためのものです。スタックに状態を追加するにはStateTの変圧器があり、Eitherのような不具合をスタックに追加するにはEitherT変圧器があります。しかし、私はExceptT(これはExceptのような失敗を追加する)を好んでいるので、私はその点で議論をするつもりです。ステートフルビットを最も外側にしたいので、モナドとしてExceptT e (State s)を使用する必要があります。

type DSL = ExceptT String (State Store) 

ステートフル動作がgetputをスペルことができ、これらは、MonadStateのすべてのインスタンス上の多型であることに留意されたいです。特に彼らは私たちのDSLモナドでうまくいくでしょう。同様に、エラーを発生させる正規の方法は、MonadError Stringのすべてのインスタンスにわたって多形であるthrowErrorです。特にDSLモナドで問題なく動作します。

だから今、私たちは

eval :: Environment -> Expression -> DSL Value 
eval _ (Literal v) = return v 
eval e (Binary op l r) = liftM2 (applyBinop op) (eval e l) (eval e r) 

あなたはまた、それ以上の多型タイプevalを与えることを検討かもしれません書きます。 DSL Valueの代わりに(MonadError String m, MonadState Store m) => m Valueを返すことができます。このタイプについての関心の2枚があります

allocateMany :: MonadState Store m => Int -> Value -> m Address 

:それはすべてのMonadState Store mのインスタンスを超える多型であるため、まず、あなたは同じように確認することができます実際には、allocateManyのために、それはあなたがそれを多相型を与えることが重要ですあたかもあなたが提案したタイプInt -> Value -> State Store Addressを持っているかのように、ステートフルな副作用しかありません。しかし、それは多型であるため、DSL Addressを返すように特化することもできるので、例えばevalで使用できます。あなたの例evalコードはこのようになります。

eval env (NewArrayC len el) = do 
    lenVal <- eval env len 
    elVal <- eval env el 
    case lenVal of 
     NumV lenNum -> allocateMany lenNum elVal 
     _   -> throwError "expected number in new-array length" 

私は本当に、非常に読みやすいと思います。そこに余りにも無関係なもの。

+0

これは素晴らしいです。ありがとうございました。 – wchargin

+0

あなたが正しいと思うのは、ステートフルなビットを最外にしたいということです(特に、これは識別子の参照が状態によって失敗する可能性があるためです)。しかし、 'ExceptT String(State Store) StateT Store(いずれかの文字列)がこれを実行します。実際、[この投稿は別のことを示唆しているようです](http://stackoverflow.com/a/5076096/732016)。説明していただけますか? – wchargin

+0

あなたは何かをベースで取得するまで、newtypesを拡張し続けるだけです。 'ExceptionT String(State Store)a'は' Store->(Store、Either String a) 'に展開され、' StateT Store(Stringを除く)は 'Store' - >どちらの文字列にも展開されます(Store、a) 'とする。あなたが望むものは、もちろんあなた次第です。 –

関連する問題