Use more inlinable definitions
I think this should fix #35, but I'm not sure how to prove it.
@ratherforky can you try this out and see if it fixes the problem you were having?
Sorry, I can't remember what code I was working on when this came up. I thought I responded on the original post, but I must've forgot 😅 I remember trying to come up with a minimal example/benchmark but I couldn't figure out exactly what caused the lack of inlining. My understanding is that GHC will already inline the current code most of the time, but it's decided by heuristics, which makes it hard to pin down a minimal example. I think it'd be pretty much the same situation with this new code.
E.g. looking at the inlining for |> and apply using ghc --show-iface Flow.hi for the code on the main branch:
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Unfolding: Core: <vanilla> apply]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
f x]
v.s. this new code:
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (y['GHC.Types.Many] :: a -> b) ->
y x]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Unfolding: Core: <vanilla> |>]
It ends up the same but with the two more or less swapped around. If we instead used this code:
{-# INLINE (|>) #-}
(|>) :: a -> (a -> b) -> b
(|>) = apply
{-# INLINE apply #-}
apply :: a -> (a -> b) -> b
apply x f = f x
We get:
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Inline: (sat-args=0),
Unfolding: Core: StableUser <0,FalseTrue> apply]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Inline: (sat-args=2),
Unfolding: Core: StableUser <2,FalseTrue>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
f x]
The big difference for apply is the Inline: (sat-args=2) and the StableUser <2,FalseTrue> vs <vanilla>. We're saying 'if apply has two arguments provided, always inline it' vs 'let GHC choose when to inline it'.[^1]
For |> it's Inline: (sat-args=0), so it's always inlined to apply whenever it's used.
I've made a pull request (#46) with INLINE pragmas and tweaked definitions to have the appropriate number of LHS arguments and keep things simple/closer to the main branch. I cut $, ., flip, and id to be more direct, but I kept $! because that seems like a good idea for non-inlining reasons since its behaviour is non-trivial.
I've included the relevant parts of the .hi files for the different versions of the code for comparison:
main branch inlining .hi
(!>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>, Unfolding: Core: <vanilla> apply']
(.>) :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>,
Unfolding: Core: <vanilla> compose]
(<!) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1C(1,L)><1L>,
Unfolding: Core: <vanilla>
\ @a @b (f['GHC.Types.Many] :: a -> b) (x['GHC.Types.Many] :: a) ->
case x of x1 { DEFAULT -> f x1 }]
(<.) :: (b -> c) -> (a -> b) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <1C(1,L)><MC(1,L)><L>,
Unfolding: Core: <vanilla>
\ @b
@c
@a
(g['GHC.Types.Many] :: b -> c)
(f['GHC.Types.Many] :: a -> b)
(x['GHC.Types.Many] :: a) ->
g (f x)]
(<|) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1C(1,L)><L>,
Unfolding: Core: <vanilla>
\ @a @b (f['GHC.Types.Many] :: a -> b) (x['GHC.Types.Many] :: a) ->
f x]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
f x]
apply' :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
case x of x1 { DEFAULT -> f x1 }]
compose :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>,
Unfolding: Core: <vanilla>
\ @a
@b
@c
(f['GHC.Types.Many] :: a -> b)
(g['GHC.Types.Many] :: b -> c)
(x['GHC.Types.Many] :: a) ->
g (f x)]
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Unfolding: Core: <vanilla> apply]
gh-35-inlining branch inlining .hi
(!>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (y['GHC.Types.Many] :: a -> b) ->
GHC.Base.$! @GHC.Types.LiftedRep @a @b y x]
(.>) :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>,
Unfolding: Core: <vanilla>
\ @a
@b
@c
(x['GHC.Types.Many] :: a -> b)
(y['GHC.Types.Many] :: b -> c) ->
GHC.Base.. @b @c @a y x]
(<!) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1C(1,L)><1L>,
Unfolding: Core: <vanilla> GHC.Base.$! @GHC.Types.LiftedRep]
(<.) :: (b -> c) -> (a -> b) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <1C(1,L)><MC(1,L)><L>,
Unfolding: Core: <vanilla> GHC.Base..]
(<|) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 1, Arity: 1,
Strictness: <1L>,
Unfolding: Core: <vanilla> \ @a @b -> GHC.Base.id @(a -> b)]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Unfolding: Core: <vanilla> |>]
apply' :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>, Unfolding: Core: <vanilla> !>]
compose :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>, Unfolding: Core: <vanilla> .>]
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>,
Unfolding: Core: <vanilla>
\ @a @b (x['GHC.Types.Many] :: a) (y['GHC.Types.Many] :: a -> b) ->
y x]
my PR inlining .hi
(!>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>, Inline: (sat-args=0),
Unfolding: Core: StableUser <0,FalseTrue>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
GHC.Base.$! @GHC.Types.LiftedRep @a @b f x]
(.>) :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>, Inline: (sat-args=2),
Unfolding: Core: StableUser <2,FalseTrue> compose]
(<!) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1C(1,L)><1L>, Inline: (sat-args=0),
Unfolding: Core: StableUser <0,FalseTrue>
GHC.Base.$! @GHC.Types.LiftedRep]
(<.) :: (b -> c) -> (a -> b) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <1C(1,L)><MC(1,L)><L>, Inline: (sat-args=2),
Unfolding: Core: StableUser <2,FalseTrue>
\ @b
@c
@a
(g['GHC.Types.Many] :: b -> c)
(f['GHC.Types.Many] :: a -> b) ->
compose @a @b @c f g]
(<|) :: (a -> b) -> a -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 1, Arity: 1,
Strictness: <1L>, Inline: (sat-args=1),
Unfolding: Core: StableUser <1,FalseTrue>
\ @a @b (f['GHC.Types.Many] :: a -> b) -> f]
apply :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Inline: (sat-args=2),
Unfolding: Core: StableUser <2,FalseTrue>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
f x]
apply' :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <1L><1C(1,L)>, Inline: (sat-args=0),
Unfolding: Core: StableUser <0,FalseTrue>
\ @a @b (x['GHC.Types.Many] :: a) (f['GHC.Types.Many] :: a -> b) ->
GHC.Base.$! @GHC.Types.LiftedRep @a @b f x]
compose :: (a -> b) -> (b -> c) -> a -> c
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 3, Arity: 3,
Strictness: <MC(1,L)><1C(1,L)><L>, Inline: (sat-args=2),
Unfolding: Core: StableUser <2,FalseFalse>
\ @a
@b
@c
(f['GHC.Types.Many] :: a -> b)
(g['GHC.Types.Many] :: b -> c)
(x['GHC.Types.Many] :: a) ->
g (f x)]
(|>) :: a -> (a -> b) -> b
[HasNoCafRefs, LambdaFormInfo: LFReEntrant 2, Arity: 2,
Strictness: <L><1C(1,L)>, Inline: (sat-args=0),
Unfolding: Core: StableUser <0,FalseTrue> apply]
[^1]: Importantly, letting GHC choose means it can also choose the arity it inlines for, whereas if we use an INLINE pragma, the minimum arity is fixed by the number of LHS variables. E.g. if we defined compose as compose f g x = f (g x), with no pragma GHC can choose to inline compose (+ 10) (* 5), but if we add an INLINE pragma then compose (+ 10) (* 5) will never be inlined because the minimum number of arguments is 3.