# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.15.2
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Practica Functioneel Programmeren
# Dit bestand bevat alle practica voor de module Functioneel Programmeren van het vak Advanced Technical Programming. De module beslaat vier colleges, waarvan de eerste drie practicumopdrachten geassocieerd hebben. Dit Jupyter Notebook bevat alle practica; probeer deze na iedere les te maken en in te leveren voor feedback.  Natuurlijk kun je ook vast vooruit kijken.
#
# Een Jupyter Notebook bestaat uit cells met code of toelichting. Sommige code-cells hoeven enkel uitgevoerd te worden om functies beschikbaar te maken, anderen moet je zelf in vullen (opdrachten staan duidelijk met kopjes gemarkeerd, je moet dan zelf de delen met `TODO` invullen). Je kunt de code per cel uitvoeren door de cel te selecteren en "run cell" te kiezen.
# De cel direct hieronder laadt de nodige modules in en definiëert enkele typevariabelen die we later nodig hebben. Als je code niet naar verwachting werkt kan het zijn dat je deze cell moet runnen, of een van de andere cels vóór de cel met error. Let erop dat als je de Jupyter-server afsluit en later verder gaat moet je deze cells opnieuw runnen.

# +
from functools import reduce
from typing import List, Callable, TypeVar, Tuple, Union, Generator
from itertools import takewhile
import re

A = TypeVar('A') 
B = TypeVar('B') 
C = TypeVar('C')


# -

# ### 3.1 — Functies

# #### Opdracht 3.1.1 — To function or not to function [PORTFOLIO]
# Hieronder staat een aantal functies gedefiniëerd. Maar let op, want het zijn niet allemaal daadwerkelijk functies zoals we die bij functioneel willen zien. Welke subroutines zijn functies, en welke niet? Waarom? Maak onderscheid tussen subroutines die alleen intern state gebruiken (en dus verder referentiëel transparant zijn) en functies die daadwerkelijk globale state aanpassen of IO gebruiken.
#
# NB: houdt er rekening mee dat puurheid erfelijk is. Een functie die een niet-pure functie gebruikt is zelf ook niet puur. Voor deze opdracht mag je ervan uitgaan dat alle built-in functies die geen IO doen of side-effects hebben puur zijn (dat is niet het geval, maar het is voor elk van deze functies mogelijk een pure functie te schrijven).

# +
# Geen pure functie. Er is hier sprake van een interne state, de enumerate actie.

def wave(string):
    output = ""
    for index, letter in enumerate(string):
        if index % 2 == 0:
            output += letter.upper()
        else:
            output += letter.lower()
    return output

# Geen pure functie. Er is hier sprake van een interne state, de while loop.

def wait_for_password():
    password = input("Say the magic word! ")
    while password != "secret":
        password = input("Say the magic word! ")
        
# Geen pure functie. De globale state wordt hier aangepast.
    
def set_width(new_width):
    global width 
    width = new_width
    
# Wel een pure functie. De globale state wordt niet aangepast. Ook is er niet sprake van een interne state.
    
def make_sense(temp_in_fahrenheit):
    return ((temp_in_fahrenheit+459.67) * 5/9)

# Geen pure functie. De takewhile aanroep is een iteratieve functie.
def latin(word):
    vowels = "aeiouAEIOU"
    if word[0] in vowels:
        return word + "ay"
    else:
        cons_cluster = "".join(takewhile(lambda x: x not in vowels, word))
        return(word[len(cons_cluster):] + cons_cluster + "ay")
  
# Geen pure functie. Deze functie roept een niet pure functie aan, namelijk latin(word)
def latinise(string):
    return re.sub(r'[a-zA-Z]+', lambda m: latin(m.group(0)), string)
    
# Geen pure functie. Deze functie roept een niet pure functie aan, namelijk latinise(string)
def say_it_in_latin(string):
    print(latinise(string))


# -

# ***
# ## College 3
# De opdrachten hieronder dienen gemaakt te worden na afloop van college 3.

# ### 3.2 — Recursie
# Tijdens het college hebben we gezien hoe je met recursie een loop kan simuleren zonder mutable state. Vertaal de "functies" hieronder naar recursieve functies.

# #### Opdracht 3.2.1 — `sum_of_squares`

# +
def sum_of_squares(values: List[float]) -> float:
    result = 0
    for x in values:
        result += x ** 2
    return result

