"In Julia, there are several built-in functions related to querying and working with types and `DataType``. \n",
"In Julia, there are several built-in functions related to querying and working with types. \n",
"Here is a selection of some important ones:\n",
"Here is a selection of some important ones:\n",
"\n",
"\n",
"- `<:`: The subtype operator used to check if a type is a subtype of another type.\n",
"- `<:`: The subtype operator used to check if a type is a subtype of another type.\n",
...
@@ -389,7 +389,9 @@
...
@@ -389,7 +389,9 @@
"outputs": [],
"outputs": [],
"source": [
"source": [
"a_dog = Dog(\"Buddy\", 3)\n",
"a_dog = Dog(\"Buddy\", 3)\n",
"a_cat = Cat(\"Kitty\", 2)"
"a_cat = Cat(\"Kitty\", 2)\n",
"\n",
"@show a_dog a_cat;"
]
]
},
},
{
{
...
@@ -465,8 +467,8 @@
...
@@ -465,8 +467,8 @@
"id": "ff7efe19",
"id": "ff7efe19",
"metadata": {},
"metadata": {},
"source": [
"source": [
"### Exercise\n",
"### Exercises\n",
"Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree."
"Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree. For example you can implement the abstract type `Bird` and then a few kinds of birds."
]
]
},
},
{
{
...
@@ -481,124 +483,77 @@
...
@@ -481,124 +483,77 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "9efaf879",
"id": "52b60c4e-1d3f-46f6-87a9-c3ab2917d8d0",
"metadata": {},
"source": [
"In Julia concrete types are always a leaf of the type tree, i.e. they cannot be inherited from each other. For a C++ or Python person (as I was before looking into Julia) this seems restrictive at first, but it takes away a lot of unnecessary complexity from the type hierachy. In Julia the structure of a library or a problem\n",
"is in many cases not converted into explict type hierachies,\n",
"as it would for OOP languages like Python or Java.\n",
"Instead it builds up implicitly from conventions which are associated with abstract or concrete types.\n",
"\n",
"For example, if one implements a concrete type for the abstract type `Number` one is expected to implement a certain set of functions (e.g. `*`, `+`, `-`, `/`, ...). Otherwise not all of the standard library and other linear algebra packages will work. The difference to a hard enforcement of interfaces is, however, that *some things* will still work. This has disadvantages as your code could break in the future, but it is extremely useful for rapidly trying something out.\n",
"In programming language theory type systems traditionally fall in two categories.\n",
"Which of the following type are subtypes of another?\n",
"In **dynamically typed** languages the type of\n",
"Try to guess first and then verify by using the operator `<:`.\n",
"a value or expression is inferred only at runtime,\n",
"which usually allows for more flexible code. Examples are Python or MATLAB.\n",
"In contrast, so-called **statically-typed** languages (think FORTRAN or C++),\n",
"require types to be already known before runtime when the program is compiled.\n",
"This allows both to check more thoroughly for errors (which can manifest in mismatched types)\n",
"and it usually brings a gain in performance because more things about the memory layout of the program is known\n",
"at compile time. As a result aspects such as vectorisation, contiguous alignment of data,\n",
"preallocation of memory can be leveraged more easily.\n",
"\n",
"\n",
"Julia is kind of both. Strictly speaking it is dynamically typed. E.g. the type of variables can change type at any point:"
"```julia\n",
"Float64 AbstractFloat Integer\n",
"Number AbstractArray Complex\n",
"Real Any Nothing\n",
"```"
]
]
},
},
{
{
"cell_type": "code",
"cell_type": "code",
"execution_count": null,
"execution_count": null,
"id": "3ef4e3d0",
"id": "688afde5-1123-4adc-affd-687b966f387e",
"metadata": {},
"metadata": {},
"outputs": [],
"outputs": [],
"source": [
"source": [
"x = Dog(\"Buddy\", 4) # x is an Dog\n",
"# TODO: implement your code"
"@show typeof(x)\n",
"\n",
"x = Cat(\"Kitty\", 3) # Now x is a Cat\n",
"@show typeof(x);"
]
]
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "a02c0a37",
"id": "9efaf879",
"metadata": {},
"metadata": {},
"source": [
"source": [
"Note, however, that the type of a *value* cannot change in Julia!\n",
"In Julia concrete types are always a leaf of the type tree, i.e. they cannot be inherited from each other. For a C++ or Python person (as a few of us) this seems restrictive at first, but it takes away a lot of unnecessary complexity from the type hierarchy. We will not give further information now, the reason will be more clear at the end of this notebook.\n",
"\n",
"\n",
"Still, Julia's strong emphasis on types are one of the reasons for its performance.\n",
"Unlike in statically typed languages, however, **type deduction in Julia** happens at runtime, right before JIT-compiling a function: The more narrowly the types for a particular piece of code can be deduced, the better the emitted machine code can be optimised. One can influence this using explicit type annotations in function arguments and intermediate expressions. Due to the to the excellent type inference capabilities of Julia, this is in general not needed, however.\n",
"\n",
"This might sound a bit unusal at first, but the result is,\n",
"that it takes almost no effort to write generic code as we will see later: Just leave off all the type annotations. Notice, that this only means that the code has no types. At runtime types are still inferred as much as possible, such that aspects like vectorisation, contiguous alignment of data, preallocation of memory *can* be taken into account by the Julia compiler."
]
},
{
"cell_type": "markdown",
"id": "27812692",
"metadata": {},
"source": [
"Three more facts about Julia types:\n",
"- In Julia all types are the same. For example, there is no difference between `Int32` and `String`, even though the first has a direct mapping to low-level instructions in the CPU and the latter has not (contrast this with e.g. C++).\n",
"- The `Nothing` type with the single instance `nothing` is the Julia equivalent to `void` in C++ or `None` in Python. It often represents that a function does not return anything or that a variable has not been initialised.\n",
"- `Any` is the root of the type tree: Any type in Julia is a subtype of `Any`."
]
]
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "0a6002c9",
"id": "20a01368-ff73-4992-afae-792962aee09f",
"metadata": {},
"metadata": {},
"source": [
"source": [
"### Exercise\n",
"## Multiple dispatch\n",
"Which of the following type are subtypes of another?\n",
"\n",
"Try to guess first and then verify by using the operator `<:`.\n",
"Multiple dispatch is probably the key feature of Julia that makes it different with respect to many other languages and give it the ability to be at the same time flexible and performant.\n",
"\n",
"To fully understand the concept of multiple dispatch, we will use an analogy, we will pretend that a programming language is a tool to write a book of food recipes. A recipe combines a method of cooking (for example baking, frying) with an ingredient (for example potatoes, carrots, fish).\n",
"\n",
" - The first possibility is to organize the book according to methods of cooking: each chapter explains in detail a method of cooking. For example, we will have **Chapter 1: baking**, **Chapter 2: frying** and so on. The drawback of this approach is that whenever we add a new ingredient, we have to change multiple chapters. This approach, focused on the action rather than on ingredients, is typical of functional programming languages.\n",
"\n",
"\n",
" - The second possibility is to organize the book according to ingredients: each chapter focuses on one ingredient. For example, we will have **Chapter 1: potatoes**, **Chapter 2: fish** and so on. The drawback of this approach is that whenever we add a new recipe, we have again to change multiple chapters. This approach is focused on the ingredients (data) and it is typical of object-oriented programming, where we will have something like:\n",
" ``` python\n",
" class potatoes()\n",
" potatoes.bake()\n",
" potatoes.fry()\n",
" ```\n",
"\n",
"\n",
" - Julia takes a third approach called **multiple dispatch** which decouples the action from the data. In our hypothetical recipe book, we will have chapters like **Chapter 1: baking potatoes**, **Chapter 2: frying potatoes**, **Chapter 3: baking fish**, **Chapter 4: frying fish** and so on. Each of the chapters will contain something like:\n",
" ``` julia\n",
" ``` julia\n",
"Float64 AbstractFloat Integer\n",
" function baking(potatoes::Potatoes)\n",
"Number AbstractArray Complex\n",
" function frying(potatoes::Potatoes)\n",
"Real Any Nothing\n",
" function baking(fish::Fish)\n",
"```"
" function frying(fish::Fish)\n",
]
" ```\n",
},
"\n",
{
" In this way, adding a new recipe for a specific kind of food does not require changing already written things. "
"cell_type": "markdown",
"id": "ebc902af",
"metadata": {},
"source": [
"##### For more details\n",
"https://docs.julialang.org/en/v1/manual/types/"
]
},
{
"cell_type": "markdown",
"id": "25a35122",
"metadata": {},
"source": [
"## Multiple dispatch"
]
]
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "eec2ef90",
"id": "a0a6aecb-bd29-472c-921b-c4135a09b026",
"metadata": {},
"metadata": {},
"source": [
"source": [
"Let us return back to the `mymult` function:"
"Let us return back to the `mymult` function and see how we can use the multiple dispatch to implement new functionality:"
]
]
},
},
{
{
...
@@ -613,7 +568,7 @@
...
@@ -613,7 +568,7 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "d99de7cd",
"id": "d27da057-cb24-45a2-b190-23ef6c8207fa",
"metadata": {},
"metadata": {},
"source": [
"source": [
"We were able to safely use this functions with a number of type combinations, but some things do not yet work:"
"We were able to safely use this functions with a number of type combinations, but some things do not yet work:"
...
@@ -631,7 +586,7 @@
...
@@ -631,7 +586,7 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "dccf4b43",
"id": "6757d995-cbc8-4c7b-be3a-fbdd9c8b984c",
"metadata": {},
"metadata": {},
"source": [
"source": [
"Let's say we wanted to concatenate the string `str` $n$ times on multiplication with an integer $n$. In Julia this functionality is already implemented by the exponentiation operator:"
"Let's say we wanted to concatenate the string `str` $n$ times on multiplication with an integer $n$. In Julia this functionality is already implemented by the exponentiation operator:"
...
@@ -649,7 +604,7 @@
...
@@ -649,7 +604,7 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "5d2af9cb",
"id": "ee1f8e46-3b4d-4f63-8e14-44ff70d7b5cf",
"metadata": {},
"metadata": {},
"source": [
"source": [
"But for the sake of argument, assume we wanted `mymult(\"abc\", 4)` and `mymult(4, \"abc\")` to behave the same way. We define two special methods:"
"But for the sake of argument, assume we wanted `mymult(\"abc\", 4)` and `mymult(4, \"abc\")` to behave the same way. We define two special methods:"
"In both of these, the syntax `str::AbstractString` and `n::Integer` means that the respective method is only\n",
"In both of these, the syntax `str::String` and `n::Integer` means that the respective method is only\n",
"considered during dispatch if the argument `str` is of type `AbstractString` or one of its concrete subtypes and similarly `n` is an `Integer` (or subtype). Since Julia always dispatches to the most specific method in case multiple methods match, this is all we need to do:"
"considered during dispatch if the argument `str` is of type `String` and similarly `n` is an `Integer` (or subtype). Since Julia always dispatches to the most specific method in case multiple methods match, this is all we need to do:"
]
]
},
},
{
{
...
@@ -707,7 +662,7 @@
...
@@ -707,7 +662,7 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "4390819c",
"id": "75814310-5d55-4f7b-8beb-903cb31366df",
"metadata": {},
"metadata": {},
"source": [
"source": [
"Notice, that the fully generic\n",
"Notice, that the fully generic\n",
...
@@ -720,6 +675,34 @@
...
@@ -720,6 +675,34 @@
"```"
"```"
]
]
},
},
{
"cell_type": "markdown",
"id": "eb674aa1-2380-4ae5-b7b2-ad7579239def",
"metadata": {},
"source": [
"### Methods\n",
"\n",
"In Julia different version of a function, which work on different kinds of arguments are called **methods**. You can see the list of methods by running this:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e52b3aee-2cbc-4cec-b131-204f9889b939",
"metadata": {},
"outputs": [],
"source": [
"methods(mymult)"
]
},
{
"cell_type": "markdown",
"id": "c72d032a-1041-4dbd-9742-b2fa4c0ae86a",
"metadata": {},
"source": [
"If you run methods on a core function like `*`, you will get quite a long list."
]
},
{
{
"cell_type": "code",
"cell_type": "code",
"execution_count": null,
"execution_count": null,
...
@@ -730,6 +713,14 @@
...
@@ -730,6 +713,14 @@
"methods(*)"
"methods(*)"
]
]
},
},
{
"cell_type": "markdown",
"id": "b0f1b3da-885d-47b0-83b5-13039d88320e",
"metadata": {},
"source": [
"Using the macro `@which` can be used to discover which specific method is being run."
]
},
{
{
"cell_type": "code",
"cell_type": "code",
"execution_count": null,
"execution_count": null,
...
@@ -737,12 +728,13 @@
...
@@ -737,12 +728,13 @@
"metadata": {},
"metadata": {},
"outputs": [],
"outputs": [],
"source": [
"source": [
"@which \"Hello\"*\"World!\""
"println(@which \"Hello\"*\"World!\")\n",
"println(@which mymult(\"a\", 3))"
]
]
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "f1c0a83e",
"id": "49e0a478-3718-48bd-8370-029530b23a3f",
"metadata": {},
"metadata": {},
"source": [
"source": [
"We can also add some method for `Animal`, `Dog`, and `Cat`:"
"We can also add some method for `Animal`, `Dog`, and `Cat`:"
...
@@ -764,7 +756,7 @@
...
@@ -764,7 +756,7 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "7d6da0ec",
"id": "ebeeb1e3-f7c0-4a0a-a3fa-539973ef0b36",
"metadata": {},
"metadata": {},
"source": [
"source": [
"Then, let's test it."
"Then, let's test it."
...
@@ -783,7 +775,129 @@
...
@@ -783,7 +775,129 @@
},
},
{
{
"cell_type": "markdown",
"cell_type": "markdown",
"id": "85f3e064",
"id": "790fc10a-4459-4b52-95f6-9718a31d4152",
"metadata": {},
"source": [
"### Abstract types and multiple dispatch\n",
"\n",
"To show the role of abstract types in multiple dispatch, we go back to our food example. First we need to clarify the hierarchy of food types. We can use abstract types for that."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bd602d8f-8074-4da6-9b05-1b9b091b7e43",
"metadata": {},
"outputs": [],
"source": [
"abstract type Food end\n",
"\n",
"abstract type Vegetables <: Food end\n",
"struct Potatoes <: Vegetables end\n",
"struct Carrots <: Vegetables end\n",
"\n",
"abstract type Fish <: FI will not be able to add him until monday as I don't have access to my notebookood end\n",
"struct Salmon <: Fish end\n",
"struct Shrimps <: Fish end # not biologically a fish, but we can consider them so from a culinary point of view"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3a9a5ca1-d549-49d9-8a91-6c9c58374efb",
"metadata": {},
"outputs": [],
"source": [
"print_type_tree(Food)"
]
},
{
"cell_type": "markdown",
"id": "508debf1-7ac1-44b3-b994-7ddc122dded8",
"metadata": {},
"source": [
"Now, we found a frying recipe which works decently for any kind of vegetable. Then we will write something like:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6e81e79-7d93-40bb-9e18-3c307c4ca8ea",
"metadata": {},
"outputs": [],
"source": [
"function frying(vegetable::Vegetables)\n",
" # make tempura\n",
" # fry\n",
"end"
]
},
{
"cell_type": "markdown",
"id": "8b67deab-9430-4eb1-968d-497065fca06e",
"metadata": {},
"source": [
"If we want to fry our vegetables, we will run"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c9b3b8b9-91ba-4d30-8e71-dca2a478d4df",
"metadata": {},
"outputs": [],
"source": [
"potatoes = Potatoes()\n",
"carrots = Carrots()\n",
"\n",
"println(@which frying(potatoes))\n",
"println(@which frying(carrots))"
]
},
{
"cell_type": "markdown",
"id": "7071213e-cc38-4fb2-a854-c5ebe0084a46",
"metadata": {},
"source": [
"But now we found an even more specific recipe that works even better for potatoes. What we will do is writing a new function which is specific for potatoes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "87cc4c94-1dc6-4515-8b23-135f7428ba1d",
"metadata": {},
"outputs": [],
"source": [
"function frying(potatoes::Potatoes)\n",
" # directly fry in oil\n",
"end"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c4a8bcc4-c551-47d2-ab9f-bdf8e42142c4",
"metadata": {},
"outputs": [],
"source": [
"println(@which frying(potatoes))"
]
},
{
"cell_type": "markdown",
"id": "db13beea-e3cc-4c6e-b996-d3f6c77fb171",
"metadata": {},
"source": [
"This example really shows the power of Julia. Multiple dispatch is good for these reasons:\n",
" - **Flexibilty:** it is possible to try out new things in a very fast way. We implement a new data type and we use the methods which are already there for abstract types.\n",
" - **Customization:** implementing custom behavior for our data types is easy, we simply need to add a custom method.\n",
" - **Efficiency:** we can tune the specific methods to be super fast with our specific data type."
]
},
{
"cell_type": "markdown",
"id": "b39955b1-eed2-4c1f-bb61-fdcc0554a7b7",
"metadata": {},
"metadata": {},
"source": [
"source": [
"### Exercise\n",
"### Exercise\n",
...
@@ -799,11 +913,103 @@
...
@@ -799,11 +913,103 @@
"source": [
"source": [
"# TODO: implement your code here."
"# TODO: implement your code here."
]
]
},
{
"cell_type": "markdown",
"id": "ccf57f28",
"metadata": {},
"source": [
"## Dynamical typing and type deduction"
]
},
{
"cell_type": "markdown",
"id": "85870a09",
"metadata": {},
"source": [
"In programming language theory type systems traditionally fall in two categories.\n",
"In **dynamically typed** languages the type of a value or expression is inferred only at runtime,\n",
"which usually allows for more flexible code. Examples are Python or MATLAB. In contrast, so-called **statically-typed** languages (think FORTRAN or C++), require types to be already known before runtime when the program is compiled. This allows both to check more thoroughly for errors (which can manifest in mismatched types) and it usually brings a gain in performance because more things about the memory layout of the program is known at compile time. As a result, aspects such as vectorization, contiguous alignment of data, preallocation of memory can be leveraged more easily.\n",
"\n",
"Julia is kind of both."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3ef4e3d0",
"metadata": {},
"outputs": [],
"source": [
"x = Dog(\"Buddy\", 4) # x is an Dog\n",
"@show typeof(x)\n",
"\n",
"x = Cat(\"Kitty\", 3) # Now x is a Cat\n",
"@show typeof(x);"
]
},
{
"cell_type": "markdown",
"id": "a02c0a37",
"metadata": {},
"source": [
"Julia's strong emphasis on types is one of the reasons for its performance and flexibility.\n",
"\n",
"When the code is precompiled before execution, the compiler has the information about the type of all the variables in the program. It will then search the best possible method for each of those. If a specific and highly efficient one is found, that will be used. If a specific one is missing, it will use the next possibility in the type tree, which will still work even if not as efficiently.\n",
"\n",
"**Note:** this look up is the reason why it is not possible to instantiate abstract types, having variables of abstract types would make this operation unnecessary complicated. Moreover, it also reflects reality: a generic vegetable does not physically exist, we only have potatoes, carrots, eggplants and so on."
]
},
{
"cell_type": "markdown",
"id": "27812692",
"metadata": {},
"source": [
"### Three more facts about Julia types:\n",
"- In Julia all types are the same. For example, there is no difference between `Int32` and `String`, even though the first has a direct mapping to low-level instructions in the CPU and the latter has not (contrast this with e.g. C++).\n",
"- The `Nothing` type with the single instance `nothing` is the Julia equivalent to `void` in C++ or `None` in Python. It often represents that a function does not return anything or that a variable has not been initialised.\n",
"- `Any` is the root of the type tree: Any type in Julia is a subtype of `Any`."
]
},
{
"cell_type": "markdown",
"id": "ebc902af",
"metadata": {},
"source": [
"##### For more details\n",
"https://docs.julialang.org/en/v1/manual/types/"
]
},
{
"cell_type": "markdown",
"id": "3e8c5b51-3ef6-4d40-9803-dcc2ab6eda3f",
"metadata": {},
"source": [
"## Exercises"
]
},
{
"cell_type": "markdown",
"id": "bd273bc8-96ea-4260-93db-4934b7291965",
"metadata": {},
"source": [
"Implement a new abstract type `Polynomial` which is a subtype of `Number`.\n",
Print a tree structure that visualizes the type hierarchy rooted at `T`.
Print a tree structure that visualizes the type hierarchy rooted at `T`.
# Parameters
# Parameters
- `T`: The type which serves as the root of the tree to print.
- `T`: The type which serves as the root of the tree to print.
- `level`: (Optional, default=`0`) An integer specifying the current recursion depth - typically not provided by the user.
- `level`: (Optional, default=`0`) An integer specifying the current recursion depth - typically not provided by the user.
- `max_level`: (Optional, default=`typemax(Int)`) An integer specifying the maximum recursion depth, i.e., how many levels deep into the type hierarchy to print.
- `max_level`: (Optional, default=`typemax(Int)`) An integer specifying the maximum recursion depth, i.e., how many levels deep into the type hierarchy to print.
- `prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
- `prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
- `subtype_prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
- `subtype_prefix`: (Optional, default=`""`) A string used internally to format the tree structure - typically not provided by the user.
# Usage
# Usage
```julia
```julia
print_type_tree(Number, max_level=7)
print_type_tree(Number, max_level=7)
```
```
"""
"""
function print_type_tree(T, level=0, max_level=typemax(Int), prefix="", subtype_prefix="")
function print_type_tree(T, level=0, max_level=typemax(Int), prefix="", subtype_prefix="")
**Exercise**: Write code to determine all subtypes of `Number`, whether they are abstract or concrete.
**Exercise**: Write code to determine all subtypes of `Number`, whether they are abstract or concrete.
%% Cell type:code id:39b8339f tags:
%% Cell type:code id:39b8339f tags:
``` julia
``` julia
# TODO: implement your code
# TODO: implement your code
```
```
%% Cell type:markdown id:dacc835e tags:
%% Cell type:markdown id:dacc835e tags:
To get the super type of a type, one can use `supertype`:
To get the super type of a type, one can use `supertype`:
%% Cell type:code id:e666712d tags:
%% Cell type:code id:e666712d tags:
``` julia
``` julia
@showsupertype(Int64)
@showsupertype(Int64)
@showsupertype(Signed)
@showsupertype(Signed)
@showsupertype(Float16)
@showsupertype(Float16)
@showsupertype(Float32)
@showsupertype(Float32)
@showsupertype(Bool);
@showsupertype(Bool);
```
```
%% Cell type:code id:147db9b0 tags:
%% Cell type:code id:147db9b0 tags:
``` julia
``` julia
function print_supertypes(T::Type)
function print_supertypes(T::Type)
println("Supertypes of $T:")
println("Supertypes of $T:")
whileT!=Any
whileT!=Any
print(T," ---> ")
print(T," ---> ")
T=supertype(T)
T=supertype(T)
end
end
println(T)# Print Any, which is the ultimate supertype of all types
println(T)# Print Any, which is the ultimate supertype of all types
end
end
```
```
%% Cell type:code id:ed566f15 tags:
%% Cell type:code id:ed566f15 tags:
``` julia
``` julia
print_supertypes(Int64);
print_supertypes(Int64);
```
```
%% Cell type:code id:1462bc0c tags:
%% Cell type:code id:1462bc0c tags:
``` julia
``` julia
print_supertypes(Float64);
print_supertypes(Float64);
```
```
%% Cell type:markdown id:808b7968 tags:
%% Cell type:markdown id:808b7968 tags:
### Custom Abstract And Concrete Types
### Custom Abstract And Concrete Types
%% Cell type:markdown id:8cbad09b tags:
%% Cell type:markdown id:8cbad09b tags:
One can define custom abstract type by using `abstract type`:
One can define custom abstract type by using `abstract type`:
%% Cell type:code id:3ebf46f6 tags:
%% Cell type:code id:3ebf46f6 tags:
``` julia
``` julia
abstract type Animalend# Abstract type
abstract type Animalend# Abstract type
```
```
%% Cell type:markdown id:c1cceba1 tags:
%% Cell type:markdown id:c1cceba1 tags:
Then, some concrete types can be created as well:
Then, some concrete types can be created as well:
%% Cell type:code id:eaab065e tags:
%% Cell type:code id:eaab065e tags:
``` julia
``` julia
struct Dog<:Animal# Concrete type inheriting from Animal
struct Dog<:Animal# Concrete type inheriting from Animal
name::String
name::String
age::Int
age::Int
end
end
struct Cat<:Animal# Another concrete type inheriting from Animal
struct Cat<:Animal# Another concrete type inheriting from Animal
name::String
name::String
age::Int
age::Int
end
end
```
```
%% Cell type:markdown id:747d5555 tags:
%% Cell type:markdown id:747d5555 tags:
In this example, we create two concrete animal, `Dog` and `Cat`.
In this example, we create two concrete animal, `Dog` and `Cat`.
One can use subtypes to obtain all subtypes of either an abstract type or a concrete type.
One can use subtypes to obtain all subtypes of either an abstract type or a concrete type.
%% Cell type:code id:b8b7fca8 tags:
%% Cell type:code id:b8b7fca8 tags:
``` julia
``` julia
subtypes(Animal)
subtypes(Animal)
```
```
%% Cell type:markdown id:466cfcf7 tags:
%% Cell type:markdown id:466cfcf7 tags:
Again, using `isabstracttype` and `isconcretetype` we have
Again, using `isabstracttype` and `isconcretetype` we have
%% Cell type:code id:3eced325 tags:
%% Cell type:code id:3eced325 tags:
``` julia
``` julia
@showisabstracttype(Animal)
@showisabstracttype(Animal)
@showisabstracttype(Dog)
@showisabstracttype(Dog)
@showisabstracttype(Cat)
@showisabstracttype(Cat)
@showisconcretetype(Animal)
@showisconcretetype(Animal)
@showisconcretetype(Dog)
@showisconcretetype(Dog)
@showisconcretetype(Cat);
@showisconcretetype(Cat);
```
```
%% Cell type:markdown id:580bf73c tags:
%% Cell type:markdown id:580bf73c tags:
The type tree of `Animal` is:
The type tree of `Animal` is:
%% Cell type:code id:40debabd tags:
%% Cell type:code id:40debabd tags:
``` julia
``` julia
print_type_tree(Animal)
print_type_tree(Animal)
```
```
%% Cell type:markdown id:f4fb75fe tags:
%% Cell type:markdown id:f4fb75fe tags:
Now, we create two instances from the concrete types:
Now, we create two instances from the concrete types:
%% Cell type:code id:6f0f092f tags:
%% Cell type:code id:6f0f092f tags:
``` julia
``` julia
a_dog=Dog("Buddy",3)
a_dog=Dog("Buddy",3)
a_cat=Cat("Kitty",2)
a_cat=Cat("Kitty",2)
@showa_doga_cat;
```
```
%% Cell type:markdown id:19797a26 tags:
%% Cell type:markdown id:19797a26 tags:
In Julia, the `isa` method is used to determine whether an instance is of a particular type, whether it is abstract or concrete:
In Julia, the `isa` method is used to determine whether an instance is of a particular type, whether it is abstract or concrete:
%% Cell type:code id:263461b1 tags:
%% Cell type:code id:263461b1 tags:
``` julia
``` julia
@showisa(a_dog,Dog)
@showisa(a_dog,Dog)
@showisa(a_dog,Animal)
@showisa(a_dog,Animal)
@showisa(a_dog,Cat)
@showisa(a_dog,Cat)
@showisa(a_cat,Cat);
@showisa(a_cat,Cat);
```
```
%% Cell type:markdown id:cbb0a853 tags:
%% Cell type:markdown id:cbb0a853 tags:
The method `typeof` is used to obtain the type of an instance:
The method `typeof` is used to obtain the type of an instance:
%% Cell type:code id:208aed02 tags:
%% Cell type:code id:208aed02 tags:
``` julia
``` julia
@showtypeof(a_dog)
@showtypeof(a_dog)
@showtypeof(a_cat);
@showtypeof(a_cat);
```
```
%% Cell type:markdown id:1be97baf tags:
%% Cell type:markdown id:1be97baf tags:
We can also get all supertypes of `Dog` and `Cat`:
We can also get all supertypes of `Dog` and `Cat`:
%% Cell type:code id:b3ca83e6 tags:
%% Cell type:code id:b3ca83e6 tags:
``` julia
``` julia
print_supertypes(Dog)
print_supertypes(Dog)
```
```
%% Cell type:code id:662a732f tags:
%% Cell type:code id:662a732f tags:
``` julia
``` julia
print_supertypes(Cat)
print_supertypes(Cat)
```
```
%% Cell type:markdown id:ff7efe19 tags:
%% Cell type:markdown id:ff7efe19 tags:
### Exercise
### Exercises
Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree.
Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree. For example you can implement the abstract type `Bird` and then a few kinds of birds.
In Julia concrete types are always a leaf of the type tree, i.e. they cannot be inherited from each other. For a C++ or Python person (as I was before looking into Julia) this seems restrictive at first, but it takes away a lot of unnecessary complexity from the type hierachy. In Julia the structure of a library or a problem
is in many cases not converted into explict type hierachies,
as it would for OOP languages like Python or Java.
Instead it builds up implicitly from conventions which are associated with abstract or concrete types.
For example, if one implements a concrete type for the abstract type `Number` one is expected to implement a certain set of functions (e.g. `*`, `+`, `-`, `/`, ...). Otherwise not all of the standard library and other linear algebra packages will work. The difference to a hard enforcement of interfaces is, however, that *some things* will still work. This has disadvantages as your code could break in the future, but it is extremely useful for rapidly trying something out.
More details see: [https://docs.julialang.org/en/v1/base/base/#Properties-of-Types](https://docs.julialang.org/en/v1/base/base/#Properties-of-Types).
%% Cell type:markdown id:ccf57f28 tags:
# Dynamical typing and type deduction
%% Cell type:markdown id:85870a09 tags:
In programming language theory type systems traditionally fall in two categories.
Which of the following type are subtypes of another?
In **dynamically typed** languages the type of
Try to guess first and then verify by using the operator `<:`.
a value or expression is inferred only at runtime,
which usually allows for more flexible code. Examples are Python or MATLAB.
In contrast, so-called **statically-typed** languages (think FORTRAN or C++),
require types to be already known before runtime when the program is compiled.
This allows both to check more thoroughly for errors (which can manifest in mismatched types)
and it usually brings a gain in performance because more things about the memory layout of the program is known
at compile time. As a result aspects such as vectorisation, contiguous alignment of data,
preallocation of memory can be leveraged more easily.
Julia is kind of both. Strictly speaking it is dynamically typed. E.g. the type of variables can change type at any point:
Note, however, that the type of a *value* cannot change in Julia!
In Julia concrete types are always a leaf of the type tree, i.e. they cannot be inherited from each other. For a C++ or Python person (as a few of us) this seems restrictive at first, but it takes away a lot of unnecessary complexity from the type hierarchy. We will not give further information now, the reason will be more clear at the end of this notebook.
Still, Julia's strong emphasis on types are one of the reasons for its performance.
More details see: [https://docs.julialang.org/en/v1/base/base/#Properties-of-Types](https://docs.julialang.org/en/v1/base/base/#Properties-of-Types).
Unlike in statically typed languages, however, **type deduction in Julia** happens at runtime, right before JIT-compiling a function: The more narrowly the types for a particular piece of code can be deduced, the better the emitted machine code can be optimised. One can influence this using explicit type annotations in function arguments and intermediate expressions. Due to the to the excellent type inference capabilities of Julia, this is in general not needed, however.
This might sound a bit unusal at first, but the result is,
that it takes almost no effort to write generic code as we will see later: Just leave off all the type annotations. Notice, that this only means that the code has no types. At runtime types are still inferred as much as possible, such that aspects like vectorisation, contiguous alignment of data, preallocation of memory *can* be taken into account by the Julia compiler.
%% Cell type:markdown id:27812692 tags:
## Multiple dispatch
Three more facts about Julia types:
Multiple dispatch is probably the key feature of Julia that makes it different with respect to many other languages and give it the ability to be at the same time flexible and performant.
- In Julia all types are the same. For example, there is no difference between `Int32` and `String`, even though the first has a direct mapping to low-level instructions in the CPU and the latter has not (contrast this with e.g. C++).
- The `Nothing` type with the single instance `nothing` is the Julia equivalent to `void` in C++ or `None` in Python. It often represents that a function does not return anything or that a variable has not been initialised.
-`Any` is the root of the type tree: Any type in Julia is a subtype of `Any`.
%% Cell type:markdown id:0a6002c9 tags:
To fully understand the concept of multiple dispatch, we will use an analogy, we will pretend that a programming language is a tool to write a book of food recipes. A recipe combines a method of cooking (for example baking, frying) with an ingredient (for example potatoes, carrots, fish).
### Exercise
- The first possibility is to organize the book according to methods of cooking: each chapter explains in detail a method of cooking. For example, we will have **Chapter 1: baking**, **Chapter 2: frying** and so on. The drawback of this approach is that whenever we add a new ingredient, we have to change multiple chapters. This approach, focused on the action rather than on ingredients, is typical of functional programming languages.
Which of the following type are subtypes of another?
Try to guess first and then verify by using the operator `<:`.
```julia
- The second possibility is to organize the book according to ingredients: each chapter focuses on one ingredient. For example, we will have **Chapter 1: potatoes**, **Chapter 2: fish** and so on. The drawback of this approach is that whenever we add a new recipe, we have again to change multiple chapters. This approach is focused on the ingredients (data) and it is typical of object-oriented programming, where we will have something like:
Float64AbstractFloatInteger
``` python
NumberAbstractArrayComplex
classpotatoes()
RealAnyNothing
potatoes.bake()
```
potatoes.fry()
```
%% Cell type:markdown id:ebc902af tags:
##### For more details
https://docs.julialang.org/en/v1/manual/types/
%% Cell type:markdown id:25a35122 tags:
- Julia takes a third approach called **multiple dispatch** which decouples the action from the data. In our hypothetical recipe book, we will have chapters like **Chapter 1: baking potatoes**, **Chapter 2: frying potatoes**, **Chapter 3: baking fish**, **Chapter 4: frying fish** and so on. Each of the chapters will contain something like:
``` julia
function baking(potatoes::Potatoes)
function frying(potatoes::Potatoes)
function baking(fish::Fish)
function frying(fish::Fish)
```
## Multiple dispatch
In this way, adding a new recipe for a specific kind of food does not require changing already written things.
Let's say we wanted to concatenate the string `str` $n$ times on multiplication with an integer $n$. In Julia this functionality is already implemented by the exponentiation operator:
Let's say we wanted to concatenate the string `str` $n$ times on multiplication with an integer $n$. In Julia this functionality is already implemented by the exponentiation operator:
In both of these, the syntax `str::AbstractString` and `n::Integer` means that the respective method is only
In both of these, the syntax `str::String` and `n::Integer` means that the respective method is only
considered during dispatch if the argument `str` is of type `AbstractString` or one of its concrete subtypes and similarly `n` is an `Integer` (or subtype). Since Julia always dispatches to the most specific method in case multiple methods match, this is all we need to do:
considered during dispatch if the argument `str` is of type `String` and similarly `n` is an `Integer` (or subtype). Since Julia always dispatches to the most specific method in case multiple methods match, this is all we need to do:
In Julia different version of a function, which work on different kinds of arguments are called **methods**. You can see the list of methods by running this:
To show the role of abstract types in multiple dispatch, we go back to our food example. First we need to clarify the hierarchy of food types. We can use abstract types for that.
But now we found an even more specific recipe that works even better for potatoes. What we will do is writing a new function which is specific for potatoes.
This example really shows the power of Julia. Multiple dispatch is good for these reasons:
-**Flexibilty:** it is possible to try out new things in a very fast way. We implement a new data type and we use the methods which are already there for abstract types.
-**Customization:** implementing custom behavior for our data types is easy, we simply need to add a custom method.
-**Efficiency:** we can tune the specific methods to be super fast with our specific data type.
Add some methods for `Animal`, `Dog` and `Cat` and other concrete type from the last exercise.
Add some methods for `Animal`, `Dog` and `Cat` and other concrete type from the last exercise.
%% Cell type:code id:6a45fa48 tags:
%% Cell type:code id:6a45fa48 tags:
``` julia
``` julia
# TODO: implement your code here.
# TODO: implement your code here.
```
```
%% Cell type:markdown id:ccf57f28 tags:
## Dynamical typing and type deduction
%% Cell type:markdown id:85870a09 tags:
In programming language theory type systems traditionally fall in two categories.
In **dynamically typed** languages the type of a value or expression is inferred only at runtime,
which usually allows for more flexible code. Examples are Python or MATLAB. In contrast, so-called **statically-typed** languages (think FORTRAN or C++), require types to be already known before runtime when the program is compiled. This allows both to check more thoroughly for errors (which can manifest in mismatched types) and it usually brings a gain in performance because more things about the memory layout of the program is known at compile time. As a result, aspects such as vectorization, contiguous alignment of data, preallocation of memory can be leveraged more easily.
Julia is kind of both.
%% Cell type:code id:3ef4e3d0 tags:
``` julia
x=Dog("Buddy",4)# x is an Dog
@showtypeof(x)
x=Cat("Kitty",3)# Now x is a Cat
@showtypeof(x);
```
%% Cell type:markdown id:a02c0a37 tags:
Julia's strong emphasis on types is one of the reasons for its performance and flexibility.
When the code is precompiled before execution, the compiler has the information about the type of all the variables in the program. It will then search the best possible method for each of those. If a specific and highly efficient one is found, that will be used. If a specific one is missing, it will use the next possibility in the type tree, which will still work even if not as efficiently.
**Note:** this look up is the reason why it is not possible to instantiate abstract types, having variables of abstract types would make this operation unnecessary complicated. Moreover, it also reflects reality: a generic vegetable does not physically exist, we only have potatoes, carrots, eggplants and so on.
%% Cell type:markdown id:27812692 tags:
### Three more facts about Julia types:
- In Julia all types are the same. For example, there is no difference between `Int32` and `String`, even though the first has a direct mapping to low-level instructions in the CPU and the latter has not (contrast this with e.g. C++).
- The `Nothing` type with the single instance `nothing` is the Julia equivalent to `void` in C++ or `None` in Python. It often represents that a function does not return anything or that a variable has not been initialised.
-`Any` is the root of the type tree: Any type in Julia is a subtype of `Any`.