Follow

Ya know the more I reflect on the languages I know the more I realize that outside of functional languages none of them really handle immutability well.

Consider that you want most of your objects to be immutable most of the time. Thats all well and good till you realize you want to be able to edit the objects in such a way that it creates duplicates that have some data changed but are likewise immutable.

This tends to stop working, almost entierly, once you get into subclassing. If you parent class has a method that returns a copy of itself with some data modified, this will break in children classes, since you want children classes to return instances of itself, not its parent.

Its not that you cant fix that, but the code gets very ugly very quickly. Generally you are forced to let the code handling the classes do the copying and editing itself, but that is pretty ugly too.

I have had this pattern problem in almost every OO language i messed with, Java, Ruby, Python, etc.

@freemo Yes, agreed. I've adopted a different mindset to classes; only use for very specific cases, plain functions are better.

@modrobert Im not sure i agree there either. Non-oo coding becomes a mess unless you employ class-like organization (structs or something) in which case your right back to the same immutability/mutability problem

@freemo I think you need to get out of that mindset.

@modrobert I dunno I do a ton of C and other non-OO languages and its usually a nightmare for complex systems... Works fine for simple and straight forward stuff though. But I usually work on the more complex stuff where it just wont cut it.

@freemo Off the top of my head, I think this would work pretty well if you use a frozen data class. Then in your manipulation methods you can use .replace (docs.python.org/3/library/data) to do the manipulations without modifying the current instance. I think it would work well with subclasses as well because it will always be calling the current class constructor.

@multimeric That is a bit limiting since you can only specify fields. In an ideal world youd have a frozen class with methods that can do complex manipulations returning copies rather than being limited to dealing with fields.

You could of course always clone, modify, and refreeze.

But yea there are solutions (this being one of them)... none of them feel very elegant to me.

@freemo I don't follow. Isn't your entire state made up of fields? What else needs to be changed? If you define all of your methods in terms of .replace instead of modifying self, it seems like it should work.

@multimeric The state is made of fields sure, but I usually dont want to manipulate the fields directly all in one place. Usually the fields would get manipulated through some methods attached to the object and usually in a way that will traverse many objects during the process. If I were dealing with a simple object with values then the problem wouldnt be much of a problem to begin with.

@freemo If you consistently use the pattern of returning .replace() for each of your manipulations, you can still use abstractions and don't have to modify everything in one place. You can also modify associated objects via this mechanism:

@dataclass(frozen=True)
class Car(Vehicle):
def service(self) -> Self:
return self.replace(
wheels = self.wheels.service(),
battery = self.battery.service()
)

@multimeric Yea thats one solution, but thats a lot of additional code you now need to do, and doesnt give you the option of switching between mutable and unmutable versions without more code.

Again there are solutions, but most of them arent very elegant.

In an ideal world an object would be mutable by default, but can be made to be immutable. You pick what makes sense for the scenario. Ideally without needing to write a lot of boilerplate code. For this solution you'd need different methods entierly for the mutable way of changing an object vs the immutable change-and-copy way.

@freemo a lot of Ruby classes defines both mutable methods that mutate the object in-place, and define methods that return a copy via `dup.do_mutation`.

@postmodern indeed they do. I mean most languages attempt to handle the immutable problem in one way or another... I find most very lacking or ugly.

@freemo True. Today I tried to implement some immutable status. Once I want to update the value, or I want to use it as a counter, then I must use some new variable and then reassemble the status object.

Mutable status is fine but then I need something like volatile or lock to ensure multithread safety...

@freemo There's the Pyrsistant (maybe spelled wrong) library that gives you "kind-of functional-style immutables".

@freemo Assembly for protecting the memory should work. It might be easier to just use Assembly nested in the code or call cookie cutter code to do the same thing.

A read of the object into protected memory would allow two objects to exist at once. The secure processor would have the immutable one and the user mode would have the mutable one. Depending on what is needed, either could be called by reference.

There's the secure processing on modern CPUs. That's going to be some awkward Assembly but the object would be as immutable as possible while remaining mutable. It could also be done by using the OS.

@AmpBenzScientist Its not the physical mechanisms that enable immutability that is the problem, there are tons of good approaches there. The issue is more around the elegance of the whole process (namely mutate-and-copy paradigms)

@freemo It is a good way to flex on peers. Perhaps MISRA C++ could be the answer.

@freemo Eiffel language manages immutable types in a rather good way. They are called "expanded" in the Eiffel jargon, but you can think to them as "value" instead of "references/entities". So, you have no references to them, but only explicit values. The classic example is an INTEGER NUMBER, or a BOOLEAN or a POINT. You cannot change 5 in 10. 5 is a value.

@mzan Thats common for most languages actually where primitive are immutable and passed by value and all other types are passed by reference. This is true for Ruby and Java for example.

Different languages define primitives differently for example in Java strings are immutable but passed by reference so effectively the same as passed by value for these purposes. In ruby however strings are mutable.

@freemo yes, there are two levels.

At the semantic/logical level, a type can be a value or a reference.

At the implementation level, if the value-object is big, you can store a reference to a shared dictionary of already instantiated immutable-objects. In OOP, this pattern is called Flyweight.

As funny side-note, in Common Lisp, the implementation level can use both an expanden value or a reference. In CL an integer is always an arbitrary long integer. You cannot have an integer overflow. If the integer is "small", then it will be a normal number ending with a 0 (or 1 depending from the implementation) in the last binary digits. If it is a long number, it will be allocated in RAM, and there will be a reference to it.

@freemo .. only for sake of security. In Eiffel you can declare new expanden/value types (i.e. an RGB color, a 3D Point and so on), while in old versions of Java you cannot declare new primitive types. Soon, you can do the same also in Java.

@mzan I cant think of why i would need that for security really...

@freemo ehm sorry, bad English on my side. I mean "only for being sure to be on the same page"

@freemo

Rust traits (~interfaces) bypass this problem by having the ability to refer to the type that implements this interface in prototypes of required functions (so you can say that an interface requires e.g. a function `append_element` that returns the same type as the the type that implements this interface).

Naturally, that can only work in languages where you cannot inherit from anything other than interfaces. (It still works for interfaces inheriting from other interfaces, though.)

@robryk I'd have to see it in action to really get an opinion of that.

@freemo
A simple example I could easily find: doc.rust-lang.org/std/clone/tr

(That might not satisfy the "in action" part, but I guess is a reasonable pointer and I couldn't quickly come up with a better example; people don't write that much Rust in functional style.)

Sign in to participate in the conversation
Qoto Mastodon

QOTO: Question Others to Teach Ourselves
An inclusive, Academic Freedom, instance
All cultures welcome.
Hate speech and harassment strictly forbidden.