They made that and it’s called pure functional programming. Take a look at Haskell
Programming
Welcome to the main community in programming.dev! Feel free to post anything relating to programming here!
Cross posting is strongly encouraged in the instance. If you feel your post or another person's post makes sense in another community cross post into it.
Hope you enjoy the instance!
Rules
Rules
- Follow the programming.dev instance rules
- Keep content related to programming in some way
- If you're posting long videos try to add in some form of tldr for those who don't want to watch videos
Wormhole
Follow the wormhole through a path of communities !webdev@programming.dev
Saves memory.
Sounds like a thing Java would do.
Because recreating entire object just to make a single change is dumb.
God help you if you've already passed the object by reference and have to chase up all the references to point at the new version!
You can safely do a shallow copy and re-use references to the unchanged members if you have some guarantee that those members are also immutable. Its called Persistent Data Structures. But that's a feature of the language and usually necessitates a GC.
Logical and human friendly answer: mutable objects are not a problem, poorly designed code is
Personal rant: why even bother with objects, just use strings, ints, floats, arrays and hashmaps (sarcascm. I have spent hours uncovering logic of large chunks of code with no declaration of what function expects and produces what)
And also, seeing endless create-object-from-data-of-other-object several times has made me want to punch the author of that code in the face. Even bare arrays and hashmaps were less insane than that clusterfuck
strings? ints? floats, arrays, HASHMAPS? so inefficient… just directly access memory!!!!
I was trying to think of a good real world example, but couldn't think of anything. But if you were to think of it as Google docs. You could just copy every doc to change it, but if you've shared it with people then you will have to share it again. It also takes up a whole lot more space to do that, sure you could delete those old ones but that takes some work too.
I'm gonna hazard a guess, just cause I'm curious, that you're coming from JavaScript.
Regardless, the answer's basically the same across all similar languages where this question makes sense. That is, languages that are largely, if not completely, object-oriented, where memory is managed for you.
Bottom line, object allocation is VERY expensive. Generally, objects are allocated on a heap, so the allocation process itself, in its most basic form, involves walking some portion of a linked list to find an available heap block, updating a header or other info block to track that the block is now in use, maybe sub-dividing the block to avoid wasting space, any making any updates that might be necessary to nodes of the linked list that we traversed.
THEN, we have to run similar operations later for de-allocation. And if we're talking about a memory-managed language, well, that means running a garbage collector algorithm, periodically, that needs to somehow inspect blocks that are in use to see if they're still in use, or can be automatically de-allocated. The most common garbage-collector I know of involves tagging all references within other objects, so that the GC can start at the "root" objects and walk the entire tree of references within references, in order to find any that are orphaned, and identify them as collectable.
My bread and butter is C#, so let's look at an actual example.
public class MyMutableObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
}
public record MyImmutableObject
{
public required ulong Id { get; init; }
public required string Name { get; init; }
}
_immutableInstance = new()
{
Id = 1,
Name = "First"
};
_mutableInstance = new()
{
Id = 1,
Name = "First"
};
[Benchmark(Baseline = true)]
public MyMutableObject MutableEdit()
{
_mutableInstance.Name = "Second";
return _mutableInstance;
}
[Benchmark]
public MyImmutableObject ImmutableEdit()
=> _immutableInstance with
{
Name = "Second"
};
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|
MutableEdit | 1.080 ns | 0.0876 ns | 0.1439 ns | 1.02 | 0.19 | - | - | NA |
ImmutableEdit | 8.282 ns | 0.2287 ns | 0.3353 ns | 7.79 | 1.03 | 0.0076 | 32 B | NA |
Even for the most basic edit operation, immutable copying is slower by more than 7 times, and (obviously) allocates more memory, which translates to more cost to be spent on garbage collection later.
Let's scale it up to a slightly-more realistic immutable data structure.
public class MyMutableParentObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required MyMutableChildObject Child { get; set; }
}
public class MyMutableChildObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required MyMutableGrandchildObject FirstGrandchild { get; set; }
public required MyMutableGrandchildObject SecondGrandchild { get; set; }
public required MyMutableGrandchildObject ThirdGrandchild { get; set; }
}
public class MyMutableGrandchildObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
}
public record MyImmutableParentObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required MyImmutableChildObject Child { get; set; }
}
public record MyImmutableChildObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
public required MyImmutableGrandchildObject FirstGrandchild { get; set; }
public required MyImmutableGrandchildObject SecondGrandchild { get; set; }
public required MyImmutableGrandchildObject ThirdGrandchild { get; set; }
}
public record MyImmutableGrandchildObject
{
public required ulong Id { get; set; }
public required string Name { get; set; }
}
_immutableTree = new()
{
Id = 1,
Name = "Parent",
Child = new()
{
Id = 2,
Name = "Child",
FirstGrandchild = new()
{
Id = 3,
Name = "First Grandchild"
},
SecondGrandchild = new()
{
Id = 4,
Name = "Second Grandchild"
},
ThirdGrandchild = new()
{
Id = 5,
Name = "Third Grandchild"
},
}
};
_mutableTree = new()
{
Id = 1,
Name = "Parent",
Child = new()
{
Id = 2,
Name = "Child",
FirstGrandchild = new()
{
Id = 3,
Name = "First Grandchild"
},
SecondGrandchild = new()
{
Id = 4,
Name = "Second Grandchild"
},
ThirdGrandchild = new()
{
Id = 5,
Name = "Third Grandchild"
},
}
};
[Benchmark(Baseline = true)]
public MyMutableParentObject MutableEdit()
{
_mutableTree.Child.SecondGrandchild.Name = "Second Grandchild Edited";
return _mutableTree;
}
[Benchmark]
public MyImmutableParentObject ImmutableEdit()
=> _immutableTree with
{
Child = _immutableTree.Child with
{
SecondGrandchild = _immutableTree.Child.SecondGrandchild with
{
Name = "Second Grandchild Edited"
}
}
};
Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|
MutableEdit | 1.129 ns | 0.0840 ns | 0.0825 ns | 1.00 | 0.10 | - | - | NA |
ImmutableEdit | 32.685 ns | 0.8503 ns | 2.4534 ns | 29.09 | 2.95 | 0.0306 | 128 B | NA |
Not only is performance worse, but it drops off exponentially, as you scale out the size of your immutable structures.
Now, all this being said, I myself use the immutable object pattern FREQUENTLY, in both C# and JavaScript. There's a lot of problems you encounter in business logic that it solves really well, and it's basically the ideal type of data structure for use in reactive programming, which is extremely effective for building GUIs. In other words, I use immutable objects a ton when I'm building out the business layer of a UI, where data is king. If I were writing code within any of the frameworks I use to BUILD those UIs (.NET, WPF, ReactiveExtensions) you can bet I'd be using immutable objects way more sparingly.
I seriously appreciate you taking the time to do this. Good info.
There are times when immutable objects are absolutely the way to go from a data safety perspective. And there are other times when speed or practicality prevail.
Never become an extremist about any particular pattern. They’re all useful - to become a master you must learn when that is.
Faster. Less memory. Maps to physical things well (e.g. a device with memory mapped registers). No garbage collection / object destruction needed. No need to initialize new objects all the time.
Simply put, because you often want to change the state of something without breaking all the references to it.
Wild off the top of my head example: you're simulating a football game. Everything is represented by objects which hold references to other objects that are relevant. The ball object is held by player object W, player object X is in collision with and holds a reference to player object Y, player Z is forming a plan to pass to player object X (and that plan object holds a reference to player object X) and so on.
You want to be able to change the state of the ball object (its position say) without creating a new object, because that would invalidate how every other existing object relates to the ball.
What you need here is not the stability in memory (i.e. of pointers, which you lose when you recreate an object) but instead just the stability of an identifier (e.g. the index into a list).
This is close, but as someone already said, an index into a list just means you are mutating the list.
Your stable "identifier" needs to be a function, ie. a reused design pattern. Compared to the list, this would be an index function which gets an element from an arbitrary list, meaning you don't have to mutate your list anymore, you just build a new one which still works with your function.
This is why languages which avoid mutation and side effects are always (to my knowledge) functional languages.
Well, but then you're basically just pushing the mutability onto the container, since you need to be able to replace elements within it.
It's a good strategy at times though. Like say you're working in a language where strings are immutable and you want a string you can change. You can wrap it in a list along the lines s=['foo']
and pass references to the list around instead. Then if you go s[0]='bar'
at some point, all the references will now see ['bar']
instead.
Well, but then you're basically just pushing the mutability onto the container
That's the point, when programming with immutable structures you always pass the mutability onto the enclosing structure.
It's a good strategy at times though. Like say you're working in a language where strings are immutable and you want a string you can change. You can wrap it in a list along the lines
s=['foo']
and pass references to the list around instead. Then if you gos[0]='bar'
at some point, all the references will now see['bar']
instead.
A list is an antipattern here IMO. Just wrap it in some dedicated object (see e.g. Java's StringBuilder
).
What if you want to change one reference in a billion reference array?
As others have pointed out, there is the issue of breaking references to objects.
There can also be a lot of memory thrashing if you have to keep reallocating and copying objects all the time. To some extent, that may be mitigated using an internment scheme for common values. In Python, for example, integers are immutable but they intern something like the first 100 or so iirc? But that doesn't work well for everything.
Any container you want to populate dynamically should probably be mutable to avoid O(N²) nastiness.
multiple other objects might be holding a reference to the object you want to change, so you'd either have to recreate those too or mutate them to let them point to the new object.
however if you can do what you want to do in a side effect free way i suggest doing that, as it is indeed easier to reason about what happens this way.
Yeah the main reason is performance. In some languages if you use a value "linearly" (i.e. there's only ever one copy) then functional style updates can get transformed to mutable in-place updates under the hood, but usually it's seen as a performance optimisation, whereas you often want a performance guarantee.
Koka is kind of an exception, but even there they say:
Note. FBIP is still active research. In particular we'd like to add ways to add annotations to ensure reuse is taking place.
From that point of view it's quite similar to tail recursion. It's often viewed as an optional optimisation but often you want it to be guaranteed so some languages have a keyword like become
to do that.
Also it's sometimes easier to write code that uses mutation. It doesn't always make code icky and hard to debug. I'd say it's more of a very mild code smell. A code musk, if you like.