4

A pure function in functional programming is the one which do not have side effects. One of the meanings of this is it can not change the values of input parameters. Can this be seen as disadvantage for the memory utilization?
e.g. Lets say I have a function which simply takes a list and add another element in it. In C++ it could be as simple as


void addElem(std::vector& vec, int a)
{
    vec.insert(a);
}

This function clearly doesn't use much memory than already taken by passed object.
But same in Haskell will come to something like this.

addElem :: [Int] -> Int -> [Int] 
addElem xs a = xs ++ [a]

Now in this computation as xs is not changing it's value. So am I correct in saying that at some time the function will be consuming the memory double size of xs, one for xs and other for the return value. Or somehow lazy invocation makes sure that actually xs is only getting returned by adding only one element at the end?
I know that Monad is the way to have something which can have side effect. But can Monad be used to simply modify the input and return it's value?
Also can changing xs ++ [a] to a:xs consumes less memory?

Manoj R
  • 3,197
  • 1
  • 21
  • 36
  • 6
    memory consumption in this case is a QoI issue, not a fundamental property of a programming paradigm. – PlasmaHH Dec 20 '12 at 14:54
  • 2
    The two pieces of code do not do the same thing. The C++ code should create a new vector, copy 'vec' to the new vector, append 'a' to the new vector, and return the new vector. – brian beuning Dec 20 '12 at 14:57
  • @brianbeuning - I don't want C++ function to be pure. I want it to change the state of input. And that is what I wanted to be the whole point of the question. – Manoj R Dec 20 '12 at 14:59
  • 4
    The "classic" solution to this in functional languages is to use a singly-linked list. You can add another node to the *start* without modifying the rest of the list. The "tail" is then shared between the original list, and the new list. – Steve Jessop Dec 20 '12 at 15:04
  • 5
    @ManojR: Then your question is *wrong*, because they're not the same function. Can I safely use your C++ version in a multi-threaded application without having to worry about elements being inserted by other threads when I don't expect it? Also, what if the vector never ends up being used? If the vector has more than a few elements, C++ will certainly use more memory in that case. Or if the vector is only traversed once after being constructed, then discarded--C++ will probably use more memory there, as well. – C. A. McCann Dec 20 '12 at 15:51
  • And that's not even getting into the myriad other problems with the question, such as using completely different data structures and naively comparing memory use between a GC'd language (without considering the lifetime of the relevant values) and a language with manual memory management. – C. A. McCann Dec 20 '12 at 15:59
  • 1
    @C.A.McCann: I think you've completely misunderstood the question. The questioner *knows* that his C++ code is not a pure function. He's asking about pure functions with similar but not identical intent, and the consequences of writing Haskell code in a particular way. This isn't a language shootout, and there is no need to defend why the Haskell function might have reasons for using more memory. The question is whether or not it does. – Steve Jessop Dec 20 '12 at 16:11
  • 4
    @SteveJessop: The intent is *not actually similar*, the particular way of writing it in Haskell is wrong if it's supposed to be anything like the C++ code, and the question of which uses more memory can't even be answered without further context. In many practical scenarios, idiomatic use of Haskell lists will consume far less memory than an "equivalent" use of C++ vectors, but in other scenarios they'll use more than an "equivalent" vector. The question is nonsense. – C. A. McCann Dec 20 '12 at 16:21
  • @SteveJessop "This isn't a language shootout". A question criticising a langauge on the basis of superficially similar but very different code, laden with implicit erroneous assumptions and not even asking whether comparable memory usage is possible? I'd call that "likely to solicit debate" every day of the week. Not constructive. A better question would ask how to do things instead, and would have a real programming problem in mind to supply some context for best solutions. Don't just ask "are arrays better than linked lists?" either - meaningless without context. – AndrewC Dec 20 '12 at 19:22
  • 1
    @AndrewC: I don't see any criticism of any language in the question. And it *does* ask whether comparable memory usage is possible, in fact it asks about *three* specific techniques whether or not they achieve the desired effect. Just because mentioning that two languages are different causes a lot of wailing and gnashing of teeth doesn't mean the question isn't constructive, it means people are terrified someone might think ill of their favourite language. But the question isn't about that, it's about how to use Haskell well. – Steve Jessop Dec 20 '12 at 19:43
  • 1
    @SteveJessop: As far as "how to use Haskell well" goes, the question is ill-formed and unanswerable because it lacks sufficient context to answer. Questions about how to prematurely optimize an underspecified program that doesn't even exist are not appropriate for SO in any language. I'm not sure why you're so intent on defending this question, or making irrelevant remarks about what you imagine people's motives are. If people want to think ill of Haskell for uninformed reasons, that's their problem, not mine. – C. A. McCann Dec 20 '12 at 20:57
  • I feel really sorry that this question was taken in criticism manner. I have started learning Haskell, and after few days of learning this is the first question that came in mind, that if the in place memory update is not allowed in functional programming, then aren't they tend to consume more memory? Sorry to all of those who got offended. – Manoj R Dec 21 '12 at 05:19
  • @ManojR: The only real, practical answer to the question is "it depends". Just because you construct a value in your code does not mean all (or any) of that value will actually exist simultaneously when the program runs, unlike C++ where if you ask for memory you get it immediately whether you really need it or not. If you have a practical question about memory use for a specific problem, that would be a more constructive question to ask. – C. A. McCann Dec 21 '12 at 14:30

