Skip to content

Commit 4daf5ad

Browse files
committed
Added 3 exercises + some text
1 parent 640a7c5 commit 4daf5ad

File tree

7 files changed

+173
-16
lines changed

7 files changed

+173
-16
lines changed

_episodes/derived_types.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ keypoints:
1414
- "Access modifiers can be applied within derived types as well as within modules to restrict access to members of that derived type."
1515
---
1616

17-
While modules allow you to package variables together in such a way that you can directly refer to those variables, derived types allow you to package together variables in such a way as to form a new compound variable. With this new compound variable you can refer to it as a group rather than only by the individual components.
17+
While modules allow you to package variables together in such a way that you can directly refer to those variables, derived types allow you to package together variables in such a way as to form a new compound variable. With this new compound variable you can refer to it as a group rather than only by the individual components. This can greatly simplify passing a group of related variables to a procedure as only one variable of a given derived type would need to be passed.
1818

1919
To create a new derived type you use the following format.
2020
~~~
@@ -29,7 +29,7 @@ You can then declare new variables of this derived type as shown.
2929
type(<type name>):: my_variable
3030
~~~
3131
{: .fortran}
32-
Finally individual elements or members of a derived type variable, or **object**, can be accessed using the `%` operator.
32+
Individual elements or members of a derived type variable, or **object**, can be accessed using the `%` operator.
3333
~~~
3434
my_variable%member1
3535
~~~
@@ -80,6 +80,8 @@ $ ./derived_types
8080
~~~
8181
{: .output}
8282

83+
In the rest of this workshop we will build on this `t_vector` derived type adding functionality as we go. We will create a set of procedures and operators which will allow us to do common operations one might want to perform on a vector abstracting away the details, such as memory management, allowing us to write code at a higher level.
84+
8385
### Creating new objects
8486
Creating new vectors is a pretty common thing that we want to do. Lets add some functions to create vectors to reduce the amount of repeated code. Lets create one to make empty vectors, `create_empty_vector` and one to create a vector of a given size allocating the required memory to hold all the elements of the vector, `create_sized_vector`.
8587

_episodes/destructors.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Destructors"
33
teaching: 10
4-
exercises: 0
4+
exercises: 5
55
questions:
66
- "What is a destructor?"
77
objectives:
@@ -10,7 +10,7 @@ keypoints:
1010
- "A destructor is used to perform clean up when an object goes out of scope."
1111
- "To create a destructor use the **final** keyword when declaring at type bound procedure instead of the procedure keyword."
1212
---
13-
One aspect I have been ignoring until now is that the memory we allocate for our vectors is never explicitly freed by our program. So far our program has been simple enough that this is not a serious issue. We have only created a few objects that allocate memory within our main program. When the program execution has completed, that memory is returned to the operating system. However, if we had a long running loop inside our program that created new objects with allocated memory and we never deallocated that memory we would have a problem as our program would steadily increase its memory usage. This is referred to as a memory leak as was mentioned in the first half of this workshop. We can manually deallocate memory as we did with allocating memory before we created a function to create new `t_vector` and `t_vector_3` objects, however there is a way to create a new special type bound procedure that is automatically called when the object goes out of scope to deallocate this memory for us. To do this we use the **final** keyword within the type definition.
13+
One aspect I have been ignoring until now is that the memory we allocate for our vectors is never explicitly freed by our program. So far our program has been simple enough that this is not a serious issue. We have only created a few objects that allocate memory within our main program. When the program execution has completed, that memory is returned to the operating system. However, if we had a long running loop inside our program that created new objects with allocated memory and we never deallocated that memory we would have a problem as our program would steadily increase its memory usage. This is referred to as a memory leak as was mentioned in the first half of this workshop. We can manually deallocate memory as we did with allocating memory, however there is a way to create a new special type bound procedure that is automatically called when the object goes out of scope to deallocate this memory for us. To do this we use the **final** keyword within the type definition.
1414

