It’s not clear from your example a+(b/c)
, but I assume you need to account for operator precedence, so that a+b/c
parses as a+(b/c)
(not (a+b)/c
), and thus also evaluates to abc/+
(not ab+c/
).
There are perhaps simpler or more idiomatic ways of going about this, but as an educational task this is a good way to learn about working with just basic recursive functions. There are two main ways to solve this task this way:
The former is more flexible and ultimately the basis of what an idiomatic Haskell solution would look like (using parser combinators), but the shunting yard algorithm has the distinct advantage here of being a simpler algorithm specifically for this task of infix/postfix conversion.
What I’ll do is sketch out and describe the structure of the implementation, to help you get unstuck on the general method, and give you the task of filling in the details.
The algorithm maintains two pieces of state, a stack of operators, and a queue of output. We can represent both as a list of characters:
type ShuntingYardState = ([Char], [Char])
To push an element to the stack or enqueue an element in the output, you’ll cons :
it onto the front of the list; to pop an element from the stack, you can use pattern matching. The output queue is strictly an accumulator for results; we never dequeue from it.
To convert an infix expression string to postfix, you start this algorithm with the initial state of an empty operator stack and empty output queue:
expression :: String -> String
expression input = shuntingYard ([], []) input
The algorithm itself has five main cases and one error case for you to handle:
shuntingYard
:: ShuntingYardState
-> String
-> String
shuntingYard
state@(operatorStack, outputQueue)
(current : rest)
-- 1. A variable term: push it to the output and proceed.
| isVariable current
= shuntingYard (variable current state) rest
-- 2. An operator: process operator precedence and proceed.
| isOperator current
= shuntingYard (operator current state) rest
-- 3. A left parenthesis: push it onto the operator stack.
| current == '('
= shuntingYard (leftParenthesis state) rest
-- 4. A right parenthesis: process grouping and proceed.
| current == ')'
= shuntingYard (rightParenthesis state) rest
-- 5. An unrecognized token: raise an error.
| otherwise
= error $ "unrecognized input: " ++ show rest
-- 6. No more input: finalize the result.
shuntingYard state []
= endOfInput state
You need to fill in the definitions of the functions implementing each case above, marked in bold. Here are their signatures and a description of their function.
Identifies a variable token, like your isLetHelp
:
isVariable :: Char -> Bool
Identifies an operator token (not a parenthesis), like your isOpHelp
:
isOperator :: Char -> Bool
Pushes a variable to the output queue:
variable :: Char -> ShuntingYardState -> ShuntingYardState
Processes an operator. This function has two cases, sketched below. In the first case, it moves operators from the operator stack to the output queue as long as they have either greater precedence than the current token (for right-associative operators like ^
), or greater or equal precedence (for left-associative operators like *
and -
). In the second case, it simply pushes the current operator token to the operator stack.
operator :: Char -> ShuntingYardState -> ShuntingYardState
operator current (op : operatorStack, outputQueue)
| op /= '('
, … -- Compare precedence & associativity.
= operator … -- Repeat.
operator current (operatorStack, outputQueue)
= … -- Push the operator and return.
Processes a left parenthesis by pushing it to the operator stack.
leftParenthesis :: ShuntingYardState -> ShuntingYardState
Processing a right parenthesis likewise has two cases: as long as there are operators remaining, move them to the output; if there are none, then expect a matching left parenthesis, or else raise an error.
rightParenthesis :: ShuntingYardState -> ShuntingYardState
rightParenthesis (op : operatorStack, outputQueue)
| op /= '('
= rightParenthesis … -- Move operator to output.
| otherwise
= … -- Reached matching left parenthesis; return.
rightParenthesis ([], outputQueue)
= … -- Mismatched right parenthesis; error.
Finally, when reaching the end of input, there are three cases. If the operator stack is empty, you can convert the queue to the final output string. Otherwise, if there are operators remaining, move them one by one to the output; if any parentheses remain, they’re missing matching right parentheses, so this is an error.
endOfInput
:: ShuntingYardState
-> String
endOfInput ([], outputQueue)
= … -- Success! Return the final result.
endOfInput (op : operatorStack, outputQueue)
| op == '('
= … -- Mismatched left parenthesis; error.
| otherwise
= … -- Operator remaining; move to output and repeat.