6 Answers6

10

Purity implies that the function cannot make destructive updates, so if

xs ++ [a]

is fully evaluated, a copy of xs must be made. That can happen in constant space if xs is lazily generated and not referenced anywhere else, so that it can be garbage collected as it is created. Or it can need a copy besides xs - but purity allows the cells of both lists to point to the same data, so the data need not be copied, only the spine (amended by the final cons). But if a copy is made, the old value xs is still available.

Also can changing xs ++ [a] to a:xs consumes less memory?

Yes, that one can simple reuse xs, all that needs is to create a new list cell and let its tail pointer point to xs.

From a comment:

I don't want C++ function to be pure. I want it to change the state of input. And that is what I wanted to be the whole point of the question.

In that case, you need an impure function/action. That is possible for some data structures to implement, but for lists, the cells have to be copied/constructed anew. But adding an element to a std::vector<T> may also need reallocation.

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
8

Yes, pure FP may be more memory-intensive than imperative programming, though it depends on how you write your programs and how smart your compiler is. Haskell compilers in particular have very strong optimizers and might be able to transform pure FP code into quite efficient machine code that reuses allocated memory. That requires writing good FP code though, as even the smartest compiler will not include optimizations to handle programs that just simulate imperative ones with superficially similar FP constructs.

Mind you, your C++ example is invalid. If you meant

v[0] = a;  // assuming v.size() > 0

then that doesn't do any allocation. If you meant

v.append(a);

then that may or may not allocate, depending on the capacity of v.

Or somehow lazy invocation makes sure that actually xs is only getting returned by adding only one element at the end?

Depends on the implementation and the use of the expression in its context. When xs ++ [a] is fully evaluated, it copies the entire list xs. If it is partially evaluated, it might do any number of allocations between none and len(xs).

Also can changing xs ++ [a] to a:xs consumes less memory?

Yes, that changes it from O(n) worst-case to O(1) worst case allocations/extra memory use. Same goes for time complexity. When handling lists in Haskell, never append to the end. If that's what you need, use a difference list.

Fred Foo
  • 355,277
  • 75
  • 744
  • 836
4

I think the relevant difference between your two examples is the choice of data structure rather than the question of pure vs impure in itself.

You can have singly linked lists in both approaches, and you can have arrays with constant time update in both. In the pure case, an update to a data structure may semantically make a copy, but that does not necessarily mean that it does so physically.

Ropes, for example, are a variant of immutable arrays that allows constant time concatenation. You can also have an immutable array abstraction with constant time functional update (where a.set(i, x) semantically returns a copy) by using a copy-on-write scheme internally that only performs a physical copy if you are actually accessing the old version after creating the new (i.e. if you make use of the persistence capability of the pure data structure which is not available in the impure case).

Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
3

ab·strac·tion [ab-strak-shuh n]

the act of considering something as a general quality or characteristic, apart from concrete realities, specific objects, or actual instances.

tradeoff

a balancing of factors all of which are not attainable at the same time

leaky abstractions

All non-trivial abstractions, to some degree, are leaky.

Functional programming can be seen as an abstraction, and since all abstractions leak, there are some tradeoffs to be made.

Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192
  • Abstractions are only leaky when you allow them to leak. In our field, that happens when effects whose possibility cannot be captured statically are nevertheless allowed to happen - nontermination, exceptions, you name it. – isekaijin Jan 15 '13 at 02:44
  • @EduardoLeón [source](http://www.joelonsoftware.com/articles/LeakyAbstractions.html) of the "leaky abstractions" claim. Exceptions [can be "captured" statically](http://hackage.haskell.org/packages/archive/mtl/latest/doc/html/Control-Monad-Error.html). – Matt Fenwick Jan 15 '13 at 17:22
3

Now in this computation as xs is not changing it's value. So am I correct in saying that at some time the function will be consuming the memory double size of xs, one for xs and other for the return value.

Not necessarily. The advantage of having immutable data is it can be shared. So compilers can optimize by sharing xs in such case. So the size will remain same as in case of c++.

Or somehow lazy invocation makes sure that actually xs is only getting returned by adding only one element at the end?

Laziness just means evaluate when the value is actually required, it does not guarantee less memory usage. On the otherhand thunks created due to laziness might use more memory.

I know that Monad is the way to have something which can have side effect. But can Monad be used to simply modify the input and return it's value?

You are partially right. Monad are used to compose code having side effects. You can very well use mutable vector and write code very similar to your c++ example in the IO monad.

Also can changing xs ++ [a] to a:xs consumes less memory?

This is compiler dependent, but I think it would copy the whole list and add element at the end. (I am no expert in this).

You can always profile your code and check memory consumption, which is the best way to learn this.

Satvik
  • 11,238
  • 1
  • 38
  • 46
1

Different languages have different common idioms, and the implementers work to make these idioms as effective as possible. I would not assume_ that because something entails extra overhead in C++, the same would be true in another language, just as I would not assume that the most effective idioms in another language would be the most effective in C++.

Having said that: I'm currently working on a large, high performance application where we often return std::vector, and we've not found this to be a problem. Things like NRVO and move semantics end up meaning that this can be very efficient in C++ as well.

James Kanze
  • 150,581
  • 18
  • 184
  • 329