Skip to content

Commit

Permalink
Moved to a more type-along approach and added some exercies
Browse files Browse the repository at this point in the history
  • Loading branch information
cgeroux committed Mar 16, 2023
1 parent c078c0e commit efe9719
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 94 deletions.
2 changes: 1 addition & 1 deletion _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ title: "Object Oriented Programming with Fortran"
# Life cycle stage of the lesson
# See this page for more details: https://cdh.carpentries.org/the-lesson-life-cycle.html
# Possible values: "pre-alpha", "alpha", "beta", "stable"
life_cycle: "pre-alpha"
life_cycle: "stable"

#------------------------------------------------------------
# Generic settings (should not need to change).
Expand Down
98 changes: 78 additions & 20 deletions _episodes/derived_types.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
---
title: "Derived Types"
teaching: 15
exercises: 0
teaching: 10
exercises: 5
questions:
- "What is a derived type?"
- "How do you use derived types?"
- "Can access to individual components of a derive type be controlled?"
objectives:
- "Create a derive type."
- "Access members of a derive type."
- "Create a derived type."
- "Access members of a derived type."
keypoints:
- "A derived type allows you to package together a number of basic types that can then be thought of collectively as one new derived type."
- "Access modifiers can be applied within derived types as well as within modules"
- "Access modifiers can be applied within derived types as well as within modules to restrict access to members of that derived type."
---

While modules allow you to package variables together in such a way that you can directly refer to those variables, as we saw previously, 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.
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.

To create a new derived type you use the following format.
~~~
Expand All @@ -34,7 +34,7 @@ Finally individual elements or members of a derived type variable, or **object**
my_variable%member1
~~~
{: .fortran}
The member variable, `member1`, is declared as part of the derived type.
The member variable, `member1`, would have been declared as part of the derived type.

A full example of creating a new derived type, `t_vector`, which holds an array of `real` values is shown below.

Expand Down Expand Up @@ -82,6 +82,15 @@ $ ./derived_types

### Creating new objects
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`.

Lets add those functions now.

~~~
$ cp derived_types.f90 derived_types_init.f90
$ nano derived_types_init.f90
~~~
{: .bash}

<div class="gitfile" markdown="1">
<div class="language-plaintext fortran highlighter-rouge">
<div class="highlight">
Expand Down Expand Up @@ -128,22 +137,71 @@ end program
[derived_types_init.f90](https://github.com/acenet-arc/fortran_oop_as_a_second_language/blob/gh-pages/code/derived_types_init.f90)
</div>

### Access modifiers
Access modifiers can be applied to derived types in a similar way to modules. Here is an example.
Since the function is declared as a `t_vector` type, members of the returned `t_vector` object can be accessed using the `%` operator from an object with the same name as the function from inside the function. This is similar to how functions normally work, however in this case the function return type is a derived type.

Lets compile and run.
~~~
module m_vector
implicit none
type t_vector
integer,private:: num_elements
real,dimension(:),allocatable:: elements
end type
...
end module
$ gfortran ./derived_types_init.f90 -o derived_types_init
$ ./derived_types_init
~~~
{: .fortran}
In this case the member variable `num_elements` could no longer be accessed from outside the module, while the `elements` member variable can be.
{: .bash}

~~~
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
~~~
{: .output}

Now we can use these functions to initialize and allocate memory for our vectors.

> ## Deallocating
> It is a good idea to match allocations to deallocations. We will add this functionality later in the [Destructors episode](../destructors) once we learn a bit more about derived types.
{: .callout}

> ## Access modifiers on derived type modifiers
> Access modifiers can be applied to derived types in a similar way to modules. Here is an example.
>
> ~~~
> module m_vector
> implicit none
>
> type t_vector
> integer,private:: num_elements
> real,dimension(:),allocatable:: elements
> end type
> ...
> end module
> ~~~
> {: .fortran}
> In this case the member variable `num_elements` could no longer be accessed from outside the module, > > while the `elements` member variable can be.
{: .callout}
> ## What is a derived type?
> Is a derived type:
> <ol type="a">
> <li markdown="1">a variable which can have multiple variable members
> </li>
> <li markdown="1">an object which can have multiple member variables
> </li>
> <li markdown="1">a datatype which can have multiple member variables
> </li>
> <li markdown="1">a datatype which can contain only a single value like `integer`,`real`,etc.
> </li>
> </ol>
> > ## Solution
> > <ol type="a">
> > <li markdown="1">**NO**: while a derived type can have multiple members, a derived type defines a new datatype where as a variable or object are the instantiation of that derived type or datatype.
> > </li>
> > <li markdown="1">**NO**: while an object has a derived type, the object is not the derived type its self.
> > </li>
> > <li markdown="1">**Yes**: a derived type is a kind of datatype that can have multiple members.
> > </li>
> > <li markdown="1">**No**: derived types can have multiple members, it is possible that they only have one member but they are not restricted to holding a single value.
> > </li>
> > </ol>
> {: .solution}
{: .challenge}
{% include links.md %}
34 changes: 32 additions & 2 deletions _episodes/destructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ exercises: 0
questions:
- "What is a destructor?"
objectives:
- "How do you create a destructor."
- "Create a destructor for our two derived types to deallocate memory."
keypoints:
- "A destructor is used to perform clean up when an object goes out of scope."
- "To create a destructor use the **final** keyword when declaring at type bound procedure instead of the procedure keyword."
---
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 with the object goes out of scope to deallocate this memory for us. To do this we use the **final** keyword within the type definition.

Expand All @@ -22,8 +23,13 @@ end type

Then as we did for the `display` subroutine, we can create a `<type-finalization-subroutine>` to do anything that needs to be done when an object of this type goes out of scope, including deallocating memory.

The below code shows the addition of two subroutines, `destructor_vector` to deallocate memory for `t_vector` objects, and `destructor_vector_3` to deallocate memory for our `t_vector_3` objects.
Lets add destructors to deallocate our memory for our two derived types.

~~~
$ cp type_bound_procedures_select_type.f90 destructor.f90
$ nano destructor.f90
~~~
{: .bash}
<div class="gitfile" markdown="1">
<div class="language-plaintext fortran highlighter-rouge">
<div class="highlight">
Expand Down Expand Up @@ -119,5 +125,29 @@ end program
[destructor.f90](https://github.com/acenet-arc/fortran_oop_as_a_second_language/blob/gh-pages/code/destructor.f90)
</div>

~~~
$ gfortran destructor.f90 destructor
$ ./destructor
~~~
{: .bash}
~~~
t_vector:
num_elements= 0
elements=
t_vector:
num_elements= 4
elements=
2.00000000
0.00000000
0.00000000
0.00000000
t_vector_3:
num_elements= 3
elements=
1.00000000
0.00000000
0.00000000
~~~
{: .output}
{% include links.md %}

26 changes: 23 additions & 3 deletions _episodes/extending_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ keypoints:
- "Type extension allows you to build upon an existing derived type to create a new derived type."
---

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

To create a new extended derived type has the following format.
~~~
Expand All @@ -23,7 +23,13 @@ Here `<parent type name>` is the name of a derived type to be extended, and `<ch

