How to make a CAF not a CAF in Haskell?

前端 未结 7 1012
南旧
南旧 2020-12-01 05:23

How do I make a Constant Applicative Form into, well, not a Constant Applicative Form, to stop it being retained for the lifetime of the program?

I\'ve tried this ap

相关标签:
7条回答
  • 2020-12-01 05:50

    You need to hide the fact that the rhs is a CAF from the optimizer. Something like this should do it.

    twoTrues :: () -> [[[Bool]]]
    twoTrues u = map (++ (True : repeat (false u))) . trueBlock <$> [1..]
    
    {-# NOINLINE false #-}
    false :: () -> Bool
    false _ = False
    
    0 讨论(0)
  • 2020-12-01 06:02

    The simplest possible solution might be to tell the compiler to inline it. (Note: this answer is untested. If it doesn't work, please comment below.)

    Even if (hypothetically) the compiler refuses to inline it for some reason, you can use cpp instead, by #defining it:

    #define twoTrues (map (++ (True : repeat False)) . trueBlock <$> [1..])
    

    (although with that approach, of course, it won't appear in the module's interface, so you can only use it within that module).

    The -cpp option tells GHC to preprocess the input file with cpp.

    Inlining would mean duplicating the code n times if you refer to twoTrues in n>1 places. However, while that's theoretically undesirable, it's probably not going to be a significant problem in practice.

    0 讨论(0)
  • 2020-12-01 06:04

    It seems to be a long-standing problem http://hackage.haskell.org/trac/ghc/ticket/917 . And in my opinion a critical one.

    0 讨论(0)
  • 2020-12-01 06:10

    With the introduction of a dummy parameter, you also have to make it look like the result actually depends on the parameter. Otherwise, GHC's cleverness might turn it into a CAF again.

    I suggest the following:

    twoTrues u = map (++ (True : repeat False)) . trueBlock <$> [(u `seq` 1)..]
    
    0 讨论(0)
  • 2020-12-01 06:12

    Generalise. If you have a constant value, can you generalise this to a function of some variable? The naming of my function in the question, twoTrues, immediately suggests that this constant is the third in a sequence zeroTrues, oneTrue, twoTrues, threeTrues etc. - and indeed it is. So generalising twoTrues into a function nTrues which takes a parameter n and deleting twoTrues, would eliminate one CAF from the program.

    As it happens, in this case, I had only considered the cases zeroTrues, oneTrue and twoTrues for my program because that was all I needed, but my program could naturally be extended to deal with nTrues for n > 2 - so generalising to nTrues would mean it would make sense to "generalise all the way up" to the users of zeroTrues, oneTrue etc. That would not always be the case.

    Note: there might still be other CAFs to deal with, either in the code, or produced by GHC's "optimisations" (which are not really optimisations in these pathological cases).

    This answer may involve more work by the programmer than is strictly necessary, however. It isn't actually necessary to generalise, as Don's answer shows.

    On the other hand, in some cases, generalising a constant can make it more clear what you are actually doing, and aid reusability. It can even reveal ways to compute a series of values in a better systematic way, and/or more efficiently.

    A note about this particular case (which can be ignored): In this particular case, I would not want to make nTrues itself into an infinite list (which would be a CAF again, reintroducing the original problem!) rather than a function. One reason is that while twoTrues could be useful in the form of an infinite list, I can't see how it would be useful (in my application, anyway) for nTrues to be in the form of an infinite list.

    0 讨论(0)
  • 2020-12-01 06:13

    A complete example

    Here's a little example that shows the situation:

    module A where
    
    big :: () -> [Int]
    big _ = [1..10^7]
    

    Looks like a function, right? But what does GHC do? It floats the enum to the top level!

    A.big1 :: [Int]
    [ Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=0, Value=False,
             ConLike=False, Cheap=False, Expandable=False,
             Guidance=IF_ARGS [] 7 0}]
    A.big1 =
      case A.$wf1 10 A.big2 of ww_sDD { __DEFAULT ->
      eftInt 1 ww_sDD
      }
    
    A.big :: () -> [Int]
    [Arity=1,    
     Unf=Unf{Src=InlineStable, TopLvl=True, Arity=1, Value=True,
             ConLike=True, Cheap=True, Expandable=True,
             Guidance=ALWAYS_IF(unsat_ok=True,boring_ok=True)
             Tmpl= \ _ -> A.big1}]
    A.big = \ _ -> A.big1
    

    Ooops!


    So what can we do?

    Turn off optimizations

    That works, -Onot, but not desirable:

    A.big :: () -> [Int]
    [GblId, Arity=1]
    A.big =
      \ _ ->
        enumFromTo
          @ Int
          $fEnumInt
          (I# 1)
          (^
             @ Int
             @ Type.Integer
             $fNumInt
             $fIntegralInteger
             (I# 10)
             (smallInteger 7))
    

    Don't inline, and more functons

    Make everything into a function, including the enumFromTo, plumbing the parameter through to the workers:

    big :: () -> [Int]
    big u = myEnumFromTo u 1 (10^7)
    {-# NOINLINE big #-}
    
    myEnumFromTo :: () -> Int -> Int -> [Int]
    myEnumFromTo _ n m = enumFromTo n m
    {-# NOINLINE myEnumFromTo #-}
    

    Now we are finally CAF-free! Even with -O2

    A.myEnumFromTo [InlPrag=NOINLINE]
      :: () -> Int -> Int -> [Int]
    A.myEnumFromTo =
      \ _ (n_afx :: Int) (m_afy :: Int) ->
        $fEnumInt_$cenumFromTo n_afx m_afy
    
    A.big [InlPrag=NOINLINE] :: () -> [Int]
    A.big = \ (u_abx :: ()) -> A.myEnumFromTo u_abx A.$s^2 lvl3_rEe
    

    Yay.


    What doesn't work?

    Turn off -ffull-laziness

    The full laziness transformation floats definitions outwards. It is on by default with -O1 or above. Let's try turning it off with -fno-full-laziness. However, it doesn't work.

    0 讨论(0)
提交回复
热议问题