From 437b6d6a57764cf62693f37383962ed43c0d8c02 Mon Sep 17 00:00:00 2001 From: Michele Nottoli <michele.nottoli@gmail.com> Date: Wed, 25 Oct 2023 12:41:51 +0200 Subject: [PATCH] Update exc. 3. --- src/3_Data_Types_Multiple_Dispatch.ipynb | 416 +++++++++++++++++------ 1 file changed, 311 insertions(+), 105 deletions(-) diff --git a/src/3_Data_Types_Multiple_Dispatch.ipynb b/src/3_Data_Types_Multiple_Dispatch.ipynb index 0bd3f65..eaad59d 100644 --- a/src/3_Data_Types_Multiple_Dispatch.ipynb +++ b/src/3_Data_Types_Multiple_Dispatch.ipynb @@ -40,7 +40,7 @@ "id": "58c7dc0c", "metadata": {}, "source": [ - "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", "\n", "- `<:`: The subtype operator used to check if a type is a subtype of another type.\n", @@ -389,7 +389,9 @@ "outputs": [], "source": [ "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 @@ "id": "ff7efe19", "metadata": {}, "source": [ - "### Exercise\n", - "Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree." + "### Exercises\n", + "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 @@ }, { "cell_type": "markdown", - "id": "9efaf879", - "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", - "\n", - "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", - "metadata": {}, - "source": [ - "# Dynamical typing and type deduction" - ] - }, - { - "cell_type": "markdown", - "id": "85870a09", + "id": "52b60c4e-1d3f-46f6-87a9-c3ab2917d8d0", "metadata": {}, "source": [ - "In programming language theory type systems traditionally fall in two categories.\n", - "In **dynamically typed** languages the type of\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", + "Which of the following type are subtypes of another?\n", + "Try to guess first and then verify by using the operator `<:`.\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", "execution_count": null, - "id": "3ef4e3d0", + "id": "688afde5-1123-4adc-affd-687b966f387e", "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);" + "# TODO: implement your code" ] }, { "cell_type": "markdown", - "id": "a02c0a37", + "id": "9efaf879", "metadata": {}, "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", - "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`." + "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": "0a6002c9", + "id": "20a01368-ff73-4992-afae-792962aee09f", "metadata": {}, "source": [ - "### Exercise\n", - "Which of the following type are subtypes of another?\n", - "Try to guess first and then verify by using the operator `<:`.\n", + "## Multiple dispatch\n", "\n", - "```julia\n", - "Float64 AbstractFloat Integer\n", - "Number AbstractArray Complex\n", - "Real Any Nothing\n", - "```" - ] - }, - { - "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" + "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", + " - 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", + " function baking(potatoes::Potatoes)\n", + " function frying(potatoes::Potatoes)\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": "eec2ef90", + "id": "a0a6aecb-bd29-472c-921b-c4135a09b026", "metadata": {}, "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 @@ }, { "cell_type": "markdown", - "id": "d99de7cd", + "id": "d27da057-cb24-45a2-b190-23ef6c8207fa", "metadata": {}, "source": [ "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 @@ }, { "cell_type": "markdown", - "id": "dccf4b43", + "id": "6757d995-cbc8-4c7b-be3a-fbdd9c8b984c", "metadata": {}, "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:" @@ -649,7 +604,7 @@ }, { "cell_type": "markdown", - "id": "5d2af9cb", + "id": "ee1f8e46-3b4d-4f63-8e14-44ff70d7b5cf", "metadata": {}, "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:" @@ -662,17 +617,17 @@ "metadata": {}, "outputs": [], "source": [ - "mymult(str::AbstractString, n::Integer) = str^n\n", - "mymult(n::Integer, str::AbstractString) = mymult(str, n)" + "mymult(str::String, n::Integer) = str^n\n", + "mymult(n::Integer, str::String) = mymult(str, n)" ] }, { "cell_type": "markdown", - "id": "c89e940f", + "id": "430aba46-e00f-4cef-9edb-062a9f8c5d77", "metadata": {}, "source": [ - "In both of these, the syntax `str::AbstractString` 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:" + "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 `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 @@ }, { "cell_type": "markdown", - "id": "4390819c", + "id": "75814310-5d55-4f7b-8beb-903cb31366df", "metadata": {}, "source": [ "Notice, that the fully generic\n", @@ -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", "execution_count": null, @@ -730,6 +713,14 @@ "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", "execution_count": null, @@ -737,12 +728,13 @@ "metadata": {}, "outputs": [], "source": [ - "@which \"Hello\"*\"World!\"" + "println(@which \"Hello\"*\"World!\")\n", + "println(@which mymult(\"a\", 3))" ] }, { "cell_type": "markdown", - "id": "f1c0a83e", + "id": "49e0a478-3718-48bd-8370-029530b23a3f", "metadata": {}, "source": [ "We can also add some method for `Animal`, `Dog`, and `Cat`:" @@ -764,7 +756,7 @@ }, { "cell_type": "markdown", - "id": "7d6da0ec", + "id": "ebeeb1e3-f7c0-4a0a-a3fa-539973ef0b36", "metadata": {}, "source": [ "Then, let's test it." @@ -783,7 +775,129 @@ }, { "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": {}, "source": [ "### Exercise\n", @@ -799,11 +913,103 @@ "source": [ "# 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", + "\n", + "TO BE FINISHED" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be3f23e1-b0f8-498f-afd2-9618cb64febb", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Julia 1.9.2", + "display_name": "Julia 1.9.3", "language": "julia", "name": "julia-1.9" }, -- GitLab