compiling-the-lambda-calculus/script.md
2024-02-11 20:44:17 -07:00

12 KiB

About Me

"For the new faces, I'm Elizabeth, or simponic on the discord..."

  • I love functional programming; I've been enjoying programs functionally for over 3 years; in LISP and Elixir mostly. I've loved every second of it. Well, maybe not every second, bust definitely most of them.
  • Soon to be SDE at AWS
  • I'm 20 years old, graduating this semester once I'm done with my final general class I missed on my grad plan (2credits only, ugh, ask me after if you want to hear the bureacracy)
  • I was president of the USU Linux club from 2021 - 2024

Of course this presentation is open source at the link you see on the board.

Flirting With Functions

Tomorrow's Valentine's day, but you realize; you don't have a Valentine!

Desparate to make yourself feel better, you turn to your computer and open Emacs, asking the M-x doctor for help.

I am the psychotherapist.  Please, describe your problems.  Each time you are finished talking, type RET twice.
> i need a valentine
Why do you say you might want to have this valentine?
> im lonely
Are you lonely often?
> sometimes
Can you elaborate on that?
> i need someone or something i can trust
What makes you believe you need someone or something you can trust?
> i want something that when i ask one question i'll get the same answer no matter what.
Is it because when you ask one question you will get the same answer no matter what that you came to me?
> yes
I see...  Well, are you sure that this is so?
> yes
Yes, hmmm... so, do you really think this is so?
> yes

well that's not super helpful, looks like the "AI stuck in a loop repeating itself" notion is nothing modern.

On our journey to get a valentine, it looks like we have stumbled across a black box. Why? Well... we could start shoving random numbers in it and see what happens.

hmm... [-1, 2]? okay, 1. [3, 4]? okay, 7. what if we try [-1, 2] again, see if it meets our criteria to be our valentine; not changing its answer. amazing!

No matter how many times we put two numbers in it gives me the same answer; we're in love with it already!

Let's look at what's behind this black box, see if there's more we can get to know.

Where could our curiosity take us?

A simple add function. That makes sense.

We're in love with its predictability, and simplicity; the assurance that no matter what we give as input or how many times it's given, the function behaves in a consistent manner.

But let's imagine, for a moment, a different kind of box. One where actions outside the box influences responses.

Imagine asking your partner about what food they want to eat. But instead of a straightforward answer based on your question alone and some state (i.e. hunger levels, craving ratios, etc), the function's response is influenced by other factors; the day of the week, and the state of the day or week prior. We can simulate this: ((go through the ratios and outputs)).

Let's see what causes this in this black box we don't love; don't pay attention to the implementation (there's some stuff we haven't talked about yet), but check out this line (MATH.RANDOM). This is a side effect; an unpredictible effect on the output.

Side effects introduce unpredictability into our programming relationships. Where output is not just determined by its input but also by the state of the outside world, or the system at the time of execution. Suddenly, the predictability we cherished is compromised.

We call a function that is unpredictable, or has side effects, "impure", and one that isn't - "pure".

It was obvious how we could cause unpredictability by including randomness. But here's another example, on left we have two impure functions.

When we execute the first block, we get the expected result of fib(5).

But because fact uses the same cache, we'll run fact(5)... fact(5) returns fib(5)!

Now, when I go and update the cache to be correct, we get the correct answer. I was able to change the behavior of the function by modifying state outside of it. Get the gist - impure vs pure functions, side effects, etc? Cool.

For now let's move back to studying our pure black boxes.

We love our black boxes a lot, and we want to share them with other people in our lives. So let's create another that takes a list of people and creates a custom valentine's letter for them.

def make_valentines_letters(people):
	letters = []
	for person in people:
		letters.append(f"Dear, {person.name}\nYour smile lights up my world. Happy Valentine's Day!")
	return letters

Good! Now they will all share our love.

[SPACE] But a few months goes by, and all our friends' birthdays are soon coming up (why are so many cs people born in June)! We don't want to make them sad, as we see here, so we make another black box to generate a letter, and in how many days we should give it to them:

def make_birthday_cards(people):
	today = new Date()

	cards = []
	for person in people:
		days_until_birthday = toDays(new Date(person.birthday, today.year) - today)
		age = today.year - person.birthday.year

		card = f"Happy Birthday {name}\nI can't believe you're already {age} years old!"
		cards.append({ "message": card, "deliver_in_days": days_until_birthday })

	return cards

There, now, we can make sure our friends are happy on their birthdays!

But, this is getting annoying; what about Christmas, Thanksgiving, or Easter? Making a new black box to make a list of new cards, going through each person, every single time to construct a list of cards, is getting really tedious.

What if we generalized this? We create a bunch of black boxes that take a person, and generate them a card, specifically; like a template for a card you could print off and fill in manually.

def valentine_letter(person):
	return f"Dear, {person.name}\nYour smile lights up my world. Happy Valentine's Day!")

def birthday_card(person):
	today = new Date()
	daysUntilBirthday = toDays(new Date(person.birthday, today.year) - today)
	newAge = today.year - person.birthday.year

	card = f"Happy Birthday {name}\nI can't believe you're already {newAge} years old!"
	cards.append({ "message": card, "deliverInDays": daysUntilBirthday })	

