diff --git a/2_Exercise.ipynb b/2_Exercise.ipynb
index b1d71735f96c4ecae47937de2d8dea5f659f861d..0bd3f65cee080bd9fffb1ac06f7b12558f1acb2d 100644
--- a/2_Exercise.ipynb
+++ b/2_Exercise.ipynb
@@ -1,16 +1,5 @@
 {
  "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "b7689bbe",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# import dependencies\n",
-    "using Test"
-   ]
-  },
   {
    "cell_type": "markdown",
    "id": "debc3515-fa08-4577-82bb-9eced849d616",
@@ -28,70 +17,115 @@
   },
   {
    "cell_type": "markdown",
-   "id": "66c9e24b-965f-490e-873f-83b4d654d4f2",
+   "id": "7128c447",
    "metadata": {},
    "source": [
-    "## Types: Abstract and Concrete\n",
-    "\n",
-    "Julia's type system includes **abstract types** and **concrete types**. \n",
+    "## Abstract and concrete types"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "723212c2",
+   "metadata": {},
+   "source": [
+    "Before we discuss multiple dispatch of functions and dispatch by types, we briefly review Julia's type system. \n",
+    "Types in Julia fall into two categories: **Abstract** and **concrete**. \n",
     "\n",
     "- **Abstract Types**: Cannot be instantiated and serve to represent general categories of objects.\n",
-    "- **Concrete Types**: Can be instantiated and are used to create objects.\n"
+    "- **Concrete Types**: Can be instantiated and are used to create objects."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "58c7dc0c",
+   "metadata": {},
+   "source": [
+    "In Julia, there are several built-in functions related to querying and working with types and `DataType``. \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",
+    "- `isabstracttype(T)`: Check if T is an abstract type.\n",
+    "- `isconcretetype(T)`: Check if T is a concrete type.\n",
+    "- `subtypes(T)`: Get a list of all immediate subtypes of type T.\n",
+    "- `supertype(T)`: Get the direct supertype of type T."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "00533545",
+   "metadata": {},
+   "source": [
+    "Abstract types such as `Integer` or `Number` are supertypes of a bunch of other types, for example:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "3885f36f-1ee4-4fa3-aaa0-3df62bcac362",
+   "id": "50b00765",
    "metadata": {},
    "outputs": [],
    "source": [
-    "abstract type Animal end  # Abstract type\n",
-    "\n",
-    "struct Dog <: Animal  # Concrete type inheriting from Animal\n",
-    "    name::String\n",
-    "    age::Int\n",
-    "end\n",
-    "\n",
-    "struct Cat <: Animal  # Another concrete type inheriting from Animal\n",
-    "    name::String\n",
-    "    age::Int\n",
-    "end\n",
-    "\n",
-    "a_dog = Dog(\"Buddy\", 3)\n",
-    "a_cat = Cat(\"Kitty\", 2)\n"
+    "@show Int32 <: Integer   # Read Int32 is a sub-type of Integer\n",
+    "@show UInt16 <: Integer  # UInt16 is a sub-type of Integer\n",
+    "@show Float32 <: Integer # Float32 is not a sub-type of Integer\n",
+    "@show Float32 <: Number  # Float32 is a sub-type of Number\n",
+    "@show Integer <: Number; # Integer is a sub-type of Nummber"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "329380e7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# by transitivity:\n",
+    "@show Int32  <: Number\n",
+    "@show UInt16 <: Number\n",
+    "@show Number <: Number;"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "aa33841d",
+   "id": "aa54bfef",
    "metadata": {},
    "source": [
-    "One can use subtypes to obtain all subtypes of either an abstract type or a concrete type."
+    "### Type properties\n",
+    "We can check type properties in various ways:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "9a30f7e1",
+   "id": "49228132",
    "metadata": {},
    "outputs": [],
    "source": [
-    "subtypes(Animal)"
+    "@show isconcretetype(Int32);"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9ffd1775",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@show isabstracttype(Integer);"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "06bb4657",
+   "id": "a778662d",
    "metadata": {},
    "source": [
-    "We can write a nice function to make the output nicer:"
+    "A fancy way is even to display a type tree:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "a5204ffa",
+   "id": "f5a1fd53",
    "metadata": {},
    "outputs": [],
    "source": [
@@ -142,482 +176,629 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "ba91649d",
+   "id": "41f4de49",
    "metadata": {},
    "outputs": [],
    "source": [
-    "print_type_tree(Animal)"
+    "print_type_tree(Number)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "87a9c84f",
+   "id": "c3722ed0",
    "metadata": {},
    "source": [
-    "We can also use this function to print the subtypes of built-in objects."
+    "**Exercise**: Write code to determine all subtypes of `Number`, whether they are abstract or concrete."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "53919ada",
+   "id": "39b8339f",
    "metadata": {},
    "outputs": [],
    "source": [
-    "print_type_tree(Number)"
+    "# TODO: implement your code"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "dacc835e",
+   "metadata": {},
+   "source": [
+    "To get the super type of a type, one can use `supertype`:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "1e23272e",
+   "id": "e666712d",
    "metadata": {},
    "outputs": [],
    "source": [
-    "# Set max_level to 2 to avoid lengthy output.\n",
-    "print_type_tree(AbstractArray, 0, 2)"
+    "@show supertype(Int64)\n",
+    "@show supertype(Signed)\n",
+    "@show supertype(Float16)\n",
+    "@show supertype(Float32)\n",
+    "@show supertype(Bool);"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "147db9b0",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "function print_supertypes(T::Type)\n",
+    "    println(\"Supertypes of $T:\")\n",
+    "    while T != Any\n",
+    "        print(T, \" ---> \")\n",
+    "        T = supertype(T)\n",
+    "    end\n",
+    "    println(T)  # Print Any, which is the ultimate supertype of all types\n",
+    "end"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ed566f15",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print_supertypes(Int64);"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1462bc0c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print_supertypes(Float64);"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "16d371f7",
+   "id": "808b7968",
    "metadata": {},
    "source": [
-    "For concrete structs, constructors are special functions that instantiate objects of a particular type, initiating the lifecycle of an object instance. \n",
-    "When an object of a custom type is created, it is initialized via constructors, ensuring that it begins its existence in a valid state.\n",
-    "\n",
-    "### Default Constructors\n",
-    "\n",
-    "The name of a constructor is the same as the name of the type and can have multiple methods, which differ in their argument types. \n",
-    "By default, Julia provides a constructor that accepts arguments for all fields, unless an `inner constructor` is provided, in which case you need to define all needed constructors yourself.\n",
-    "For instance, if you have a type named `MyType`, the constructor(s) for this type will also be named `MyType`."
+    "### Custom Abstract And Concrete Types"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8cbad09b",
+   "metadata": {},
+   "source": [
+    "One can define custom abstract type by using `abstract type`:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "277c436a",
+   "id": "3ebf46f6",
    "metadata": {},
    "outputs": [],
    "source": [
-    "struct MyType1\n",
-    "    a::Int\n",
-    "    b::Float64\n",
+    "abstract type Animal end  # Abstract type"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c1cceba1",
+   "metadata": {},
+   "source": [
+    "Then, some concrete types can be created as well:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "eaab065e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "struct Dog <: Animal  # Concrete type inheriting from Animal\n",
+    "    name::String\n",
+    "    age::Int\n",
     "end\n",
     "\n",
-    "# The default constructor is MyType1\n",
-    "obj_1 = MyType1(1, 2.0)\n",
-    "\n",
-    "@testset \"Test MyType1\" begin\n",
-    "    # Testing with some valid inputs\n",
-    "    obj_1 = MyType1(1, 2.0)\n",
-    "    @test obj_1.a == 1 \n",
-    "    @test obj_1.b == 2.0 \n",
-    "end"
+    "struct Cat <: Animal  # Another concrete type inheriting from Animal\n",
+    "    name::String\n",
+    "    age::Int\n",
+    "end\n"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "2051b0ac",
+   "id": "747d5555",
    "metadata": {},
    "source": [
-    "### Inner and Outer Constructors\n",
-    "\n",
-    "Julia features inner and outer constructors. Inner constructors are defined inside the block of the type definition and have access to its private internals, while outer constructors are defined outside and call an inner constructor to create an object."
+    "In this example, we create two concrete animal, `Dog` and `Cat`. \n",
+    "One can use subtypes to obtain all subtypes of either an abstract type or a concrete type."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "51d774f8",
+   "id": "b8b7fca8",
    "metadata": {},
    "outputs": [],
    "source": [
-    "struct MyType2\n",
-    "    a::Int\n",
-    "    b::Float64\n",
-    "    \n",
-    "    # Inner constructor\n",
-    "    function MyType2(a, b)\n",
-    "        a < 0 && throw(ArgumentError(\"a must be non-negative\"))\n",
-    "        new(a, b)\n",
-    "    end\n",
-    "end\n",
+    "subtypes(Animal)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "466cfcf7",
+   "metadata": {},
+   "source": [
+    "Again, using `isabstracttype` and `isconcretetype` we have"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3eced325",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@show isabstracttype(Animal)\n",
+    "@show isabstracttype(Dog)\n",
+    "@show isabstracttype(Cat)\n",
     "\n",
-    "# Outer constructor\n",
-    "MyType2(a::Int) = MyType2(a, 0.0)"
+    "@show isconcretetype(Animal)\n",
+    "@show isconcretetype(Dog)\n",
+    "@show isconcretetype(Cat);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "580bf73c",
+   "metadata": {},
+   "source": [
+    "The type tree of `Animal` is:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "b859da95",
+   "id": "40debabd",
    "metadata": {},
    "outputs": [],
    "source": [
-    "@testset \"Test MyType2\" begin\n",
-    "    @testset \"Innter constructor with valid inputs\" begin\n",
-    "        obj_21 = MyType2(100, 11.34)\n",
-    "        @test obj_21.a == 100 \n",
-    "        @test obj_21.b == 11.34\n",
-    "    end\n",
-    "    @testset \"Inner constructor with invalid inputs\" begin\n",
-    "        @test_throws ArgumentError MyType2(-100, 1.0)\n",
-    "    end\n",
-    "    @testset \"Outer constructor with valid inputs\" begin\n",
-    "        obj_22 = MyType2(2)\n",
-    "        @test obj_22.a == 2\n",
-    "        @test obj_22.b == 0.0 \n",
-    "    end\n",
-    "    @testset \"Outer constructor with invalid inputs\" begin\n",
-    "        @test_throws ArgumentError MyType2(-1)\n",
-    "    end\n",
-    "end"
+    "print_type_tree(Animal)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "e65f0f9a",
+   "id": "f4fb75fe",
    "metadata": {},
    "source": [
-    "In the example, the inner constructor validates that `a` is non-negative, providing a measure of control and validation over the initialization process."
+    "Now, we create two instances from the concrete types:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6f0f092f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "a_dog = Dog(\"Buddy\", 3)\n",
+    "a_cat = Cat(\"Kitty\", 2)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "bde87d55",
+   "id": "19797a26",
    "metadata": {},
    "source": [
-    "### Parametric Constructors\n",
-    "\n",
-    "Parametric types can also have constructors, with parameters specified in the type definition, offering greater flexibility."
+    "In Julia, the `isa` method is used to determine whether an instance is of a particular type, whether it is abstract or concrete:\n"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "9e5c0f17",
+   "id": "263461b1",
    "metadata": {},
    "outputs": [],
    "source": [
-    "struct MyType3{T1<:Real, T2<:Real}\n",
-    "    a::T1\n",
-    "    b::T2\n",
-    "    \n",
-    "    # Inner constructor\n",
-    "    function MyType3(a::T1, b::T2) where {T1, T2}\n",
-    "        a < 0 && throw(ArgumentError(\"a must be non-negative\"))\n",
-    "        new{T1, T2}(a, b)\n",
-    "    end\n",
-    "end"
+    "@show isa(a_dog, Dog)\n",
+    "@show isa(a_dog, Animal)\n",
+    "@show isa(a_dog, Cat)\n",
+    "@show isa(a_cat, Cat);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cbb0a853",
+   "metadata": {},
+   "source": [
+    "The method `typeof` is used to obtain the type of an instance:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "58e3d497",
+   "id": "208aed02",
    "metadata": {},
    "outputs": [],
    "source": [
-    "@testset \"Test MyType3\" begin\n",
-    "    @testset \"Valid inputs\" begin\n",
-    "        obj_31 = MyType3(1, 2.0)\n",
-    "        @test obj_31.a == 1 \n",
-    "        @test obj_31.b == 2.0 \n",
-    "        obj_32 = MyType3(1.0, 2.0)\n",
-    "        @test obj_32.a == 1.0\n",
-    "        @test obj_32.b == 2.0 \n",
-    "        obj_33 = MyType3(1, 2)\n",
-    "        @test obj_33.a == 1 \n",
-    "        @test obj_33.b == 2\n",
-    "    end\n",
-    "end"
+    "@show typeof(a_dog)\n",
+    "@show typeof(a_cat);"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "8b60210e",
+   "id": "1be97baf",
    "metadata": {},
    "source": [
-    "Exercise: `MyType3` can only take a real number. Please write `MyType4` to improve it, using the guidelines below:\n",
-    "\n",
-    "- Accept `Number` as inputs.\n",
-    "- Modify the inner constructor to handle `Complex` numbers for argument `a`. Specifically, if a complex number is supplied, bypass any checking mechanisms.\n",
-    "- Add an outer constructor like in `MyType3`."
+    "We can also get all supertypes of `Dog` and `Cat`:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "4edd8d4e",
+   "id": "b3ca83e6",
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO\n",
-    "struct MyType4\n",
-    "    a\n",
-    "    b\n",
-    "end"
+    "print_supertypes(Dog)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "662a732f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print_supertypes(Cat)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "59da2faf",
+   "id": "ff7efe19",
    "metadata": {},
    "source": [
-    "The new MyType4 should pass all of the following tests:"
+    "### Exercise\n",
+    "Write code to implement some abstract and concrete subtypes of `Animal`, and use `print_type_tree` to view the type tree."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "d1cfb791",
+   "id": "2fbfbb88",
    "metadata": {},
    "outputs": [],
    "source": [
-    "@testset \"Test MyType4\" begin\n",
-    "    @testset \"Inner constructor with valid inputs\" begin\n",
-    "        obj_41 = MyType4(1, 2.0)\n",
-    "        @test obj_41.a == 1 \n",
-    "        @test obj_41.b == 2.0 \n",
-    "        obj_42 = MyType4(1.0, 2.0)\n",
-    "        @test obj_42.a == 1.0\n",
-    "        @test obj_42.b == 2.0 \n",
-    "        obj_43 = MyType4(1, 2)\n",
-    "        @test obj_43.a == 1 \n",
-    "        @test obj_43.b == 2\n",
-    "        obj_44 = MyType4(Complex(1.0, 1.0), 2)\n",
-    "        @test obj_44.a == 1.0 + 1.0im\n",
-    "        @test obj_44.b == 2\n",
-    "    end\n",
-    "    @testset \"Inner constructor with invalid inputs\" begin\n",
-    "        @test_throws ArgumentError MyType4(-1, 1)\n",
-    "    end\n",
-    "    @testset \"Outer constructor with valid inputs\" begin\n",
-    "        @test_throws ArgumentError MyType4(1)\n",
-    "    end\n",
-    "\n",
-    "    @testset \"Outer constructor with invalid inputs\" begin\n",
-    "        @test_throws ArgumentError MyType4(-1)\n",
-    "    end\n",
-    "end"
+    "# TODO: implement your code"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "6bd93a18-6569-4253-a836-0601b77c8bf2",
+   "id": "9efaf879",
    "metadata": {},
    "source": [
-    "## Dynamical Types"
+    "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": "c7784fbb",
+   "id": "ccf57f28",
    "metadata": {},
    "source": [
-    "Dynamical types in Julia allow variables to change their type dynamically. \n",
-    "Julia uses `dynamic typing`, which means that the type of a variable is checked at runtime."
+    "# 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\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",
+    "Julia is kind of both. Strictly speaking it is dynamically typed. E.g. the type of variables can change type at any point:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "a77d6d3c-e838-45c7-bd71-249c8547281e",
+   "id": "3ef4e3d0",
    "metadata": {},
    "outputs": [],
    "source": [
     "x = Dog(\"Buddy\", 4)  # x is an Dog\n",
-    "println(typeof(x))\n",
+    "@show typeof(x)\n",
     "\n",
     "x = Cat(\"Kitty\", 3)  # Now x is a Cat\n",
-    "println(typeof(x))"
+    "@show typeof(x);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a02c0a37",
+   "metadata": {},
+   "source": [
+    "Note, however, that the type of a *value* cannot change in Julia!\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",
+   "id": "0a6002c9",
+   "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",
+    "\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": "bb3f86ee-ec12-41a4-b29c-a622bc95af8d",
+   "id": "25a35122",
    "metadata": {},
    "source": [
-    "## Multiple Dispatch of Functions"
+    "## Multiple dispatch"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "37ef5d37",
+   "id": "eec2ef90",
    "metadata": {},
    "source": [
-    "Multiple dispatch is one of Julia's key features. \n",
-    "It allows defining function behavior across many combinations of argument types.\n",
-    "Different method definitions are dispatched based on the types of all function arguments."
+    "Let us return back to the `mymult` function:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "6e409d52-290d-4e47-a4ba-5030ff45b66c",
+   "id": "c43dc846",
    "metadata": {},
    "outputs": [],
    "source": [
-    "# Define a generic function speak\n",
-    "speak(animal::Animal) = \"Some generic animal noise\"\n",
-    "\n",
-    "# Use multiple dispatch to define method for specific types\n",
-    "speak(animal::Dog) = \"Woof! I am $(animal.name), a dog.\"\n",
-    "speak(animal::Cat) = \"Meow! I am $(animal.name), a cat.\"\n",
-    "\n",
-    "# Test the methods\n",
-    "println(speak(a_dog))\n",
-    "println(speak(a_cat))"
+    "mymult(x, y) = x * y"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "e28a025f",
+   "id": "d99de7cd",
    "metadata": {},
    "source": [
-    "## Union Types"
+    "We were able to safely use this functions with a number of type combinations, but some things do not yet work:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "65dbe501",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "mymult(2, \" abc\")"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "9dd9a6fa",
+   "id": "dccf4b43",
    "metadata": {},
    "source": [
-    "Union types are a useful feature in Julia that allow a variable to take on values of several different types. \n",
-    "It allows more flexibility when dealing with variables that might have different types in different situations."
+    "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:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "9a11afbb",
+   "id": "8c18999b",
    "metadata": {},
    "outputs": [],
    "source": [
-    "const CatOrDog = Union{Cat, Dog}\n",
-    "\n",
-    "function process_input(input::CatOrDog)\n",
-    "    speak(input)\n",
-    "end\n",
-    "\n",
-    "process_input(a_dog)\n",
-    "process_input(a_cat)"
+    "\"abc\"^4"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "5117e6dc",
+   "id": "5d2af9cb",
+   "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:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "fa0af502",
    "metadata": {},
+   "outputs": [],
    "source": [
-    "## Parametric Types"
+    "mymult(str::AbstractString, n::Integer) = str^n\n",
+    "mymult(n::Integer, str::AbstractString) = mymult(str, n)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "6fddf5da",
+   "id": "c89e940f",
    "metadata": {},
    "source": [
-    "Parametric types allow you to define types that are parameterized by other types.\n",
-    "It provides the ability to create a function or type that works with different data types."
+    "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:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "daa25661",
+   "id": "5fb0009b",
    "metadata": {},
    "outputs": [],
    "source": [
-    "struct Point{T}\n",
-    "    x::T\n",
-    "    y::T\n",
-    "end\n",
-    "\n",
-    "p1 = Point{Int}(1, 2)\n",
-    "p2 = Point{Float64}(1.0, 2.0)\n",
-    "p3 = Point{String}(\"one\", \"two\")\n",
-    "\n",
-    "println(p1)\n",
-    "println(p2)\n",
-    "println(p3)"
+    "mymult(2, \" abc\")"
    ]
   },
   {
-   "cell_type": "markdown",
-   "id": "408f1815",
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ee10ad21",
    "metadata": {},
+   "outputs": [],
    "source": [
-    "## Function Overloading"
+    "@which mymult(2, \" abc\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "91192540",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@which mymult(\"def \", UInt16(3))"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "1b83770e",
+   "id": "4390819c",
    "metadata": {},
    "source": [
-    "Function overloading allows you to define different versions of a function for different types or numbers of arguments.\n",
-    "It provides flexibility and improves code readability and performance."
+    "Notice, that the fully generic\n",
+    "```julia\n",
+    "mymult(x, y) = x * y\n",
+    "```\n",
+    "is actually an abbreviation for\n",
+    "```julia\n",
+    "mymult(x::Any, y::Any) = x * y\n",
+    "```"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "af6ad638",
+   "id": "de3e4302",
    "metadata": {},
    "outputs": [],
    "source": [
-    "add(x::Dog, y::Cat) = x.age + y.age\n",
-    "add(x::Cat, y::Dog) = x.age + y.age\n",
-    "add(x::Dog, y::Dog) = x.age + y.age\n",
-    "add(x::Cat, y::Cat) = x.age + y.age\n",
-    "\n",
-    "println(add(a_dog, a_cat))          # Calls the first version\n",
-    "println(add(a_cat, a_dog))  # Calls the second version"
+    "methods(*)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "be365ee3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@which \"Hello\"*\"World!\""
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "7f2723a1",
+   "id": "f1c0a83e",
    "metadata": {},
    "source": [
-    "The function overloading above can be simplified by the following one using generic function."
+    "We can also add some method for `Animal`, `Dog`, and `Cat`:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "853117bc",
+   "id": "bfb965d6",
    "metadata": {},
    "outputs": [],
    "source": [
-    "add(x::Animal, y::Animal) = x.age + y.age"
+    "# Define a generic function speak\n",
+    "speak(animal::Animal) = \"Some generic animal noise\"\n",
+    "# Use multiple dispatch to define method for specific types\n",
+    "speak(animal::Dog) = \"Woof! I am $(animal.name), a dog.\"\n",
+    "speak(animal::Cat) = \"Meow! I am $(animal.name), a cat.\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7d6da0ec",
+   "metadata": {},
+   "source": [
+    "Then, let's test it."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "705971b5",
+   "id": "14bda70c",
    "metadata": {},
    "outputs": [],
    "source": [
-    "struct Tiger<:Animal\n",
-    "    name::String\n",
-    "    age::Int\n",
-    "end\n",
-    "\n",
-    "a_tiger = Tiger(\"Ravi\", 3)\n",
-    "\n",
-    "print(add(a_tiger, a_dog))"
+    "@show speak(a_dog)\n",
+    "@show speak(a_cat);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "85f3e064",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "Add some methods for `Animal`, `Dog` and `Cat` and other concrete type from the last exercise."
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "907ced09",
+   "id": "6a45fa48",
    "metadata": {},
    "outputs": [],
-   "source": []
+   "source": [
+    "# TODO: implement your code here."
+   ]
   }
  ],
  "metadata": {