-1

I'm trying to make a function for syntax tree arithmetic operations and thus far I'm almost where I want it to be. In the code I attach you can see my current function definitions. eval is the function that decides what to do with each operation and foldAndPropagateConstants is the main function. parse is a simple parser that takes a String of a mathematical expression and returns the equivalent tree. e.g.

ghci> parse "3+x"
BinaryOperation Plus (Leaf (Constant 3)) (Leaf (Variable "x"))

The issue I'm facing is how to have the evaluated values to be used in subsequent operations. For example, this operation should work as follows:

ghci> foldAndPropagateConstants [("x", parse "1+2+3"), ("y", parse "5*x + 7")]
[("x",Leaf (Constant 6)),("y",Leaf (Constant 37))]

Notice that the function should use the value that "x" got, when calculating the value for "y". Thing is, I can't seem to find a way to use "x"'s value in my eval function.

--foldAndPropagateConstants :: [(String, Exprv)] -> [(String, ExprV)]

eval :: ExprV -> Int
eval (Leaf (Variable n)) = --this part is what's missing
eval (Leaf (Constant n)) = n
eval (BinaryOperation Plus expr1 expr2) = eval expr1 + eval expr2
eval (BinaryOperation Times expr1 expr2) = eval expr1 * eval expr2
eval (UnaryOperation Minus expr1) = -1 * eval expr1

foldAndPropagateConstants (x:xs) = [(fst x, parse (show (eval(snd x)))) ] : foldAndPropagateConstants xs
foldAndPropagateConstants _ = []
Will Ness
  • 70,110
  • 9
  • 98
  • 181
MoodMojo
  • 47
  • 5
  • 5
    Usually this is done by adding an "environment" parameter to `eval` which stores the values of all variables, as a `Data.Map` or similar. – luqui Mar 28 '20 at 15:35
  • Probably should've pointed out that I'm rather new to Haskell. Not sure how that would be done. @luqui – MoodMojo Mar 28 '20 at 15:42
  • Adding a parameter to your function's type and definition is something you can learn by reading any number of resources. –  Mar 28 '20 at 18:04
  • As @luqui said, the best is to use an environment such as as List or a Map, in which you store variables and their values – olirwin Mar 28 '20 at 18:53

2 Answers2

3

Edit: I seem to have answered only this part of the question:

I can't seem to find a way to use "x" value in my eval function.

