Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

defining functions with/without lambdas

Which difference in does it make if I define a function with a lambda expression or without so when compiling the module with GHC

f :: A -> B
f = \x -> ...

vs.

f :: A -> B
f x = ...

I think I saw that it helps the compiler to inline the function but other than that can it have an impact on my code if I change from the first to the second version.

I am trying to understand someone else's code and get behind the reasoning why this function is defined in the first and not the second way.

like image 596
epsilonhalbe Avatar asked Dec 11 '22 21:12

epsilonhalbe


2 Answers

To answer that question, I wrote a little program with both ways, and looked at the Core generated:

f1 :: Int -> Int
f1 = \x -> x + 2
{-# NOINLINE f1 #-}

f2 :: Int -> Int
f2 x = x + 2
{-# NOINLINE f2 #-}

I get the core by running ghc test.hs -ddump-simpl. The relevant part is:

f1_rjG :: Int -> Int
[GblId, Arity=1, Str=DmdType]
f1_rjG =
  \ (x_alH :: Int) -> + @ Int GHC.Num.$fNumInt x_alH (GHC.Types.I# 2)

f2_rlx :: Int -> Int
[GblId, Arity=1, Str=DmdType]
f2_rlx =
  \ (x_amG :: Int) -> + @ Int GHC.Num.$fNumInt x_amG (GHC.Types.I# 2)

The results are identical, so to answer your question: there is no impact from changing from one form to the other.


That being said, I recommend looking at leftaroundabout's answer, which deals about the cases where there actually is a difference.

like image 127
madjar Avatar answered Dec 13 '22 10:12

madjar


First off, the second form is just more flexible (it allows you to do pattern matching, with other clauses below for alternative cases).

When there's only one clause, it's actually equivalent to a lambda... unless you have a where scope. Namely,

f = \x -> someCalculation x y
 where y = expensiveConstCalculation

is more efficient than

f x = someCalculation x y
 where y = expensiveConstCalculation

because in the latter, y is always recalculated when you evaluate f with a different argument. In the lambda form, y is re-used:

  • If the signature of f is monomorphic, then f is a constant applicative form, i.e. global constant. That means y is shared throughout your entire program, and only someCalculation needs to be re-done for each call of f. This is typically ideal performance-wise, though of course it also means that y keeps occupying memory.
  • If f s polymorphic, then it is in fact implicitly a function of the types you're using it with. That means you don't get global sharing, but if you write e.g. map f longList, then still y needs to be computed only once before getting mapped over the list.

That's the gist of the performance differences. Now, of course GHC can rearrange stuff and since it's guaranteed that the results are the same, it might always transform one form to the other if deemed more efficient. But normally it doesn't.

like image 35
leftaroundabout Avatar answered Dec 13 '22 09:12

leftaroundabout