Then, we can use a black box that takes a list of people, and applies this template to each person.

def buildCards(people, cardMaker):
	cards = []
	for person in people:
		card = cardMaker(person)
		cards.append(card)
	return cards
	
people = [{"name": "Joseph", birthday: new Date()}, {"name": "DeeDee", birthday: new Date()}]
buildCards(people, birthdayCard)

The ability in a language to pass a function around like this - like a variable - is what makes functions "first class". And the buildCards function takes a function as input, making it a "higher order function". (TODO: slides)

Functional Reproduction

After exploring the simple yet profound beauty of higher order black boxes, our journey into the functional programming romance takes an intriguing turn. What's better than one black box? A black box that creates more black boxes.

All life on Earth contains instructions in the form of DNA. During mitosis, special mechanisms in our cells read the instructions and create new instructions to give to another cell.

Very similarly, we can give our black boxes DNA from parents to reproduce and create new child black boxes. But in our world of black boxes, we refer to this DNA as "closures".

def cardGeneratorFor(person, type):
	def personBirthdayCard():
		return birthdayCard(person)
	def personValentineLetter():
		return valentineLetter(person)

	if type == "valentine":
		return personValentineLetter
	if type == "birthday":
		return personBirthdayCard
	raise NotImplementedError

joseph = {"name": "Joseph", birthday: new Date()}
josephValentineCardGenerator = cardGenerators(joseph, "valentine")

print(josephValentineCard())

Here we've created two children with the DNA of cardGenerators; i.e., we get the "person" from the parent black box's DNA.

We can even go a step further:

def cardGenerators(person):
	def personBirthdayCard():
		return birthdayCard(person)
	def personValentineLetter():
		return valentineLetter(person)

	def messageType(type):
		if type == "valentine":
			return personValentineLetter
		if type == "birthday":
			return personBirthdayCard
		raise NotImplementedError()

	return messageType

joseph = {"name": "Joseph", birthday: new Date()}
josephValentineCard = (cardGenerators(joseph))("valentine")
print(josephValentineCards)

Wait, do you guys smell that?

Quick Aside: Delicious Curry

Sneakily, we've actually started preparing curry!

We've broken up the input of the black box ~cardGenerator~ into it and its child. This is called ~currying~, and it's something we'll touch on in more detail later.

There's another way that we could implement the same functionality - "partial function application"; a fancy name for a simple idea:

def josephCard(type):
	joseph = {"name": "Joseph", birthday: new Date()}
	cardGenerator = cardGeneratorFor(joseph, type)
	return cardGenerator()

print(josephCardGenerator("valentine")

Immutability

We briefly mentioned side effects and alluded them to the unpredictability of a partner. We love our black boxes because they're reliable.

But, we've actually had an impostor among us (AMOGUS sound??) throughout this presentation. Specifically in ~buildCards~:

def buildCards(people, cardMaker):
	cards = []
	for person in people: # side effect (mutation of person)
		card = cardMaker(person)
		cards.append(card) # side effect (mutation of cards)
	# someone could do something nasty with "cards" before it's returned here
	return cards

cards = buildCards(people, valentineMaker)

This function actually has a small side effect - though it's contained entirely within the function - when we append to the cards list. If we were to go about living a side effect free life, that means we can't change anything.

Loops inherently produce side effects (mutation of the loop variable), so we need to eliminate them.

So how do we sus out an impostor? With copies and recursion.

def build_cards(people, card_maker):
	if (len(people) == 0): # base case, no more people to process
		return []
	
	# get the first person in the list and make their card
	person = people[0]
	card = card_maker(person)
	
	rest_people = people[1:] # get sublist of everyone except the first
	return [card] + build_cards(rest_people, card_maker)

Here we're not changing anything at all (except the stack), with the same functionality; this code is "immutable". When we call ~build_cards~ on a list of people, we're 100% certain we're not gonna get something odd short of a bug.

At a high level there are so many benefits to immutability:

  • Concurrency (TODO: go into more detail)
  • Easier to verify correctness.
  • Compiler optimizations.
  • Easy to read and understand.

But there are also downsides:

  • Keeping immutability requires more computation (data structures)
  • More difficult to write (huge selling point for Object Oriented Programming; encapsulation)

But like all relationships, we need to compromise with our black boxes. Side effects are effectively unavoidable as programmers; when people press the button, they don't want the computation to just exist out there in the computer void. No! They want to see the pop up box, and the value updated on their account. We need to be able to mutate state where the tradeoff of extra compute time is unavoidable like in I/O or a relatively large database.

Writing Code Functionally

Determining if a natural number is even or odd in the best way possible is a problem that's stood unsolved since the dawn of time. We'll use this incredibly difficult task to show how we can refactor some code to use the paradigms we just learned.

========

Part Two - A Deeper Dive

The Lambda Calculus

Completeness of the Lambda Calculus

Mention how alonzo church actually failed on his first attempt, natural numbers

Writing a Compiler

A Compiler As A Sequence of Transformations

Continuation Passing Style

=======

Conclusion