I asked about this before, but it seems I phrased the question too narrowly. So let's see if I can explain what I'm actually after.
Suppose I have some type that supports several binary operators, each with varying precedence and associativity. How do I write a Show
instance that correctly brackets sub-expressions?
I know I'm being dense here, but I get this wrong every single time I try to do it. There must be some mechanical procedure you can follow to make this work out correctly, but I cannot find it. Can somebody walk me through an example?
I know this ultimately boils down to wrapping everything in showParen
, and showing sub-expressions using showsPrec
with the right magic number, and I can make it almost work, but it never quite works right in all circumstances.
Edit: Consider the following code
data Expr = Const Int | Expr :+: Expr | Expr :-: Expr | Expr :*: Expr | Expr :/: Expr infixl 6 :+: infixl 6 :-: infixl 7 :*: infixl 7 :/: instance Show Expr where showsPrec p e0 = case e0 of Const n -> shows n x :+: y -> showParen (p > 6) $ (showsPrec 6 x) . (" :+: " ++) . (showsPrec 6 y) x :-: y -> showParen (p > 6) $ (showsPrec 6 x) . (" :-: " ++) . (showsPrec 6 y) x :*: y -> showParen (p > 7) $ (showsPrec 7 x) . (" :*: " ++) . (showsPrec 7 y) x :/: y -> showParen (p > 7) $ (showsPrec 7 x) . (" :/: " ++) . (showsPrec 7 y)
This almost works correctly:
*Main> Const 1 :+: Const 2 :*: Const 3 :+: Const 4 1 :+: 2 :*: 3 :+: 4 *Main> (Const 1 :+: Const 2) :*: (Const 3 :+: Const 4) (1 :+: 2) :*: (3 :+: 4)
But not quite:
*Main> Const 1 :+: Const 2 :-: Const 3 :-: Const 4 1 :+: 2 :-: 3 :-: 4 *Main> Const 1 :+: Const 2 :-: (Const 3 :-: Const 4) 1 :+: 2 :-: 3 :-: 4
So it looks like the precedence is OK, but the associativity is borked.
Operator precedence determines the grouping of terms in an expression and decides how an expression is evaluated. Certain operators have higher precedence than others; for example, the multiplication operator has a higher precedence than the addition operator.
The multiplication, modulus and division are evaluated first in left-to-right order (i.e., they associate from left to right) because they have higher precedence than addition and subtraction. The addition and subtraction are applied next. These are also applied left to right.
Operator precedence specifies the order of operations in expressions that contain more than one operator. Operator associativity specifies whether, in an expression that contains multiple operators with the same precedence, an operand is grouped with the one on its left or the one on its right.
The following Show
instance will print the Expr
type with minimal parentheses:
data Expr = Const Int | Expr :+: Expr | Expr :-: Expr | Expr :*: Expr | Expr :/: Expr infixl 6 :+: infixl 6 :-: infixl 7 :*: infixl 7 :/: instance Show Expr where showsPrec p e0 = case e0 of Const n -> showParen (p > 10) $ showString "Const " . showsPrec 11 n x :+: y -> showParen (p > 6) $ showsPrec 6 x . showString " :+: " . showsPrec 7 y x :-: y -> showParen (p > 6) $ showsPrec 6 x . showString " :-: " . showsPrec 7 y x :*: y -> showParen (p > 7) $ showsPrec 7 x . showString " :*: " . showsPrec 8 y x :/: y -> showParen (p > 7) $ showsPrec 7 x . showString " :/: " . showsPrec 8 y
This results in:
*Main> Const 1 :+: Const 2 :*: Const 3 :+: Const 4 Const 1 :+: Const 2 :*: Const 3 :+: Const 4 *Main> (Const 1 :+: Const 2) :*: (Const 3 :+: Const 4) (Const 1 :+: Const 2) :*: (Const 3 :+: Const 4) *Main> Const 1 :+: Const 2 :-: Const 3 :-: Const 4 Const 1 :+: Const 2 :-: Const 3 :-: Const 4 *Main> Const 1 :+: Const 2 :-: (Const 3 :-: Const 4) Const 1 :+: Const 2 :-: (Const 3 :-: Const 4)
The general rule is
infix n
: use showParen (p > n)
, showsPrec (n+1)
on the left, and showsPrec (n+1)
on the rightinfixl n
: use showParen (p > n)
, showsPrec n
on the left, and showsPrec (n+1)
on the rightinfixr n
: use showParen (p > n)
, showsPrec (n+1)
on the left, and showsPrec n
on the rightshowParen (p > 10)
and showsPrec 11
on the argumentsFollowing this rule will always yield correct syntax with minimal parentheses, except in one corner case: It can produce ambiguous output if you have infixl
and infixr
constructors with the same precedence level. As long as you don't do that, you should be fine.
How did I know what arguments to use with showParen
? It's to match what Haskell does for derived Show
instances. We can test those like this:
data T = P :# P | T P deriving Show infix 6 :# data P = P instance Show P where showsPrec p P = shows p
We can see that with infix 6 :#
, the Show T
instance calls showsPrec 7
on the arguments to :#
, and also it shows parentheses only at precedences > 6:
*Main> showsPrec 6 (P :# P) "" "7 :# 7" *Main> showsPrec 7 (P :# P) "" "(7 :# 7)"
And for the ordinary constructor T
, the generated instance calls showsPrec 11
on the argument and shows parens at precedences > 10:
*Main> showsPrec 10 (T P) "" "T 11" *Main> showsPrec 11 (T P) "" "(T 11)"
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