2

If I have a program like

main = print ( (+) <$> Just 1 <*> Just 2 )

will the compiler decide to reduce the parts of my program that do not depend on IO like

( (+) <$> Just 1 <*> Just 2 ) => (Just 3)?

Or does the program still create a new function from (+) <$> Just 1 and then apply that to Just 2 at run-time?

Michiel Borkent
  • 34,228
  • 15
  • 86
  • 149
  • 1
    Closely related: [*Why are (constant) expressions not evaluated at compile time in Haskell?*](https://stackoverflow.com/q/19259114/2751851); [*Evaluating a function at compile time with Template Haskell*](https://stackoverflow.com/q/9243794/2751851); [*Force pre-computation of a constant*](https://stackoverflow.com/q/6122287/2751851). For a simple example of why that doesn't happen automatically, consider `(+) <$> Just (length [1..]) <*> Just 2`. – duplode Mar 19 '18 at 12:52
  • Thank you! Is this basically the halting problem as in the compiler cannot analyze if an expression will terminate during compile time? – Michiel Borkent Mar 19 '18 at 12:58
  • 2
    Yup, that's the basic idea -- though terminating after a very long time would be bad as well. Also, as Daniel Wagner points out, GHC does what you describe in some very specific cases. – duplode Mar 19 '18 at 13:08

1 Answers1

8

Let's ask GHC!

% echo 'main = print ((+) <$> Just 1 <*> Just 2)' > test.hs
% ghc -O2 -ddump-simpl test.hs
[1 of 1] Compiling Main             ( test.hs, test.o )

==================== Tidy Core ====================
Result size of Tidy Core
  = {terms: 42, types: 47, coercions: 9, joins: 0/0}

-- RHS size: {terms: 1, types: 0, coercions: 0, joins: 0/0}
Main.main4 :: Integer
[GblId,
 Caf=NoCafRefs,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 100 0}]
Main.main4 = 3

-- RHS size: {terms: 9, types: 11, coercions: 0, joins: 0/0}
Main.main3 :: [Char]
[GblId,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=False, ConLike=False,
         WorkFree=False, Expandable=False, Guidance=IF_ARGS [] 60 30}]
Main.main3
  = case GHC.Show.$w$cshowsPrec4 11# Main.main4 (GHC.Types.[] @ Char)
    of
    { (# ww3_a23H, ww4_a23I #) ->
    GHC.Types.: @ Char ww3_a23H ww4_a23I
    }

-- RHS size: {terms: 3, types: 1, coercions: 0, joins: 0/0}
Main.main2 :: [Char]
[GblId,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=False, ConLike=False,
         WorkFree=False, Expandable=False, Guidance=IF_ARGS [] 30 0}]
Main.main2 = ++ @ Char GHC.Show.$fShowMaybe1 Main.main3

-- RHS size: {terms: 4, types: 0, coercions: 0, joins: 0/0}
Main.main1
  :: GHC.Prim.State# GHC.Prim.RealWorld
     -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)
[GblId,
 Arity=1,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 40 60}]
Main.main1
  = GHC.IO.Handle.Text.hPutStr2
      GHC.IO.Handle.FD.stdout Main.main2 GHC.Types.True

-- RHS size: {terms: 1, types: 0, coercions: 3, joins: 0/0}
main :: IO ()
[GblId,
 Arity=1,
 Unf=Unf{Src=InlineStable, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True,
         Guidance=ALWAYS_IF(arity=0,unsat_ok=True,boring_ok=True)
         Tmpl= Main.main1
               `cast` (Sym (GHC.Types.N:IO[0] <()>_R)
                       :: ((GHC.Prim.State# GHC.Prim.RealWorld
                            -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)) :: *)
                          ~R#
                          (IO () :: *))}]
main
  = Main.main1
    `cast` (Sym (GHC.Types.N:IO[0] <()>_R)
            :: ((GHC.Prim.State# GHC.Prim.RealWorld
                 -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)) :: *)
               ~R#
               (IO () :: *))

-- RHS size: {terms: 2, types: 1, coercions: 3, joins: 0/0}
Main.main5
  :: GHC.Prim.State# GHC.Prim.RealWorld
     -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)
