diff --git a/11_functional_programming.ipynb b/11_functional_programming.ipynb index 48401994..ca2ac2d0 100644 --- a/11_functional_programming.ipynb +++ b/11_functional_programming.ipynb @@ -11,25 +11,34 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Table of contents\n", - "\n", - "- [Immutability](#Immutability)\n", - "- [Composition](#Composition)\n", - "- [Higher Order Functions / Functions as Values](#Higher-Order-Functions-/-Functions-as-Values)\n", - "- [Referential transparency](#Referential-transparency)\n", - "- [Type system](#Type-system)\n", - "- [Filtering](#Filtering)\n", - "- [Reducing](#Reducing)\n", - "- [List comprehensions](#List-comprehensions)\n", - "- [Combinining and splitting iterators](#Combinining-and-splitting-iterators)\n", - "- [Exercises](#Exercises)\n", - " - [Exercise 1: Pure or impure functions? 🌶️](#Exercise-1:-Pure-or-impure-functions?-🌶️)\n", - " - [Exercise 2: Keeping only multiples of n 🌶️](#Exercise-2:-Keeping-only-multiples-of-n-🌶️)\n", - " - [Exercise 3: Transposing a Matrix 🌶️🌶️](#Exercise-3:-Transposing-a-Matrix-🌶️🌶️)\n", - " - [Exercise 4: Flattening list of lists 🌶️🌶️](#Exercise-4:-Flattening-list-of-lists-🌶️🌶️)\n", - " - [Exercise 5: Counting initials 🌶️🌶️🌶️](#Exercise-5:-Counting-initials-🌶️🌶️🌶️)\n", - " - [Exercise 6: Counting initials frequency 🌶️🌶️🌶️](#Exercise-6:-Counting-initials-frequency-🌶️🌶️🌶️)\n", - " - [Exercise 7: Finding palindromes 🌶️🌶️🌶️](#Exercise-7:-Finding-palindromes-🌶️🌶️🌶️)\n" + "## Table of Contents\n", + " - [References](#References)\n", + " - [Introduction](#Introduction)\n", + " - [Why Functional Programming?](#Why-Functional-Programming?)\n", + " - [The basic principles of functional programming](#The-basic-principles-of-functional-programming)\n", + " - [Pure Functions (Purity)](#Pure-Functions-(Purity))\n", + " - [Exercise on pure functions](#Exercise-on-pure-functions)\n", + " - [Quiz on pure functions](#Quiz-on-pure-functions)\n", + " - [Immutability](#Immutability)\n", + " - [Composition](#Composition)\n", + " - [Exercise on composition](#Exercise-on-composition)\n", + " - [Higher-Order Functions / Functions as Values](#Higher-Order-Functions-(HoF)-/-Functions-as-Values)\n", + " - [Referential transparency](#Referential-transparency)\n", + " - [Type systems](#Type-systems)\n", + " - [Mapping / Iteration ](#Mapping-/-Iteration)\n", + " - [Mapping](#Mapping)\n", + " - [Filtering](#Filtering)\n", + " - [Reducing](#Reducing)\n", + " - [Exercises on Iteration and Mapping](#Exercises-on-Iteration-and-Mapping)\n", + " - [List comprehensions](#List-comprehensions)\n", + " - [Exercise: Keeping only multiples of `n`](#Exercise:-Keeping-only-multiples-of-n)\n", + " - [Combinining and splitting iterators](#Combinining-and-splitting-iterators)\n", + " - [Exercises](#Exercises)\n", + " - [Exercise 1: Transposing a Matrix](#Exercise-1:-Transposing-a-Matrix)\n", + " - [Exercise 2: Flattening list of lists](#Exercise-2:-Flattening-list-of-lists)\n", + " - [Exercise 3: Counting initials](#Exercise-3:-Counting-initials)\n", + " - [Exercise 4: Counting initials frequency](#Exercise-4:-Counting-initials-frequency)\n", + " - [Exercise 5: Finding palindromes](#Exercise-5:-Finding-palindromes)" ] }, { @@ -40,83 +49,88 @@ "source": [ "# Important modules\n", "import functools\n", - "import itertools\n", - "import tutorial.functional_programming as fp" + "import itertools" ] }, { "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "skip" - } - }, + "metadata": {}, "source": [ - "\n", - "---\n", - "\n", - "\n", - "## References\n", + "# References\n", "Here are some additional references to help you in self-learning. Next to each link, we write if this is a video, text or another type of material\n", "\n", - "- The [functools reference](https://docs.python.org/3/library/functools.html) from the Python standard library (text)\n", - "- The [itertools reference](https://docs.python.org/3/library/itertools.html) from the Python standard library (text)\n", - "- [Functional programming howto](https://docs.python.org/3/howto/functional.html) from the Python documentation\n", - "- A [good, but old introduction](https://www.youtube.com/watch?v=Ta1bAMOMFOI) of functional programming (video)\n", - "- A [very high level](https://www.youtube.com/watch?v=Qa8IfEeBJqk) introduction of functional programming (video, advanced). Interesting, but not Python-specific as it refers to Haskell\n", - "- A [general introduction on functional programming](https://www.youtube.com/watch?v=8z_bUIl_uPo). Very worth watching as it uses Python for the examples (video)\n", - "- [Principles of functional programming](https://dev.to/jamesrweb/principles-of-functional-programming-4b7c)\n", - "\n", - "\n", - "\n", - "## Introduction\n", + "- The [functools reference](https://docs.python.org/3/library/functools.html) from the Python standard library (text).\n", + "- The [itertools reference](https://docs.python.org/3/library/itertools.html) from the Python standard library (text).\n", + "- [Functional programming howto](https://docs.python.org/3/howto/functional.html) from the Python documentation (text).\n", + "- A [good, but old introduction](https://www.youtube.com/watch?v=Ta1bAMOMFOI) of functional programming (video).\n", + "- A [very high level](https://www.youtube.com/watch?v=Qa8IfEeBJqk) introduction of functional programming (video, advanced).\n", + " Interesting, but not Python-specific as it refers to Haskell.\n", + "- A [general introduction on functional programming](https://www.youtube.com/watch?v=8z_bUIl_uPo).\n", + " Very worth watching as it uses Python for the examples (video).\n", + "- [Principles of functional programming](https://dev.to/jamesrweb/principles-of-functional-programming-4b7c) (text)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction\n", "Functional programming is an approach to programming where programs are built by composing and running functions that perform a series of transformations on data.\n", "This contrasts with the approach of *imperative programming*, where programs are written as a series of statements which modify the *state* of the computation environment.\n", "Typically, within functional programming, great focus is placed on *composition*, *immutability* and *purity*.\n", - "We are going to define these terms in more detail later.\n", - "\n", - "## Why Functional Programming\n", - "Why do we choose functional programming? There are a series of advantages to this approach, namely:\n", + "We are going to define these terms in more detail later." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Why Functional Programming?\n", + "There are a series of advantages to this approach, namely:\n", "- **Debugging** and **testing** are easy: there are no surprises because every function only does one thing and does not affect any other piece of the program.\n", - "- **Parallelization** is trivial: functions are just small *boxes* that take one input and produce an output and do not depend on other parts of the code in an implicit manner through global variables or other shared pieces of state. Thus, it's easy to make several functions run in parallel.\n", - "\n", - "## The basic principles of functional programming\n", - "All modern programming languages have *functions* (or methods, procedures, subroutines, subprograms); these are groups of program statements that perform a certain computation. Functions are defined once for the whole program and can be reused at will throughout the program whenever we need to perform the specific computation they are defined for. Using functions, we can split our code in smaller units that are only responsible for a specific *functionality*; this helps us structuring our code in a clean and understandable form. \n", + "- **Parallelization** is trivial: functions are just small *boxes* that take one input and produce an output and do not depend on other parts of the code in an implicit manner through global variables or other shared pieces of state.\n", + " Thus, it's easy to make several functions run in parallel." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "# The basic principles of functional programming\n", + "All modern programming languages have *functions* (or methods, procedures, subroutines, subprograms); these are groups of program statements that perform a certain computation.\n", + "Functions are defined once for the whole program and can be reused at will throughout the program whenever we need to perform the specific computation they are defined for.\n", + "Using functions, we can split our code in smaller units that are only responsible for a specific *functionality*; this helps us structuring our code in a clean and understandable form. \n", "\n", "The main principles we use in functional programming are:\n", "\n", "- *Purity*\n", - "- *Functions as values* (*higher order functions*)\n", "- *Immutability of data*\n", "- *Composition*\n", + "- *Functions as values* (*higher-order functions*)\n", "- *Referential transparency*\n", "- *Type systems*\n", "\n", "In the next few sections, we are going to briefly explore these concepts with some examples." ] }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "notes" - }, - "tags": [] - }, - "source": [ - "### Pure Functions (Purity)" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In functional programming, we try to strive for *purity*, that is we want to define and use functions that only depend on their input, always return the same output for the same inputs and do not have any *side effects*, that is they do not indirectly affect any other part of our program.\n", + "## Pure Functions (Purity)\n", + "In functional programming, we try to strive for *purity*.\n", + "We want to define and use functions that only depend on their input, always return the same output for the same inputs and do not have any *side effects*, that is they do not indirectly affect any other part of our program.\n", "You can think of these functions as mathematical functions. \n", - "Other examples of side effects are:\n", - "- printing to the program output\n", - "- reading or writing files\n", - "- generating and using random numbers\n", + "Some examples of side effects are:\n", + "\n", + "- Printing to the program output\n", + "- Reading or writing files\n", + "- Generating and using random numbers\n", + "\n", "To better understand this concept, let us look at the function `my_first_pure_function` we defined below:" ] }, @@ -186,7 +200,6 @@ }, "outputs": [], "source": [ - "x = [\"short\", \"list\"] \n", "def do_something(y: str) -> None:\n", " x.append(y)" ] @@ -214,6 +227,7 @@ }, "outputs": [], "source": [ + "x = [\"short\", \"list\"]\n", "print(x)\n", "do_something(\"a\")\n", "print(x)" @@ -223,7 +237,52 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you see from the output, the function modified the list `x`. Therefore, this function is not pure. This leads us to the next principle, **immutability**." + "As you see from the output, the function modified the list `x`.\n", + "Therefore, this function is **not pure**.\n", + "This leads us to the next principle, **immutability**." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise on pure functions\n", + "\n", + "
\n", + "

Question

\n", + " Inside the solution function below, rewrite the previous example, so that it now creates a pure function instead.\n", + " The function should append a given array with a given element.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_pure_function(array: list, new_element: str):\n", + " \"\"\"A pure function that appends a given array with a given element.\n", + "\n", + " Args:\n", + " array: the initial list\n", + " new_element: the element to be added to the list\n", + " Returns:\n", + " - the appended list\n", + " \"\"\"\n", + " array.append(new_element)\n", + " return array" ] }, { @@ -232,7 +291,7 @@ "tags": [] }, "source": [ - "#### Quiz of pure functions" + "### Quiz on pure functions" ] }, { @@ -253,7 +312,24 @@ "tags": [] }, "source": [ - "### Immutability" + "## Immutability\n", + "\n", + "Previously, we saw this example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = [\"short\", \"list\"] \n", + "def do_something(y: str) -> None:\n", + " x.append(y)\n", + "\n", + "print(x)\n", + "do_something(\"a\")\n", + "print(x)" ] }, { @@ -265,10 +341,9 @@ "tags": [] }, "source": [ - "\n", - "When writing programs in functional style, we usually avoid functions like `do_something`.\n", - "Instead of modifying existing data (*mutation*), you write functions that transform your data and return new objects.\n", - "In the case of the function above, we would rewrite it as follows:" + "But when writing programs in functional style, we prefer to avoid functions like `do_something`.\n", + "Instead of modifying existing data (*mutation*), we write functions that transform the data and return new objects.\n", + "So, another way to rewrite the function above could be:" ] }, { @@ -300,7 +375,7 @@ "source": [ "The output shows that `do_something_immutable` does not change `x`.\n", "It also does not reference to `x` outside of the scope but takes it as an argument.\n", - "Instead of modfying the original `x` list, it returns a new list.\n", + "Instead of modifying the original `x` list, it returns a new list.\n", "When we want to keep immutability, this is the style we work with.\n", "If we adopt this style, it is easier to reason about the flow of our program as there are no variable that are being modified *at distance* by other function calls." ] @@ -311,21 +386,12 @@ "tags": [] }, "source": [ - "### Composition" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "notes" - }, - "tags": [] - }, - "source": [ - "Another important aspect of functional programming is `composition`. This means that whenever we want to perform multiple operations, we avoid writing intermediate variables, especially so when these are in the global state of the program.\n", + "## Composition\n", "\n", - "Instead, we design our functions in such a way that one functions output can be directly fed into the next function.\n", + "Another important aspect of functional programming is `composition`.\n", + "This means that whenever we want to perform multiple operations, we avoid writing intermediate variables, especially so when these are in the global state of the program.\n", + "\n", + "Instead, we design our functions in such a way that one function's output can be directly fed into the next function.\n", "\n", "Additionally – but this is not as useful in Python – we use the associative property of function application:\n", "\n", @@ -335,8 +401,7 @@ "\n", "where we used $\\circ$ to express the composition of functions.\n", "\n", - "In Python, we can do this using a [`lambda` expression](https://docs.python.org/3/tutorial/controlflow.html?#lambda-expressions):\n", - "\n" + "In Python, we can do this using a [`lambda` expression](https://docs.python.org/3/tutorial/controlflow.html?#lambda-expressions):" ] }, { @@ -363,31 +428,62 @@ "In this way, we can break down a complex calculation in a series of simpler ones.\n", "This is useful for many things:\n", "\n", - "- It is easier to find problems in a smaller function\n", - "- We do our work only once: every time we need the same action, we just call the same function" + "- It is easier to find problems in a smaller function.\n", + "- We do our work only once: every time we need the same action, we just call the same function." ] }, { "cell_type": "markdown", - "metadata": { - "tags": [] - }, + "metadata": {}, + "source": [ + "### Exercise on composition\n", + "\n", + "
\n", + "

Question

\n", + " Inside the solution function below, write the solution of equation x^2 + y^2, using composition.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "### Higher Order Functions / Functions as Values" + "%%ipytest\n", + "\n", + "def solution_composition(x: int, y: int):\n", + " \"\"\"A function that solves the equation x^2 + y^2, using composition.\n", + "\n", + " Args:\n", + " x: the first integer\n", + " y: the second integer\n", + " Returns:\n", + " - the result of the equation\n", + " \"\"\"\n", + "\n", + " return" ] }, { "cell_type": "markdown", "metadata": { - "slideshow": { - "slide_type": "notes" - }, "tags": [] }, "source": [ - "Another important principle of functional programming is that **functions are values**.\n", - "In programming languages (like Python) that support a functional style, we can manipulate functions with the language, we can pass them around in a variable and even use functions as parameters for another function.\n", + "## Higher-Order Functions (HoF) / Functions as Values\n", "\n", + "Another important principle of functional programming is that **functions are values**.\n", + "In programming languages that support a functional style (like Python), functions are treated as first-class objects: we can assign them to variables, pass them as arguments to other functions, and manipulate them just like any other value.\n", "As an example, consider the function `function_caller`:" ] }, @@ -418,7 +514,8 @@ } }, "source": [ - "This function takes a function `f` and its argument `args` as input and return the result of calling the function, while additionally printing a message on the standard output of the program. Let's try this out.\n", + "This function takes a function `f` and its argument `args` as input and returns the result of calling `f`, while additionally printing a message on the standard output of the program.\n", + "Let's try this out.\n", "To do so, we define a new function `add_five`:" ] }, @@ -469,31 +566,21 @@ }, "source": [ "This example was a bit convoluted, but it shows that in Python we can use functions as values and even pass them as arguments to other functions.\n", - "\n", "This is useful in many cases; a typical example is numerical optimization, where we want to find the parameters of a function that minimize a certain criterion.\n", - "\n", - "Other than these specific use cases, there are some common *higher-order functions*, or functions that take other functions as parameters, that are common in most programming languages.We will look at a few of them in a following section." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "### Referential transparency" + "Other than these specific use cases, there are some common *higher-order functions* (HoF), or functions that take other functions as parameters, that are common in most programming languages.\n", + "We will look at a few of them in a following section." ] }, { "cell_type": "markdown", "metadata": { - "slideshow": { - "slide_type": "notes" - }, "tags": [] }, "source": [ - "Another principle of functional programming is called [**referential transparency**](https://en.wikipedia.org/wiki/Referential_transparency). This complex-sounding name simply means that we can replace any expression with its value without changing the behavior of the program.\n", + "## Referential transparency\n", + "\n", + "Another principle of functional programming is called [**referential transparency**](https://en.wikipedia.org/wiki/Referential_transparency).\n", + "This complex-sounding name simply means that we can replace any expression with its value without changing the behavior of the program.\n", "\n", "Consider the following Python code:" ] @@ -524,9 +611,12 @@ } }, "source": [ - "If `transparent_function` is referentially transparent, we can replace its value in the expression `y1 = transparent_function(1) + 5` and we obtain the same value. In other words, the value of `y` does not change if we execute it again, as shown in the code above: we have two expressions `y1` and `y2` that use the same code and because `x` is referentially transparent, their values are equal.\n", + "If `transparent_function` is referentially transparent, we can replace the function call `transparent_function(1)` with its result without changing the meaning of the expression.\n", + "For example, in `y1 = transparent_function(1) + 5`, the result of `y1` will always be the same every time the code runs.\n", + "This is demonstrated by two expressions, `y1` and `y2`, that both use `transparent_function(1)`, and because the function is referentially transparent, `y1` and `y2` will have equal values.\n", + "\n", "\n", - "In this case, this is true because the value of `x()` is simply 5 and is invariant.\n", + "In this case, this is true because the value of `x` is simply 1 and is invariant.\n", "\n", "However, consider this other example:" ] @@ -546,7 +636,6 @@ "def non_transparent_function() -> int:\n", " return random.randint(0, 10)\n", "\n", - "\n", "# With a lamba we capture the expression x() + 5 in a function,\n", "# we get its value when we call it like a function\n", "y1 = lambda: non_transparent_function() + 5\n", @@ -573,15 +662,8 @@ "tags": [] }, "source": [ - "## Type system" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ + "## Type systems\n", + "\n", "A typical trait of (modern) functional programming is the heavy use of the *type system*.\n", "This provides a set of rules to classify expressions and values in the language into classes called *types*.\n", "You encountered some basic types in the introduction, for example, `int`, `str` or `float`.\n", @@ -618,7 +700,7 @@ "def greet(name: str, age: int):\n", " print(f\"Hello {name}, you are {age} years old\")\n", " \n", - "greet(\"Simone\", 34)" + "greet(\"John Doe\", 34)" ] }, { @@ -634,7 +716,7 @@ "metadata": {}, "outputs": [], "source": [ - "greet(\"Simone\", \"age\")" + "greet(25, \"person\")" ] }, { @@ -642,11 +724,13 @@ "metadata": {}, "source": [ "In a language with stricter typing discipline like Java or C++, we would not even be able to compile our program.\n", - "Python lacks this strictness. However, there are tools like [mypy](https://mypy-lang.org/) that can be used to check the type consistency of Python programs without running them.\n", + "Python lacks this strictness.\n", + "However, there are tools like [mypy](https://mypy-lang.org/) that can be used to check the type consistency of Python programs without running them.\n", "\n", "We can achieve a similar effect using execution-time type checks.\n", - "To facilitate our work, we first construct a `Person` class tha encapsulates the attributes of a person.\n", - "We will see more about classes in the object-oriented programming tutorial. For the moment, you can see a class as container of data and related functions." + "To facilitate our work, we first construct a `Person` class that encapsulates the attributes of a person.\n", + "We explain more about classes in the notebooks dedicated to object-oriented programming ([introductory](./05_object_oriented_programming.ipynb), [advanced](./13_object_oriented_programming_advanced.ipynb)).\n", + "If you are not familiar with this concept, for the moment, you can see a class as a container of data and related functions." ] }, { @@ -682,7 +766,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Because of the `isinstance` check in `__init_`, we aren't able to construct a `Person` if `age` is a string (type `str`). Likewise, we cannot call `greet_better` with a value other than a `Person`.\n" + "Because of the `isinstance` check in `__init_`, we aren't able to construct a `Person` if `age` is a string (type `str`).\n", + "Likewise, we cannot call `greet_better` with a value other than a `Person`." ] }, { @@ -691,15 +776,10 @@ "metadata": {}, "outputs": [], "source": [ - "greet_better(Person(\"Simone\", 34))\n", + "greet_better(Person(\"John\", 34))\n", "\n", "try:\n", - " greet_better(Person(\"Simone\", \"d\"))\n", - "except ValueError as e:\n", - " print(f\"Ooops, it does not work: {e}\")\n", - " \n", - "try:\n", - " greet_better((\"Simone\", \"d\"))\n", + " greet_better(Person(\"Jane\", \"d\"))\n", "except ValueError as e:\n", " print(f\"Ooops, it does not work: {e}\")" ] @@ -717,34 +797,24 @@ "tags": [] }, "source": [ - "## Mapping / Iteration " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "notes" - }, - "tags": [] - }, - "source": [ + "# Mapping / Iteration\n", + "\n", "In this section, we will look at the application of some of the principles stated above in Python.\n", - "An important application of functional programming in Python is the manipulation of iterables and lists." + "An important application of functional programming in Python is the manipulation of iterables and lists.\n", + "\n", + "Python has the `map` built-in used to apply a given function to all the elements of any `iterable`.\n", + "\n", + "
\n", + "

Note

\n", + " Reminder: an iterable is any object capable of returning its elements one at a time\n", + "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Using map and filter\n", - "\n", - "Python has the `map` built-in used to apply a given function to all the elements of any `iterable`.\n", - "\n", - "
\n", - "

Note

\n", - " Reminder: an iterable is any object capable of returning its elements one at a time\n", - "
\n", + "## Mapping\n", "\n", "Let's see an example of a `map` using the `add_five` function:" ] @@ -770,7 +840,8 @@ } }, "source": [ - "The output is a bit confusing! The reason is that `map` does not return a list but a [map](https://docs.python.org/3/library/functions.html#map) object.\n", + "The output is a bit confusing!\n", + "The reason is that `map` does not return a list but a [map](https://docs.python.org/3/library/functions.html#map) object.\n", "This object is **lazy**, that means that the values are only generated when we access them, for example by iterating over the `map` object. \n", "Fortunately, we can easily convert this into a list by calling the `list` function:" ] @@ -796,7 +867,7 @@ } }, "source": [ - "This is equivalent to using a for loop:\n" + "This is equivalent to using a for loop:" ] }, { @@ -838,13 +909,13 @@ "tags": [] }, "source": [ - "#### Filtering\n", - "Another basic higher order function is `filter`.\n", - "As the name says, this function is used to filter an `iterable` using a *predicate function*.\n", - "This is a function that takes a value and return `true` or `false`. With `true`, the current element is kept, with `false` it is discarded.\n", - "We now try to write a predicate function that only keeps even numbers.\n", + "## Filtering\n", "\n", - "" + "Another basic higher-order function (HoF) is `filter`.\n", + "As the name says, this function is used to filter an `iterable` using a *predicate function*.\n", + "This is a function that takes a value and returns `true` or `false`.\n", + "With `true`, the current element is kept, with `false` it is discarded.\n", + "We now try to write a predicate function that only keeps even numbers." ] }, { @@ -869,7 +940,8 @@ } }, "source": [ - "Now we are ready to try `filter` using `is_even`. Because `filter` returns a `filter` object, we wrap it in `list` to directly see the result as a list:" + "Now we are ready to try `filter` using `is_even`.\n", + "Because `filter` returns a `filter` object, we wrap it in `list` to directly see the result as a list:" ] }, { @@ -894,17 +966,19 @@ "tags": [] }, "source": [ - "#### Reducing\n", - "A third basic HoF is *reduction*.\n", + "## Reducing\n", + "\n", + "The third basic HoF is *reduction*.\n", "This is a function that takes a function `f(x, y)` of two arguments and an iterable `it` and applies the function to every element in the iterable *cumulatively* to produce one value.\n", "It works in the following way:\n", "\n", "- The first argument of `f`, `x`, is the **current value of the accumulation**. In the beginning, this corresponds to the first element of `it`.\n", "- The second argument of `f`, `y`, is the **current element of the iterable**. In the beginning, this corresponds to the second element of `it`.\n", "\n", - "Because of this behaviour, this function is handy to compute sums or similar aggregations over a list. In Python, this function is available in the [`functools` module](https://docs.python.org/3/library/functools.html), part of Python's standard library.\n", + "Because of this behaviour, this function is handy to compute sums or similar aggregations over a list.\n", + "In Python, this function is available in the [`functools` module](https://docs.python.org/3/library/functools.html), part of Python's standard library.\n", "\n", - "As an example of using `reduce`, consider the following snippet:\n" + "As an example of using `reduce`, consider the following snippet:" ] }, { @@ -919,7 +993,6 @@ "source": [ "import functools\n", "\n", - "\n", "def spy(x: int, y: int) -> int:\n", " val = x * y\n", " print(f\"x: {x}, y:{y}, result: {val}\")\n", @@ -941,6 +1014,83 @@ "The print statement in `spy` helps us see what goes on inside the function at each step in `reduce`." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercises on Iteration and Mapping" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext tutorial.tests.testsuite" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

Question

\n", + " Modify the function solution_filter_even to filter only the even elements of the list input_data. For your solution, you cannot use explicit for loops.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_filter_even(input_data: list[int]) -> list[int]:\n", + " \"\"\"A function that filters only the even elements of a given list.\n", + "\n", + " Args:\n", + " input_data: the initial list\n", + " Returns:\n", + " - the filtered list with only even elements\n", + " \"\"\"\n", + "\n", + " return" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "

Question

\n", + " Modify the function solution_add_one to add the integer number 1 to each element of the list input_data.\n", + " For your solution, you cannot use explicit for loops.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%ipytest\n", + "\n", + "def solution_add_one(input_data: list[int]) -> list[int]:\n", + " \"\"\"A function that adds the integer number 1 to each element of a given list.\n", + "\n", + " Args:\n", + " input_data: the initial list of integers\n", + " Returns:\n", + " - the edited list after adding 1 to each element\n", + " \"\"\"\n", + "\n", + " return" + ] + }, { "cell_type": "markdown", "metadata": { @@ -950,9 +1100,10 @@ "tags": [] }, "source": [ - "### List comprehensions\n", + "# List comprehensions\n", + "\n", "Many of the operations in the previous section can be performed in a different (some would say more *pythonic*) way using [list comprehensions](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions).\n", - "These look like mini for-loops inside square brackets and are very useful to write programs in a functional style while keeping code more readable than using higher order functions. \n", + "These look like mini for-loops inside square brackets and are very useful to write programs in a functional style while keeping code more readable than using higher order functions.\n", "\n", "For example, if we want to double all integers between 1 and 10 and store the result in a list, we can proceed in the classical imperative way:" ] @@ -1000,7 +1151,7 @@ "source": [ "doubles = map(lambda x: x * 2, range(10))\n", "\n", - "print(list(doubles))\n" + "print(list(doubles))" ] }, { @@ -1116,11 +1267,28 @@ }, { "cell_type": "markdown", - "metadata": { - "tags": [] - }, + "metadata": {}, "source": [ - "#### Exercises on iteration/mapping" + "### Exercise: Keeping only multiples of `n`\n", + "\n", + "
\n", + "

Question

\n", + " Given a list of integers, write a function that only keeps the numbers that are multiples of a given constant k.\n", + " \n", + "
\n", + "\n", + "\n", + "
\n", + "

Hints

\n", + " The operator % (modulo) gives the remainder of division. a % k is 0 when a is a multiple of k. For more information, consult the chapter on basic datatypes. \n", + "
" ] }, { @@ -1132,13 +1300,6 @@ "%reload_ext tutorial.tests.testsuite" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "1. Modify the function `solution_filter_even` to filter only the even elements of the list `input_data`. For your solution, you **cannot** use explicit `for` loops." - ] - }, { "cell_type": "code", "execution_count": null, @@ -1147,33 +1308,17 @@ "source": [ "%%ipytest\n", "\n", - "def solution_filter_even(input_data: list[int]) -> list[int]:\n", - " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "2. Modify the function `solution_add_one` to add the integer number `1` to each element of the list `input_data`. For your solution, you **cannot** use explicit `for` loops." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%ipytest\n", + "def solution_multiples_of_n(my_list: list[int], k: int) -> list[int]:\n", + " \"\"\"A function that keeps only the multiples of k from a given list.\n", "\n", - "def solution_add_one(input_data: list[int]) -> list[int]:\n", - " \"\"\"\n", - " Write your solution here\n", + " Args:\n", + " my_list: the initial list\n", + " k: the integer number\n", + " Returns:\n", + " - the filtered list\n", " \"\"\"\n", - " pass" + "\n", + " return" ] }, { @@ -1184,15 +1329,18 @@ } }, "source": [ - "### Combinining and splitting iterators\n", + "# Combinining and splitting iterators\n", + "\n", "Sometimes, we need to iterate over multiple lists in a special fashion, for example over two lists in parallel or we want to compute all the combinations of elements of two lists etc.\n", "We can solve many ot these problems with the use of list compherensions, but sometimes there are more elegant solutions by using the [`itertools`](https://docs.python.org/3/library/itertools.html) module, a part of Python's standard library.\n", "\n", "Lets see some example of problems we can solve by using these tools.\n", "\n", - "- Iterating over two lists in parallel: \n", - " consider the two lists `numbers = [1, 2, 3, 4, 5]` and `words = [\"one\", \"two\", \"three\" , \"four\", \"five\"]`. We want to produce a list of strings consisting of the content of `numbers` and `words`, such that each number in `numbers` corresponds to the right word in `words`. We can do this by using the `zip` buil-in function:\n", - "`zip` takes a number of `iterables` as an input and returns a list of tuples. The i-th element of this list is a tuple with the i-th element of each iterable, proceeding until the shortes iterable is exhausted. Using `zip`, we solve our problem like this:" + "- Iterating over two lists in parallel: consider the two lists `numbers = [1, 2, 3, 4, 5]` and `words = [\"one\", \"two\", \"three\" , \"four\", \"five\"]`.\n", + " We want to produce a list of strings consisting of the content of `numbers` and `words`, such that each number in `numbers` corresponds to the right word in `words`.\n", + " We can do this by using the `zip` buil-in function: `zip` takes a number of `iterables` as an input and returns a list of tuples.\n", + " The `i-th` element of this list is a tuple with the `i-th` element of each iterable, proceeding until the shortes iterable is exhausted.\n", + " Using `zip`, we solve our problem like this:" ] }, { @@ -1218,10 +1366,10 @@ } }, "source": [ - "- Producing permutations of elements.\n", - " Suppose we have a list containing the letters `[\"A\", \"B\", \"C\", \"D\", \"E\"]` and we want to produce all four letter words from them.\n", - " We can use `itertools.permutations`.\n", - " Once again, we wrap the operation in `list` to obtain a list as output:" + "- Producing permutations of elements.\n", + " Suppose we have a list containing the letters `[\"A\", \"B\", \"C\", \"D\", \"E\"]` and we want to produce all four letter words from them.\n", + " We can use `itertools.permutations`.\n", + " Once again, we wrap the operation in `list` to obtain a list as output:" ] }, { @@ -1247,7 +1395,9 @@ } }, "source": [ - "Another useful trick to know when working with lists is [**unpacking**](https://docs.python.org/3/tutorial/controlflow.html#tut-unpacking-arguments). In Python, we can extract elements from a list using the assignment statement. For example, if we have a two-element list, we can write" + "- [**Unpacking**](https://docs.python.org/3/tutorial/controlflow.html#tut-unpacking-arguments) is another useful trick to know when working with lists. \n", + " In Python, we can extract elements from a list using the assignment statement.\n", + " For example, if we have a two-element list, we can write" ] }, { @@ -1360,7 +1510,7 @@ "tags": [] }, "source": [ - "## Exercises" + "# Exercises" ] }, { @@ -1380,115 +1530,19 @@ } }, "source": [ - "### Exercise 1: Pure or impure functions? 🌶️\n", - "\n", - "For each of the functions below, determine whether they are pure or impure" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "slideshow": { - "slide_type": "subslide" - } - }, - "outputs": [], - "source": [ - "def fun1(a: \"list[str]\") -> None:\n", - " a.append(\"b\")\n", - "\n", - "\n", - "def fun2(a: int) -> int:\n", - " return a + 2\n", - "\n", - "\n", - "def fun3(a: \"dict[str, str]\") -> None:\n", - " a[\"test\"] = \"dest\"\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Exercise 2: Keeping only multiples of n 🌶️\n", + "### Exercise 1: Transposing a Matrix\n", "\n", - "Given a list `L` of integers, write a function that only keeps the numbers that are multiples of a given constant `k`.\n", + "Consider a matrix represented row-wise as a list of lists `[[1, 2, 3], [4, 5, 6], [7, 8, 8]]`.\n", + "Write a function that returns its transpose, the matrix obtained by exchanging rows and columns.\n", "\n", - "- Example 1: given `nums = [1, 2, 3, 4, 5]`, and `k = 2`, the result must be `[2, 4]`\n", - "\n", - "- Example 2: given `nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ` and `k = 5`, the result is `[5, 10]`\n", + "- Example 1: given `matrix=[[1, 0], [0, 1]]`, the result must be `[[1, 0], [0, 1]]`\n", + "- Example 2: given `matrix=[[1, 2, 3], [4, 5, 6], [7, 8, 8]]` the result must be `[1, 4, 7], [2, 5, 8], [3, 6, 8]]`\n", "\n", "\n", "\n", "
\n", "

Hints

\n", - "
    \n", - "
  • \n", - " The operator % (modulo) gives the remainder of division. a % k is 0 when a is a multiple of k. For more information, consult the chapter on basic datatypes. \n", - "
  • \n", - "
  • \n", - " Write your function in the cell below inside of the solution_exercise1 function. The function receives a list l as an input and should return another list\n", - "
  • \n", - "
\n", - "
\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "slideshow": { - "slide_type": "subslide" - } - }, - "outputs": [], - "source": [ - "%%ipytest\n", - "\n", - "def solution_exercise2(l: \"list[int]\", k: int):\n", - " \"\"\"\n", - " Write your solution here\n", - " \"\"\"\n", - " pass" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Exercise 3: Transposing a Matrix 🌶️🌶️\n", - "\n", - "Consider a matrix `M` represented row-wise as a list of lists `[[1, 2, 3], [4, 5, 6], [7, 8, 8]]`.\n", - "Write a function that returns the transpose of `M`, the matrix obtained by exchanging rows and columns\n", - "\n", - "- Example 1: given `M=[[1, 0], [0, 1]]`, the result must be `[[1, 0], [0, 1]]`\n", - "- Example 2: given `M=[[1, 2, 3], [4, 5, 6], [7, 8, 8]]` the result must be `[1, 4, 7], [2, 5, 8], [3, 6, 8]]`\n", - "\n", - "\n", - "\n", - "
\n", - "

Hints

\n", - "
    \n", - "
  • \n", - " Write your function in solution_exercise3\n", - "
  • \n", - " The zip function behaves similar to a transposition: zip([1,2], [3,4]) = [(1,3), (3,4)])\n", - "
  • \n", - "
  • \n", - " Write your function in the cell below inside of the solution_exercise3 function. The function receives a the \"matrix\" as an input m and should return another list\n", - "
  • \n", - "