def better_sum_of_squares(values: List[float]) -> float:
    if len(values) == 0:
        return 0
    else:
        return (values[-1] ** 2) + better_sum_of_squares(values[:-1])
    
    return None

print("Result:", sum_of_squares([1, 2, 3]))
print("Result 2:", better_sum_of_squares([1, 2, 3]))
print("Sum of Squares:", (sum_of_squares([1,2,3]) == better_sum_of_squares([1,2,3])))


# -

# #### Opdracht 3.2.2 — `repeat`

# +
def repeat(item: A, times: int) -> List[A]:
    result = []
    for _ in range(0, times):
        result = result + [item]
    return result

def better_repeat(item: A, times: int) -> List[A]:
    if times <= 0:
        return []
    else:
        return [item] + better_repeat(item, times - 1)

print("Result:", repeat("X", 3))
print("Result 2:", better_repeat("X", 3))

print("repeat:", (repeat("X", 3) == better_repeat("X", 3)))


# -

# #### Opdracht 3.2.3 — `reverse`

# +
def reverse(list: List[A]) -> List[A]:
    result = []
    for item in list:
        result = [item] + result
    return result

def better_reverse(list: List[A]) -> List[A]:
    if len(list) == 0:
        return []
    else:
        return [list[-1]] + better_reverse(list[:-1])

print("Result:", reverse([1,2,3,4,5]))
print("Result 2:", better_reverse([1,2,3,4,5]))


print("reverse:", (reverse([1,2,3,4,5]) == better_reverse([1,2,3,4,5])))


# -

# #### Opdracht 3.2.4 — Tail recursion
# Hieronder staat de recursieve functies voor de Fibonacci-getallen, zoals deze in de reader is gedefiniëerd. Maak een versie van deze functie met tail recursie.

