Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Combinatorial Interview Problems with Backtrac...

Combinatorial Interview Problems with Backtracking Solutions - From Imperative Procedural Programming to Declarative Functional Programming - Partย 1

In this deck series we are going to do the following for each of three combinatorial problems covered in chapter fourteen of a book called Coding Interview Patterns โ€“ Nail Your Next Coding Interview :
* see how the book describes the problem
* view the bookโ€™s solution to the problem, which exploits backtracking
* view the bookโ€™s imperative Python code for the solution
* translate the imperative code from Python to Scala
* explore Haskell and Scala functional programming solutions.

Avatar for Philip Schwarz

Philip Schwarz PRO

November 16, 2025
Tweet

More Decks by Philip Schwarz

Other Decks in Programming

Transcript

  1. Combinatorial Interview Problems with Backtracking Solutions From Imperative Procedural Programming

    to Declarative Functional Programming Programming Paradigms Imperative Declarative Procedural Object Oriented Functional Logic @philip_schwarz slides by https://fpilluminated.org/ ๐ผ๐‘š๐‘š๐‘ข๐‘ก๐‘Ž๐‘๐‘–๐‘™๐‘–๐‘ก๐‘ฆ ๐ฟ๐‘–๐‘ ๐‘ก ๐‘€๐‘œ๐‘›๐‘Ž๐‘‘ ๐‘€๐‘ข๐‘ก๐‘Ž๐‘๐‘–๐‘™๐‘–๐‘ก๐‘ฆ ๐‘…๐‘’๐‘๐‘ข๐‘Ÿ๐‘ ๐‘–๐‘œ๐‘› ๐‘‘๐‘Ž๐‘ก๐‘Ž ๐ฟ๐‘–๐‘ ๐‘ก ๐‘Ž = | ๐‘Ž โˆถ [๐‘Ž] ๐‘๐‘ข๐‘Ÿ๐‘’ ๐‘ฅ = ๐‘ฅ ๐‘ฅ๐‘  โ‰ซ= ๐‘“ = [๐‘ฆ | ๐‘ฅ โŸต ๐‘ฅ๐‘ , ๐‘ฆ โŸต ๐‘“ ๐‘ฅ] ๐Ÿ”Ž๐Ÿง‘๐Ÿ’ป๐Ÿ’ก
  2. In this deck series we are going to do the

    following for each of three combinatorial problems covered in chapter fourteen of a book called Coding Interview Patterns โ€“ Nail Your Next Coding Interview : โ€ข see how the book describes the problem โ€ข view the bookโ€™s solution to the problem, which exploits backtracking โ€ข view the bookโ€™s imperative Python code for the solution โ€ข translate the imperative code from Python to Scala โ€ข explore Haskell and Scala functional programming solutions. See next slide for the first combinatorial problem that we are going to tackle. @philip_schwarz
  3. Find All Subsets Return all possible subsets of a given

    set of unique integers. Each subset can be ordered in any way, and the subsets can be returned in any order. Example: Input: nums = [4,5,6] Output: [[], [4], [4,5], [4,5,6], [4,6], [5], [5,6], [6]] Intuition The key intuition for solving this problem lies in understanding that each subset is formed by making a specific decision for every number in the input array: to include the number or exclude it. For example, from the array [4,5,6], the subset [4,6] is created by including 4, excluding 5, and including 6. From Chapter 14: Backtracking @alexxubyte Alex Xu @shaungunaw Shaun Gunawardane [] [4] [] [4,5] [4] [5] [] [4,5,6] [4,5] [4,6] [4] [5,6] [5] [6] [] include 4 exclude 4 include 5 exclude 5 include 5 exclude 5 include 6 exclude 6 include 6 exclude 6 include 6 exclude 6 include 6 exclude 6 โ€ข The authors solve the problem using a recursive function called which considers each number in turn, recursively calling itself, once after deciding to add the number to a copy of a subset grown so far, and once after deciding not to add it, with the base case adding the subset grown so far to an accumulator that is a growing result list of subsets. โ€ข The reader is walked through a series of diagrams visualising the individual steps of the recursive backtracking process. โ€ข See the diagram below for a compressed representation of all the steps.
  4. def find_all_subsets(nums: List[Int]) -> List[List[Int]]: res = [] backtrack(0, [],

    nums, res) res def backtrack( i: Int, current_subset: List[Int], nums: List[Int], res: List[List[Int]] ) -> None: // Base case: if all elements have been considered, add the // current subset to the output. if i == len(nums): res.append(current_subset[:]) return // Include the current element and recursively explore all paths // that branch from this subset. current_subset.append(nums(i)) backtrack(i + 1, current_subset, nums, res) // Exclude the current element and recursively explore all paths // that branch from this subset. current_subset.pop() backtrack(i + 1, current_subset, nums, res) [] [4] [] [4,5] [4] [5] [] [4,5,6] [4,5] [4,6] [4] [5,6] [5] [6] [] include nums[i] exclude nums[i] include nums[i] exclude nums[i] include nums[i] exclude nums[i] include nums[i] exclude nums[i] include nums[i] exclude nums[i] include nums[i] exclude nums[i] include nums[i] exclude nums[i] nums=[4,5,6] nums=[4,5,6] nums=[4,5,6] nums=[4,5,6] i=0 i=1 i=2 i=3 Here is the Python code for computing subsets. It represents a set as a list. To compute the set of all subsets of a set of size N, method backtrack needs to make 2N-1 decisions, each with two choices available: including a number, or excluding it. Making a decision (choosing) is followed by a recursive call. The total number of calls required is 2N+1-1. @alexxubyte Alex Xu @shaungunaw Shaun Gunawardane This program is side-effecting, in that it adds and removes numbers to and from mutable lists current_subset and res. The diagram below shows the computation of the subsets of a set of size three. E.g for a set with three elements, the number of decisions is 23-1 = 8-1 = 7 and the number of calls is 23+1-1 = 16-1 = 15. index of number we need to consider next subset grown so far numbers to generate subsets with subsets generated so far
  5. Here is a high-level diagram capturing the shape of the

    search process used to compute the powerset of a set. Each circle represents the decision of whether or not to add an item to a subset that is being generated, and each square represents the identification of a subset to be included in the powerset. In part two of this series, in which look at two more combinatorial problems, weโ€™ll compare their corresponding diagrams with this one.
  6. Next, letโ€™s turn to the translation of the Python program

    into Scala. The program uses Pythonโ€™s mutable list. Is there a corresponding collection in Scala? The answer depends on what we want to accomplish. In a real-world scenario, armed with our functional and non-functional requirements, it could make sense to answer the question only after studying several relevant slides contained in the deck shown below.
  7. For our modest purposes however, we are just going to

    pick a collection based on some of the following facts, some of them from that deck: 1. .Python provides both an array type and a list type, both being mutable 2. The book decided to solve the problem using mutable lists, and in particular, using their following functions: selecting the Nth item, appending an item, and removing the last item. 3. The recommended general-purpose go-to sequential collections for the combinations of mutable/immutable and indexed/linear are shown in the first table below (from the deck shown on the previous slide) 4. If we consider that the Python program uses a mutable list, which is a linear collection, then according to the table, it seems to make sense to pick a Scala ListBuffer. 5. If we consider the following extract from book Programming in Scala, it doesnโ€™t make perfect sense to pick a Scala ListBuffer in that a ListBuffer is not actually a list: โ€œAs the name implies, a ListBuffer is backed by a List and supports efficient conversion of its elements to a Listโ€ 6. If we consider this other extract from Programming in Scala, it also doesnโ€™t make sense to pick a Scala ArrayBuffer because it also is not a list: โ€œan ArrayBuffer is backed by an array, and can be quickly converted into one.โ€ 7. If we are going to pick a buffer type, it can make sense to pick a Scala ArrayBuffer because according to the operation complexity table below (also from the deck shown on the previous slide) the time complexity of selecting the Nth item is lower in an ArrayBuffer (interestingly though, the time complexity of removing an item, an operation used by the program, is not shown!). Neither ListBuffer nor ArrayBuffer seem to be a perfect match for Pythonโ€™s mutable list, but we have to pick one, so based on (3) and (4), letโ€™s pick ListBuffer. Immutable Mutable Indexed Vector ArrayBuffer Linear (Linked lists) List ListBuffer head tail apply update prepend append insert ListBuffer Linear Mutable O(1) O(n) O(n) O(n) O(1) O(1) O(n) ArrayBuffer Indexed Mutable O(1) O(n) O(1) O(1) O(n) amort O(1) O(n)
  8. On the next slide we can see the translation of

    the program from Python to Scala. Note that when we import the ListBuffer type, we rename it to List. We do this purely to reduce the number of non-essential visible differences between the two programs. Also, if you are coding along as you go through the deck, you can change the collection used by the Scala program from ListBuffer to ArrayBuffer simply by replacing ListBuffer with ArrayBuffer in the import statement.
  9. def find_all_subsets(nums: List[Int]): List[List[Int]] = val res = List.empty[List[Int]] backtrack(0,

    List(), nums, res) res def backtrack( i: Int, current_subset: List[Int], nums: List[Int], res: List[List[Int]] ): Unit = // Base case: if all elements have been considered, add the // current subset to the output. if i == nums.length then return res.append(current_subset.clone) // Include the current element and recursively explore all paths // that branch from this subset. current_subset.append(nums(i)) backtrack(i + 1, current_subset, nums, res) // Exclude the current element and recursively explore all paths // that branch from this subset. current_subset.dropRightInPlace(1) backtrack(i + 1, current_subset, nums, res) def find_all_subsets(nums: List[Int]) -> List[List[Int]]: res = [] backtrack(0, [], nums, res) res def backtrack( i: Int, current_subset: List[Int], nums: List[Int], res: List[List[Int]] ) -> None: // Base case: if all elements have been considered, add the // current subset to the output. if i == len(nums): res.append(current_subset[:]) return // Include the current element and recursively explore all paths // that branch from this subset. current_subset.append(nums(i)) backtrack(i + 1, current_subset, nums, res) // Exclude the current element and recursively explore all paths // that branch from this subset. current_subset.pop() backtrack(i + 1, current_subset, nums, res) import scala.collection.mutable.ListBuffer as List
  10. @main def main: Unit: assert(find_all_subsets(List()) == List(List())) assert( find_all_subsets(List(1, 2,

    3)).toSet.map(_.toSet) == Set( Set(1, 2, 3), Set(1, 2), Set(1, 3), Set(2, 3), Set(1), Set(2), Set(3), Set.empty) ) assert( find_all_subsets(List(1, 2, 3)).sorted.mkString("\n") == """|ListBuffer() |ListBuffer(1) |ListBuffer(1, 2) |ListBuffer(1, 2, 3) |ListBuffer(1, 3) |ListBuffer(2) |ListBuffer(2, 3) |ListBuffer(3)""โ€ .stripMargin ) Letโ€™s test a bit the Scala version of the program
  11. On the Rosetta Code site we are reminded that the

    set of all subsets of a set is called its powerset, so in what follows, when we write a function that computes subsets, we are going to call it powerset. Also, as seen in the definition of powerset on the site (see next slide), the type of items in a set need not be a digit or an integer, so the powerset functions that we are going to write are going to be generic.
  12. In Introduction to Functional Programming (IFP), there is a Combinatorial

    functions section which defines ๐‘ ๐‘ข๐‘๐‘ , a powerset function which is recursive, and in which a set is implemented as an immutable list. Richard Bird http://www.cs.ox.ac.uk/people/richard.bird/ Philip Wadler https://github.com/wadler
  13. 5.6 Combinatorial functions Many interesting problems are combinatorial in nature,

    that is, they involve selecting or permuting elements of a list in some desired manner. This section describes several combinatorial functions of widespread utility. Initial segments. The function ๐‘–๐‘›๐‘–๐‘ก๐‘  returns the list of all initial segments of a list, in order of increasing length. For example: โ€ฆ Subsequences. The function ๐‘ ๐‘ข๐‘๐‘  returns a list of all subsequences of a list. For example: ? ๐‘ ๐‘ข๐‘๐‘  โ€๐‘’๐‘Ÿ๐‘Žโ€ [โ€œโ€, โ€โ€œ๐‘Žโ€, โ€โ€œ๐‘Ÿโ€, โ€œ๐‘Ÿ๐‘Žโ€, โ€œ๐‘’โ€, โ€โ€œ๐‘’๐‘Žโ€, โ€œ๐‘’๐‘Ÿโ€, โ€โ€œ๐‘’๐‘Ÿ๐‘Žโ€] The ordering of this list is a consequence of the particular definition of ๐‘ ๐‘ข๐‘๐‘  given below. If ๐‘ฅ๐‘  has length ๐‘›, then ๐‘ ๐‘ข๐‘๐‘  ๐‘ฅ๐‘  has length 2๐‘›. This can be seen by noting that each of the ๐‘› elements in ๐‘ฅ๐‘  might be either present or absent in a subsequence, so there are 2 ร— โ‹ฏ ร— 2 (๐‘› times) possibilities. A recursive definition of ๐‘ ๐‘ข๐‘๐‘  is: ๐‘ ๐‘ข๐‘๐‘  [ ] = [[ ]] ๐‘ ๐‘ข๐‘๐‘  ๐‘ฅ: ๐‘ฅ๐‘  = ๐‘ ๐‘ข๐‘๐‘  ๐‘ฅ๐‘  ++ ๐‘š๐‘Ž๐‘ ๐‘ฅ: ๐‘ ๐‘ข๐‘๐‘  ๐‘ฅ๐‘  That is, the empty list has one subsequence, namely itself. A non-empty list ๐‘ฅ: ๐‘ฅ๐‘  has as subsequences all the subsequences of ๐‘ฅ๐‘ , together with those subsequences formed by following ๐‘ฅ with each possible subsequence of ๐‘ฅ๐‘ . Notice that this definition differs from that of inits in only one place, but has a considerably different meaning. Interleave. โ€ฆ โ€ฆ โ€“- Appends two lists (++) :: [a] -> [a] -> [a] In Haskell a string is a list of characters.
  14. Letโ€™s code the function in Haskell and Scala and try

    it out. def powerset[A](as: List[A]): List[List[A]] = as match case Nil => List(Nil) case a::rest => powerset(rest) ++ powerset(rest).map(a::_) powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = powerset as ++ map (a:) powerset as haskell> powerset [4,5,6] [[],[6],[5],[5,6],[4],[4,6],[4,5],[4,5,6]] scala> powerset(List(4,5,6)) List(List(), List(6), List(5), List(5, 6), List(4), List(4, 6), List(4, 5), List(4, 5, 6)) def powerset[A](as: List[A]): List[List[A]] = as match case Nil => List(Nil) case a::rest => val subsets = powerset(rest) subsets ++ subsets.map(a::_) powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = subsets ++ map (a:) subsets where subsets = powerset as Same as above but making a single recursive call.
  15. Exercise 2.32: We can represent a set as a list

    of distinct elements, and we can represent the set of all subsets of the set as a list of lists. For example, if the set is (1 2 3), then the set of all subsets is (() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3)). Complete the following definition of a procedure that generates the set of subsets of a set and give a clear explanation of why it works: (define (subsets s) (if (null? s) (list `()) (let ((rest (subsets (cdr s)))) (append rest (map <??> rest))))) (define (subsets s) (if (null? s) (list `()) (let ((rest (subsets (cdr s)))) (append rest (map (lambda (ss) (cons (car s) ss)) rest))))) By the way, if we take our powerset function and do some minor renaming and switch from using where to using the equivalent let, we see that the function is the same as the following subsets function that is the solution of an exercise in Structure and interpretation of Computer Programs (SICP). powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = subsets ++ map (a:) subsets where subsets = powerset as subsets :: [a] -> [[a]] subsets [] = [[]] subsets (a:as) = let rest = subsets as in rest ++ map (a:) rest SICP Scheme Scheme Haskell
  16. While this first definition of the powerset function is nice,

    it is possible to come up with simpler definitions by exploiting the link that exists between non-determinism and the list monad. One of the places where the link is explained is in a 2001 paper called Functional Quantum Programming. The paper takes a function that computes all the segments of a list, and shows how to simplify the function by using the expressive power of the list monad. Letโ€™s go over the relevant section of the paper (in the next three slides). After that, we are going to look at a few Haskell and Scala implementations of the segments function. We are then going to come up with simpler definitions of the powerset function by using an approach based on the one seen in the paper. ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ++ ๐‘š๐‘Ž๐‘ ๐‘Ž: ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก๐‘  ๐‘Ž๐‘  ++ ๐‘š๐‘Ž๐‘ ๐‘Ž: ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก๐‘  ๐‘คโ„Ž๐‘’๐‘Ÿ๐‘’ ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘ 
  17. Functional Quantum Programming Shin-Cheng Mu Richard Bird DRAFT Abstract It

    has been shown that non-determinism, both angelic and demonic, can be encoded in a functional language in different representation of sets. โ€ฆ โ€ฆ 1 Introduction โ€ฆ 2 Non-determinism and the list monad Before going into quantum computing, we will first review some known facts about lists, list monads, and their use for modelling non-determinism. As an example, consider the problem of computing an arbitrary (consecutive) segment of a given list. An elegant way to formulate the problem is to use two relations, or two non-deterministic functions ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ and ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ . The relation ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ โˆท ๐‘Ž โ†’ [๐‘Ž] non-deterministically returns an arbitrary prefix of the given list, while ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ โˆท ๐‘Ž โ†’ [๐‘Ž] returns an arbitrary suffix. The problem is thus formulated as ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก = ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ. Working in a functional language, however, we do not have non-deterministic functions, as they violate the basic requirement for being a function โ€“ to yield the same value for the same input. Nevertheless, non-deterministic functions can be simulated by functions returning the set of all possible solutions [7]. A function ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  returning the set of all prefixes of the input list can be defined by: ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) where ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› returns a singleton set, ๐‘ข๐‘›๐‘–๐‘œ๐‘› performs set union, and ๐‘š๐‘Ž๐‘ is overloaded for sets.
  18. One possible representation of sets in Haskell is via lists,

    i.e, ๐ญ๐ฒ๐ฉ๐ž ๐‘บ๐’†๐’• ๐‘Ž = ๐‘Ž In this case, the two set constructing functions above can simply be defined by ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘ฅ = [๐‘ฅ] and union = ++ . Similarly, ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  can be defined by: ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘Ž: ๐‘ฅ `๐‘ข๐‘›๐‘–๐‘œ๐‘›` (๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) The function ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘ , which returns the set of all consecutive segments of a given list, can thus be composed as below with the help of primitive list operators ๐‘š๐‘Ž๐‘ and ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก. ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜ ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  The use of a ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก after a ๐‘š๐‘Ž๐‘ to compose two list-returning functions is a general pattern captured by the list monad. This is how the โ‰ซ= operator for the list monad is defined. ๐‘ฅ โ‰ซ= ๐‘“ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก (๐‘š๐‘Ž๐‘ ๐‘“ ๐‘ฅ) The instance of โ‰ซ= above has type ๐‘Ž โ†’ ๐‘Ž โ†’ ๐‘ โ†’ [๐‘]. We can think of it as an apply function for lists, applying a list-returning function to a list of values. โ€“- Appends two lists (++) :: [a] -> [a] -> [a] Scalaโ€™s equivalent of โ‰ซ= is flatMap. โ€“- Concatenate a list of lists concat :: [[a]] -> [a] -- Sequentially compose two actions, passing any value -- produced by the first as an argument to the second. (>>=) :: Monad m => m a -> (a -> m b) -> m b If for example ๐‘ฅ = ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ and ๐‘“ = ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  then (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ) โ‰ซ= ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก (๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ )
  19. Furthermore, Haskell programmers are equipped with a convenient do-notation for

    monads. The functions ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  and ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  can be re-written in do-notation as below. ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` (do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› (๐‘Ž: ๐‘ฆ)) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ฅ = do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘ง โ† ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘ง The do-notation gives programmers a feeling that they are dealing with a single value rather than a set of them. In the definition for ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘ , for instance, identifiers ๐‘ฆ and ๐‘ง have type ๐‘Ž . It looks like we take one arbitrary prefix of ๐‘ฅ , calling it ๐‘ฆ, take one arbitrary suffix of ๐‘ฆ, calling it ๐‘ง , and return it. The fact that there is a whole set of values to be processed is taken care of by the underlying โ‰ซ= operator. Simulating non-determinism with sets represented by lists is similar to angelic non-determinism in logic programming. All the answers are enumerated in a list and are ready to be taken one by one. In fact, the list monad has close relationship with backtracking and has been used to model the semantics of logic programming [10]. ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜ ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘ 
  20. The next three slides show Haskell and Scala code for

    ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘ , ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  and ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘ , together with an example of using each function.
  21. type Set a = [a] singleton :: a -> Set

    a singleton a = [a] union :: Set [a] -> Set [a] -> Set [a] union = (++) prefixes :: [a] -> Set [a] prefixes [] = singleton [] prefixes (a:x) = (singleton []) `union` (map (a:) (prefixes x)) ghci> prefixes [1,2,3,4] [[],[1],[1,2],[1,2,3],[1,2,3,4]] type Set[A] = List[A] def singleton[A](a:A): Set[A] = List(a) // Commented this out because there is no need to provide it, as it is a // Scala built-in function. Note however that it is deprecated in favour // of built-in function concat. // def union[A](x: Set[A], y: Set[A]): Set[A] = x ++ y def prefixes[A](as: List[A]): Set[List[A]] = as match case Nil => singleton(Nil) case a::x => singleton(Nil) union prefixes(x).map(a::_) scala> prefixes(List(1,2,3,4)) val res0: List[List[Int]] = List(List(), List(1), List(1, 2), List(1, 2, 3), List(1, 2, 3, 4)) ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ)
  22. suffixes :: [a] -> Set [a] suffixes [] = singleton

    [] suffixes (a:x) = (a:x) `union` (suffixes x) suffixes :: [a] -> Set [a] suffixes [] = singleton [] suffixes (a:x) = (singleton (a:x)) `union` (suffixes x) Couldn't match expected type: Set [a] with actual type: [a] ghci> suffixes [1,2,3,4] [[1,2,3,4],[2,3,4],[3,4],[4],[]] def suffixes[A](as: List[A]): Set[List[A]] = as match case Nil => singleton(Nil) case a::x => singleton(a::x) union suffixes(x) def suffixes[A](as: List[A]): Set[List[A]] = as match case Nil => singleton(Nil) case a::x => (a::x) union suffixes(x) scala> suffixes(List(1,2,3,4)) val res1: List[List[Int]] = List(List(1, 2, 3, 4), List(2, 3, 4), List(3, 4), List(4), List()) Found: List[A | List[A]] Required: List[List[A]] Fix compilation error Fix compilation error ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘Ž: ๐‘ฅ `๐‘ข๐‘›๐‘–๐‘œ๐‘›` (๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) The Functional Quantum Programming paper was a draft after all
  23. ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜

    ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  segments :: [a] -> Set [a] segments = concat . map suffixes . prefixes def segments[A](as: List[A]): Set[List[A]] = concat(prefixes(as).map(suffixes(_))) ghci> segments [1,2,3,4] [[],[1],[],[1,2],[2],[],[1,2,3],[2,3],[3],[],[1,2,3,4],[2,3,4],[3,4],[4],[]] scala> segments(List(1,2,3,4)) val res0: List[List[Int]] = List(List(), List(1), List(), List(1,2), List(2), List(), List(1,2,3), List(2,3), List(3), List(), List(1,2,3,4), List(2,3,4), List(3,4), List(4), List()) // NOTE โ€“ the concat provided by Scala appends a list to // another, and is the replacement for deprecated union def concat[A](as: List[List[A]]): List[A] = as.flatten Duplicate empty lists! Well, the Functional Quantum Programming paper was a draft after all. See next slide for a fix.
  24. segments :: Eq a => [a] -> Set [a] segments

    = nub . concat . map suffixes . prefixes def segments[A](as: List[A]): Set[List[A]] = concat(prefixes(as).map(suffixes(_))).distinct If we want to remove the duplicate empty lists seen in the results on the previous slide we can do that by adding a call to Haskellโ€™s nub function (nub means โ€˜essenceโ€™ ) and to Scalaโ€™s distinct function. The Eq a => in the signature of the segments function is needed because the nub function needs to compare list elements for equality. import Data.List (nub) -- removes duplicate elements nub :: Eq a => [a] -> [a] Eq a => also needs to be added to the signatures of the suffixes and prefixes functions.
  25. The next two slides show how the Haskell and Scala

    code for ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  and ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  changes when we use the syntactic sugar of Haskellโ€™s do notation and Scalaโ€™s for notation. Where the Haskell code uses the ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› function, the Scala code uses the ๐‘๐‘ข๐‘Ÿ๐‘’ function. haskell> :type pure pure :: Applicative f => a -> f a haskell> :type return return :: Monad m => a -> m a scala> :type cats.Applicative[List].pure Any => List[Any] scala> :type cats.Monad[List].pure Any => List[Any] Cats
  26. ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘ 

    ๐‘Ž: ๐‘ฅ = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` (do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› (๐‘Ž: ๐‘ฆ)) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ฅ = do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘ง โ† ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘ง ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜ ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  prefixes :: [a] -> Set [a] prefixes [] = singleton [] prefixes (a:x) = (singleton []) `union` (map (a:) (prefixes x)) prefixes :: [a] -> Set [a] prefixes [] = return [] prefixes (a:x) = return [] `union` (do y <- prefixes x return (a:y)) segments :: [a] -> Set [a] segments = concat . map suffixes . prefixes segments :: [a] -> Set [a] segments x = do y <- prefixes x z <- suffixes y return z Switching to do notation
  27. ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘ 

    ๐‘Ž: ๐‘ฅ = ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` (do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› (๐‘Ž: ๐‘ฆ)) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ฅ = do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘ง โ† ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘ง ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘Ž: ๐‘ฅ = ๐‘ ๐‘–๐‘›๐‘”๐‘™๐‘’๐‘ก๐‘œ๐‘› `๐‘ข๐‘›๐‘–๐‘œ๐‘›` ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ) ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜ ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  def prefixes[A](as: List[A]): Set[List[A]] = as match case Nil => singleton(Nil) case a::x => singleton(Nil) union prefixes(x).map(a::_) def prefixes[A](as: List[A]): Set[List[A]] = as match case Nil => Nil.pure case a::x => Nil.pure union (for y <- prefixes(x) yield a::y) def segments[A](as: List[A]): Set[List[A]] = concat(prefixes(as).map(suffixes(_))) def segments[A](as: List[A]): Set[List[A]] = for y <- prefixes(x) z <- suffixes(y) yield z Switching to for notation Cats
  28. As seen in the previous two slides, using the do

    notation in ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก โˆ˜ ๐‘š๐‘Ž๐‘ ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  โˆ˜ ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  is worth considering ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ฅ = do ๐‘ฆ โ† ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ ๐‘ง โ† ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฆ ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘ง but I think that exploiting this equivalence ๐‘ฅ โ‰ซ= ๐‘“ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก ๐‘š๐‘Ž๐‘ ๐‘“ ๐‘ฅ is also worthwhile: ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  โˆท ๐‘Ž โ†’ ๐‘บ๐’†๐’• [๐‘Ž] ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘ฅ = ๐‘๐‘Ÿ๐‘’๐‘“๐‘–๐‘ฅ๐‘’๐‘  ๐‘ฅ โ‰ซ= ๐‘ ๐‘ข๐‘“๐‘“๐‘–๐‘ฅ๐‘’๐‘ 
  29. Having just seen how the definition of ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  can be

    simplified using this equivalence ๐‘ฅ โ‰ซ= ๐‘“ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก ๐‘š๐‘Ž๐‘ ๐‘“ ๐‘ฅ Letโ€™s now go back to our ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก function and see if we are able to do something similar ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ++ ๐‘š๐‘Ž๐‘ ๐‘Ž: ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  How about using this equivalence? ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘ฅ๐‘  โ‰ซ= ๐œ†๐‘ฅ๐‘ . [๐‘ฅ๐‘ , ๐‘ฅ: ๐‘ฅ๐‘ ] = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ++ ๐‘š๐‘Ž๐‘ ๐‘Ž: (๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘ ) Here is how using the equivalence improves the definition of ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  โ‰ซ= ๐œ†๐‘Ž๐‘ . [๐‘Ž๐‘ , ๐‘Ž: ๐‘Ž๐‘ ] We can improve this further by extracting an ๐‘Ž๐‘‘๐‘‘ function: ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  โ‰ซ= ๐‘Ž๐‘‘๐‘‘ ๐‘Ž ๐‘คโ„Ž๐‘’๐‘Ÿ๐‘’ ๐‘Ž๐‘‘๐‘‘ ๐‘Ž ๐‘Ž๐‘  = [๐‘Ž๐‘ , ๐‘Ž: ๐‘Ž๐‘ ] I find both versions an improvement on the original.
  30. Now letโ€™s take our first improved version of ๐‘ ๐‘’๐‘”๐‘š๐‘’๐‘›๐‘ก๐‘  ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก

    โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  โ‰ซ= ๐œ†๐‘Ž๐‘ . [๐‘Ž๐‘ , ๐‘Ž: ๐‘Ž๐‘ ] and see how it looks if we replace โ‰ซ= with the syntactic sugar of do notation: ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = do ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก โ† ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ๐‘Ÿ๐‘’๐‘ ๐‘ข๐‘™๐‘ก โ† [๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก, ๐‘Ž: ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก] ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘Ÿ๐‘’๐‘ ๐‘ข๐‘™๐‘ก We can arguably improve this by extracting an ๐‘”๐‘Ÿ๐‘œ๐‘ค function, which is just like the ๐‘Ž๐‘‘๐‘‘ function that we introduced earlier, but with its parameters swapped round: ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก โˆท ๐‘Ž โ†’ [ ๐‘Ž ] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก = [[ ]] ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž: ๐‘Ž๐‘  = do ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก โ† ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก ๐‘Ž๐‘  ๐‘Ÿ๐‘’๐‘ ๐‘ข๐‘™๐‘ก โ† ๐‘”๐‘Ÿ๐‘œ๐‘ค ๐‘ ๐‘ข๐‘๐‘ ๐‘’๐‘ก ๐‘Ž ๐‘Ÿ๐‘’๐‘ก๐‘ข๐‘Ÿ๐‘› ๐‘Ÿ๐‘’๐‘ ๐‘ข๐‘™๐‘ก where ๐‘”๐‘Ÿ๐‘œ๐‘ค ๐‘Žs ๐‘Ž = [๐‘Ž๐‘ , ๐‘Ž: ๐‘Ž๐‘ ]
  31. def powerset[A](as: List[A]): List[List[A]] = as match case Nil =>

    List(Nil) case a::rest => val subsets = powerset(rest) subsets ++ subsets.map(a::_) powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = subsets ++ map (a:) subsets where subsets = powerset as powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = powerset as >>= add a powerset :: [a] -> [[a]] powerset [] = [[]] powerset (a:as) = do subset <- powerset as result <- grow subset a return result def powerset[A](as: List[A]): List[List[A]] = as match case Nil => List(Nil) case a::rest => for subset <- powerset(rest) result <- grow(subset,a) yield result grow :: [a] -> a -> [[a]] grow subset element = [subset, element:subset] add :: a -> [a] -> [[a]] add = flip grow def grow[A](subset: List[A], element: A) = List(subset, element::subset) def add[A](element: A)(subset: List[A]): List[List[A]] = grow(subset,element) def powerset[A](as: List[A]): List[List[A]] = as match case Nil => List(Nil) case a::rest => powerset(rest).flatMap(add(a)) As a recap, here is the code for the three different versions of ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก that we have covered so far, with some very minor renaming and changes. Of course we would also have the option, if preferable, to inline subordinate functions add and grow.
  32. (define (concat xss) (fold-right append `() xss)) (define (flatmap proc

    seq) (concat (map proc seq))) > (concat (list (list 1 2) (list 3 4)) ) (1 2 3 4) > (flatmap (lambda (x) (list x x)) (list 1 2 3)) (1 1 2 2 3 3) By the way, equivalence ๐‘ฅ โ‰ซ= ๐‘“ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก ๐‘š๐‘Ž๐‘ ๐‘“ ๐‘ฅ also makes an appearance in SICP SICP The combination of mapping and accumulating with append is so common in this sort of program that we will isolate it as a separate procedure: (define (flatmap proc seq) (accumulate append `() (map proc seq))) It is the same equivalence because the accumulate function in SICP is a fold, and folding the append function over a list of lists is what the concat function does. Letโ€™s define concat and flatmap using MIT/GNU Scheme and try it out โ€“- Concatenate a list of lists concat :: [[a]] -> [a] concat = foldr (++) [] ๐‘ ๐‘’๐‘ž โ‰ซ= ๐‘๐‘Ÿ๐‘œ๐‘ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก ๐‘š๐‘Ž๐‘ ๐‘๐‘Ÿ๐‘œ๐‘ ๐‘ ๐‘’๐‘ž ๐‘ฅ โ‰ซ= ๐‘“ = ๐‘๐‘œ๐‘›๐‘๐‘Ž๐‘ก ๐‘š๐‘Ž๐‘ ๐‘“ ๐‘ฅ Scheme Haskell Scheme
  33. The three recursive definitions of the ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก function that we

    have considered so far are good, but can we can come up with an even simpler definition, one that doesnโ€™t even use recursion? The way we are going to do that is by further exploiting the link between the list monad and non-determinism / backtracking. We are going to have a go at writing a definition of the ๐‘๐‘œ๐‘ค๐‘’๐‘Ÿ๐‘ ๐‘’๐‘ก function using just the do notation (syntactic sugar for โ‰ซ=). As a first step, armed purely with the do notation, letโ€™s do the following: 1. write function powerset0, which returns the powerset of [ ] 2. rename the function powerset1 and get it to return the powerset of [1] 3. rename the function powerset2 and get it to return the powerset of [1,2] 4. rename the function powerset3 and get it to return the powerset of [1,2,3] 5. rename the function powerset3b and get it to return the powerset of [x0,x1,x2] where x0 is 1, x1 is 2, and x2 is 3
  34. -- powerset of [] powerset0 = do let ss0 =

    [] [ss0] > powerset0 [[]] Computes the powerset of an empty set by returning a set containing the only subset of the empty set, i.e the empty set itself. Step 1 - write function powerset0, which returns the powerset of [ ] ss0 stands for a subset with 0 elements
  35. -- powerset of [] powerset0 = do let ss0 =

    [] [ss0] > powerset0 [[]] Computes the powerset of an empty set by returning a set containing the only subset of the empty set, i.e the empty set itself. -- powerset of [1] powerset1 = do let ss0 = [] [ss0, 1:ss0] > powerset1 [[],[1]] Computes the powerset of a singleton set by returning a set containing both the single subset of the empty set and the result of adding the setโ€™s item to the subset. Step 2 - rename the function powerset1 and get it to return the powerset of [1]
  36. Computes the powerset of a singleton set by returning a

    set containing both the single subset of the empty set and the result of adding the setโ€™s item to the subset. Computes the powerset of a two-item set by computing the subsets of a set containing one item and returning a set containing both the subsets, and the result of adding one more item to each of the subsets. -- powerset of [1,2] powerset2 = do let ss0 = [] ss1 <- [ss0, 1:ss0] [ss1, 2:ss1] > powerset2 [[],[2],[1],[2,1]] -- powerset of [1] powerset1 = do let ss0 = [] [ss0, 1:ss0] > powerset1 [[],[1]] Step 3 - rename the function powerset2 and get it to return the powerset of [1,2]
  37. Computes the powerset of a two-item set by computing the

    subsets of a set containing one item and returning a set containing both the subsets, and the result of adding one more item to each of the subsets. Computes the powerset of a three-item set by computing the subsets of a set containing two items and returning a set containing both the subsets, and the result of adding one more item to each of the subsets. -- powerset of [1,2,3] powerset3 = do let ss0 = [] ss1 <- [ss0, 1:ss0] ss2 <- [ss1, 2:ss1] [ss2, 3:ss2] > powerset3 [[],[3],[2],[3,2],[1],[3,1],[2,1],[3,2,1]] -- powerset of [1,2] powerset2 = do let ss0 = [] ss1 <- [ss0, 1:ss0] [ss1, 2:ss1] > powerset2 [[],[2],[1],[2,1]] Step 4 - rename the function powerset3 and get it to return the powerset of [1,2,3]
  38. Name the items of the set x0, x1 and x2

    -- powerset of [x0,x1,x2] powerset3b = do let ss0 = [] [x0,x1,x2] = [1,2,3] ss1 <- [ss0, x0:ss0] ss2 <- [ss1, x1:ss1] [ss2, x2:ss2] -- powerset of [1,2,3] powerset3 = do let ss0 = [] ss1 <- [ss0, 1:ss0] ss2 <- [ss1, 2:ss1] [ss2, 3:ss2] Step 5 - rename the function powerset3b and get it to return the powerset of [x0,x1,x2] where x0 is 1, x1 is 2, and x2 is 3 grow :: [a] -> a -> [[a]] grow subset element = [subset, element:subset] Remember the grow function? On the next slide we replace the following with invocations of grow: [ss0, x0:ss0] ,[ss1, x1:ss1] ,[ss2, x2:ss2]
  39. Extract into a function called grow the logic which given

    one of the subsets computed so far, and the next item, returns a list containing both the subset and the result of adding the item to the subset -- powerset of [x0,x1,x2] powerset3b = do let ss0 = [] [x0,x1,x2] = [1,2,3] ss1 <- [ss0, x0:ss0] ss2 <- [ss1, x1:ss1] [ss2, x2:ss2] -- powerset of [x0,x1,x2] powerset3c = do let grow xs x = [xs, x:xs] ss0 = [] [x0,x1,x2] = [1,2,3] ss1 <- grow ss0 x0 ss2 <- grow ss1 x1 grow ss2 x2 On the next slide we simply bump up the index numbers in ss0, ss1, ss2, x0, x1, x2.
  40. -- powerset of [x0,x1,x2] powerset3c = do let grow xs

    x = [xs, x:xs] ss0 = [] [x0,x1,x2] = [1,2,3] ss1 <- grow ss0 x0 ss2 <- grow ss1 x1 grow ss2 x2 -- powerset of [x1,x2,x3] powerset3d = do let grow xs x = [xs, x:xs] ss1 = [] [x1,x2,x3] = [1,2,3] ss2 <- grow ss1 x1 ss3 <- grow ss2 x2 grow ss3 x3 Bump up index numers On the next slide we just rename ss1, ss2, ss3 and the grow function.
  41. -- powerset of [x1,x2,x3] powerset3d = do let grow xs

    x = [xs, x:xs] ss1 = [] [x1,x2,x3] = [1,2,3] ss2 <- grow ss1 x1 ss3 <- grow ss2 x2 grow ss3 x3 -- powerset of [x1,x2,x3] powerset3e = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,x3] = [1,2,3] a2 <- f a1 x1 a3 <- f a2 x2 f a2 x2 Renaming: grow ร  f ss<n> ร  a<n>
  42. While this functionโ€™s logic does show how the powerset of

    a set of items can be computed purely using do notation, the logic is not generic with respect to the size of the set: every time the size changes, we have to change the logic to reflect that. How can we make the logic generic? Letโ€™s start by renaming the function powersetn and making it generic on paper: here is how we can express the fact that the logic depends on the size of the set -- powerset of [x1,x2,x3] powerset3e = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,x3] = [1,2,3] a2 <- f a1 x1 a3 <- f a2 x2 f a2 x2 -- powerset of [x1,x2,โ€ฆ,xn-1 ,xn ] powersetn = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,โ€ฆ,xn]=[1,2,โ€ฆ,n-1,n] a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ an <- f an-1 xn-1 f an xn -- powerset of [x1,x2,x3] powerset3e = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,x3] = [1,2,3] a2 <- f a1 x1 a3 <- f a2 x2 f a2 x2 picture comparison
  43. -- powerset of [x1,x2,โ€ฆ,xm] powersetm = do let f xs

    x = [xs, x:xs] a1 = [] [x1,x2,โ€ฆ,xm]=[1,2,โ€ฆ,m] a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ f am xm -- powerset of [x1,x2,โ€ฆ,xn-1,xn] powersetn = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,โ€ฆ,xn]=[1,2,โ€ฆ,n-1,n] a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ an <- f an-1 xn-1 f an xn picture comparison In preparation for the next step, letโ€™s rename n to m and drop the functionโ€™s penultimate line, which is superfluous in that its presence can be inferred from the rest of the logic.
  44. The reason why we have gone to the trouble of

    working out the definition of powersetm is that the logic of this function is a special case of the logic of a function that is called foldM and is provided by both Haskell and Scalaโ€™s Cats library. -- powerset of [x1,x2,โ€ฆ,xm] powersetm = do let f xs x = [xs, x:xs] a1 = [] [x1,x2,โ€ฆ,xm ]=[1,2,โ€ฆ,m] a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ f am xm foldM f a1 [x1, x2, โ€ฆ, xm] == do a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ f am xm extract from the Haskell documentation for foldM the function we have written See the next two slides for the Haskell and Scala Cats documentation for foldM.
  45. powersetm = do let f xs x = [xs, x:xs]

    a1 = [] [x1,x2,โ€ฆ,xm]=[1,2,โ€ฆ,m] a2 <- f a1 x1 a3 <- f a2 x2 โ€ฆ f am xm powersetm = let f xs x = [xs, x:xs] in foldM f [] [1,2,โ€ฆ,m] Function foldM does a monadic fold, a monadic version of an ordinary fold. While our powersetm function operates on the list monad, foldM operates on any monad. When we get foldM to operate on the list monad, its behaviour is the same as that captured by the logic in our powersetm function. For that reason, we are able to reimplement powersetm as follows: def powersetm = val f = (xs:List[Int],x:Int) => List(xs, x::xs) val a1 = Nil val List(x1, x2, x3) = List(1,2,3) for a2 <- f(a1,x1) a3 <- f(a2,x2) subset <- f(a3,x3) yield subset def powersetm = val f = (xs:List[Int],x:Int) => List(xs, x::xs) List(1,2,โ€ฆ,m).foldM(Nil)(f)
  46. import cats.implicits.* def powerset[A](as: List[A]): List[List[A]] = as.foldM(Nil)(grow) def grow[A](as:

    List[A], a: A) = List(as, a::as) import Control.Monad (foldM) powerset :: [a] -> [[a]] powerset = foldM grow [] grow :: [a] -> a -> [[a]] grow as a = [as, a:as] powersetm = let f xs x = [xs, x:xs] in foldM f [] [1,2,โ€ฆ,m] def powersetm = val f = (xs:List[Int],x:Int) => List(xs, x::xs) List(1,2,โ€ฆ,m).foldM(Nil)(f) From the two back-of-an-envelope functions (since they refer to m) on the left, we can finally derive executable Haskell and Scala powerset functions.
  47. > powerset [] [[]] > powerset [4] [[],[4]] > powerset

    [4,5] [[],[5],[4],[5,4]] > powerset [4,5,6] [[],[6],[5],[6,5],[4],[6,4],[5,4],[6,5,4]] > powerset [4,5,6,7] [[],[7],[6],[7,6],[5],[7,5],[6,5],[7,6,5],[4],[7,4],[6,4],[7,6,4],[5,4],[7,5,4],[6,5,4],[7,6,5,4]] Letโ€™s take the Haskell version of the foldM-based powerset function for a spin.
  48. > powerset(Nil) val res0: List[List[Nothing]] = List(List()) > powerset(List(4)) val

    res1: List[List[Int]] = List(List(), List(4)) > powerset(List(4,5)) val res2: List[List[Int]] = List(List(), List(5), List(4), List(5, 4)) > powerset(List(4,5,6)) val res3: List[List[Int]] = List(List(), List(6), List(5), List(6, 5), List(4), List(6, 4), List(5, 4), List(6, 5, 4)) > powerset(List(4,5,6,7)) val res4: List[List[Int]] = List(List(), List(7), List(6), List(7, 6), List(5), List(7, 5), List(6, 5), List(7, 6, 5), List(4), List(7, 4), List(6, 4), List(7, 6, 4), List(5, 4), List(7, 5, 4), List(6, 5, 4), List(7, 6, 5, 4)) And now the Scala version.
  49. The next slide shows the Haskell and Scala code for

    the final version of the powerset function, and compares it with the imperative Python code of the find_all_subsets function that we saw at the beginning of this deck. The slide after that is the same, except that it inlines the grow function.
  50. def find_all_subsets(nums: List[Int]) -> List[List[Int]]: res = [] backtrack(0, [],

    nums, res) res def backtrack( i: Int, current_subset: List[Int], nums: List[Int], res: List[List[Int]] ) -> None: // Base case: if all elements have been considered, add the // current subset to the output. if i == len(nums): res.append(current_subset[:]) return // Include the current element and recursively explore all paths // that branch from this subset. current_subset.append(nums(i)) backtrack(i + 1, current_subset, nums, res) // Exclude the current element and recursively explore all paths // that branch from this subset. current_subset.pop() backtrack(i + 1, current_subset, nums, res) import cats.implicits.* def powerset[A](as: List[A]): List[List[A]] = as.foldM(Nil)(grow) def grow[A](as: List[A], a: A) = List(as, a::as) import Control.Monad (foldM) powerset :: [a] -> [[a]] powerset = foldM grow [] grow :: [a] -> a -> [[a]] grow as a = [as, a:as] Cats
  51. def find_all_subsets(nums: List[Int]) -> List[List[Int]]: res = [] backtrack(0, [],

    nums, res) res def backtrack( i: Int, current_subset: List[Int], nums: List[Int], res: List[List[Int]] ) -> None: // Base case: if all elements have been considered, add the // current subset to the output. if i == len(nums): res.append(current_subset[:]) return // Include the current element and recursively explore all paths // that branch from this subset. current_subset.append(nums(i)) backtrack(i + 1, current_subset, nums, res) // Exclude the current element and recursively explore all paths // that branch from this subset. current_subset.pop() backtrack(i + 1, current_subset, nums, res) import cats.implicits.* def powerset[A](as: List[A]): List[List[A]] = as.foldM(Nil)((as, a) => List(as, a::as)) import Control.Monad (foldM) powerset :: [a] -> [[a]] powerset = foldM (\as a -> [as, a:as]) [] Cats
  52. If you want to know more about the foldM function,

    which is very interesting because its behaviour depends on the particular Monad that it operates on, see the following deck, or the deck series on the next slide. https://fpilluminated.org/