diff --git a/3_Advanced_Data_Types.ipynb b/3_Advanced_Data_Types.ipynb
index abaa7dde6bc1af46dd742b80d74b6fc62691fe64..bb61f351c2d59a9bcc28a3dfd14bedccae1efebd 100644
--- a/3_Advanced_Data_Types.ipynb
+++ b/3_Advanced_Data_Types.ipynb
@@ -348,7 +348,7 @@
     "t3 = tuple()\n",
     "@show typeof(t3)\n",
     "\n",
-    "t4 = tuple(1, 'a', π)\n",
+    "t4 = tuple(1, 'a', π, 12.0)\n",
     "@show typeof(t4);"
    ]
   },
@@ -370,6 +370,24 @@
     "t4[2]"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "2519be57-8c10-4878-a772-017b099f6a4c",
+   "metadata": {},
+   "source": [
+    "It is also possible to get multiple elements by using slices."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c48ac07f-3a4e-4bb8-9551-2973ea8d2f9c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@show t4[1:3];"
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "aeda8602-f9da-4173-a692-54e2e9a0e178",
@@ -591,10 +609,358 @@
     "updated_person = (person..., number = \"0711 ...\")"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "347fe4ea-5b63-4fb8-91bc-2b7b8a0c6a30",
+   "metadata": {},
+   "source": [
+    "### Exercises\n",
+    "  - Write a function `swap_first_last` that takes a tuple and returns a new tuple with the first and last elements swapped. Tip: use a combination of the `...` operator and of this slice `[2:end-1]`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3721b654-b182-426d-b799-a69e0b53a550",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "markdown",
+   "id": "647530cd-480a-414f-a8ec-1375e05a9e1b",
+   "metadata": {},
+   "source": [
+    "## Structs"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "00b9bdb1-86ad-47f6-b8c3-88e85878eaff",
+   "metadata": {},
+   "source": [
+    "At this point you know how to use functions to organize code and built-in types to organize data. The next step is to learn how to build your own types to organize both code and data.\n",
+    "\n",
+    "We have used many of Julia’s built-in types; now we are going to define a new type. As an example, we will create a type called Point that represents a point in two-dimensional space.\n",
+    "    \n",
+    "There are several ways we might represent points in Julia:\n",
+    "  - We could store the coordinates separately in two variables, x and y.\n",
+    "  - We could store the coordinates as elements in an tuple.\n",
+    "  - We could create a new type to represent points as objects.\n",
+    "\n",
+    "Here we will investigate the third option."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9aa53d46-033b-4559-a5d8-e95357a257a5",
+   "metadata": {},
+   "source": [
+    "A programmer-defined composite type is also called a struct. The struct definition for a point looks like this:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c2e3f646-2d82-46a4-a265-01b919412c6d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "struct Point\n",
+    "    x::Real\n",
+    "    y::Real\n",
+    "end"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fb9be134-9730-4dbd-9e05-51dd94e9d8e0",
+   "metadata": {},
+   "source": [
+    "The header indicates that the new struct is called Point. The body defines the attributes or fields of the struct. The Point struct has two fields: x and y.\n",
+    "\n",
+    "A struct is like a factory for creating objects. To create a point, you call Point as if it were a function having as arguments the values of the fields. When Point is used as a function, it is called a constructor. The constructor returns an instance of the object."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "197339a2-0d8b-4882-bab2-80ba56410f21",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p = Point(3.0, 4.0)\n",
+    "@show p;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b0134aa7-f356-4d71-9d46-959bd3cfe89a",
+   "metadata": {},
+   "source": [
+    "The type speficication (`::Real`) is optional, but can be helpful to enforce correct usage of the struct."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "849f1460-d228-4b22-a797-1077d75d1a2d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p = Point(\"a\", 4.0)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e3011b23-816d-4aa2-be61-7eb8f4aee9d1",
+   "metadata": {},
+   "source": [
+    "The values of the fields can be accessed using the `.` operator."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "00d090e4-d74d-4fad-ae8b-369c1046e48b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@show p.x p.y;\n",
+    "@show distance = sqrt(p.x^2 + p.y^2);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "08748a8c-f76c-4f14-b3d2-9db99fb28e87",
+   "metadata": {},
+   "source": [
+    "Structs are however by default immutable, after construction the fields can not change value:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "91bd6248-a358-43e3-90cc-6b8398445aaf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p.x = 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "629c1004-5d0f-4093-b546-473c94c91b44",
+   "metadata": {},
+   "source": [
+    "Where required, mutable composite types can be declared with the keyword mutable struct. Here is the definition of a mutable point:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "804c2c32-7a84-4261-88d8-1bad6ba5e18c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "mutable struct MPoint\n",
+    "    x::Real\n",
+    "    y::Real\n",
+    "end\n",
+    "p = MPoint(3.0, 4.0)\n",
+    "p.x = 2.0\n",
+    "@show p;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1bd558dd-e6c3-4dc0-9476-768c59178708",
+   "metadata": {},
+   "source": [
+    "A third option is to let some fields of an unmutable struct to be mutable. For example a dictionary inside an unmutable struct can be modified."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "62cab9ae-5e58-401c-bc93-17720914e530",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "struct Book\n",
+    "    title::String\n",
+    "    author::String\n",
+    "    properties::Dict{String, Any}\n",
+    "end\n",
+    "\n",
+    "book = Book(\"Der Hobbit\", \"J.R.R. Tolkien\", Dict(\"available\" => false))\n",
+    "@show book\n",
+    "book.properties[\"available\"] = true\n",
+    "@show book;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "31e2098b-5079-4cb5-bf64-a7a78754af32",
+   "metadata": {},
+   "source": [
+    "You can pass an instance as an argument in the usual way. For example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e6113221-0c03-484a-99e0-f7d4fab70f03",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "function printpoint(p)\n",
+    "    println(\"($(p.x), $(p.y))\")\n",
+    "end\n",
+    "printpoint(p)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d5c6fa9e-c31d-4aff-a3fd-db3449904519",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "function printbook(book)\n",
+    "    println(\"Title: $(book.title)\")\n",
+    "    println(\"Author: $(book.author)\")\n",
+    "    available = book.properties[\"available\"]\n",
+    "    println(\"Available: $(available)\")\n",
+    "end\n",
+    "printbook(book)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3b12a004-81d9-4bbc-9e2e-d2c281dcbe18",
+   "metadata": {},
+   "source": [
+    "Functions can return instances as return values."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9c57c9f7-c4fd-4263-a55f-62f123aaaa8a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "function find_center(point1, point2)\n",
+    "    x = (point1.x + point2.x)/2.0\n",
+    "    y = (point1.y + point2.y)/2.0\n",
+    "    return Point(x, y)\n",
+    "end\n",
+    "\n",
+    "point1 = Point(0.0, 0.0)\n",
+    "point2 = Point(10.0, 10.0)\n",
+    "\n",
+    "@show find_center(point1, point2);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "078b31ab-11e1-4596-8271-f0d33418eab8",
+   "metadata": {},
+   "source": [
+    "### Exercise\n",
+    "\n",
+    "  - Write a function called `point_distance` which takes two points as arguments and returns the Euclidean distance between them."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b5957a05-e396-41c3-9b8f-f14f089115ab",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "attachments": {},
+   "cell_type": "markdown",
+   "id": "0fb1f6bc-e6e5-4897-bbb6-880e40e3bf20",
+   "metadata": {},
+   "source": [
+    "### References and values\n",
+    "\n",
+    "Each object (instance of a struct) is stored at some memory address. The operator `===` checks if two variables point to the same memory address of the object. For example\n",
+    "\n",
+    "book_copy = book\n",
+    "@show book_copy === book;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "636b6713-f4ed-4220-a159-2918bd44f78e",
+   "metadata": {},
+   "source": [
+    "This means that any change made to `book_copy` will also modify `book`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a9c46bac-d234-4616-a604-63bdf03b1393",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "book.properties[\"available\"] = true\n",
+    "book_copy.properties[\"available\"] = false\n",
+    "\n",
+    "@show book;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6bcbf983-40fa-4fc8-8eca-7cd7c45778dc",
+   "metadata": {},
+   "source": [
+    "If a new, distinct, object is needed (in other words, a copy by value), we can use the function `deepcopy`. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c36d23c8-2292-4568-aef4-7416c6f3e538",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "book_copy = deepcopy(book)\n",
+    "@show book_copy === book;"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "02cc3ec7-e712-4c4b-abf5-9bfc98e68a91",
+   "metadata": {},
+   "source": [
+    "Finally, the `==` operator between structs defaults to the `===` operator as Julia has no way of knowing how to compare custom structs. However, it is always possible to reimplement the `==` operator for our custom types."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3372eb89-cde6-4040-aa51-b4e76af97851",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import Core\n",
+    "\n",
+    "function ==(book1::Book, book2::Book)\n",
+    "    return book1.title == book2.title && book1.author == book2.author\n",
+    "end"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "3409d1a3-85da-470c-bc5b-4ba9ecb53591",
+   "id": "03594f56-48ee-4152-a42f-8e3588c3ab2e",
    "metadata": {},
    "outputs": [],
    "source": []