Well, better late than never, I guess.
comp1
and comp2
are not only equivalent semantically, but even syntactically.
Writing arguments on the LHS of an equals sign of a definition is just syntactic sugar, so these two functions are equivalent:
id1 x = x
id2 = \x -> x
Edit: I realised I didn't really answer your questions, so here you are:
There is a difference for GHC when these are annotated with INLINE
pragmas, as GHC stores in its Core representation the Unfolding of a function and for which arity it can be unfolded (That's the Guidance=ALWAYS_IF(arity=1,...)
part), so it can actually matter in practice.
I don't think it does, as the comp1
and comp2
can't be distinguished after desugaring to Core, on which all optimisations operate. So when GHC wants to create a new unfolding, it will probably do so for manifest arity (e.g. the number of leading lambdas).
Inlining is mostly not beneficial for unsaturated bindings, see below. The same actually goes for the comp1
example: The reason why we want this to happen is not that we care about eliminating a function call. Rather we want comp1
to be specialised to the f
and g
parameters, regardless of what concrete x
we apply the specialisation to. There is actually an optimisation pass that should do this kind of work, called constructor specialisation (more on that below). INLINE
is even quite a misfit to use here: This still won't specialise a call like comp1 (const 5)
, where it's 'obvious' that this should be reduced to const 5
.
Consequently, this won't change much, as long as you don't sprinkle every let-bound thing with INLINE
pragmas. Even then it's questionable if that brings any benefit: The point is that it just makes no sense to inline unsaturated calls without any further motives (e.g. specialisation of a function to a concrete argument) and other than that it will just blow up code size at some point, so it probably may even make things slower.
End Edit
One reason I think why unsaturated calls to bindings aren't inlined is that mostly they don't bring any new optimization opportunities.
f = \x y -> 1 + (x * y)
g = \x y -> (1 + x) * y
Inlining f 16
yields \y -> 1 + (16*y)
, which isn't really much simpler than f 16
. On the contrary, code size increased considerably (which is the single biggest drawback of inlining).
Now, if there was a call like g 16
this would yield \y -> (1 + 16) * y
which would optimize to \y -> 17 * y
. But these kinds of opportunities are detected by another optimization pass, constructor or call-pattern specialization. The insight here is that 1 + x
can be simplified if we know the value of x
. Since we call g
with a literal (e.g. a value), it is beneficial to specialize g
for that particular call site, e.g. g16 = \y -> 17 *y
. No need to inline g
, also other call sites might share the code generated for g16
.
That's just one example of how inlining doesn't need to be done while still having efficient code. There are many other optimizations that in interplay with the inliner achieve what you want. Eta-expansion for example will make sure that calls are as saturated as possible:
main = print (f 2)
f = g 1
g x y = x + y
Since f
is always called with 1 argument, we can eta-expand it:
f eta = g 1 eta
Now the call to g
is saturated and can be inlined. Dito for f
, so eventually this reduces to
main = print 3
f eta = 1 + eta
g x y = x + y
Modulo dead-code elimination.