They're not exactly the same:
Case 1:
x = foo()
y = bar(x)
z = bar(x)
Case 2:
y = bar(foo())
z = bar(foo())
In case 1 the value of x must be kept in memory across the entire execution of bar(x) since you need it for the second invocation of bar. In case 2, the result of foo() can be discarded once bar is done with it.
To use a contrived example, imagine a program that has 100 mb of available memory.
foo() returns a data struct that is 95 mb in size.
def bar(some_big_data_struct)
some_small_data = some_big_data_struct[:some_key]
[A bunch of other code that allocates and then releases 90 mb]
return some_other_small_data
end
In Case 1 I get an OOM error. In Case 2 I (or the runtime) can reclaim the 95 mb that some_big_data_struct uses and the program works.
Clearly that's a contrived situation but it illustrates my original point. There's a huge gap between theoretical pure functions and what we have to deal with in the real world. These sorts of FP tutorials never go into the weeds and explore these problems.