# +
def fib_rec(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_rec(n-1) + fib_rec(n-2)
                
def fib_tail(n, running_total = 0):
    #TODO
    return None
                
print("fib:", (fib_rec(13) == fib_tail(13)))


# -

# ### 3.3 — Anonieme functies
# Zoals in imperatief programmeren een variabele een waarde (een string, een floating-point getal, etc.) kan bevatten, en deze variabelen gebruikt kunnen worden als argumenten en return values, kan dit bij functioneel programmeren ook met functies. Zo kan de functie $\lambda x. x+1$ toegekend worden aan de variabele `succ`. Op deze manier verschilt een lambda-functie niet veel van een subroutine zoals je die gewend bent: `succ_lambda` en `succ_func` zijn in het voorbeeld hieronder equivalent:

# +
def succ_func(x: int) -> int: # niet anoniem
    return x+1

succ_lambda = lambda x: x+1 # anoniem en vervolgens gebonden

print(succ_func(41))
print(succ_lambda(41))
# -

# #### Opdracht 3.3.1 — Functies in lijsten
# Maak een lijst van tenminste vier anonieme functies die een geheel getal in een ander geheel getal omzetten. 
# Maak vervolgens een functie die elke functie uit de lijst toepast op een getal (natuurlijk met recursie in plaats van een loop), zodat het resultaat een lijst met getallen is. De functie neemt twee argumenten: de lijst van functies en een getal om elke functie op toe te passen.

# +
my_functions = [lambda x: x * 2, lambda x: x * x, lambda x: x ^ 2, lambda x: x / 2] #TODO

def apply_everything(functions: List[Callable[[int], int]], integer: int) -> int:
    if len(functions) == 0:
        return []
    else:
        return [functions[0](integer)] + apply_everything(functions[1:], integer)

print(apply_everything(my_functions, 42))
# -

# #### Opdracht 3.3.2 — Functies filteren [PORTFOLIO]
# Maak een tweede recursieve functie, die wederom een getal en een lijst van functies neemt. De functie kijkt voor elke functie uit de lijst kijkt wat het resultaat is, en levert een nieuwe functie-lijst op met enkel die functies waar het resultaat even was.

# +
my_functions = [lambda x: x + 1, lambda x: x + 2, lambda x: x * 2, lambda x: x * 3]

def filter_even_on_input(functions: List[Callable[[int], int]], input: int) \
                         -> List[Callable[[int], int]]:
    if len(functions) == 0:
        return []
    else:        
        return ([] if functions[0](input) % 2 else [functions[0]]) + filter_even_on_input(functions[1:], input)

filtered_functions = filter_even_on_input(my_functions, 47)
print(filtered_functions)
print(apply_everything(my_functions, 47))
print(apply_everything(filtered_functions, 47)) # should only be evens
# -

# ***
# ## College 4
# De opdrachten hieronder dienen gemaakt te worden na afloop van college 4.

# ### 4.1 — Hogere-orde functies
# Vorig college hebben we kennis gemaakt met functies als first-class citizens. Vandaag gaan we zien wat ons dit aan expressiviteit op levert. Specifiek het feit dat een functie een parameter kan zijn voor een andere functie. Een functie die een functie als argument heeft noemen we een hogere-orde functie. Voorbeelden zijn `map` die een functie toepast op elk item in een lijst en `reduce` die recursief een lijst samenvoegt. Hieronder passen we bijvoorbeeld de `succ_lambda` toe op ieder element in een lijst:

list(map(succ_lambda, [1,2,3]))

# Het is nu niet nodig de functie aan een variabele toe te kennen (tenzij je de functie vaker nodig hebt natuurlijk); de volgende code doet hetzelfde, maar gebruikt de lambda direct als argument van map:

list(map(lambda x: x+1, [1,2,3]))


# #### Opdracht 4.1.1 — `reduce` met lambda
# Gebruik nu de functie `reduce` en een lambda $\lambda x y . x + 2y$ op de lijst `[1,2,4]`. 

# +
#TODO
# -

# #### Opdracht 4.1.2 — `fac`
# Gebruik vervolgens `reduce`, een lambda en `range` om de faculteit van een input getal te geven. Je antwoord moet in de vorm van een lambda toegekend worden aan de variabele `fac`. Hieronder staat code die je zelf aan moet vullen, met een regel om je lambda te testen.

# +
#TODO
# -

# #### Opdracht 4.1.3 — `foldr` met `reduce` [PORTFOLIO]
# Nu gaan we de functie `reduce` (of zoals we hem zijn tegengekomen: `foldl1`) gebruiken om een `foldr` te definiëren:

# +
def foldr(f: Callable[[A,B], B], init: B, list: List[A]) -> B:
    if len(list) == 0:
        return init
    else:
        head, *tail = list
        return f(head, foldr(f, init, tail))

print(foldr(lambda x, y: x-y, 0, [1,2,3])) # -> 2


# -

# #### Opdracht 4.1.4 — `zip` [PORTFOLIO]
# Tijdens het college hebben we gezien dat `zipWith` uit te drukken is in `zip`. Andersom kan natuurlijk ook: druk `zip` uit met behulp van `zipWith`.

# +
def zip_with(f : Callable[[A, B], C], xs : List[A], ys : List[B]) -> List[C]:
    if len(xs) == 0 or len(ys) == 0:
        # Base case
        return []
    else:
        x, *xrest = xs
        y, *yrest = ys
        return [f(x,y)] + zip_with(f, xrest, yrest)
    
def zip(xs : List[A], ys : List[B]) -> List[Tuple[A,B]]:
    return zip_with(lambda x, y: (x, y), xs, ys)

print(zip([1,2],[3,4])) # --> [(1, 3), (2, 4)]
print(zip([1,2],[3])) # --> [(1, 3)]
print(zip([1,2],[])) # --> []


# -

# ***
# ## College 5
# De opdrachten hieronder dienen gemaakt te worden na afloop van college 5.

# ### 5.1 — Recursieve Datatypes
# In de slides hebben we een recursieve definitie van de natuurlijke getallen gezien, de zogenaamde Peano nummers. Hieronder zie je een voorbeeld-implementatie in Python:

# +
class Peano: # Python staat niet toe dat we Peano als type gebruiken voordat de klasse gedefinieerd is.
    def phantom(): # Daarom definieren we hem twee keer. De tweede keer bestaat Peano reeds en kunnen
        return None # we deze als type gebruiken.
    
class Peano:
    def __init__(self, x: Union[Peano,None]) -> Peano:
        self.val = x
    @classmethod
    def succ(cls, x: Union[Peano,None]) -> Peano:
        return Peano(x)
    @classmethod
    def zero(cls) -> Peano:
        return Peano(None)
    def __int__(self):
        if self.val == None:
            return 0
        else:
            return 1 + int(self.val)
    def __str__(self):
        if self.val == None:
            return "Z"
        else:
            return "S(" + str(self.val) + ")"

peano_zero = Peano.zero()
peano_one = Peano.succ(peano_zero) # Peano(Peano.zero)
peano_two = Peano.succ(peano_one)  # Peano(Peano(Peano.zero))

print(peano_zero)
print(peano_one)
print(peano_two)

print(int(peano_zero))
print(int(peano_one))
print(int(peano_two))


# -

# #### Opdracht 5.1.1 — Natuurlijke getallen
# Er zijn echter meerdere recursieve definities te bedenken; een daarvan gaan jullie zelf in Python definiëren. Deze ziet er als volgt uit:
#
# - $0$ is een natuurlijk getal
# - Als $n$ een natuurlijk getal is, dan is $2n$ een natuurlijk getal
# - Als $n$ een natuurlijk getal is, dan is $2n+1$ een natuurlijk getal
#
# Ook met deze definitie zijn alle natuurlijke getallen te definiëren. Maar, _don't take my word for it_, en check het zelf: Definiëer een datatype voor de natuurlijke getallen zoals hierboven beschreven. Defineer variabelen `one`, `two`...`twenty` in deze notatie. Schrijf ook overal de type-annotaties bij!
#
# Wat valt je op? Is er een formule te verzinnen om gegeven een getal te bepalen hoeveel constructors genest moeten worden? Is er een snelle manier om, zeg, 6561 in deze notatie om te zetten?  
#   
# *Bonus:* wat moeten we aanpassen om $0$ als waarde te verbieden?

# +
class Number:
    def phantom():
        return None # Hier hoef je niets aan te doen
    
class Number:
    def __init__(self):
        None #TODO
    @classmethod
    def d(cls, v): # d = Double
        None #TODO
    @classmethod
    def p(cls, v): # p = double Plus one
        None #TODO
    @classmethod
    def z(cls): # z = Zero
        None #TODO
    def __int__(self): # waarde weergeven als int
        None #TODO
    def __str__(self): # waarde weergeven als string van de vorm P(D(P(P(Z))))
        None #TODO

z = Number.z()
def d(v: Number) -> Number:
    return Number.d(v)
def p(v: Number) -> Number:
    return Number.p(v)

zero = z                      # 0
one = p(z)                    # 1
two = None #TODO
three = None #TODO
four = None #TODO
five = None #TODO
six = None #TODO
seven = None #TODO
eight = None #TODO
nine = None #TODO
ten = None #TODO
eleven = None #TODO
twelve = None #TODO
thirteen = None #TODO
fourteen = None #TODO
fifteen = None #TODO
sixteen = None #TODO
seventeen = None #TODO
eighteen = None #TODO
nineteen = None #TODO
twenty = None #TODO

""" # Zodra alles is ingevuld kun je deze map gebruiken om alle getallen te printen. Let op! Om deze map te laten
    # werken moeten er geen None elementen in de lijst meer zitten en moet de functie int() voor alle elementen
    # gedefinieerd zijn (alles moet dus een Number zijn, en __int__(self) moet voor Number geimplementeerd zijn.
    # Tot die tijd krijg je een error, dus kun je de map beter tussen tripple quotes laten staan...
list(map(lambda num: print(int(num), num), [zero, one, two, three, four, five, six, seven, eight, nine, ten, 
                                            eleven, twelve, thirteen, fourteen, fifteen, sixteen, seventeen, 
                                            eighteen, nineteen, twenty]))
"""


# -

# ### 5.2 — Functionele Datastructuren
# Hieronder zien we een iets meer uitgebreid voorbeeld van een functionele datastructuur. Functies die de datastructuur aanpassen hebben de nieuwe structuur als return value, en het origineel blijft ongewijzigd. 
#
# #### Opdracht 5.2.1 — Poor man's Git [PORTFOLIO]
# De datastructuur in deze opdracht geeft een elementaire boomstructuur (specifiek, een Rose Tree) waarin een revisie-geschiedenis bewaard wordt. De positie binnen de boom (de versie die "checked out" is) bevindt zich altijd in de root van de boom en kan dus snel opgevraagd worden. Het deel van de boom boven de root wordt in een `thread` variabele opgeslagen. Bij een `descend(hash)` wordt de node met `hash` de nieuwe root, en komt de oude root in de thread van de nieuwe root te staan. Bij een `ascend()` wordt deze van de thread afgehaald en als nieuwe root teruggegeven. We willen elk element in de boom slechts één keer opslaan; bij een `descend(hash)` is de nieuwe root niet ook nog steeds het kind van de oude root (die nu in de `thread` zit).
#
# <img src="zipper.png" />
#
# Een intuitieve manier om naar `descend(hash)` te kijken is alsof de hele boom aan de nieuwe root wordt opgetild. Alles wat zich boven de root bevond komt er nu onder te hangen.
#
# Vul de code aan waar `#TODO` staat en beantwoord de volgende vragen:
#
#  - Wat zijn de sommen en producten in het type History?
#  - Ascend en Descend worden genegeerd als ze onmogelijk zijn (een ascend vanuit de root node levert de root node op). Het nadeel van deze aanpak is dat ascend(descend(x,_)) == descend(ascend(x,_)) == x niet op gaat. Beschrijf een aanpak om dit wel zo te maken. 
#  - Wat zou een alternatieve manier zijn om met behulp van een som-type met falende ascends/descends om te gaan? Zou de bovenstaande gelijkheid met deze aanpak wel op gaan?
#  
# Tip: schrijf een `__str__()` functie voor de klassen, zodat je kan zien wat er gebeurt...

# +
class Rev:
    """Phantom class om Rev als type te kunnen gebruiken."""
    def phantom():
        return None
    
class History:
    """Phantom class om History als type te kunnen gebruiken."""
    def phantom():
        return None
    
class Rev:
    """Klasse/datatype voor een revisie met description (commit message) en hash (voor nu gewoon een meegegeven int)."""
    def __init__(self, desc: str, hash: int) -> Rev:
        """Constructor"""
        self.__desc = desc
        self.__hash = hash
        
    def desc(self) -> str:
        """Getter voor de description."""
        return self.__desc
    
    def hash(self) -> int:
        """Getter voor de hash."""
        return self.__hash
    
    def update(self, desc: Union[None, str] = None, hash: Union [None, int] = None) -> Rev:
        """Schrijf een functie die de description en hash update. Denk eraan dat we een functionele datastructuur willen,
           en dat we dus een nieuwe instantie maken in plaats van de oude aan te passen! De nieuwe desc en hash kunnen
           None zijn, in welk geval de oude gebruikt wordt."""
        if not hash and not desc:
            # Return myself
            return self
        else:
            # Return a new instance
            return Rev(desc, hash)
        
    def is_hash(self, hash: int) -> bool:
        """Predicaat: Is this the Revision we're looking for?"""
        return self.__hash == hash

class History:
    """Node in een Rose-tree van Rev's."""
    def __init__(self, revision: Rev, children: List[History] = [], thread: Union[None,History] = None) -> History:
        """Constructor"""
        self.rev = revision
        self.children = children
        self.thread = thread
        
    def add_child(self, hist: History) -> History:
        """Schrijf een functie die een nieuwe subhistory toevoegt."""
        # Append to children and return new instance.
        return History(self.rev, self.children + [hist], self.thread)
       
    def remove_child(self, hash: int) -> History:
        """Schrijf een functie die een subhistory verwijdert. Op het moment van een descend(hash) wordt de huidige node
           in de thread opgeslagen. De oude root moet de nieuwe niet langer als child beschouwen (zie afbeelding)."""
        return History(self.rev, list(filter(lambda x: not x.is_hash(hash), self.children)), self.thread)
    
    def commit(self, rev: Rev) -> History:
        """Maak een History-object voor de nieuwe revision aan, hang deze onder de huidige root en descend."""
        return self.add_child(History(rev)).descend(rev.hash())
    
    def descend(self, hash: int) -> History:
        """Descend naar een child node met een gegeven hash."""
        def find_correct_child(children: List[History]) -> Union[None, History]:
            """Zoek recursief de children door naar een child met de juiste hash..."""
            if len(children) > 0:
                head, *tail = children
                if head.is_hash(hash): # Als gevonden: child wordt de nieuwe node, oude node zonder child in de thread
                    return head.add_to_thread(self.remove_child(hash))
                else:
                    return find_correct_child(tail) # Anders zoeken we verder
            else: # De lijst is leeg, er is niets gevonden
                return self # We veranderen niets
        return find_correct_child(self.children)
       

    def ascend(self) -> History:
        """Schrijf een functie die ascend naar de bovenliggende node. Als er geen bovenliggende node in de thread is verandert
           er niets."""
        
        # Check above node within thread exists. If so, return myself.
        if not self.thread:
            return self
        else:
            # print(self.thread)
            return self.thread.add_child(self.remove_thread())

    def update(self, action: Callable[[Rev],Rev]) -> History:
        """Schrijf een functie die een (lambda) van Rev naar Rev accepteert en een nieuwe History maakt waarbij de meegegeven
           action op de Rev is toegepast."""
        return History(action(self.rev), self.children, self.thread)
    
    def remove_thread(self) -> History:
        """Verwijdert de thread voor bij een ascend."""
        return History(self.rev, self.children, None)
    
    def add_to_thread(self, parent: History) -> History:
        """Zet de oude parent in de thread."""
        return History(self.rev, self.children, parent)
    
    def root(self) -> History:
        """Schrijf een functie die recursief blijft ascenden tot er geen thread meer is (maw: zoek de initial commit.)"""
        
        # Is there a thread?
        if not self.thread:
            return self
        else:
            # Go deeper.
            return self.ascend().root()
    
    def head(self) -> Rev:
        """Getter voor de revisie van deze node, i.e. de huidige HEAD."""
        return self.rev
    
    def is_hash(self, hash: int) -> bool:
        """Predicaat: komt de hash van de huidige Rev overeen met de hash die we zoeken?"""
        return self.rev.is_hash(hash)
    
    def prettyDumpChildren(self, children : List[History]) -> str:
        """ Pretty dump all children within a tree in string format. """
        if not len(children):
            return ''
        elif len(children) == 1:
            return str(children[0])
        
        head, *tail = children
        return str(head) + ',' + self.prettyDumpChildren(tail)
    
    def __str__(self):
        return '<History: ' + self.rev.desc() + ' with children [' + self.prettyDumpChildren(self.children) + '] and thread ' + str(self.thread)
    
h = History(Rev("Initial commit",1))
h2 = h.commit(Rev("Added README.md",2))

print("No children: ", h)
print("After commit: ", h2)
print("Reascended: ", h2.ascend())

print("Invalid descend (no change): ", h.descend(3))
print("Invalid ascend (no change): ", h.ascend())

complex = History(Rev("Initial commit",1)).commit(Rev("Added README.md",2)).commit(Rev("Actual work",4)).root() \
                                          .commit(Rev("Fork, as READMEs are for wimps!",3))
print("Complex: ", complex)
print("Complex, ascend: ", complex.ascend())

print("Updating revision desc of head: ", h2.update(lambda x: x.update(desc = "Altered space and time!")))

""" Als alles goed is ingevuld zou het resultaat er zo uit moeten zien:

No children:  <History: "Initial commit" with children [] and thread None>
After commit:  <History: "Added README.md" with children [] and thread <History: "Initial commit" with children [] and thread None>>
Reascended:  <History: "Initial commit" with children [<History: "Added README.md" with children [] and thread None>] and thread None>
Invalid descend (no change):  <History: "Initial commit" with children [] and thread None>
Invalid ascend (no change):  <History: "Initial commit" with children [] and thread None>
Complex:  <History: "Fork, as READMEs are for wimps!" with children [] and thread <History: "Initial commit" with children [<History: "Added README.md" with children [<History: "Actual work" with children [] and thread None>] and thread None>] and thread None>>
Complex, ascend:  <History: "Initial commit" with children [<History: "Added README.md" with children [<History: "Actual work" with children [] and thread None>] and thread None>, <History: "Fork, as READMEs are for wimps!" with children [] and thread None>] and thread None>
Updating revision desc of head:  <History: "Altered space and time!" with children [] and thread <History: "Initial commit" with children [] and thread None>>
"""; None
# -

# ***
# ## College 6
# De opdracht hieronder dient gemaakt te worden na afloop van college 6. Deze week is er slechts één opdracht en deze hoeft niet in het portfolio. De opdracht dient als een introductie van concepten die jullie kunnen gebruiken bij de eindopdracht van dit vak.
#
# ### 6.1 — Functional Reactive Programming en Comprehensions
# In deze opdracht gaan we aan de slag met FRP: manipulatie van (potentieel eindeloze) streams data, waarbij resultaten constant berekend worden telkens als er nieuwe invoer beschikbaar is. In Python gebruiken we hiervoor generators, die technisch gezien niet functioneel zijn (maar in plaats daarvan een interne state gebruiken).
#
# Een eigenschap van generators is dat we deze als (oneindige) lijsten kunnen gebruiken. Om tijdens development infinite loops te voorkomen is de lengte van streams echter beperkt. Generators in Python zijn stateful, wat betekent dat we ieder element dat we uit de generator halen "verbruikt" hebben. Bij meerdere analyses kan het dus zijn dat je data opraakt. Om dit te voorkomen kun je `tee()` uit `itertools` gebruiken.
#
# We maken voor het beschrijven van generators gebruik van *lijst comprehensies*, of in dit geval generator comprehensies vanwege de potentiële eindeloosheid van de streams. Het verschil is dat de generator *lazy* is, en enkel waardes uitrekent wanneer deze gevraagd worden. Lijsten in Python zijn *eager*, wat betekent dat deze meteen worden uitgerekend. Omdat onze streams eindeloos moeten kunnen zijn is dit geen optie. In notatie is het enige verschil dat lijstcomprehensies de vierkante haken gebruiken `[f(a) for a in stream() if p(a)]`.
#
# Beide vormen worden gebruikt om collecties te maken op basis van invoer lijsten / streams, functies en predicaten (functies naar `bool`, e.g. `isEven(i: int) -> bool`). In het voorbeeld hierboven wordt ieder element uit `stream()` een voor een aan `a` gebonden, en als `p(a)` gelijk is aan `True` wordt `f(a)` aan de uitvoer toegevoegd. `p(a)` kan gezien worden als een filter, en `f(a)` als een functie die over de lijst gemapt wordt.    

# #### Opdracht 6.1.1 — Pathfinder
# Breid de code hieronder uit met nog 3 analyses:
# * bereken de variance op de temperatuur
# * bereken de gemiddelde hoogte over een periode
# * verzin zelf nog een interessante analyse (je mag meetwaardes aan de data_stream generator toevoegen)

# +
from random import random
from collections import namedtuple
from statistics import mean
import math

Measurement = namedtuple('Measurement', 't lon lat h temp')

def data_stream() -> Generator[Measurement, None, None]:
    """Deze generator stelt de data van onze pathfinder voor. Iedere 24 uur stuurt de robot een update met de huidige locatie, 
       hoogte in meters boven NMP en de gemeten temperatuur in Kelvin. Daarnaast krijgt onze pathfinder per dag een 5% kans om 
       te stoppen met communiceren en leeft deze maximaal 1000 dagen. Op deze manier werken we niet met echt eindeloze lijsten:
       hoewel dit in principe moet kunnen kan een klein foutje tot eindeloze loops en kernel crashes leiden. Zodra je ervan
       overtuigd bent dat je code hiermee om kan gaan kun je de if-conditie in de loop weghalen en zien wat er gebeurt.
       """
    t = 0
    lon = 19.13
    lat = 33.22
    alive = True
    while alive:
        lon += 0.5 * random() - 0.25
        lat += 0.5 * random() - 0.25
        t += 1
        height = 100 * math.sin(lon) * abs(math.cos(lat)) ** 0.5
        temp = 230 + 50 * random()
        #if random() > 0.95 or t > 1000: # Comment these lines for endless fun!
        #    alive = False               # Uncomment them for a sandbox. 
        yield Measurement(t, lon, lat, height, temp)
        
def below_NMP(m: Measurement) -> bool:
    return m.h < 0

def temperature_in_celsius(m: Measurement) -> float:
    return m.temp - 273.15

def safe_mean(xs: Generator[float, None, None]) -> Union[None, float]:
    """mean(xs) is geen totale functie en kan errors geven. Beter checken we de lengte van de invoer, maar helaas heeft een
       generator geen lengte."""
    try:
        return mean(xs)
    except StatisticsError:
        return None

def analyse():
    """Hier worden de temperature_in_celsius transformatie en het below_NMP predicaat gebruikt om een stream te genereren van
       celsius-temperaturen van alle datapunten onder "zeeniveau". Hier kunnen we een deel van pakken om een gemiddelde te
       berekenen (merk op dat het niet mogelijk is een gemiddelde van een oneindige stream te nemen)."""
    temps_below_NMP = (temperature_in_celsius(point) for point in data_stream() if below_NMP(point))
    print("Mean temperature below NMP (first 100 measurements): ", safe_mean(itertools.islice(temps_below_NMP, 100)))
    print("Mean temperature below NMP (second 100 measurements): ", safe_mean(itertools.islice(temps_below_NMP, 100)))
    print("Mean temperature below NMP (third 100 measurements: ", safe_mean(itertools.islice(temps_below_NMP, 100)))
    

analyse()
