Let's say we have a memory-intensive class like an Image
, with chainable methods like Resize()
and ConvertTo()
.
If this class is immutable, won't it take a huge amount of memory when I start doing things like i.Resize(500, 800).Rotate(90).ConvertTo(Gif)
, compared to a mutable one which modifies itself? How to handle a situation like this in a functional language?
If this class is immutable, won't it take a huge amount of memory?
Typically your memory requirements for that single object might double, because you might have an "old copy" and a "new copy" live at once. So you can view this phenomenon, over the lifetime of the program, as having one more large object allocated than you might in a typical imperative program. (Objects that aren't being "worked on" just sit there, with the same memory requirements as in any other language.)
How to handle a situation like this in a functional language?
Do absolutely nothing. Or more accurately, allocate new objects in good health. If you are using an implementation designed for functional programming, the allocator and garbage collector are almost certainly tuned for high allocation rates, and everything will be fine. If you have the misfortune to try to run functional code on the JVM, well, performance won't be quite as good as with a bespoke implementation, but for most programs it will still be fine.
Can you provide more detail?
Sure. I'm going to take an exceptionally simple example: 1000x1000 greyscale image with 8 bits per pixel, rotated 180 degrees. Here's what we know:
To represent the image in memory requires 1MB.
If the image is mutable, it's possible to rotate 180 degrees by doing an update in place. The amount of temporary space needed is enough to hold one pixel. You write a doubly nested loop that amounts to
for (i in columns) do
for (j in first half of rows) do {
pixel temp := a[i, j];
a[i, j] := a[width-i, height-j];
a[width-i, height-j] := tmp
}
If the image is immutable, it's required to create an entire new image, and temporarily you have to hang onto the old image. The code is something like this:
new_a = Image.tabulate (width, height) (\ x y -> a[width-x, height-y])
The tabulate
function allocates an entire, immutable 2D array and initializes its contents. During this operation, the old image is temporarily occupying memory. But when tabulate
completes, the old image a
should no longer be used, and its memory is now free (which is to say, eligible for recycling by the garbage collector). The amount of temporary space required, then, is enough to hold one image.
While the rotation is going on, there's no need to have copies of objects of other classes; the temporary space is needed only for the image being rotated.
N.B. For other operations such as rescaling or rotating a (non-square) image by 90 degrees, it is quite likely that even when images are mutable, a temporary copy of the entire image is going to be necessary, because the dimensions change. On the other hand, colorspace transformations and other computations which are done pixel by pixel can be done using mutation with a very small temporary space.