
Copying data is wasteful, mutating data is dangerous - feross
https://pythonspeed.com/articles/minimizing-copying/
======
DecoPerson
I'm not super familiar with interior mutablility, but I don't think this is
the same "interior mutablility" as used in Rust.

Interior mutablility:

A, immutable, points to B.

B, immutable, points to C.

C, mutable, points to D.

D, immutable, points to E.

Even though A and B are immutable -- B points to C mutably, so you can modify
its contents. Some "interior" part of A (and of B) is mutable. You can't
change which C is pointed to by B (because B is immutable, or rather we only
have an immutable reference to it), but it's open season on the contents of C.

What this article describes:

We have 2 operations.

We are passed in Array A.

We can either:

\- Apply both operations to A, modifying it even though the "owner" may not
want this.

\- Copy before each operation (or as part of them), which leaves A untouched,
but requires triple the memory usage.

\- Cooy before (or as part of) the first operation only. The second operation
can be applied in-place on the copy. This leaves A untouched and only requires
double the memory.

They're totally different concepts. I don't know what you'd call the second
concept -- I think the important part is that A remains untouched, so...
"Avoiding side effects while reducing memory usage", or "How to make
performant pure functions" ?

~~~
DecoPerson
I also see this as a bit of a premature optimization. The triple memory usage
one may be significantly faster than the in-place one -- and the memory usage
is temporary so unless you're in a constrained environment that's not a
concern.

Avoiding unintentional or unexpected side effects is a good thing. Pure
functions are great. Do whatever you can do keep your visualization helper
thing pure and avoid bugs.

Then, once it works, if you have performance issues, do some profiling.

If the profiling shows this helper method as a hotspot, then it's worth
optimizing.

At that point, you'd want to follow standard Python, numpy, and pandas
optimization strategies -- which I'm not familiar with.

Aimless optimization like this is a waste of time. Good program structure
(data is available when you need it without tonnes of calls), the right choice
of data structures and algorithms (hashmap vs list), the right choice of
underlying tools (Julia vs Python) and targeted optimization (profiling) are
the ways to go.

~~~
kllrnohj
The slowest thing CPUs do by far is access memory. I don't see any possibly
way that, for the given example, using more memory would _ever_ be faster. At
best 3*N all fits in cache and it's not as bad, but even then it's still at
least one more cache miss which means it will still be slower even if it's not
by much. Temporary memory is the worst type of memory. It's cache misses &
pipeline stalls for no point.

"Premature optimization" is not an excuse to write obviously bad code. Fixing
this simple thing now is much faster, and simpler, than digging a big hole,
jumping in it, and then hoping you can find something to pull yourself back
out later.

In the same way it's important to scatter about asserts and verify parameter
arguments do in fact conform to the documented contract, it's also important
to always keep an eye out for openly wasteful code.

~~~
davedx
> it's also important to always keep an eye out for openly wasteful code.

No, that's the point. It's really _not_. If your "openly wasteful code" does
its job well and is readable and maintainable, it DOES NOT MATTER that it is
"wasteful".

If, on the other hand, the wasteful code causes your program to execute in 60
minutes instead of 10 seconds every time you run it, then it does matter and
you should optimize it.

Why is this such a difficult concept?

~~~
kllrnohj
If you're trying to use Knuth's quote from 1974 as your justification for that
concept then you've misunderstood his argument and should read the full thing,
not the single phrase.

This article is not advocating premature optimization as Knuth was discussing
nor does the article's advice result in the evil that Knuth was warning
against.

If you have some other evidence or paper justifying why we can ignore obvious
inefficiencies please provide a source. That's a rather radical claim to make,
though, which may be why you find people struggle with the concept.