1515
~~~
1616
type <type-name>
@@ -148,5 +148,72 @@ $ ./destructor
148148
0.00000000
149149
~~~
150150
{: .output}
151+
152+
> ## No allocated check?
153+
> What happens if we don't check that memory is allocated before de-allocating it in our destructor? Lets copy our last code and comment out those lines and see.
154+
> ~~~
155+
> $ cp destructor.f90 destructor_no_check.f90
156+
> $ nano destructor_no_check.f90
157+
> ~~~
158+
> {: .bash}
159+
> ~~~
160+
> ...
161+
> subroutine destructor_vector(self)
162+
> implicit none
163+
> type(t_vector):: self
164+
>
165+
> !if(allocated(self%elements)) then
166+
> deallocate(self%elements)
167+
> !endif
168+
> end subroutine
169+
>
170+
> subroutine destructor_vector_3(self)
171+
> implicit none
172+
> type(t_vector_3):: self
173+
>
174+
> !if(allocated(self%elements)) then
175+
> deallocate(self%elements)
176+
> !endif
177+
> end subroutine
178+
> ...
179+
> ~~~
180+
> {: .fortran}
181+
> ~~~
182+
> $ gfortran -g destructor_no_check.f90 -o destructor_no_check
183+
> $ ./destructor_no_check
184+
> ~~~
185+
> {: .bash}
186+
> What happens and why?<br/> Note: the `-g` option provides extra debugging information, such as file and line numbers in the backtrace.
187+
> > ## Solution
188+
> > ~~~
189+
> > t_vector:
190+
> > num_elements= 0
191+
> > elements=
192+
> > t_vector:
193+
> > num_elements= 4
194+
> > elements=
195+
> > 2.00000000
196+
> > 0.00000000
197+
> > 0.00000000
198+
> > 0.00000000
199+
> > At line 37 of file destructor.f90
200+
> > Fortran runtime error: Attempt to DEALLOCATE unallocated 'self'
201+
> >
202+
> > Error termination. Backtrace:
203+
> > #0 0x7fd37fc21730 in ???
204+
> > #1 0x7fd37fc22289 in ???
205+
> > #2 0x7fd37fc22906 in ???
206+
> > #3 0x401929 in __m_vector_MOD_destructor_vector
207+
> > at /home/user100/fortran_oop/destructor.f90:37
208+
> > #4 0x40100d in __m_vector_MOD___final_m_vector_T_vector
209+
> > at /home/user100/fortran_oop/destructor.f90:86
210+
> > #5 0x401d86 in MAIN__
211+
> > at /home/user100/fortran_oop/destructor.f90:101
212+
> > #6 0x401e15 in main
213+
> > at /home/user100/fortran_oop/destructor.f90:89
214+
> > ~~~
215+
> > {: .output}
216+
> {: .solution}
217+
{: .challenge}
151218
{% include links.md %}
152219

_episodes/extending_types.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Extended Types"
33
teaching: 10
4-
exercises: 0
4+
exercises: 5
55
questions:
66
- "How do you extend a type?"
77
- "Why can it be useful to extend a type?"
@@ -14,7 +14,7 @@ keypoints:
1414

1515
It is pretty common to use vectors to represent positions in 3D space. Lets create a new derived type which always has only three components. However, it would be really nice if we could reuse our more general vector type to represent one of these specific 3 component vectors. You can do this by using type extension. Type extension allows you to add new members (or not) to an existing type to create a new derived type.
1616

17-
To create a new extended derived type has the following format.
17+
To create a new extended derived type use the following format.
1818
~~~
1919
type,extends(<parent type name>):: <child type name>
2020
<member variable declarations>
@@ -106,4 +106,42 @@ $ ./type_extension
106106
~~~
107107
{: .output}
108108

109+
> ## Which type is being extended?
110+
> In the following code snippet which type is being extended?
111+
> ~~~
112+
> ...
113+
> type, extends(B):: A
114+
> end type
115+
> ...
116+
> type(C) function D()
117+
> implicit none
118+
> D%thing=1.0
119+
> end function
120+
> ...
121+
> ~~~
122+
> {: .fortran}
123+
> <ol type="a">
124+
> <li markdown="1">`A`
125+
> </li>
126+
> <li markdown="1">`B`
127+
> </li>
128+
> <li markdown="1">`C`
129+
> </li>
130+
> <li markdown="1">`D`
131+
> </li>
132+
> </ol>
133+
> > ## Solution
134+
> > <ol type="a">
135+
> > <li markdown="1">**No**: close, but `A` is the new derived type which extends the existing `B` derived type.
136+
> > </li>
137+
> > <li markdown="1">**Yes**: the existing derived type `B` is being extended to create a new derived type `A`.
138+
> > </li>
139+
> > <li markdown="1">**No**: `C` is a derived type, but from this code snippet it is impossible to tell if it has been extended to a new derived type somewhere else in the code.
140+
> > </li>
141+
> > <li markdown="1">**No**: `D` is actually a function name, not a derived type at all.
142+
> > </li>
143+
> > </ol>
144+
> {: .solution}
145+
{: .challenge}
146+
109147
{% include links.md %}

