Context
The question in the title is actually a generic version of a specific problem I am having. Feel free to answer the general question or this specific question below.
I am implementing some function that traverse a pure untyped lambda calculus AST and replacing the variables with De-Bruijn indices. There are two representation of the AST, external (with variable names) and internal (with indices):
type Id = String
data TermEx
= VarE Id
| LamE Id TermEx
| AppE TermEx TermEx
data TermIn
= VarI Int
| LamI TermIn
| AppI TermIn TermIn
A quick refresher on how De-Bruijn's indexing works is found on page 6 of this pdf:
http://ttic.uchicago.edu/~pl/classes/CMSC336-Winter08/lectures/lec4.pdf
Current Solution
A regular recursive function that does what I want:
encode' :: TermEx -> TermIn
encode' = go [] where
go en te = case te of
VarE x -> case elemIndex x en of
Just i -> VarI
LamE x t -> LamI $ go (x:en) t
AppE t t' -> AppI (go en t) (go en t') -- * see comment below
Comment: note function application AppI
constitutes to a split in the AST, and at each split, a fresh "local" state is spawned.
Question
Ideally, I want to describe this as some monadic computation that keeps track of a new local state each time the AST splits, my first attempt:
type DeBruijn = forall m. (Monad m, Functor m) => StateT [Id] m TermIn
does not work since all branches will share the same state, throwing off the index. So how do you describe this seemingly very common pattern of computation?
Steps to solve recurrence relation using recursion tree method: Draw a recursive tree for given recurrence relation. Calculate the cost at each level and count the total no of levels in the recursion tree. Count the total number of nodes in the last level and calculate the cost of the last level.
The recursion tree shows us that the results obtained from processing the two subtrees of the root N can be used to compute the result for the tree rooted at N. Similarly for other nodes. The leaves of this recursion tree would be fibonacci(1) or fibonacci(2) both of which represent the base cases for this recursion.
Recursion tree method is used to solve recurrence relations. Generally, these recurrence relations follow the divide and conquer approach to solve a problem, for example T(n) = T(n-1) + T(n-2) + k, is a recurrence relation as problem size 'n' is dividing into problems of size n-1 and n-2.
A recursive data structure is a data structure that is partially composed of smaller or simpler instances of the same data structure. For example, linked lists and binary trees can be viewed as recursive data structures.
You need the Reader monad transformer:
import Control.Monad.Trans.Reader
import Control.Applicative
import Data.Maybe
import Data.List
type DeBruijnT = ReaderT [Id]
encode :: (Applicative m, Monad m) => TermEx -> DeBrujinT m TermIn
encode t = case t of
VarE x -> reader (maybe (error "!") VarI . elemIndex x)
LamE x t -> withReaderT (x:) $ LamI <$> encode t
AppE t t' -> AppI <$> encode t <*> encode t'
Here, withReaderT executes its computation (2nd arg) in local environment, which is the result of applying 1st arg to current environment, so, when new bindings are introduced in branches after split, they would not mess up in the same environment.
Also, if you really want to create new environments on split only, and keep old one on new variable introductions, you can use State monad transformer in such way:
type DeBruijnT = StateT [Id]
encode :: (Applicative m, Monad m) => TermEx -> DeBruijnT m TermIn
encode t = case t of
VarE x -> gets (maybe (error "!") VarI . elemIndex x)
LamE x t -> withStateT (x:) $ LamI <$> encode t
AppE t t' -> do
s <- get
AppI <$> lift (evalStateT (encode t ) s)
<*> lift (evalStateT (encode t') s)
Here, instead of binding subcomputations on split to current monadic thread, new monadic threads are spawned and evaluated with current state.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With