------
TheFuntastic
As a game developer working in managed languages (aka Unity/C#) this problem
is one of my biggest headaches. I was hoping the article would provide divine
insight, unfortunately the recommend solution doesn't really solve the problem
(as I see it).

Whilst 2n array alloc is better than 3n, both are creating short lived memory
objects. Do that in a hot code path and suddenly you've got tremendous garbage
pressure your garbage collector is going to choke on.

One can optimise this by calculating results into a cached array, but that
creates opportunities for bugs (results of previous caches persisting). I
would dearly love to see a solution that allows me to code in a functional
style without sacrificing performance.

~~~
strictfp
Arena style allocation helps. You still get the allocations, but GC is cheap.
IMO both Java and the CLR are in desperate need of multiple heaps with
separate GCs to solve these problems. One big heap simply doesn't scale well.
Erlang is the only example I know of that does this right. I know that the
golang GC is low-latency, but I'm not familiar with if it can sustain high
allocation rates.

~~~
lostcolony
Nope.

Golang still uses one global heap; it just allows for stack based allocation
in situations it can do it (avoiding putting stuff on the global heap), so
copies _can_ be cheap re: GC.

~~~
jen20
The CLR also allows stack allocation, and many of the newer .NET Core
libraries have been built with a focus on eliminating unnecessary allocations.

~~~
WorldMaker
Some of the biggest of .NET Core's advances in stack allocation (and resulting
performance boosts) likely won't make it into Unity until around the release
of .NET 5, though. It should be interesting to see how Unity adapts when the
tools show up.

------
perfunctory
> But mutation is a cognitive burden: you need to think much harder about what
> your code is doing.

One way to alleviate the problem is to use naming convention.

    
    
      normalize(a)       # mutates in place. note: no return value.
      b = normalized(a)  # no mutation. returns a new instance. note the adjective.
    

Python does this in the standard library

    
    
      a.sort()
      b = sorted(a)

~~~
kburman
I like how ruby way of declaration if a method does mutate object or not but a
exclamation mark at end to tell it modifies the object

a = a.sort or a.sort!

~~~
jonnycomputer
I like the idea of adding an exclamation or some other mark to the individual
parameters in the method declaration since a method might mutate some but not
all parameters.

------
mrkeen
I'm surprised no-one has mentioned Haskell's ST library yet. It hits this
exact use-case, and comes with some pretty sweet safeguards from the type
system.