Since your question does not include a Minimal, Reproducible Example, here is a simplified version of what you seem to be doing (that doesn't contain variables) that both contains the data definition and the eval function:

module Eval where

data Expr
  = Constant Int
  | UnOp UnaryOperation Expr
  | BinOp BinaryOperation Expr Expr
  deriving (Eq, Show)

data UnaryOperation
  = UnaryMinus
  | UnaryFactorial
  | UnaryAbsolute
  deriving (Eq, Show)

data BinaryOperation
  = Plus
  | Minus
  | Times
  | Divide
  deriving (Eq, Show)

eval :: Expr -> Int
eval (Constant n) = n
eval (UnOp UnaryMinus e) = negate (eval e)
eval (UnOp UnaryFactorial e) = product [1..eval e]
eval (UnOp UnaryAbsolute e) = abs (eval e)
eval (BinOp bop e1 e2) = evalBinOp bop (eval e1) (eval e2)

evalBinOp :: BinaryOperation -> Int -> Int -> Int
evalBinOp Plus = (+)
evalBinOp Minus = (-)
evalBinOp Times = (*)
evalBinOp Divide = div

Extending this evaluator with another constructor in data Expr, and extending the eval function with an "environment" as luqui suggests, which in this case is a list of name-value pairs:

data Expr
  = Constant Int
  | Variable String
  | UnOp UnaryOperation Expr
  | BinOp BinaryOperation Expr Expr
  deriving (Eq, Show)

-- ...

eval :: Expr -> [(String, Int)] -> Int
eval (Constant n) _env = n
eval (Variable s) env = lookup' s env
eval (UnOp UnaryMinus e) env = negate (eval e env)
eval (UnOp UnaryFactorial e) env = product [1..eval e env]
eval (UnOp UnaryAbsolute e) env = abs (eval e env)
eval (BinOp bop e1 e2) env = evalBinOp bop (eval e1 env) (eval e2 env)

-- ...

lookup' :: String -> [(String, Int)] -> Int
lookup' s [] = error ("Could not find variable " ++ s)
lookup' s ((t,n):env)
  | s == t = n
  | otherwise = lookup' s env

It seems to me that the most urgent improvement to this evaluator is better error handling using error-aware return types. I made the lookup' helper function because the standard library function Data.List.lookup uses the safer Maybe return type which would encourage the rewrite I'm suggesting:

eval :: Expr -> [(String, Int)] -> Maybe Int
eval (Constant n) _env = pure n
eval (Variable s) env = lookup s env
eval (UnOp UnaryMinus e) env =
  case eval e env of
    Just n -> pure (negate n)
    Nothing -> Nothing
eval (UnOp UnaryFactorial e) env =
  eval e env >>= \n ->
  pure (product [1..n])
eval (UnOp UnaryAbsolute e) env =
  abs <$> eval e env
eval (BinOp bop e1 e2) env = do
  n1 <- eval e1 env
  n2 <- eval e2 env
  pure (evalBinOp bop n1 n2)

I've used a different style in each function body, but they're all variations of a similar theme: case-of uses explicit pattern matching, which gets tedious. (Imagine doing the eval (BinOp ...) body using case-of.) Explicit use of the >>= operator is... I suppose some people like it, but do notation looks prettier. The <$> applicative style is the neatest of them all in this case, I think.

What you could do next is make the env implicit by using the Reader monad: It is a bit messy that only one function body in eval actually uses it, and all the others either throw it away or pass it along.

sshine
  • 15,635
  • 1
  • 41
  • 66
  • Neat. I haven't understood it fully yet, but may I ask why you use evalBinOp? is it like an added functionality? – MoodMojo Mar 31 '20 at 18:40
  • So, I think I understand your solution thus far, which I must say is pretty neat. One confusion I'm facing is how to pass the second variable in the definition of `foldAndPorpagateConstants` function. The definition I have: `foldAndPropagateConstants (x:xs) = [(fst x, parse (show (eval(snd x)))) ] : foldAndPropagateConstants xs` Gives me an error which I think is because eval has too few arguments after modifying it to have an "environment" variable. – MoodMojo Mar 31 '20 at 19:14
  • @MoodMojo: The `evalBinOp` is just to avoid four cases directly in `eval` just as there are three cases for `UnOp`. It is only a matter of convenience. The question you are asking about `foldAnd...` is outside the scope of this answer. Asking follow-up questions that warrant long answers in comments is not going to work on StackOverflow. – sshine Apr 02 '20 at 12:59
  • Sure. Your answer does cover what you said it does so thanks for that. – MoodMojo Apr 02 '20 at 19:01
2

What you want is for

foldAndPropagateConstants [("x", parse "1+2+3"), ("y", parse "5*x + 7"), ("z", parse "x+y-1")]

to be equivalent to

 =  let s0 = []

        r1 = parse' "1+2+3" s0
        -- r1 = Leaf (Constant 6)
        s1 = [("x",6)]

        r2 = parse' "5*x + 7" s1 
        -- r2 = Leaf (Constant 37)
        s2 = [("x",6),("y",37)]

        r3 = parse' "x+y-1" s2 
        -- r3 = Leaf (Constant 42)
        s3 = [("x",6),("y",37),("z",42)]
    in
       [r1,r2,r3]

parse' is like parse, but it can also consult a store of values known so far which it receives as its second argument.

The above is easier encoded with functions if restructured as

 =  let s0 = []

        (s1, r1) = parse'' "1+2+3" s0
        -- r1 = Leaf (Constant 6)
        -- s1 = [("x",6)]

        (s2, r2) = parse'' "5*x + 7" s1 
        -- r2 = Leaf (Constant 37)
        -- s2 = [("x",6),("y",37)]

        (s3, r3) = parse'' "x+y-1" s2 
        -- r3 = Leaf (Constant 42)
        -- s3 = [("x",6),("y",37),("z",42)]
    in
       snd (s3, [r1,r2,r3])

Incidentally this pattern of state-passing computations is known as State Monad, but that's a matter for another day.

The above fits the recursion pattern when expressed as

foldAndPropagateConstants [("x", parse "1+2+3"), ("y", parse "5*x + 7"), ("z", parse "x+y-1")] = 
    snd $ foldAndPropagateConstants' 
            [("x", parse "1+2+3"), ("y", parse "5*x + 7"), ("z", parse "x+y-1")] 
            []

foldAndPropagateConstants' [("x", parse "1+2+3"), ("y", parse "5*x + 7"), ("z", parse "x+y-1")] s0 =
    let 
        (s1, r1) = parse'' "1+2+3" s0
        (sn, rs) = foldAndPropagateConstants' [("y", parse "5*x + 7"), ("z", parse "x+y-1")] s1 
    in
       (sn, r1 : rs)

-- and

foldAndPropagateConstants' [] s0 = (s0, [])

And now, Generalize! (by replacing an example value with a symbolic one).

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • But how will it actually calculate the value of the expression? You don't seem to use a helper function for that purpose in your solution like `eval` so I'm not sure how you got this to work. – MoodMojo Mar 31 '20 at 18:35
  • I didn't go into the specifics here. Just concerned myself with the general structure of the code, as you asked how can the previously computed values be used in the subsequent computations (that's how I interpreted your question). – Will Ness Mar 31 '20 at 18:38
  • so this answer is an expansion of [luqui's comment](https://stackoverflow.com/questions/60901892/haskell-using-previously-computed-values-in-next-function-calls-in-a-sequence/60908414?noredirect=1#comment107750278_60901892). – Will Ness Mar 31 '20 at 18:38
  • Ah I see. Thanks once again, Will. :) – MoodMojo Mar 31 '20 at 18:45
  • I'll try to use what you guys explained, but to ensure I understood correctly, you are making another function, namely `foldAndPropagateConstants'` to keep the environment variable as luqui suggested, right? and the same with `parse'` – MoodMojo Mar 31 '20 at 18:56
  • something like that. and parse', (and parse'') need to be augmented accordingly to fit its new use. – Will Ness Mar 31 '20 at 18:58