[GblId,
 Arity=1,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 20 60}]
Main.main5
  = GHC.TopHandler.runMainIO1
      @ ()
      (Main.main1
       `cast` (Sym (GHC.Types.N:IO[0] <()>_R)
               :: ((GHC.Prim.State# GHC.Prim.RealWorld
                    -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)) :: *)
                  ~R#
                  (IO () :: *)))

-- RHS size: {terms: 1, types: 0, coercions: 3, joins: 0/0}
:Main.main :: IO ()
[GblId,
 Arity=1,
 Unf=Unf{Src=InlineStable, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True,
         Guidance=ALWAYS_IF(arity=0,unsat_ok=True,boring_ok=True)
         Tmpl= Main.main5
               `cast` (Sym (GHC.Types.N:IO[0] <()>_R)
                       :: ((GHC.Prim.State# GHC.Prim.RealWorld
                            -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)) :: *)
                          ~R#
                          (IO () :: *))}]
:Main.main
  = Main.main5
    `cast` (Sym (GHC.Types.N:IO[0] <()>_R)
            :: ((GHC.Prim.State# GHC.Prim.RealWorld
                 -> (# GHC.Prim.State# GHC.Prim.RealWorld, () #)) :: *)
               ~R#
               (IO () :: *))

-- RHS size: {terms: 1, types: 0, coercions: 0, joins: 0/0}
Main.$trModule4 :: GHC.Prim.Addr#
[GblId,
 Caf=NoCafRefs,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 20 0}]
Main.$trModule4 = "main"#

-- RHS size: {terms: 2, types: 0, coercions: 0, joins: 0/0}
Main.$trModule3 :: GHC.Types.TrName
[GblId,
 Caf=NoCafRefs,
 Str=m1,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 10 20}]
Main.$trModule3 = GHC.Types.TrNameS Main.$trModule4

-- RHS size: {terms: 1, types: 0, coercions: 0, joins: 0/0}
Main.$trModule2 :: GHC.Prim.Addr#
[GblId,
 Caf=NoCafRefs,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 20 0}]
Main.$trModule2 = "Main"#

-- RHS size: {terms: 2, types: 0, coercions: 0, joins: 0/0}
Main.$trModule1 :: GHC.Types.TrName
[GblId,
 Caf=NoCafRefs,
 Str=m1,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 10 20}]
Main.$trModule1 = GHC.Types.TrNameS Main.$trModule2

-- RHS size: {terms: 3, types: 0, coercions: 0, joins: 0/0}
Main.$trModule :: GHC.Types.Module
[GblId,
 Caf=NoCafRefs,
 Str=m,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Value=True, ConLike=True,
         WorkFree=True, Expandable=True, Guidance=IF_ARGS [] 10 30}]
Main.$trModule = GHC.Types.Module Main.$trModule3 Main.$trModule1



Linking test ...

As always, the core is a bit verbose. Let me show you a bit of eye-tracking as I tried to read it.

:Main.main
  = Main.main5
    `cast` ...

Main.main5
  = ...runMainIO1 ... (Main.main1 `cast` ...)

Main.main1
  = ...hPutStr2 ...stdout Main.main2 ...

Main.main2 = ++ ...fShowMaybe1 Main.main3

Main.main3
  = case ...showsPrec4 11# Main.main4 ...
    of
    {- after a bit of squinting... -}
    x -> x

Main.main4 = 3

To me, that 3 looks suspiciously like 1+2. For paranoia, I tried replacing 1 and 2 with 10 and 20, and indeed saw a similar structure with 30 at the bottom. So that looks like pretty convincing evidence that it does this computation at compile-time.

That said, I expect this is probably pretty specific to numeric computations on simple numeric types. Compile-time computation of more exciting types has a strange and confusing time-space tradeoff, and the usual way to handle that situation is to give the programmer control of it as much as possible.

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • I just tried the same thing with `main = print ((++) <$> Just [1,2] <*> Just [3,4])`, and it did compile to basically `Just [1,2,3,4]` – Fyodor Soikin Mar 19 '18 at 15:40