[https://wiki.haskell.org/Monad/ST](https://wiki.haskell.org/Monad/ST)

It allows you mutate arrays and other stuff "on the inside" like this article
suggests, but it's also able to wrap up the operation so it appears pure from
the outside, so the caller doesn't have to guess or rely on naming conventions
to figure out the mutability.

It's your choice on whether you want to do the copy on the way in ('thawing'),
on the way out ('freezing'), or both, by using the 'in-place' variants of thaw
and freeze.

~~~
pwm
Exactly. The only problem is that you need a language with a sufficiently
powerful type system that can express both phantom and existential types.

~~~
chii
> The only problem is that you need a language with a sufficiently powerful
> type system

why is that a problem?

~~~
skosch
Because everyone and their grandma can whip up a quick Python script after a
few days of practice, whereas learning Haskell is a much bigger investment?

------
xg15
This seems like a problem that would be suited for an optimizing compiler.

You could imagine some hypothetical language in which _all_ objects are
immutable, as far as the programmer is concerned - however the compiler is
allowed to reuse objects behind the scenes if there is provably no way that a
side-effect could be introduced.

E.g., in such a language, the first variant would be the only legal form to
write the normalizing function:

    
    
      low = array1.min()
      high = array1.max()
      array2 = array1 - low
      array3 = array2 / (high - low)
      return array3
    

However, this could be safely turned into the following during compilation:

    
    
      low = array1.min()
      high = array1.max()
      array2 = array1 - low
      array2 /= (high - low)
      return array2
    

... because array2 is not visible outside the function and therefore could not
cause side-effects.

~~~
zzzeek
it is theoretically possible that a library like numpy or pandas could do this
directly. That is, if you take each of these transformation operations and
store them, but not actually run them until needed, that is, when someone
accesses the individual array values, you only need to make one copy and there
is no impact on the programmer to have to worry about this.

I was doing something similar for SQLAlchemy recently. SQLAlchemy is all about
capturing Python operators and operands into data structures that represent
intent which can be invoked later.

edit: here is a demonstration:
[https://gist.github.com/zzzeek/caa4a7ed94f326fbbc031acecb9d7...](https://gist.github.com/zzzeek/caa4a7ed94f326fbbc031acecb9d7a44)

edit edit: it is actually possible with numpy by using the Dask library:
[https://dask.org/](https://dask.org/)

~~~
masklinn
The problem with the library doing this is you end up with the same issue as
in Haskell: you accumulate thunks and both memory use and location of CPU
hotspots becomes much harder to predict and troubleshoot.

~~~
zzzeek
so...turn off the "thunking" feature while you are debugging.

~~~
JeremyBanks
That won't help when you're debugging the performance implications of
thunking.

~~~
zzzeek
compare memory / callcounts / time spent with thunking turned on vs. off would
give a good idea of it.

i can say for my side of things thunking is a _super_ huge performance gain as
it allows for quick gathering of expression intent and then the computation
side on the other side of a cache.

------
cultureswitch
This is rather dumb. The only reason there is an intermediate array in the
original copy-based code is because the normalization is badly written. After
you obtain max and min, normalization is a single map() function, not two.

As for the overall problem: always choose immutability and then make it faster
by first taking advantage of optimization patterns that immutability allows
you to use safely. In a case like this, doing the normalization lazily is what
springs to mind.

If memory allocation and de-allocation is an issue, use more explicit memory
management. Though by experience it is rarely the actual problem. There's
basically no chance that it is an issue here, Python interpreters aren't
stupid enough to give back memory to the OS immediately after a variable falls
out of scope.

~~~
markus92
Do you have any links to some examples or help on how to do this exactly?

Reason being that in our group we have a ton of normalization code that looks
_exactly_ like the 'bad' example from this post. As we use relatively large
datasets (4D MRI images), memory is a bit scarce...

~~~
naveen99
Pytorch has a built in normalize function:
[https://pytorch.org/docs/master/_modules/torchvision/transfo...](https://pytorch.org/docs/master/_modules/torchvision/transforms/functional.html#normalize)
It just subtracts the mean and divides by standard deviation, both in place.

------
nayuki
How I would summarize the article is that mutation should be confined to
internal objects that have not been published yet or are temporaries that are
thrown away.

Theoretically, you can make every API mutate data. If the user didn't want to
mutate data, they can explicitly make a copy first. Whereas if there only
exists an immutable API to return new objects, then it's hard or impossible to
get the benefits of mutating in place.

As far as I know, only Rust, C++, and C give support at the type-checking
level to denote which functions will modify the interior of their arguments.
This way, you can distinguish behaviors easily - for example in Rust:

    
    
      impl Vector {
          // The method reads this current vector and returns a new one.
          pub fn normalize(&self) -> Vector { ... }
          
          // The method reads and modifies this current vector in place.
          pub fn normalize(&mut self) { ... }
      }
    

The article gave this subtly flawed example in Python:

    
    
      def normalize_in_place(array: numpy.ndarray):
          low = array.min()
          high = array.max()
          array -= low
          array /= high - low
        
      def visualize(array: numpy.ndarray):
          normalize_in_place(array)
          plot_graph(array)
      
      data = generate_data()
      if DEBUG_MODE:
          visualize(data)
      do_something(data)
    

In Rust, it would be a type error because visualize() takes a reference but
normalize_in_place() takes a mutable reference:

    
    
      fn normalize_in_place(array: &mut numpy::ndarray) {
          let low = array.min();
          let high = array.max();
          array -= low;
          array /= high - low;
      }
      
      fn visualize(array: &numpy::ndarray) {
          normalize_in_place(array);  // ERROR
          plot_graph(array);
      }
      
      let data: numpy::ndarray = generate_data();
      if DEBUG_MODE {
        visualize(&data);
      }
      do_something(&data);

~~~
masklinn
> As far as I know, only Rust, C++, and C give support at the type-checking
> level to denote which functions will modify the interior of their arguments.

1\. C and C++ allow casting away constness, which may or may not be UB
depending how the parameter is defined

2\. Swift also provides this ability to an extent: _struct_ parameters have to
be flagged `inout` to be mutable

I'm sure there are others.

~~~
nwallin
Rust can throw away constness in unsafe blocks, which carries the same caveat
emptor that const_cast carries.

~~~
steveklabnik
Only in certain circumstances, though.

------
benibela
That is how strings work in Delphi/Pascal.

All strings have a reference count. When you change a string, and the ref
count is not one, it is copied before the change. If the ref count is one, it
is not copied

------
capableweb
Seems like "both libraries make copies of the data, which means you’re using
even more RAM." is a assumption (that copy data leads to more RAM usage) and
I'm guessing the libraries (haven't used them myself, nor python, so not sure
how feasible this is) can be implemented like a persistent data structure
([https://en.wikipedia.org/wiki/Persistent_data_structure](https://en.wikipedia.org/wiki/Persistent_data_structure))
and therefore get both immutability (which leads to less bugs, no mutability)
and not that high RAM usage. Clojure is a famous example of something that
implements persistent data structures.

~~~
perfunctory
> can be implemented like a persistent data structure

you don't get much benefit from persistent structures if every element of an
array is modified, like in the examples in the post.

~~~
pipeep
If the data structure has a reference count of one, you can safely mutate the
data structure instead. Many persistent immutable data structure libraries use
this as an additional optimization.

------
anaphor
There are also data structures such as zippers which are immutable but
minimize the amount of data you need to copy, at the cost of potentially using
more space overall. Zippers are cool because they can be generalized to many
different types of data structures as long as they can be expressed as
algebraic data types.

------
jackcviers3
If your language allows mutation:

1\. Immutable first. 2\. Data structures should use partial copying [1]. 3\.
Data structures should be lazy by default. 4\. When you've profiled a
bottleneck, and identified memory allocation and copying as the bottleneck
culprit, then use a mutable data structure internal to the operation (copy
your lazy immutable data structure once into the mutable one, taking only the
portion of your lazy structure that you need, operate on the mutable structure
in place, then return an immutable, lazy copy of that structure out of your
operation.

This will isolate the need for mutation reasoning to performance critical
parts of your application/library, and let you still reason about the rest of
your program with referential integrity, while avoiding performance
bottlenecks.

Never use a mutable structure in a mutable reference at the same time. You
don't need both.

1\.
[https://www.google.com/url?sa=t&source=web&rct=j&url=https:/...](https://www.google.com/url?sa=t&source=web&rct=j&url=https://www.amazon.com/Purely-
Functional-Structures-Chris-Okasaki-
ebook/dp/B00AKE1V04&ved=2ahUKEwjb9YfGk_nmAhXULc0KHYaRAw0QFjAXegQIBBAH&usg=AOvVaw2G1ZviAl6c7fVGecHERvwq)

------
alexhutcheson
In other languages this is less of an issue, because function signatures can
include information about whether the argument can be modified or not. For
example, in C++:

    
    
        // Definitely doesn't modify 'array', because 'array' is copied.
        vector<int> Normalize(vector<int> array);
        // Almost certainly doesn't modify 'array'.
        vector<int> Normalize(const vector<int>& array);
        // Probably *does* modify 'array' (otherwise author would have used
        // const reference instead of pointer.)
        void Normalize(vector<int>* array);
    

In Python every function uses the 3rd approach, so you have to signal to the
user in some other way whether the argument will be modified or not. In Ruby
and some other languages the convention is to append "!" to the function name,
but in Python most people seem to just mention it in the docstring.

~~~
pansa
There is some convention in Python - for example `reverse` and `sort` are
mutating, whereas `reversed` and `sorted` are not.

However, I don’t know if there is a good reason why the mutating versions are
methods and the non-mutating ones are standalone functions.

------
333c
Why does this blog post have a recaptcha?

In addition, I just finished reading the Rust book last night, and I don't
believe this is what interior mutability is (at least as used in Rust).

~~~
nhumrich
I see no recaptcha.

~~~
333c
Here's a screenshot:
[https://i.imgur.com/a/84Hb9Jq.png](https://i.imgur.com/a/84Hb9Jq.png)

------
perfunctory
> To reduce memory usage, you can use in-place operations like +=

btw, I think it's a design flaw in python that `a += b` is not always
equivalent to `a = a + b`.

~~~
notduncansmith
As someone who has never used Python seriously I have to ask... why not?

~~~
perfunctory
Because I once learned that `a += b` is just a shortcut for `a = a + b`, just
a syntax sugar. Now I have to constantly remind myself that it's not the case
in python.

edit: I might have misunderstood your question. If you meant "why it's not
equivalent" please see the falkaer's answer.

~~~
im3w1l
They aren't equivalent in C++ either.

~~~
perfunctory
My C++ is a bit rusty. Does it manifest itself in the standard library as
well? The problem with python (in my view) is that this behaviour is
implemented in the standard library and therefor propagates to the 3rd-party
libraries by convention.

~~~
zabzonk
> Does it manifest itself in the standard library as well?

Yes. For example for std::string `a = a + b;` will (at least notionally)
create a temporary string and then use the assignment operator. `a += b;` will
not do this.

------
drej
The problem and solution are broadly fine, but having done a bit of
performance critical work, I usually dislike when a function allocates without
a) me asking, b) me having the option to tell it not to.

I like passing in pre-allocated containers to house the results of my
computation. That way I have not only predictable memory consumption, but I
can also avoid costly allocation (and de-allocation) in tight loops. Having
worked with Go a bit, there were times when I used this (like passing in byte
slices to readers), but oftentimes you couldn't just tell a function not to
allocate (which it did for safety), because you and only you knew it'd be safe
to modify the structure in place.

The bottom line is that people should not be "always copy/always use pure
functions" or "always use mutable structures, because performance!!!", but be
aware of what the upsides and downsides of each are.

------
nnq
Better pattern is to follow the style of Numpy or Pandas and other libs (that
OP also mentioned, but then failed to apply it in his own examples), _letting
the caller /user of your functions choose, if/when they want better
performance, to opt-in for in-place modification_, like have either:

    
    
        def normalize(data, inplace=False):
            if not inplace:
                data = data.copy()
            ...
    

or:

    
    
        def normalize(data, out=None):
            if out is None:
                out = data.copy()
            ...
    

_Please, do follow this pattern for any libraries you release, having value-
semantics-by-default to prevent shooting yourself in the foot + opt-in in-
place mutation option for better memory performance is the right thing, and it
's quite easy too with Python, Numpy and Pandas!_

~~~
ourlordcaffeine
Julia has this as well, functions with a '!' at the end mutate the data,
functions without make a copy.

So you have the functions 'filter' and 'filter!', 'sort' and 'sort!' etc.

------
wbillingsley
Scala's collections API also uses some mutable internal data structures
locally (and temporarily) within some functions that present a pure API.

List[T] is one of the classic immutable data structures, and a List[T] cannot
have its contents modified. But within the prependAll method, it can create a
mutable List factory (ListBuffer) to do its work building the new list that it
will return.

So, if you follow this (contrived) example in your IDE, calling this:

    
    
      val f = List(4, 5, 6).prependedAll[Int](Array(1, 2, 3))
    

will take you inside StrictOptimizedSeqOps, which does this:

    
    
      override def prependedAll[B >: A](prefix: IterableOnce[B]): CC[B] = {
        val b = iterableFactory.newBuilder[B]
        b ++= prefix
        b ++= this
        b.result()
      }
    

The mutable data structure doesn't escape the function, so to the caller it
remains referentially transparent.

------
ajuc
There's a 3rd option - keeping a modifications' log and applying them all
destructively on just 1 temporary copy only when the result is needed.

    
    
        A = normalized(X*b - M*N*k); //creates a pipeline
        A.calculate(); //calculates all the stages destructively on the new copy

------
hannofcart
I think one way numerical computation APIs can offer optimizations of this
sort is to expose a chaining API.

For eg:

chain(my_large_array).op1().op2().op3().result()

Using this, the chain() start could copy the input data once, but thereafter
do a series of in place mutations in op1, op2 and op3 to improve performance.

Do numpy APIs provide such facilities?

~~~
pornel
Rust iterators combine like that if the operations are per-element (iter + map
+ collect).

For optimizing complex access patterns you'd need a language like Halide.

------
namelosw
Or just use an immutable language or library and you'll have the best of both
worlds. Unless you're writing an operating system. But the example was given
in Python so I guess it's perfectly fine.

The idea of immutable data structure is just like strings in Java or other
modern languages: it seems like a reference type but works like a
value/primitive type. It maintains a constant pool that all "foo"s in the same
virtual machine points to the same reference. For immutable data structures,
they work in the same way.

On top of that, there's more underlying sharing mechanism. For List(1,2,3),
it's actually might sharing the same List(1,2) with List(1,2,4) to minimize
the waste.

------
roywiggins
This seems more like "defensive copying" than "interior mutability" to me.

Ideally you'd have a compiler that could perform optimizing magic to detect
when the copy isn't needed, but that's not going to happen in CPython.

------
kstenerud
This is why you want a two-tiered API: High level and low level. The high
level API chooses a static trade-off in performance, size, etc for the 80%
case, and the low level API gives more explicit control over what's going on.

In this case, you could create the function normalized() for the 80% case, and
normalize_in_place() for users who need to optimize this path (choosing to
take on the extra responsibilities that come with it).

------
vmchale
I think Futhark approaches this with uniqueness types. A bit strange (maybe
that's just me?) but it works well with arrays.

Also it compiles to Python nicely.

------
DarkWiiPlayer
What about lazy evaluation though? That could get the functions 2*A memory
down to just A while still keeping the time O(n)¹

Of course, maybe the author just chose to limit this blog post to this one
solution, but it wouldn't have hurt to at least mention some further
optimizations :)

¹ And some O(1) data like the min and max values of the Array

------
bfield
Specifically on the point about Pandas, last time I checked the inplace flag
did not save any memory. Under the hood, it creates a temporary copy which is
copied back to the original dataframe, and then the temporary gets removed
next garbage collection cycle. If that is no longer the case I would love to
know about it though!

------
blt
Matlab gives you these semantics by default. IMO, it's a big reason why it has
been so successful with non-CS programmers. Mutation is often more intuitive,
but as long as you break your code into functions, you automatically limit the
scope of mutability bugs that can occur.

------
wlib
Isn't this solved perfectly with an affine or linear type-aware optimizing
compiler? Where all data is immutable unless the types prove to the compiler
that there are no mutation conflicts. I believe Rust implements this and Idris
2 does so as well with quantitative type theory.

~~~
masklinn
Rust would be very different and the problem wouldn't exist in the first
place, because the function would signal its behaviour and intent through
taking the parameter by reference, mutable reference or value.

~~~
wlib
That's what I meant

------
IshKebab
This is just a straightforward memory optimisation; nothing to do with
interior mutability.

------
ed_balls
Another option is to have a data structure that you have frozen and then you
unfroze it for the hot path and then freeze it again when you exit it.

------
lsb
Does the Numba JIT do this? This definitely seems like a compiler optimization
that I want to mechanically run on my code

------
pmarreck
Don’t functional data structures basically “diff” data, which is a reasonable
compromise?

------
DrFell
I have been mutating data for decades without any unexpected behavior
gremlins.

------
zackmorris
Another option is for the language's runtime to use copy-on-write (COW):

[https://en.wikipedia.org/wiki/Copy-on-
write](https://en.wikipedia.org/wiki/Copy-on-write)

So basically every buffer can be made immutable and then only copied when
written to, using something like refcounts and diff trees to internally track
mutations from an immutable parent buffer. Languages like Clojure due this to
manage state. I believe the Redux also does this internally, although I've
only studied it and haven't had a chance to use it professionally.

In practice, this looks like a language where everything is value-based
instead of reference-based.

So like, in C# where you pass objects by reference, a COW language passes
everything by const value. Then an imperative language statically analyzes the
code to detect mutation (my own theory is that this can never be accomplished
with 100% certainty, at least not with a simple implementation). So basically
buffers act like Git instead of byte arrays, creating a new snapshot with
every mutation.

Or functional languages can disallow mutation altogether, or reduce the code
into its most minimal representation and handle mutation at special breaks in
execution (like monads). I'm grossly oversimplifying all of this and probably
using the wrong terminology, but am more than 50% confident that I can derive
what I'm talking about, so will just go with it hah.

I think that the complex imperative implementation I'm talking about is
probably Rust. The simple functional implementation would look like a pure
immutable Lisp running within the Actor model, handling state only through IO,
and dropping monads altogether. The catch being that complex functional code
might have so many breaks in execution that it would practically be happening
on every single line and begin to look like an imperative language with all
its flaws. This is the primary reason why I shy away from impure functional
languages like Haskell, because I don't think they have solved this core issue
of mutation clearly enough (not to mention, I think functional language syntax
is generally write-only and not readable by others or yourself in 6 months
hahah). As far as I'm concerned, this is still an open problem.

In another life, I want to make an imperative language that's primarily
immutable, that uses higher order functions (or better yet, statically
analyzes code for side effects and converts foreach loops into higher order
functions automagically), and transpiles to a purely immutable Lisp. It would
be associative array-based and look like Javascript. The idea being that we
could write code in the human-readable imperative style and let the computer
distill it down to its pure functional equivalent.

~~~
tome
> not to mention, I think functional language syntax is generally write-only
> and not readable by others or yourself in 6 months

This is rather tangential, and sorry to hijack your message to get on my
soapbox, but I and many others who program in Haskell and a dynamic language
(Python in my case) find that Haskell code we've written is _far_ easier to
come back to than code we've written in the dynamic language.

~~~
zackmorris
Ya I tend to agree actually, but in the real world I keep getting scolded for
being too "functional". I think that it might come down to the easy != simple
argument, which the whole CS industry is still grappling with in other areas
as well.

------
londons_explore
Copy on write?