\n", + " The zip function behaves similarly to a transposition: zip([1,2], [3,4]) = [(1,3), (2,4)])\n", "
\n" ] }, @@ -1504,11 +1558,17 @@ "source": [ "%%ipytest\n", "\n", - "def solution_exercise3(m: \"list[list[int]]\") -> \"list[list[int]]\":\n", - " \"\"\"\n", - " Write your solution here\n", + "def solution_exercise1(matrix: list[list[int]]) -> list[list[int]]:\n", + " \"\"\"A function that returns the transpose of a given matrix, by using the zip function.\n", + " The transpose of a matrix is obtained by swapping the rows and columns of the matrix.\n", + "\n", + " Args:\n", + " matrix: the initial matrix\n", + " Returns:\n", + " - the transposed matrix\n", " \"\"\"\n", - " pass\n" + "\n", + " return" ] }, { @@ -1519,23 +1579,13 @@ } }, "source": [ - "### Exercise 4: Flattening list of lists 🌶️🌶️\n", - "\n", - "Imagine we receive a list of lists `l` like `[[1, 2], [3, 4]]`. Write a function that converts this list into a `flat` list like `[1, 2, 3, 4]`. \n", - "\n", - "To write your function, just complete the code in `solution_exercise4`\n", + "### Exercise 2: Flattening list of lists\n", "\n", + "Imagine we receive a list of lists like `[[1, 2], [3, 4]]`. Write a function that converts this list into a `flat` list like `[1, 2, 3, 4]`. \n", "\n", "
\n", "

Hints

\n", - "
    \n", - "
  • \n", - " This is a good exercise to use functools.reduce \n", - "
  • \n", - "
  • \n", - " Write your function in the cell below inside of the solution_exercise4 function. The function receives a list l as an input and should return another list\n", - "
  • \n", - "
\n", + " This is a good exercise to use functools.reduce \n", "
" ] }, @@ -1551,12 +1601,16 @@ "source": [ "%%ipytest\n", "\n", - "def solution_exercise4(l: \"list[list[any]]\") -> \"list[any]\":\n", - " \"\"\"\n", - " Write your solution here\n", + "def solution_exercise2(my_list: list[list[any]]) -> list[any]:\n", + " \"\"\"A function that returns a flattened list from a given list of lists, by using funtools.reduce\n", + "\n", + " Args:\n", + " my_list: the initial list of lists\n", + " Returns:\n", + " - the flattened list\n", " \"\"\"\n", - " pass\n", - "\n" + "\n", + " return" ] }, { @@ -1567,9 +1621,10 @@ } }, "source": [ - "### Exercise 5: Counting initials 🌶️🌶️🌶️\n", + "### Exercise 3: Counting initials\n", "\n", - "Given a list `w` of English words, write a function that counts how many words begin with each letter of the English alphabet and returns the result as an **alphabetically sorted** list of tuples `(letter, count)`. We do not distinguish between uppercase and lowercase letters (\"A\" and \"a\" are considered the same). \n", + "Given a list of English words, write a function that counts how many words begin with each letter of the English alphabet and returns the result as an **alphabetically sorted** list of tuples `(letter, count)`.\n", + "We do not distinguish between uppercase and lowercase letters (\"A\" and \"a\" are considered the same). \n", "\n", "\n", "
\n", @@ -1579,9 +1634,6 @@ " Consider the functions sorted and itertools.groupby from the Python standard library.\n", " \n", "
  • \n", - " Write your function in the cell below inside of the solution_exercise5 function. The function receives a list w as an input and should return another list\n", - "
  • \n", - "
  • \n", " To ensure consistent capitalization you can use the lower() method of str\n", "
  • \n", " \n", @@ -1601,12 +1653,19 @@ "source": [ "%%ipytest\n", "\n", - "def solution_exercise5(w: list[str]) -> list[(str, int)]:\n", - " \"\"\"\n", - " Write your solution here\n", + "def solution_exercise3(words: list[str]) -> list[(str, int)]:\n", + " \"\"\"A function that counts the number of words from a given list that start with each letter of the alphabet, using sorted() and itertools.groupby.\n", + " The function should be case insensitive, using lower() to ensure consistent capitalization.\n", + " It should return a list of tuples, where each tuple contains a letter and the number of words that start with that letter.\n", + " The list of tuples should be sorted in alphabetical order.\n", + "\n", + " Args:\n", + " words: the initial list of words\n", + " Returns:\n", + " - the alphabetically sorted list of tuples\n", " \"\"\"\n", - " pass\n", - "\n" + "\n", + " return" ] }, { @@ -1617,25 +1676,16 @@ } }, "source": [ - "### Exercise 6: Counting initials frequency 🌶️🌶️🌶️\n", - "If you could solve the previous exercise, you now have a list `l` of the form `[('a', 20), ('b', 30), ...]`.\n", - "If you cannot, do not worry: you will receive the correct input automatically as `l` inside the function `solution_exercise6`\n", - "\n", + "### Exercise 4: Counting initials frequency\n", "\n", - "Write a function that computes the *relative frequency* of each letter in the list `l`. \n", + "If you could solve the previous exercise, you now have a list of the form `[('a', 20), ('b', 30), ...]`.\n", + "If you could not, do not worry: you will receive the correct input automatically as an argument inside the function `solution_exercise4`.\n", "\n", + "Write a function that computes the *relative frequency* of each letter in that list and returns a list of tuples `(letter, frequency)`.\n", "\n", "
    \n", "

    Hints

    \n", - "
      \n", - "
    • \n", - " The relative frequence of a value a in a list is simply the number of time it appears in that\n", - " list over the total lenght of the list\n", - "
    • \n", - "
    • \n", - " Write your function in the cell below inside of the solution_exercise6 function. The function receives a list l as an input and should return another list\n", - "
    • \n", - "
    \n", + " The relative frequence of a value a in a list is simply the number of times it appears in that list, over the total length of the list.\n", "
    " ] }, @@ -1651,8 +1701,17 @@ "source": [ "%%ipytest\n", "\n", - "def solution_exercise6(l: \"list[(str, int)]\") -> \"list[(str, float)]\":\n", - " pass" + "def solution_exercise4(my_list: list[(str, int)]) -> list[(str, float)]:\n", + " \"\"\"A function that computes the relative frequency of each letter from a given list of tuples.\n", + " The relative frequency is calculated as the number of occurrences of a letter in the list, divided by the length of the list.\n", + "\n", + " Args:\n", + " my_list: the initial list of tuples (letter, count) which counts how many words start with each letter\n", + " Returns:\n", + " - the list of tuples (letter, frequency) with the relative frequency of each letter\n", + " \"\"\"\n", + "\n", + " return" ] }, { @@ -1663,8 +1722,9 @@ } }, "source": [ - "### Exercise 7: Finding palindromes 🌶️🌶️🌶️\n", - "Consider again the `words` list of strings. Write a function that returns the list of all *palindromes*. A *palindrome* is a word (any string longer than 1) that reads the same left-to-right and right-to-left.\n", + "### Exercise 5: Finding palindromes\n", + "\n", + "Consider again the `words` list of strings. Write a function that returns the list of all *palindromes*. A *palindrome* is a word (any string longer than 1) that reads the same left-to-right and right-to-left.\n", "\n", "For example:\n", "- rotator\n", @@ -1672,9 +1732,6 @@ "- noon\n", "- radar\n", "\n", - "\n", - "\n", - "\n", "
    \n", "

    Hints

    \n", "
      \n", @@ -1682,10 +1739,10 @@ " A single character does not count as a palindrome.\n", " \n", "
    • \n", - " The words are available as the input words to solution_exercise7.\n", + " The words are available as the input to solution_exercise3.\n", "
    • \n", "
    \n", - "
    \n" + "
    " ] }, { @@ -1700,8 +1757,18 @@ "source": [ "%%ipytest\n", "\n", - "def solution_exercise7(words: \"list[str]\") -> \"list[str]\":\n", - " pass" + "def solution_exercise5(words: list[str]) -> list[str]:\n", + " \"\"\"A function that returns a list of words that are palindromes, from a given list of words.\n", + " A palindrome is a word that reads the same forward and backward.\n", + " A word is any string longer than 1 character.\n", + "\n", + " Args:\n", + " words: the initial list of words\n", + " Returns:\n", + " - the list of palindrome words\n", + " \"\"\"\n", + "\n", + " return" ] } ], @@ -1722,7 +1789,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.12.10" }, "vscode": { "interpreter": { diff --git a/tutorial/quiz/functional_programming.py b/tutorial/quiz/functional_programming.py index 1cba6f20..50d60525 100644 --- a/tutorial/quiz/functional_programming.py +++ b/tutorial/quiz/functional_programming.py @@ -4,7 +4,7 @@ class PureFunctions(Quiz): def __init__(self, title=""): q1 = Question( - question="""Is the following function pure:
    def f(x: int) -> int: \n    return x + 1
    """, + question="""Is the following function pure?
    def f(x: int) -> int: \n    return x + 1
    """, options={ "Yes": "Correct!", "No": "Why not? The function does not have any side effects.", @@ -14,7 +14,7 @@ def __init__(self, title=""): shuffle=True, ) q2 = Question( - question="""Is the following function pure:
    a = []\ndef f():\n    a.append(1)
    """, + question="""Is the following function pure?
    a = []\ndef f():\n    a.append(1)
    """, options={ "Yes": "Wrong! Notice that a is not an argument to the function", "No": "Correct", @@ -24,7 +24,7 @@ def __init__(self, title=""): shuffle=True, ) q3 = Question( - question="""Is the following function pure:
    def f(a: list[int]) -> list[int]:\n    return a+[1]
    """, + question="""Is the following function pure?
    def f(a: list[int]) -> list[int]:\n    return a+[1]
    """, options={ "Yes": "Correct!", "No": "Wrong! Notice that a is an argument to the function and we return a new list by concatenating a and [1].", @@ -33,5 +33,15 @@ def __init__(self, title=""): hint="Purity means that the function does not have any side effects, for example changing variables other than the inputs, opening files, etc.", shuffle=True, ) + q4 = Question( + question="""Is the following function pure?
    def f(a: dict[str, str]) -> None:\n    a["test"] = "dest"
    """, + options={ + "Yes": "Wrong! Notice that a is an argument to the function and we add a new key-value pair to this dictionary.", + "No": "Correct!", + }, + correct_answer="No", + hint="Purity means that the function does not have any side effects, for example changing variables other than the inputs, opening files, etc.", + shuffle=True, + ) - super().__init__(questions=[q1, q2, q3]) + super().__init__(questions=[q1, q2, q3, q4]) diff --git a/tutorial/tests/test_11_functional_programming.py b/tutorial/tests/test_11_functional_programming.py index 9cef97fb..892d3c12 100644 --- a/tutorial/tests/test_11_functional_programming.py +++ b/tutorial/tests/test_11_functional_programming.py @@ -10,9 +10,60 @@ import requests from numpy.typing import NDArray +# +# Example: Pure Function +# -def reference_exercise2(my_list: List[int], k: int) -> List[int]: - return [i for i in my_list if i % k == 0] + +def reference_pure_function(array, new_element): + return array + [new_element] + + +@pytest.mark.parametrize( + "array, new_element", + [ + ([1, 2, 3], 4), + (["cat", "dog"], "bird"), + ], +) +def test_pure_function(array, new_element, function_to_test): + output_array = function_to_test(array, new_element) + assert id(output_array) != id(array), "The arrays must be different objects." + assert output_array == reference_pure_function(array, new_element) + + +# +# Example: Composition +# + + +def reference_composition(x, y): + def square(x: int) -> int: + return x * x + + def add(x: int, y: int) -> int: + return x + y + + def eq(x: int, y: int) -> int: + return add(square(x), square(y)) + + return eq(x, y) + + +@pytest.mark.parametrize( + "x, y", + [ + (2, 4), + (5, 10), + ], +) +def test_composition(x, y, function_to_test): + assert function_to_test(x, y) == reference_composition(x, y) + + +# +# Example: Filter Even Numbers +# def check_for_loop_in_body(fun: Callable) -> bool: @@ -39,12 +90,17 @@ def reference_filter_even(my_list: List[int]) -> List[int]: def test_filter_even(function_to_test: Callable, my_list: List[int]): res = function_to_test(my_list) assert isinstance(res, list), "The function you wrote does not return a list" - assert res == reference_filter_even(my_list), ( - "The list you return is not equal to the expected solution" - ) assert not check_for_loop_in_body(function_to_test), ( "You are not allowed to use a for loop in this exercise" ) + assert res == reference_filter_even(my_list), ( + "The list you return is not equal to the expected solution" + ) + + +# +# Example: Add 1 to Each Element +# def reference_add_one(my_list: List[int]) -> List[int]: @@ -68,6 +124,15 @@ def test_add_one(function_to_test: Callable, my_list: List[int]): ) +# +# Example: Keeping only multiples of n +# + + +def reference_multiples_of_n(my_list: List[int], k: int) -> List[int]: + return [i for i in my_list if i % k == 0] + + @pytest.mark.parametrize( "my_list, k", [ @@ -75,33 +140,43 @@ def test_add_one(function_to_test: Callable, my_list: List[int]): (list(range(100)), 5), ], ) -def test_exercise2( +def test_multiples_of_n( function_to_test: Callable[[List[int]], int], my_list: List[int], k: int, ): - assert function_to_test(my_list, k) == reference_exercise2(my_list, k) + assert function_to_test(my_list, k) == reference_multiples_of_n(my_list, k) + + +# +# Exercise 1: Transposing a Matrix +# -def reference_exercise3(x: List[List[int]]) -> List[List[int]]: - return [list(i) for i in zip(*x)] +def reference_exercise1(matrix: List[List[int]]) -> List[List[int]]: + return [list(i) for i in zip(*matrix)] @pytest.mark.parametrize( "my_input", [(np.eye(3)), (np.random.randint(0, 100, size=(4, 4)))], ) -def test_exercise3( +def test_exercise1( function_to_test: Callable[[List[List[int]]], List[List[int]]], my_input: NDArray ): res = function_to_test(my_input.tolist()) assert ( - res == reference_exercise3(my_input.tolist()) + res == reference_exercise1(my_input.tolist()) and res == my_input.transpose().tolist() ) -def reference_exercise4(my_list: List[List[Any]]) -> List[Any]: +# +# Exercise 2: Flattening list of lists +# + + +def reference_exercise2(my_list: List[List[Any]]) -> List[Any]: return functools.reduce(lambda x, y: x + y, my_list) @@ -112,47 +187,64 @@ def reference_exercise4(my_list: List[List[Any]]) -> List[Any]: [["a", "b", "c"], ["d", "f", "e"], ["another"]], ], ) -def test_exercise4( +def test_exercise2( function_to_test: Callable[[List[List[Any]]], List[Any]], my_input: List[List[Any]], ): - assert function_to_test(my_input) == reference_exercise4(my_input) + assert function_to_test(my_input) == reference_exercise2(my_input) + + +# +# Exercise 3: Counting initials +# @functools.lru_cache -def get_data_exercise5() -> List[str]: +def get_data_exercise3() -> List[str]: words = requests.get("https://www.mit.edu/~ecprice/wordlist.10000").text return words.splitlines() -def reference_exercise5(w: List[str]) -> List[Tuple[str, int]]: +def reference_exercise3(words: List[str]) -> List[Tuple[str, int]]: return [ (k, len(list(v))) - for k, v in itertools.groupby(sorted(w, key=lambda x: x[0]), key=lambda x: x[0]) + for k, v in itertools.groupby( + sorted(words, key=lambda x: x[0]), key=lambda x: x[0] + ) ] -def test_exercise5(function_to_test: Callable[[List[str]], List[Tuple[str, int]]]): - data = get_data_exercise5() - assert function_to_test(data) == reference_exercise5(data) +def test_exercise3(function_to_test: Callable[[List[str]], List[Tuple[str, int]]]): + data = get_data_exercise3() + assert function_to_test(data) == reference_exercise3(data) -def reference_exercise6(my_list: List[Tuple[str, int]]) -> List[Tuple[str, float]]: +# +# Exercise 4: Counting initials frequency +# + + +def reference_exercise4(my_list: List[Tuple[str, int]]) -> List[Tuple[str, float]]: total = sum(map(lambda x: x[1], my_list)) # noqa: C417 return [(letter, freq / total) for letter, freq in my_list] -def test_exercise6( +def test_exercise4( function_to_test: Callable[[List[Tuple[str, int]]], List[Tuple[str, float]]], ): - input_data = reference_exercise5(get_data_exercise5()) - assert function_to_test(input_data) == reference_exercise6(input_data) + input_data = reference_exercise3(get_data_exercise3()) + assert function_to_test(input_data) == reference_exercise4(input_data) -def reference_function_exercise7(my_list: List[str]) -> List[str]: - return list(filter(lambda x: x == x[::-1] and len(x) > 1, my_list)) +# +# Exercise 5: Finding palindromes +# -def test_exercise7(function_to_test: Callable[[List[str]], List[str]]): - data = get_data_exercise5() - assert function_to_test(data) == reference_function_exercise7(data) +def reference_exercise5(words: List[str]) -> List[str]: + return list(filter(lambda x: x == x[::-1] and len(x) > 1, words)) + + +def test_exercise5(function_to_test: Callable[[List[str]], List[str]]): + data = get_data_exercise3() + assert function_to_test(data) == reference_exercise5(data)