Side-Effect Free Functions
Write Code as Little Transactions
Issue: 7.1 (November/December 2008)
Author: Charles Yeomans
Article Description: No description available.
Article Length (in bytes): 4,885
Starting Page Number: 54
RBD Number: 7121
Resource File(s): None
Related Link(s): None
Known Limitations: None
Excerpt of article text...
In programming, a method is said to have a side effect when it changes the state of the program. Programs without side effects are much easier to debug. They are also much less interesting. So the trick is to isolate code that changes state. For example... An object holds a list of weak references to other objects. From time to time, you want to cull dead references from the list. Here is a standard procedural implementation. Sub RemoveDeadRefs() for i as Integer = UBound(me.RefList) downTo 0 if me.RefList(i).Value is nil then me.RefList.Remove i end if next End Sub Let's see how we can improve it. The first improvement is to replace the reference to the object property me.RefList with a parameter to be supplied by the caller. Sub RemoveDeadRefs(theList() as WeakRef) for i as Integer = UBound(theList) downTo 0 if theList(i).Value is nil then theList.Remove i end if next End Sub This simple change decouples the method code from the rest of the class. In fact, it makes it clear that the method perhaps does not belong in the class; instead, it is a method of WeakRef arrays used by our class. My new complaint about the code is that it modifies the passed parameter. This creates two problems. First, it is slightly harder to test. Second, it is not exception-safe. If something goes wrong during the execution of the method, the parameter may be left in an unstable or unanticipated state. There is not much to go wrong in this method, of course, but if you write the code with that assumption, you might change the code later and forget about the assumption. We can remove both problems by converting this method from a subroutine that modifies its parameter to a function that returns a new array. Function RemoveDeadRefs(theList() as WeakRef) as WeakRef() dim filteredList() as WeakRef for each ref as WeakRef in theList if ref.Value <> nil then filteredList.Append ref end if next return filteredList End Function RemoveDeadRefs is now a side-effect free function. Note that we were also able to replace the downto iteration with a simpler for each loop. The result is code that is trivially easy to test. It is also better-behaved with respect to exceptions. You would invoke this function elsewhere in a class as follows. me.RefList = RemoveDeadRefs(me.RefList) A side effect of the refactoring is faster code. It turns out that in REALbasic it is usually faster to filter and copy arrays than to remove elements in place. The real benefit, though, is that you can now be reasonably certain that this assignment will either succeed or raise an exception. If an exception is raised, the assignment does not happen, thus protecting the state of the object from corruption. In other words, we have redesigned the remove dead references operation as a transaction. Database transactions are characterized by a set of four criteria: atomicity, consistency, isolation, and durability (ACID). It is perhaps useful to examine our little example in terms of them. Atomicity means that all steps of a transaction succeed, or the transaction as a whole fails. Dead reference removal potentially contains several steps, depending on the number of dead references removed, followed by the assignment of the result. If something goes wrong in RemoveDeadRefs, an exception should be raised which will prevent the final assignment from occurring. Thus our refactoring satisfies atomicity. Consistency means that the state of a database remains valid following the transaction, whether or not it succeeds. Here, we require the program state to remain valid following the execution of RemoveDeadRefs. To see that our code satisfies consistency, first note that the parameter passed to RemoveDeadRefs is not modified. This is important, because it is part of the program state. Then the succeed-or-raise-exception nature of the method itself ensures that the program state remains consistent. Isolation means that the changes made during the transaction are invisible to any other process. RemoveDeadRefs does not change the input parameter, as I just mentioned, and it only modifies local variables. Thus those modifications are not available to any other thread. This matters, because there is a loop in this code, so you should expect context switches if other threads are executing. And the code should satisfy isolation even in the presence of reentrant execution, which can happen in REALbasic code in certain situations. Durability refers to the persistence of the transaction once it has been committed. I am not certain that it makes sense in this context, so we will sidestep it. When you find yourself in a refactoring mood, take a look at your code and think in terms of transactions for changing state. I think it will make your development easier.
...End of Excerpt. Please purchase the magazine to read the full article.
Article copyrighted by REALbasic Developer magazine. All rights reserved.