############################################################
# review list comprehensions
############################################################


# We will begin lecture tomorrow by recapping the last few examples and
# exercises in yesterday's lecture code that we didn't get to (lines
# 278+). Try to work those out yourself if you haven't already.

# The goal is to get some confidence in interpreting list comprehensions
# and in writing some yourself. It's okay if you don't get them all, and
# we'll review them together. It's more important to understand the next
# section for tomorrow's lecture.


############################################################
# generating all combinations
############################################################


# In lecture tomorrow, we'll be considering the task of generating all
# possible combinations out of a collection of items. For example, if we
# have a set of integers S = {1, 2, 3, 4}, there are four possible
# combinations of just one item:

# {1}, {2}, {3}, {4}

# There are six possible combinations of two items:

# {1, 2}, {1, 3}, {1, 4}, {2, 3}, {2, 4}, {3, 5}

# And there are four and one combinations of three and four items,
# respectively:

# {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4}
# {1, 2, 3, 4}

# Finally, by convention, the empty set ({} in mathematical notation,
# set() in Python notation) is also a valid combination of no items.

# In total, there are 16 possible combinations. Together, they form
# what's called the **power set** of the original set S = {1, 2, 3, 4}.
# Mathematically, the power set is written as 2^S. This notation
# expresses the idea that there are two possibility for each item: to
# either not be part of a combination, or to be part of one. (Hamlet was
# just looking for a friend group.)

# In class, we'll formalize this reasoning in code, and thinking
# recursively will be an essential part of that. Thus, as a warm-up,
# let's consider an easier version of the problem that we can approach
# recursively. Rather than generating all possible combination, let's
# just generate one, and let's do it stochastically. This essentially
# has the effect of sampling from the power set.

# For explanatory purposes, it helps to consider a fixed ordering of the
# items in our set. Let's turn S into a list T = [1, 2, 3, 4].

# A very common pattern in addressing a sequence of items recursively is
# to separate it into a first element and a sequence of the rest. Thus,
# we can:
#  a) focus on handling that first element,
#  b) rely on recursion to give us an answer on the rest, and
#  c) combine our reasoning from a) and b) to get a final answer.

# In the case of our problem, we separate the input list of four numbers
# into the first number 1 and the remaining sequence [2, 3, 4]. Thus:
#  a) We have a choice whether to include 1 (random choice).
#  b) We get any subset from [2, 3, 4] (recursive computation).
#  c) We either don't add 1 to that subset, or we do add 1, and
#     return the result as our final answer.

# The code below expresses this recursive logic. However, it's missing
# its base case. When the remaining sequence becomes empty, it is no
# longer valid to separate it into a first and rest. Fill in what needs
# to be returned in this base case. Then, you will be able to run the
# test function below, which samples a few possible subsets of T.


import random


def make_subset(T):
    # base case
    if len(T) == 0:
        return ...  # TODO

    # recursive case
    first, rest = T[0], T[1:]
    if random.random() < 0.5:
        return make_subset(rest)
    else:
        return {first} | make_subset(rest)


def test_make_subset():
    T = list({1, 2, 3, 4})
    for _ in range(10):
        print(make_subset(T))


test_make_subset()
