Skip to content

Read1/Show1 instances for Complex use incorrect precedence

The Read1 and Show1 instances for Complex handle precedence in a way that is inconsistent with Complex's Read and Show instances. This is best illustrated by example:

module Main (main) where

import Data.Complex (Complex(..))
import Data.Functor.Classes (readPrec1, showsPrec1)
import Text.ParserCombinators.ReadPrec (readPrec_to_S)
import Text.Read (Read(..))

comp :: Complex Int
comp = 1 :+ 1

compareInstances :: Int -> IO ()
compareInstances p = do
  let precBanner = " (at precedence " ++ show p ++ ")"
  putStrLn $ "Read vs. Read1" ++ precBanner
  print (readPrec_to_S readPrec  p "1 :+ 1" :: [(Complex Int, String)])
  print (readPrec_to_S readPrec1 p "1 :+ 1" :: [(Complex Int, String)])
  putStrLn ""
  putStrLn $ "Show vs. Show1" ++ precBanner
  putStrLn $ showsPrec  p comp ""
  putStrLn $ showsPrec1 p comp ""
  putStrLn ""

main :: IO ()
main = do
  compareInstances 6
  compareInstances 7

If you run this with GHC 9.2.1-alpha1, it will produce the following output:

Read vs. Read1 (at precedence 6)
[(1 :+ 1,"")]
[(1 :+ 1,"")]

Show vs. Show1 (at precedence 6)
1 :+ 1
1 :+ 1

Read vs. Read1 (at precedence 7)
[]
[(1 :+ 1,"")]

Show vs. Show1 (at precedence 7)
(1 :+ 1)
1 :+ 1

At precedence 6, the Read1/Show1 instances behave analogously to the Read/Show instances, as expected. At precedence 7, however, they produce different results! In particular:

  • The Read instance will fail to parse "1 :+ 1" at precedence 7, since it should be surrounded by parentheses at that precedence due to :+'s fixity of 6. The Read1 instance, however, will incorrectly parse the same string without the required parentheses.
  • The Show instance will print 1 :+ 1 with surrounding parentheses at precedence 7, again because of :+'s fixity of 6. The Show1 instance, however, will incorrectly omit these parentheses.

Both issues are caused by the Read1/Show1 instances incorrectly assuming that :+ has a fixity of 9, rather than its actual fixity of 6. Because the Read and Show instances are derived, it automatically uses the correct fixity, but the handwritten Read1/Show1 instances do not mirror this. This could be fixed as simply as:

diff --git a/libraries/base/Data/Functor/Classes.hs b/libraries/base/Data/Functor/Classes.hs
index 6a0d008982..d672c340d7 100644
--- a/libraries/base/Data/Functor/Classes.hs
+++ b/libraries/base/Data/Functor/Classes.hs
@@ -848,11 +848,13 @@ instance Eq1 Complex where
 -- [(2 % 3 :+ 3 % 4,"")]
 --
 instance Read1 Complex where
-    liftReadPrec rp _  = parens $ prec 9 $ do
+    liftReadPrec rp _  = parens $ prec complexPrec $ do
         x <- step rp
         expectP (Symbol ":+")
         y <- step rp
         return (x :+ y)
+      where
+        complexPrec = 6
 
     liftReadListPrec = liftReadListPrecDefault
     liftReadList     = liftReadListDefault
@@ -863,8 +865,10 @@ instance Read1 Complex where
 -- "2 :+ 3"
 --
 instance Show1 Complex where
-    liftShowsPrec sp _ d (x :+ y) = showParen (d >= 10) $
-        sp 10 x . showString " :+ " . sp 10 y
+    liftShowsPrec sp _ d (x :+ y) = showParen (d > complexPrec) $
+        sp (complexPrec+1) x . showString " :+ " . sp (complexPrec+1) y
+      where
+        complexPrec = 6
 
 -- Building blocks

These instances were introduced in GHC 9.2, so there's still time to fix them before the final 9.2.1 release. Patch incoming.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information