|
| 1 | +# [Multiple dispatch and composition](@id manual_objets) |
| 2 | + |
| 3 | +In this document we will learn how types and methods relate in Julia. |
| 4 | + |
| 5 | +## Types |
| 6 | + |
| 7 | +A type in Julia is a structure that collects related data and can be assigned specific |
| 8 | +behavior (see below on *methods*). We create types using the `struct` keyword (with optional |
| 9 | +modifiers) and listing the data fields that the type will contain (and we recommend to |
| 10 | +include the type of data, for performance reasons). |
| 11 | + |
| 12 | +For example, we can create types that represent leaves and fruits of a plant with some |
| 13 | +basic properties. |
| 14 | + |
| 15 | +```julia |
| 16 | +struct Leaf |
| 17 | + length::Float64 |
| 18 | + width::Float64 |
| 19 | + weight::Float64 |
| 20 | + color::String |
| 21 | +end |
| 22 | + |
| 23 | +struct Fruit |
| 24 | + radius::Float64 |
| 25 | + weight::Float64 |
| 26 | + color::String |
| 27 | +end |
| 28 | +``` |
| 29 | + |
| 30 | +We can create instances of these types: |
| 31 | + |
| 32 | +```julia |
| 33 | +L = Leaf(10.0, 5.0, 1.0, "green") |
| 34 | +F = Fruit(1.0, 0.5, 1.0, "red") |
| 35 | +``` |
| 36 | + |
| 37 | +All the data fields in a type are public by default, which means that we can access them |
| 38 | +directly. |
| 39 | + |
| 40 | +```julia |
| 41 | +L.length |
| 42 | +F.color |
| 43 | +``` |
| 44 | + |
| 45 | +## Methods |
| 46 | + |
| 47 | +Functions in Julia can be defined to operate on specific types of data if you annotate the |
| 48 | +type of arguments in the function definition. For example, we can define a function to |
| 49 | +calculate the surface area of a plant organ, but the formula to be used is different for |
| 50 | +leaves and fruits. We could define two different functions, one for each type (e.g., |
| 51 | +`area_leaf` and `area_fruit`), but this would make the code more quite cumbersome and hard |
| 52 | +to manage. Instead, we can define a single function `area` that will |
| 53 | +behave differently depending on the type of the argument passed to it: |
| 54 | + |
| 55 | +```julia |
| 56 | +# Area of a leaf assuming an ellipse |
| 57 | +function area(organ::Leaf) |
| 58 | + pi*organ.length*organ.width/4 |
| 59 | +end |
| 60 | + |
| 61 | +# Area of a fruit assuming a sphere |
| 62 | +function area(organ::Fruit) |
| 63 | + 4*pi*organ.radius^2 |
| 64 | +end |
| 65 | +``` |
| 66 | + |
| 67 | +We can now call the function `area` with either a leaf or a fruit and it will return the |
| 68 | +correct area: |
| 69 | + |
| 70 | +```julia |
| 71 | +area(L) |
| 72 | +area(F) |
| 73 | +``` |
| 74 | + |
| 75 | +This is an example of *multiple dispatch*, which is a key feature of Julia. It allows us to |
| 76 | +define functions that can operate on different types of data and have different behavior |
| 77 | +depending on the type of the argument passed to it. The *dispatch* part means that the |
| 78 | +call to the function `area` will be dispatched to the correct method based on the type of the |
| 79 | +argument passed to it. |
| 80 | + |
| 81 | +Dispatch will work on the combination of the types of all arguments, not just the |
| 82 | +first one. For example, let's define two types of pests that can affect a plant, a larva that |
| 83 | +can infest fruits and a caterpillar that can infest leaves. We want to test whether a particular |
| 84 | +pest can infest a particular organ of the plant. We can define the types and methods as |
| 85 | +follows: |
| 86 | + |
| 87 | +```julia |
| 88 | +struct Larva |
| 89 | +end |
| 90 | +struct Caterpillar |
| 91 | +end |
| 92 | +# Method to test whether a larva can infest an organ |
| 93 | +infest(pest::Larva, organ::Fruit) = true |
| 94 | +infest(pest::Larva, organ::Leaf) = false |
| 95 | +# Method to test whether a caterpillar can infest an organ |
| 96 | +infest(pest::Caterpillar, organ::Fruit) = false |
| 97 | +infest(pest::Caterpillar, organ::Leaf) = true |
| 98 | +``` |
| 99 | + |
| 100 | +Note that we did not add any fields to the pest types, as for now we are only interested in |
| 101 | +whether they can infest a particular organ or not. Also, we are defining the methods using |
| 102 | +a simpler syntax (without the `function` and `end` keyword) as they are quite simple. |
| 103 | + |
| 104 | +Note that you can also define methods where the arguments are not annotated with specific |
| 105 | +types. This will become a default method that will be called if the types of the arguments |
| 106 | +do not match any of the the other methods. For example, we can add a default method that |
| 107 | +returns `false` by default: |
| 108 | + |
| 109 | +```julia |
| 110 | +infest(pest, organ) = false |
| 111 | +``` |
| 112 | + |
| 113 | +This means that if we call `infest` with a pest and an organ that are not |
| 114 | +`Larva` or `Caterpillar` and `Fruit` or `Leaf`, respectively, it will return `false`. Of |
| 115 | +course, we can add more specific methods later to handle other types of pests or organs. |
| 116 | + |
| 117 | +Note that you can define methods for types that you did not create, not just for your |
| 118 | +own types, even if they are stored in packages you downloaded from the internet. This |
| 119 | +allows extending functionality of existing types and allows your own types to interact |
| 120 | +with types defined by someone else. |
| 121 | + |
| 122 | +Also, you can sometimes define types and methods that are meant to use by some algorithm in |
| 123 | +a package, also extending the functionality of that package. For example, in VPL, you can |
| 124 | +define types that are meant to be used as nodes in graphs and this can be achieved by simply |
| 125 | +defining a couple of methods for specific functions defined in VPL (like the `feed!` method |
| 126 | +to generate geometry). The flexibility of multiple dispatch is one of the key features of |
| 127 | +Julia. |
| 128 | + |
| 129 | +## Abstract types |
| 130 | + |
| 131 | +Abstract types are an optional feature in Julia that allows implementing a reduced form of |
| 132 | +*inheritance* in Julia. The idea is that one can define a method for an abstract type and |
| 133 | +any type that inherits from that abstract type will match that method. Abstract types can |
| 134 | +also inherit from other abstract types, allowing to create a hierarchy of types. |
| 135 | + |
| 136 | +For example, we could define an abstract type `Organ` from which all plants organs will |
| 137 | +inherit. Abstract types do not contain any data and we cannot create instances of them, |
| 138 | +they are really just tags for asigning methods. Inheritance is indicated with |
| 139 | +the symbol `<:` after the name of the type. |
| 140 | + |
| 141 | +Let's create a new version of the `Leaf` and `Fruit` types that inherit from an `Organ` |
| 142 | +abstract type. Unfortunately, Julia does not allow redefining types (unless you put them |
| 143 | +in a module and import said module, we will do this in the VPL tutorials), so we will just |
| 144 | +call them `Leaf2` and `Fruit2` for the purpose of this example: |
| 145 | + |
| 146 | +```julia |
| 147 | +abstract type Organ end |
| 148 | + |
| 149 | +struct Leaf2 <: Organ |
| 150 | + length::Float64 |
| 151 | + width::Float64 |
| 152 | + weight::Float64 |
| 153 | + color::String |
| 154 | +end |
| 155 | + |
| 156 | +struct Fruit2 <: Organ |
| 157 | + radius::Float64 |
| 158 | + weight::Float64 |
| 159 | + color::String |
| 160 | +end |
| 161 | +``` |
| 162 | + |
| 163 | +We could now define a method that operates on any organ, regardless of its type, |
| 164 | +for example, to extract the color of the organ: |
| 165 | + |
| 166 | +```julia |
| 167 | +get_color(organ::Organ) = organ.color |
| 168 | +``` |
| 169 | + |
| 170 | +And we can call it with any organ type that inherits from `Organ`: |
| 171 | + |
| 172 | +```julia |
| 173 | +L2 = Leaf2(10.0, 5.0, 1.0, "green") |
| 174 | +F2 = Fruit2(1.0, 0.5, 1.0, "red") |
| 175 | +println("Leaf2 color: ", get_color(L2)) |
| 176 | +println("Fruit2 color: ", get_color(F2)) |
| 177 | +``` |
| 178 | + |
| 179 | +Note that if we now define a method of `get_color` for `Leaf2` or `Fruit2`, it will |
| 180 | +override the method for `Organ` and that one will be called instead. That is, the method |
| 181 | +defined for the abstract type will be called only if there is no more specific method |
| 182 | +defined for the concrete type. Abstract types and inheritance are not as important in Julia |
| 183 | +as in traditional object-oriented programming languages. |
| 184 | + |
| 185 | +In the context of VPL, you will need to define some types to extend the functionality of the |
| 186 | +package and in those cases you will need to inherit from specific abstract types defined in |
| 187 | +VPL. For example, when defining your own type of data structures to be used as node in |
| 188 | +dynamic graphs (see tutorials for examples) those types will need to inherit from the |
| 189 | +`Node` abstract type defined in VPL. This allows the internal code for graph rewriting to |
| 190 | +handle objects defined by the user (which are obviously not known by the VPL developers |
| 191 | +ahead of time). |
| 192 | + |
| 193 | +In addition, the user will have to define specific methods for their data structures that |
| 194 | +are expected by the VPL |
| 195 | +in order for their internal algorithms to work properly. This is known as an *interface* |
| 196 | +and it is a common practice in Julia programming. For example, in the most common version |
| 197 | +of FSP models built with VPL, the user will have to define data types that inherit from |
| 198 | +`Node` and define a method for the `feed!` function that generates the geometry associated |
| 199 | +to each type of node. If you omit the `feed!` method, you will still be able to use those |
| 200 | +types as nodes in the dynamic graphs but they will not generate any geometry (which in some |
| 201 | +cases it may be what you want). |
| 202 | + |
| 203 | +## Composition |
| 204 | + |
| 205 | +In the previous sections we have seen how to define types and methods in Julia, as well as |
| 206 | +how to use abstract types to create a hierarchy of types. These two approaches allow for |
| 207 | +reuse of methods for multiple types (that share the same abstract type) as well as extending |
| 208 | +code to work with new types. However, the methods that are being reused expected certain |
| 209 | +data to be expected in the type. For example, the `get_color` method above expects that the |
| 210 | +type has a `color` field, otherwise it will throw an error. There is therefore a need to |
| 211 | +reuse data as well, not just methods. This is where *composition* comes into play. |
| 212 | + |
| 213 | +Composition is a design principle in which a complex object is composed of simpler objects. |
| 214 | +The idea is that the simpler objects implement a specific functionality with associated data |
| 215 | +such that we can add functionality to a type by composing it with other types. Let's define |
| 216 | +the functionality *growth* that will confer any organ the ability to grow. We will assume |
| 217 | +a logistic growth model, where current growth rate is proportional to the current weight. We |
| 218 | +thus need to keep track of the current weight, the maximum weight that the organ can attain |
| 219 | +and the relative growth rate. We can define a type that implements this functionality as follows: |
| 220 | + |
| 221 | +```julia |
| 222 | +mutable struct Growth |
| 223 | + weight::Float64 # Current weight of the organ |
| 224 | + max_weight::Float64 # Maximum weight of the organ |
| 225 | + rgr::Float64 # Relative growth rate |
| 226 | +end |
| 227 | +``` |
| 228 | + |
| 229 | +Note that we added the `mutable` keyword to the type definition, which means that |
| 230 | +instances of this type can be modified after they are created. This is important because |
| 231 | +we will need to update the current weight of the organ as it grows. We can then define a |
| 232 | +method that will update the current weight of the organ based on the |
| 233 | +growth rate and the maximum weight: |
| 234 | + |
| 235 | +```julia |
| 236 | +function grow!(growth::Growth) |
| 237 | + if growth.weight < growth.max_weight |
| 238 | + growth.weight += growth.rgr*growth.weight*(1 - growth.weight/growth.max_weight) |
| 239 | + end |
| 240 | + return nothing |
| 241 | +end |
| 242 | +``` |
| 243 | + |
| 244 | +Note that we modify the `weight` field of the `growth` instance in place, which is |
| 245 | +possible because we defined the type as `mutable`. The `grow!` function will not return |
| 246 | +anything, it will just update the `weight` field of the `growth` instance. |
| 247 | + |
| 248 | +We can now define new types of leaves and fruits that will have the ability to grow by |
| 249 | +composing them with the `Growth` type. As explained before, we need to define new types |
| 250 | +because we did not put them in their own module and import it: |
| 251 | + |
| 252 | +```julia |
| 253 | +struct Leaf3 <: Organ |
| 254 | + length::Float64 |
| 255 | + width::Float64 |
| 256 | + color::String |
| 257 | + growth::Growth # Composition with Growth type |
| 258 | +end |
| 259 | +struct Fruit3 <: Organ |
| 260 | + radius::Float64 |
| 261 | + color::String |
| 262 | + growth::Growth # Composition with Growth type |
| 263 | +end |
| 264 | +``` |
| 265 | + |
| 266 | +Note how we have replaced the previous `weight` field with a `growth` field that |
| 267 | +contains an instance of the `Growth` type. We can now create instances of `Leaf3` and `Fruit3` |
| 268 | +and pass an instance of `Growth` to them: |
| 269 | + |
| 270 | +```julia |
| 271 | +L3 = Leaf3(10.0, 5.0, "green", Growth(1.0, 0.1, 0.1)) |
| 272 | +F3 = Fruit3(1.0, "red", Growth(1.0, 0.2, 0.1)) |
| 273 | +``` |
| 274 | + |
| 275 | +Of course our types would need to be improved as their dimensions are now decoupled from the |
| 276 | +weight of the organ itself, but remember that this is just an example to illustrate features |
| 277 | +of the Julia language and in a real model we would later add changes to the types as needed |
| 278 | +(in fact, you are likely to develop models in this iterative, dynamic way, rather than |
| 279 | +figure everything out ahead of time). We can now call the `grow!` method on the |
| 280 | +`growth` field of the `Leaf3` and `Fruit3` instances to update their |
| 281 | +current weight: |
| 282 | + |
| 283 | +```julia |
| 284 | +grow!(L3.growth) |
| 285 | +grow!(F3.growth) |
| 286 | +``` |
| 287 | + |
| 288 | +And the weight of the organs will be updated accordingly. We can access the current weight |
| 289 | +of the organs by accessing the `weight` field of the `growth` field: |
| 290 | + |
| 291 | +```julia |
| 292 | +L3.growth.weight |
| 293 | +F3.growth.weight |
| 294 | +``` |
| 295 | + |
| 296 | +## Method forwarding |
| 297 | + |
| 298 | +We have seen how to compose types in Julia to add functionality to them. However, this |
| 299 | +means that we need to access the methods of the composed type through the field name, which |
| 300 | +can be cumbersome. For example, we need to call `grow!(L3.growth)` |
| 301 | +to grow the leaf, which is not very intuitive. We can use *method forwarding* to |
| 302 | +make this more intuitive. Method forwarding is a technique that allows us to define methods |
| 303 | +that forward the call to the method of the composed type. For example, we can define a |
| 304 | +method for the `grow!` function that forwards the call to the `growth` field of the organ. |
| 305 | +We can define this method per organ type or, if we expect all organs to grow, we can define |
| 306 | +it for the abstract type `Organ` so that all organs will have the same behavior: |
| 307 | + |
| 308 | +```julia |
| 309 | +function grow!(organ::Organ) |
| 310 | + grow!(organ.growth) |
| 311 | +end |
| 312 | +``` |
| 313 | + |
| 314 | +Now we can call `grow!(L3)` and `grow!(F3)` to grow the leaf and the fruit, respectively: |
| 315 | + |
| 316 | +```julia |
| 317 | +grow!(L3) |
| 318 | +grow!(F3) |
| 319 | +``` |
| 320 | + |
| 321 | +Here we are using multiple dispatch and inheritance to use the `grow!` method on all organs |
| 322 | +while relying on type composition to implement the actual growth functionality. If you need |
| 323 | +to forward many methods you may want to use a macro to automate the process or use existing |
| 324 | +packages that already implement such macros (e.g., `MethodForwarding.jl` or `ForwardMethods.jl`). |
| 325 | + |
| 326 | +At this point you may be wondering why we did not just define the `grow!` method |
| 327 | +directly in the `Leaf3` and `Fruit3` types. The reason is modularity. By separating the |
| 328 | +data structures and methods according to functionality, we can encapsulate the relevant code |
| 329 | +and make it easier to use, without necessarily having to know all the details. This approach |
| 330 | +is currently being used in the VPLverse package `Ecophys.jl`, where data structures for, for |
| 331 | +example, photosynthesis are defined with associated methods. Thus, adding the ability to |
| 332 | +photosynthesize to a plant organ is as simply as adding one of the relevant data types from |
| 333 | +`Ecophys.jl` and calling the relevant method. |
| 334 | + |
| 335 | +## About object-oriented programming |
| 336 | + |
| 337 | +If one searches online whether Julia implements object-oriented programming (OOP), the results |
| 338 | +will be mixed as it depends entirely on how does one define OOP. |
| 339 | +If the definition matches what is understood by OOP in languages |
| 340 | +such as C++, Java or Python (i.e. *classic* OOP), then the answer is simply no. The reason for this is that: |
| 341 | + |
| 342 | +- Julia only allows inheritance from abstract types. This means that concrete types in |
| 343 | + Julia can inherit methods from their parent types but not data. |
| 344 | +- Data types in Julia encapsulate data but not methods (i.e., objects in Julia do not own |
| 345 | + methods). |
| 346 | + |
| 347 | +If one defines OOP as a paradigm that requires encapsulation of data |
| 348 | +(but not necessarily methods) and inheritance of methods (but not necessarily data) then |
| 349 | +the approach used in Julia and described above would qualify as OOP. |
| 350 | +That is, the answer to whether Julia implements OOP depends entirely |
| 351 | +on how one defines the paradigm and there is simply no right or wrong way of defining concepts. |
| 352 | + |
| 353 | +If you are transitioning form a language with *classic* OOP you will need |
| 354 | +to rethink how to organize your code if you want to stick to a *Julian* way of programming. |
| 355 | +Essentially you should replace inheritance of data with object composition (plus optionally |
| 356 | +method forwarding) and use multiple method dispatch for functionality (what in *classic* OOP |
| 357 | +would be *interfaces*). Searching online for *composition over inheritance* may help with |
| 358 | +the transition (e.g., https://en.wikipedia.org/wiki/Composition_over_inheritance). |
0 commit comments