_episodes/interfaces.md

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Interface Blocks"
33
teaching: 10
4-
exercises: 0
4+
exercises: 5
55
questions:
66
- "What is an interface block?"
77
- "How can one be used to create a constructor for a derived type?"
@@ -12,7 +12,7 @@ keypoints:
1212
- "Procedures that are part of the same generic interface block must be distinguishable from each other based on the number, order, and type of arguments passed."
1313
---
1414

15-
As mentioned previously it is a very common task to create new objects of a derived type and perform some initialization of the members. We have already created some functions to do this. One of the functions, `create_empty_vector` takes no arguments and creates a new empty vector, while `create_sized_vector` creates a new vector of a specific size passed to the function. Both of these functions do the same thing, create a new `t_vector` object and initialize it. One might imagine many different such initialization routines, perhaps ones that take another `t_vectors` or `t_vector_3` objects to use to initialize a new `t_vector` object as a copy of the passed vector. All of these creation, or initialization, functions do basically the same thing but in a slightly different way depending on the arguments passed to them. It starts to get a bit tedious to have to remember all the names of these different initialization functions. If the compiler could somehow distinguish these functions automatically based on the number and type of arguments rather than the procedure name so that we could call the same generic procedure name and it would pick the correct procedure implementation based on the arguments we passed it.
15+
As mentioned previously it is a very common task to create new objects of a derived type and perform some initialization of the members. We have already created some functions to do this. One of the functions, `create_empty_vector` takes no arguments and creates a new empty vector, while `create_sized_vector` creates a new vector of a specific size passed to the function. Both of these functions do the same thing, create a new `t_vector` object and initialize it. One might imagine many different such initialization routines, perhaps ones that take another `t_vector` or `t_vector_3` objects to use to initialize a new `t_vector` object as a copy of the passed vector. All of these creation, or initialization, functions do basically the same thing but in a slightly different way depending on the arguments passed to them. It starts to get a bit tedious to have to remember all the names of these different initialization functions. If the compiler could somehow distinguish these functions automatically based on the number and type of arguments rather than the procedure name so that we could call the same generic procedure name and it would pick the correct procedure implementation based on the arguments we passed it.
1616

1717
It turns out there was a feature added to Fortran 2003, called **interface blocks** which allow multiple procedures to be mapped to one name. The basic syntax of an interface block is as follows.
1818
~~~
@@ -24,9 +24,9 @@ end interface
2424
~~~
2525
{: .fortran}
2626

27-
This allows one to call the procedure `<new-procedure-name>` and will be mapped onto different procedure implementations `<existing-procedure-name1>`, `<existing-procedure-name-2>`, etc. based on the type, number, and order of arguments passed to the procedure when calling it. Since the number, type, and order of arguments is the only way for the compiler to know which procedure to call, all procedures listed in the interface block must have different types and or number of arguments.
27+
This allows one to call the procedure `<new-procedure-name>` and it will be mapped onto different procedure implementations `<existing-procedure-name1>`, `<existing-procedure-name-2>`, etc. based on the type, number, and order of arguments passed to the procedure when calling it. Since the number, type, and order of arguments is the only way for the compiler to know which procedure to call, all procedures listed in the interface block must have different types and or number of arguments.
2828

29-
Lets use interface blocks to group our creation functions for `t_vector` and `t_vector_3` into one procedure name to initialize each of the derived types. It is common to use the name of the derived type as the name of creation function which returns a new object of that type. Functions defined in this way are referred to as **constructors** as they *construct* new objects of the derived type.
29+
Lets use interface blocks to group our creation functions for `t_vector` and `t_vector_3` into one procedure name to initialize each of the derived types. It is common to use the name of the derived type as the name of the creation function which returns a new object of that type. Functions defined in this way are referred to as **constructors** as they *construct* new objects of the derived type.
3030

3131
~~~
3232
$ cp type_extension.f90 interface_blocks.f90
@@ -114,5 +114,45 @@ The way we have used interface blocks above is what is called a **generic interf
114114

115115
There is another way to use interface blocks without specifying a generic procedure name to map the listed procedures to, referred to as **explicit interfaces**. Explicit interface blocks can be used to define a procedure without actually listing the implementation of it. These are useful when using procedures declared in different compilation units which will be linked into the final program later. This is a bit like a forward declaration or a function prototype in C/C++. If your procedures are declared inside a module, as we have been doing, these explicit interfaces are created for you.
116116

