The steps for implementing a custom assignment are the same as those for an operator, with a few minor differences. Like other arithmetic operators, assignment is an operation with two operands, one on the left and one on the right. Unlike other arithmetic operators that take two input arguments and return a new value as a result, assignment modifies the value of the left operand in-place. This places a requirement on our method definition–it must be a subroutine (not a function) with the first argument defined as intent(in out):
subroutine assign_field(self, f)
class(Field), intent(in out) :: self ❶
type(Field), intent(in) :: f ❷
call from_field(self, f) ❸
call self % sync_edges() ❹
end subroutine assign_field
❶ Field that we’re assigning to and is an input and output argument
❷ Field that we’re assigning to and is an input argument
❸ Initializes metadata of the resulting field
Like before, here we also use from_field to initialize the field metadata. Since from_ field also copies the values of the data component from f to self, all that’s left for us to do is synchronize with the sync_edges method. Once we have this method defined, we bind it to the type and instruct the compiler to associate it with the assignment operator:
type Field
...
contains
...
procedure, private, pass(self) :: assign_field ❶
generic :: assignment(=) => assign_field ❷
...
end type Field
❶ Binds the subroutine to the type
❷ Associates the assignment operator with the assign_field method
There’s not much new here except for one tidbit–implementing an assignment requires generic :: assignment(=) keywords instead of generic :: operator(). Otherwise, everything else works the same way as with other operators.
The core of our solver from listing 10.7 now becomes the following listing.
Listing 10.10 Main loop of the tsunami simulator, with synchronization on assignment
time_loop: do n = 1, num_time_steps ❶
...
u = u - (u * diffx(u) / dx & ❷
+ v * diffy(u) / dy & ❷
+ g * diffx(h) / dx) * dt ❷
v = v - (u * diffx(v) / dx & ❸
+ v * diffy(v) / dy & ❸
+ g * diffy(h) / dy) * dt ❸
h = h - (diffx(u * (hm + h)) / dx & ❹
+ diffy(v * (hm + h)) / dy) * dt ❹
...
end do time_loop
❶ Iterates for num_time_steps time steps
❷ Calculates the new value of velocity in the x axis
❸ Calculates the new value of velocity in the y axis
❹ Calculates the new value of water height
At first look, this is the same code as in listing 10.7, except for one minor difference–we did away with the sync_edges subroutine calls after each field update. The synchronization is now implicit in the assignment operations for each Field instance. This is not only about having to write fewer lines of code! It’s about not having to worry about when exactly we need to synchronize parallel processes, which is a big part of what makes parallel programming hard. It’s also about being able to write code that can run in both serial and parallel modes without any modifications.
If you’ve cloned the application’s Git repository on GitHub, you can build and run it like this:
make ch10