Immutability
Immutability is one really good concept in programming. It's really really good. Immutable means that you don't change an object state nor re-assign variable once it is created. You create a value and then after that you can only read it. If you want to modify it, you need to create a new instance ...
Immutability is one really good concept in programming. It's really really good. Immutable means that you don't change an object state nor re-assign variable once it is created. You create a value and then after that you can only read it. If you want to modify it, you need to create a new instance and assign to a new variable. For example, the following codes are not immutable.
i = i + 1 # re-assignment
array.push 3 # change state
Some of the Ruby's good practice comes from immutability so actually you've already familiar with this concept without realizing so. You might feel that it heavily limits your capability of programming. But you will soon know that it leads you to higher level as a programmer. Constraint is a mother of invention. And history of programming language is a history of constraints.
Example1: Array
Look at the following code. This code get two list of users from a list of all users, one is the list of normal users and the other is the list of paid users.
users = User.all normal_users = [] paid_users = [] users.each do |u| case u.type when :normal normal_users << u when :paid paid_users << u when :left # do nothing end end
You might think it's OK. But I don't think so. It's not ruby-ish at all. There are two mutable variables normal_users and paid_users. So we want to stop this way.
The best code I can think of is as follows.
users = User.all normal_users, paid_users = users.group_by(&:type).values_at(:normal, :paid)
As you can see, the modified code is much
- Compact
- Intuitive
- Easy to understand
- Easy to maintain
You might think it is because Ruby's group_by and values_at methods are strong. Partly it is right. But partly not. These strong methods are created to accomplish immutability. And those benefits come from immutability.
So if you find a mutation inside some loop, you can reconsider to change it into a immutable way. Then you will find your code becomes much cleaner.
Example2: Boolean
generate = false generate = true if user.paid_user? generate = false if user.new? generate = true if user.normal_user?
In this case the generate variable is mutated and it is modified many times. And it is a bit hard to understand. So let's refactor it.
Obviously the above code is equivalent to the following code.
generate = user.paid_user? && ! user.new? && user.normal_user?
Can you explain why these two codes are the same? If you can explain it, that's great. That's really surprising because they are not equivalent.
Actually the original code is equivalent to the following code.
generate = ( user.paid_user? && ! user.new?) || user.normal_user?
Please have a time to compare these codes by yourself. It might be a bit surprising that there are || and && mixed.
So that's the problem of the original code. At first sight it looks easy to understand. But actually some confusing dependencies between lines are hidden behind the code. If you swap the 3rd and 4th lines of the original code, it changes the meaning, but such a constraint is not visible at first glance. You can notice such a dependency after you load the code into your brain and imagine how it will run, which takes much time.
On the other hand, in the last code, the logic and constraints is very clear from it's syntax. You can easily know from the bracket that you can not swap ! user.new? and user.normal_user?. It visually prevent you from making a stupid mistake.
This is basically what a state annoyingly does for us. We can write a program that depends on the state of variables. But the state is not visible in the code. So IDE nor compiler can not help you. And the state depends on the order of execution. So it's fragile by moving some lines of code. You need to spend time to imagine how it runs to understand in which state the variable is for every specific line of code. So it takes time. These are the part of reasons functional people say state is evil (there are more reasons) and love immutability.
To be more clear, these problem happens because the meaning of a variable changes before/after the mutation.
i = i + 1
Think about this code. The value of i changes at this line. So reading before this line and after this line requires you to refresh your memory. If there are many more mutation on many variables, it makes the code much harder to understand. But if the code is immutable, then any variable has only one meaning. If you look at one variable, then it's meaning is always same through it's scope. That makes things very simple.
It might be interesting to know that this concept was introduced by mathematician. In a mathematics world, we can never write an equation like i = i + 1. Mathematicial will deduce from the equation that 0 = 1, by reducing i from both side of the equation