117+
> ## Add a procedure to an interface
118+
> What happens if we add the `create_size_3_vector` to the `t_vector` generic interface? Make a copy of our last source file and add the line `procedure:: create_size_3_vector` to the `t_vector` interface as shown below.
119+
> ~~~
120+
> $ cp interface_blocks.f90 interface_test.f90
121+
> $ nano interface_test.f90
122+
> ~~~
123+
> {: .bash}
124+
> ~~~
125+
> module m_vector
126+
> implicit none
127+
>
128+
> type t_vector
129+
> ...
130+
> end type
131+
>
132+
> interface t_vector
133+
> procedure:: create_empty_vector
134+
> procedure:: create_sized_vector
135+
> procedure:: create_size_3_vector
136+
> end interface
137+
> ...
138+
> ~~~
139+
> {: .fortran}
140+
> ~~~
141+
> $ gfortran -o interface_test interface_test.f90
142+
> ~~~
143+
> {: .bash}
144+
> What happens when you try to compile and run it and why?
145+
> > ## Solution
146+
> > During compilation the following error message is printed out
147+
> > ~~~
148+
> > ...
149+
> > Error: Ambiguous interfaces in generic interface 't_vector' for ‘create_empty_vector’ at (1) and ‘create_size_3_vector’ at (2)
150+
> > ...
151+
> > ~~~
152+
> > {: .output}
153+
> > This is because the `create_empty_vector` and the `create_size_3_vector` functions both have no arguments. Since the compiler uses the number and type of arguments to decide which function to call there is no way for the compiler to know which function should be called when no arguments are given.
154+
> {: .solution}
155+
{: .challenge}
156+
117157
{% include links.md %}
118158

_episodes/introduction.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,23 @@ There are some basic principles of OOP:
1919
* **Abstraction**: wrap up complex actions into simple verbs.
2020
* **Encapsulation**: keep state and logic internal.
2121
* **Inheritance**: new types can inherit properties and function from existing types and modify and extend those.
22-
* **Polylmorphism**: different types of objects can have the same methods that are internally handled different depending on the type.
22+
* **Polymorphism**: different types of objects can have the same methods that are internally handled differently depending on the type.
2323

2424
Many features have been added to Fortran over the years which has allowed Fortran programs to be written with increasingly object-oriented designs if desired.
2525

26-
However, I should note that care should be taken while designing programs to make use of OOP practices. If not carefully designed it can result in very bad outcomes for performance. It has been stated that premature optimization is the root of all evil, and to some degree that is true. In that you shouldn't worry too much about optimizing code until you know what is important to optimize, e.g. what takes most of the time. However, when using OOP designs some consideration needs to be given up front to performance otherwise significant re-writing will have to happen to allow for later optimizations. For example, it is often a bad idea to have an array of objects and one should instead favor a object containing arrays.
26+
However, I should note that care should be taken while designing programs to make use of OOP practices. If not carefully designed OOP can result in very bad outcomes for performance. It has been stated that premature optimization is the root of all evil, and to some degree that is true. In that you shouldn't worry too much about optimizing code until you know what is important to optimize, e.g. what takes most of the time. However, when using OOP designs some consideration needs to be given up front to performance otherwise significant re-writing will have to happen to allow for later optimizations. For example, it is often a bad idea to have an array of objects and one should instead favor an object containing arrays.
2727

28-
The above principles of OOP should be taken with a grain of salt rather than perfect rules to always follow.
28+
Here is an example, as a grad student I was writing a hydrodynamics code for a course. I made every cell in my grid an object with members like, density, pressure and velocities. To create my grid I made an array of cell objects and for every cell I called a constructor to initialize it. More over, when updating a property such as density over the whole grid, all the other cell properties had to be loaded into the smaller and faster caches along with it. This would increase the likely hood of cache misses and cause data to be loaded/unloaded into the caches more than otherwise needed. I got very poor performance as compared to other students who didn't use an OOP style. That isn't to say I couldn't have used an OOP style and gotten comparable performance, if I for example made the grid an object which contained arrays of the basic properties of the fluid. So blindly applying the ideas of OOP can have some serious performance implications and require some major structural changes to code if left to later optimization stages. Some up front thought about performance isn't always a bad thing. The trick is not to worry about details too early, but rather try to limit it to larger code design decisions.
29+
30+
There are of course benefits to the basic OOP principles, in particular they help to reduce the mental load when dealing with large complex code bases by allowing the person reading the code to get a high level understanding of what is going on by abstracting away details. It also helps by reducing the need for duplicating code by using inheritance and polymorphism to allow the same code to operate on different data types. This reduces the amount of code that needs to be debugged and maintained.
31+
32+
The above principles of OOP should be considered as general guidelines rather than perfect rules to always follow.
2933

3034
* Fortran 90 introduced:
3135
* modules
3236
* derived data types
3337
* interface blocks
38+
* operator overloading
3439
* Fortran 2003 introduced:
3540
* type extension
3641
* type-bound procedures

0 commit comments

Comments
 (0)