Our new 3 component vector however, doesn't need any new member variables so we don't need to add any new ones. However, by having a distinct derived data type for our 3 component vector will allow us to use specific procedures that work with it as apposed to the those for the more general vector, as we shall see shortly.

Below we show how to create our new 3 component vector, `t_vector_3`, as an extension of the original general vector derived type. We have also added a new `create_size_3_vector` function to create new 3 component vectors. The `...` in the below code indicates that the body of the type or procedure has been omitted for brevity and is the same as shown in previous code listings. The file name below the code listing will take you to a web page with the full code listing.
Lets create a new `t_vector_3` derived type and a `create_size_3_vector` to create new ones.

~~~
$ cp derived_types_init.f90 type_extension.f90
$ nano type_extension.f90
~~~
{: .bash}

<div class="gitfile" markdown="1">
<div class="language-plaintext fortran highlighter-rouge">
Expand Down Expand Up @@ -84,4 +90,18 @@ end program
[type_extension.f90](https://github.com/acenet-arc/fortran_oop_as_a_second_language/blob/gh-pages/code/type_extension.f90)
</div>

{% include links.md %}
~~~
$ gfortran type_extension.f90 -o type_extension
$ ./type_extension
~~~
{: .bash}

~~~
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
location%elements(1)= 1.00000000
~~~
{: .output}

{% include links.md %}
25 changes: 22 additions & 3 deletions _episodes/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ keypoints:
- "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."
---

As mentioned previously it is a very common task to create new objects of a derived type and perform some initialization of the members of that derive type and we had created some functions to do this. One of the functions, `create_empty_vector` takes no arguments and creates a new empty vector, while another procedure, `create_sized_vector` creates a new vector of a specific size passed to the procedure. 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 other `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.
As mentioned previously it is a very common task to create new objects of a derived type and perform some initialization of the members of that derive type and we had created some functions to do this. One of the functions, `create_empty_vector` takes no arguments and creates a new empty vector, while another procedure, `create_sized_vector` creates a new vector of a specific size passed to the procedure. 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.

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.
~~~
Expand All @@ -24,9 +24,15 @@ end interface
~~~
{: .fortran}

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.
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.

Lets use interface blocks to group our creation functions for `t_vector` and `t_vector_3` into one procedure name to initialize each derived type. 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.
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.

~~~
$ cp type_extension.f90 interface_blocks.f90
$ nano interface_blocks.f90
~~~
{: .bash}

<div class="gitfile" markdown="1">
<div class="language-plaintext fortran highlighter-rouge">
Expand Down Expand Up @@ -91,6 +97,19 @@ end program
[interface_blocks.f90](https://github.com/acenet-arc/fortran_oop_as_a_second_language/blob/gh-pages/code/interface_blocks.f90)
</div>

~~~
$ gfortran interface_blocks.f90 interface_blocks
$ ./interface_blocks
~~~
{: .bash}
~~~
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
location%elements(1)= 1.00000000
~~~
{: .output}

The way we have used interface blocks above is what is called a **generic interface** as it maps one generic procedure name we specified to multiple specific procedures. This is very similar to what other object oriented languages call **overloading**.

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.
Expand Down
Loading

0 comments on commit efe9719

Please sign in to comment.