diff --git a/examples/sum_of_even_squares.ts b/examples/sum_of_even_squares.ts new file mode 100644 index 0000000..b4c1aad --- /dev/null +++ b/examples/sum_of_even_squares.ts @@ -0,0 +1,151 @@ +const isEven = (n: number) => { + let even = true; // 0 is even + + for (let i = 1; i <= n; i++) { + even = !even; + } + + return even; +}; + +const sumOfEvenSquares = (upTo: number) => { + let sum = 0; + + for (let i = 1; i <= upTo; i++) { + if (isEven(i)) { + sum += i * i; + } + } + + return sum; +}; + +/** + * functional + */ + +const isEvenHelper = (curr: number, max: number, previous = false) => { + if (curr === max) { + return !previous; + } + + return isEvenHelper(curr + 1, max, !previous); +}; + +const isEvenFn = (n: number) => { + return isEvenHelper(0, n); +}; + +const map = ( + transform: (x: number, ind: number) => number, + list: number[], + index = 0, +) => { + if (list.length === 0) { + return []; + } + const [head, ...tail] = list; + const transformed = transform(head, index); + + return [transformed].concat(map(transform, tail, index + 1)); +}; + +const filter = (predicate: (x: number) => boolean, list: number[]) => { + if (list.length === 0) { + return []; + } + + const [head, ...tail] = list; + if (predicate(head)) { + return [head].concat(filter(predicate, tail)); + } + return filter(predicate, tail); +}; + +const reduce = ( + accumulation: (x: number, accumulator: number) => number, + accumulator: number, + list: number[], +) => { + if (list.length === 0) { + return accumulator; + } + + const [head, ...tail] = list; + const newAccumulator = accumulation(head, accumulator); + return reduce(accumulation, newAccumulator, tail); +}; + +const curriedReduce = ( + accumulation: (x: number, accumulator: number) => number, +) => { + return (accumulator: number) => { + const reduce = (list: number[]) => { + if (list.length === 0) { + return accumulator; + } + + const [head, ...tail] = list; + const newAccumulator = accumulation(head, accumulator); + + return curriedReduce(accumulation)(newAccumulator)(tail); + }; + + return reduce; + }; +}; + +const sum = curriedReduce((x, acc) => x + acc)(0); + +const sumOfEvenSquaresFn = (upTo: number) => { + const numbersUpTo = Array(upTo + 1) + .fill(0) + .map((_, i) => i); + const squares = numbersUpTo.filter((x) => isEvenFn(x)).map((x) => x * x); + + return sum(squares); +}; + +/** + * test + */ + +const assert = (predicate: () => boolean, message: string) => { + const start = performance.now(); + + console.log(message + "..."); + const result = predicate(); + + const runtimeFmt = ` in ${(performance.now() - start).toFixed(5)}ms`; + if (result) { + console.log(" ... PASSED" + runtimeFmt); + return; + } + + console.error(" ... FAILED" + runtimeFmt); + throw new Error("predicate failed for test: " + message); +}; + +/* + Σ(2i)^2 = 4 * Σ(i)^2 = 4 * (n * (n + 1) * (2 * n + 1)) / 6 + */ +const constantTimeMathMagic = (x: number) => { + const n = Math.floor(x / 2); // even numbers + return (4 * (n * (n + 1) * (2 * n + 1))) / 6; +}; + +const test = (sumEvenSquarer: (x: number) => number) => { + assert(() => sumEvenSquarer(5) === 2 ** 2 + 4 ** 2, "up to 5"); + assert( + () => sumEvenSquarer(10) === 2 ** 2 + 4 ** 2 + 6 ** 2 + 8 ** 2 + 10 ** 2, + "up to 10", + ); + + //assert( + // () => sumEvenSquarer(75_000) === constantTimeMathMagic(75_000), + // "up to 75,000", + //); +}; + +test(sumOfEvenSquares); +test(sumOfEvenSquaresFn); diff --git a/public/amogus.png b/public/amogus.png new file mode 100644 index 0000000..8b43c27 Binary files /dev/null and b/public/amogus.png differ diff --git a/public/img/cell_division.mp4 b/public/img/cell_division.mp4 new file mode 100644 index 0000000..de621f1 Binary files /dev/null and b/public/img/cell_division.mp4 differ diff --git a/public/img/curry.mp4 b/public/img/curry.mp4 new file mode 100644 index 0000000..4a0e553 Binary files /dev/null and b/public/img/curry.mp4 differ diff --git a/public/img/me.jpg b/public/img/me.jpg deleted file mode 100644 index 697009b..0000000 Binary files a/public/img/me.jpg and /dev/null differ diff --git a/public/img/pengy.png b/public/img/pengy.png new file mode 100644 index 0000000..99585cf Binary files /dev/null and b/public/img/pengy.png differ diff --git a/script.md b/script.md index 088ed0f..3d2978d 100644 --- a/script.md +++ b/script.md @@ -1,18 +1,16 @@ -About Me -=== +# 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. +- 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, but 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'm 20 years old, graduating this semester - 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 -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. @@ -58,13 +56,13 @@ Imagine asking your partner about what food they want to eat. But instead of a s 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. +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)`. +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)! @@ -74,33 +72,10 @@ 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. -```python -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: -```python -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. @@ -111,156 +86,404 @@ Then, we can use a new black box that takes a list of people, and applies this t 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". -Functional Reproduction -=== +# 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". +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"; a function with its "environment" - the variables it has access to - attached to it. [SPACE] -``` -def cardGeneratorFor(person, type): - def personBirthdayCard(): - return birthdayCard(person) - def personValentineLetter(): - return valentineLetter(person) +Here we've created a black box that outputs a black box to create a certain type of letter for a person encoded in its DNA. If the type is "birthday", the output is a birthday card generator, and if it's "valentine", it's a valentine letter generator. - 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. +When we put in "Alan Turing" and "valentine", we obtain a black box with "person" (in this case, Alan Turing) in its DNA [OPEN DEV TOOLS -> valentineCardGenerator]. Now we evaluate the black box, and it returns a valentine letter for that person. 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 -=== +# 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. +We've broken up the input of the black box ~cardGenerator~ into a series of functions with a single input. This is called ~currying~. -There's another way that we could implement the same functionality - "partial function application"; a fancy name for a simple idea: +To curry the function `cardGeneratorFor` we: +first created a new closure over the person to provide a function that takes only the type +returned that closure +removed the now unnecessary type argument since that's now curried +and finally changed the function signature to reflect the curried function -``` -def josephCard(type): - joseph = {"name": "Joseph", birthday: new Date()} - cardGenerator = cardGeneratorFor(joseph, type) - return cardGenerator() +There's another way that we could implement the same functionality - "partial function application"; a fancy name for a simple idea; locking some arguments to partial inputs in a function and passing the rest to be applied. -print(josephCardGenerator("valentine") -``` +# Immutability -Immutability -=== -We briefly mentioned side effects and alluded them to the unpredictability of a partner. We love our black boxes because they're reliable. +We briefly mentioned side effects and alluded them to the unpredictability of a partner choosing their food. We love our black boxes because they're reliable. But, we've actually had an impostor among us throughout this presentation. -But, we've actually had an impostor among us (AMOGUS sound??) throughout this presentation. Specifically in `buildCards`: +Specifically in `buildCards` [SPACE] -``` -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 +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_ lest something else depends on it. -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. +In this case, looping through "people" inherently produces a side effect via mutation of the loop variable, so we need to eliminate it. 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), but we've kept 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 change something and get something odd, short of a bug. -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 immutable functional programming: -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. +- Readability and reasoning about code. Functional, immutable code reads like a book! +- Concurrency. When everything is immutable, it's much easier to prevent race conditions and deadlock. If you can avoid a mutex or semaphore here and there, that simplifies things so much. You also know for certain every thread or processor is working on the same data; it couldn't have changed when it was distributed. +- No side effects. I can't think about how many times I've spent hours poring through a code base to find where exactly something is changing or what it can effect. This helps verify correctness and makes unit testing a dream. +- I suppose this is more subjective, but it gives you an entirely new view of how computers work. When you work with functions you're directly treating the computer as an automaton or turing machine. There's so much to learn around functional programming, type theory, and it touches almost any given field of mathematics and computer science. But there are also downsides: -+ Keeping immutability requires more computation (data structures) -+ More difficult to write (huge selling point for Object Oriented Programming; encapsulation) + +- Keeping immutability requires more computation in many cases (special data structures and copying) + - Typical arrays go out the window in most applications. The cons cell / linked list is much preferred. - You also don't typically have mutable datastructures like hash maps. Instead, you need to reach to balanced trees or tries. It takes a lot of practice but you _can_ always write code just as efficiently in terms of time complexity. +- All things considered, to write and learn, it's difficult. The rabbit hole is as deep as your imagination. I haven't even talked about monads or treating code as data (macros); barely even scratched the surface. 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. +# Writing Code Functionally +The best way to learn is by practicing. So we'll spend some time refactoring some code to be more functional and develop some higher order functions and practice currying. + +We'll start with a simple example: a function that takes a list of numbers and returns the sum of the squares of the even numbers. + +For millenia humans have thought about how best to find if a number is even. So the first function is the _most efficient_ way to do so; we know 0 is even, and then numbers alternatve between even and odd. We could prove this by induction but it's up to the reader. + +Then for our `sumOfEvenSquares` we go through all the numbers `upTo` and add its square if it's even. + +To make this functional, we first need to keep `isEven` pure and immutable. We can do this again via recursion; 0 is even, so we do the same alternation between even and odd. + +For sumOfEvenSquares, we can make some higher order function to filter out the even numbers, and then map over the list to square them, and then reduce to sum them. + +```typescript +const isEvenFn = (n: number) => { + if (n === 0) { + return true; + } + + return !isEvenFn(n - 1); +}; + +const map = ( + transform: (x: number, ind: number) => number, + list: number[], + index = 0 +) => { + if (list.length === 0) { + return []; + } + + const [head, ...tail] = list; + const transformed = transform(head, index); + return [transformed].concat(map(transform, tail, index + 1)); +}; + +const filter = (predicate: (x: number) => boolean, list: number[]) => { + if (list.length === 0) { + return []; + } + + const [head, ...tail] = list; + if (predicate(head)) { + return [head].concat(filter(predicate, tail)); + } + return filter(predicate, tail); +}; + +const reduce = ( + accumulation: (x: number, acc: number) => number, + accumulator: number, + list: number[] +) => { + if (list.length === 0) { + return accumulator; + } + + const [head, ...tail] = list; + const newAccumulator = accumulation(head, accumulator); + return reduce(accumulation, newAccumulator, tail); +}; + +const sumOfEvenSquaresFn = (upTo: number) => { + const numbersUpTo = map((_, i) => i, Array(upTo + 1).fill(0)); + const evens = filter((x) => isEvenFn(x), numbersUpTo); + const evensSquared = map((x) => x * x, evens); + + return reduce((x, acc) => x + acc, 0, evensSquared); +}; +``` + +And all our tests pass! + +We can also use currying to make our code more readable and easier to use; for example, we can curry our reduce function to implement a sum function. + +```typescript +const curriedReduce = (accumulation: (x: number, acc: number) => number) => { + return (accumulator: number) => { + const reduce = (list: number[]) => { + if (list.length === 0) { + return accumulator; + } + + const [head, ...tail] = list; + const newAccumulator = accumulation(head, accumulator); + return curriedReduce(accumulation)(newAccumulator)(tail); + }; + + return reduce; + }; +}; + +const sum = curriedReduce((x: number, acc: number) => x + acc)(0); + +... +return sum(evensSquared); +... +``` + +And our tests pass once again! But one of them is commented... hmm... it's pretty big + +And, we fail, getting a "maximum call stack size exceeded" error in our incredibly naive recursive implementation of `filter`, `map`, and `reduce`. Thankfully javascript has these builtin, so we'll use those. + +```typescript +const sumOfEvenSquaresFn = (upTo: number) => { + const numbersUpTo = Array(upTo + 1) + .fill(0) + .map((_, i) => i); + + return numbersUpTo + .filter((x) => isEvenFn(x)) + .map((x) => x * x) + .reduce((x, acc) => x + acc, 0); +}; +``` + +There, much shorter. But, we still get the same error. This time in `isEven` at the recursive call. + +Now, we're gonna start getting into the weeds. The reason this is happening is because essentially the code is being transformed into: + +```typescript +const isEvenFn = (n: number) => { + if (n === 0) { + return true; + } + + const isEven = isEvenFn(n - 1); + return !isEven; +}; +``` + +So everytime we check the number previous, we create a whole new stack frame to evaluate `isEvenFn(n-1)` and return control to the callee. Then we evaluate "!isEven" which requires that stack frame. + +The runtime I'm using, Bun, uses JavaScriptCore - which supports Tail Call Optimization (which is, sadly, not in the JS spec). What is TCO? Tail Call Optimizations allows us to write recursive functions that overwrite the same stack frame when the last expression evaluated does not leave control of the function. + +To do this, we need to ensure that inside of isEvenFn, the last expression evaluated is always `isEvenFn`, not `!isEven`. + +It takes a bit of ingenuity, but to do this, we can use an accumulator _inside an argument_ and change the logic up a bit. Here's what I mean: + +```typescript +const isEvenHelper = (curr: number, max: number, previous = false) => { + if (curr === max) { + return !previous; + } + return isEvenHelper(curr + 1, !previous); +}; +const isEvenFn = (n: number) => { + return isEvenHelper(0, n); +}; +``` + +And there we go! It's passing. But, it's much much slower than the iterative solution. But from my experimentation, Tail Call Optimization won't help us much here since we spend most of our time doing our isEven check in linear time, and we're constantly doing garbage collection whereas our iterative solution has nothing to clean up. + +The keen eyed among you might notice that since we can represent the sum of the first $n$ natural number's squares as (n _ (n + 1) _ (2 \* n + 1)) / 6, then transform each natural number to its nearest even entry by scaling by two, and halve the bounds of the sum. Which is constant time. Yeah. But that's not as fun. ======== -Part Two - A Deeper Dive -=== +# Part Two - Marrying Functions -The Lambda Calculus -=== +# The Lambda Calculus +The Lambda calculus, developed by Alonzo Church in the 1930s, is a "formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution". -Completeness of the Lambda Calculus -=== -Mention how alonzo church actually failed on his first attempt, natural numbers +There are three rules that define the semantics of the Lambda Calculus: -Writing a Compiler -=== +1. "x": A variable is a character or string representing a parameter, and is a valid lambda term. +2. "(λ x . t)" is an "abstraction" - a function definition, taking as input the bound variable x and returning the lambda term t. +3. (M N) is an "application", applying a lambda term M with a lambda term N. -A Compiler As A Sequence of Transformations -=== +There, we're done; the rest is an exercise to the reader. Have a good day everyone! No... I wouldn't be that mean. -Continuation Passing Style -=== +We can "evaluate" a lambda term by performing three types of reduction: + +1. α - conversion: Renaming of bound variables to avoid name clashes (out of scope - we're gonna assume user knows not to bind variable names already chosen elsewhere). +2. β - reduction: Application of a function to an argument. +3. η - reduction: Conversion of a function to a point-free form (out of scope). + +To perform a β-reduction, we substitute the argument into the body of the function, replacing all instances of the bound variable with the argument. + +For example, given the lambda term `(λ x . x) y`, we can do a β-reduction to obtain the value `y` after substituting x. This folllows pretty intuitively, but more formally - we define substitution recursively - given M and N as lambda terms and x,y as variables, as follows: + +1. x[x := N] = N +2. y[x := N] = y` if y != x +3. (M N)[x := P] = (M[x := P]) (N[x := P]) +4. (λ x . M)[x := N] = λ x . M +5. (λ y . M)[x := N] = λ y . (M[x := N]) if y != x and y is not _free_ in N + +A variable is _free_ in a lambda term if it is not bound by a λ abstraction. In other words, a free variable is one that is not a parameter of the function, or in the immediate scope of an abstraction. + +# Lambda Calculus as a Computational Model + +We encode all computation and values in the lambda calculus as a series of functions and applications. For example, we can encode a natural number n as a function that takes a function f and a value x, and applies f to x n times. + +1. `0 = λ f . λ x . x` (no application of f) +2. `1 = λ f . λ x . f x` (one application of f ) +3. `2 = λ f . λ x . f (f x)` (two applications of f) + +This is called the "Church encoding" of the natural numbers. + +We can also define the successor function `succ` as a function that takes a church encoded natural number n, and a function f, and a value x, and performs f on x n times, and then once more. + +1. `succ = λ n . λ f . λ x . n f (f x)` + +For example, we do an application of `succ` on `0`: `(succ 0) = 1`, and `(succ 1) = 2`. + +1. We perform substituion on ,n with the church encoding for 0 +2. Substituting g with f has no effect and returns the term (lambda y . y) +3. (lambda y . y) is the identity function. + +We're Done! We've constructed 1 from 0. We'll go through the same process to construct 2 from 1. + +We can define the boolean values `true` and `false` as functions that take two arguments and return the first and second, respectively. + +1. `true = λ x . λ y . x` +2. `false = λ x . λ y . y` + +We can then define the logical operators `and`, `or`, and `not` in terms of these boolean values. + +1. `and = λ p . λ q . p q false` +2. `or = λ p . λ q . p true q` +3. `not = λ p . p false true` + +(a few examples...) + +Recursion is also possible in the lambda calculus via the funny-looking thing on the board, which you've probably seen before - the Y combinator. The Y combinator should satisfy the equation `(Y f) = (f (Y f))`, and is defined as follows: + +1. `Y = λ f . (λ x . f (x x)) (λ x . f (x x))` + +If we let it run by itself, we can see how our recursion works. We can kinda see some formation of a "stack" of sorts. + +When we apply the Y combinator to a function, it will apply the function to itself, and then apply the function to itself again, and so on, until the function returns a value. + +# Writing a Quick Lambda Calculus Interpreter + +I think the best way to learn the lambda calculus is by building an interpreter for it, it's a quick exercise and it's a lot of fun. + +When writing a Lambda Calculus interpreter, there are a few decisions to be made: + +- [ ] is it the typed or untyped lambda calculus? +- [ ] is the language lazy or strict / eager? +- [ ] call by value or call by name? (call by value - environment, closures) + +``` + LambdaTerm + = Abstraction + / Application + / Variable + + Application + = LPAREN _? left:LambdaTerm _? right:LambdaTerm _? RPAREN { + return { left, right }; + } + + Abstraction + = LPAREN _? LAMBDA _? param:Variable _? DOT _? body:LambdaTerm _? RPAREN { + return { param, body }; + } + + Variable + = name:([a-z] [A-Z0-9a-z]*) { return { name: name[0] + name[1].join('') }; } + + LAMBDA = "λ" / "\" + + DOT = "." + + LPAREN = "(" + + RPAREN = ")" + + _ = (" " / "\n" / "\t" / "\t\n")+ + + private substitute( + ast: LambdaTerm, + param: string, + replacement: LambdaTerm + ): LambdaTerm { + const node = ast as any; + + if (this.isVariable(node)) { + return node.name === param ? replacement : node; + } else if (this.isApplication(node)) { + const left = this.substitute(node.left, param, replacement); + const right = this.substitute(node.right, param, replacement); + + return { left, right } as Application; + } + + return { + param: node.param, + body: this.substitute(node.body, param, replacement) as Abstraction, + }; + } + + public fullBetaReduction(ast: LambdaTerm = this.ast): LambdaTerm { + const node = ast as any; + + if (this.isApplication(node)) { + const { left, right } = node as Application; + + if (this.isAbstraction(left)) { + return this.fullBetaReduction( + this.substitute(left.body, left.param.name, right) + ); + } + + return { + left: this.fullBetaReduction(left), + right: this.fullBetaReduction(right), + } as Application; + } + + if (this.isAbstraction(node)) { + return { + param: node.param, + body: this.fullBetaReduction(node.body), + } as Abstraction; + } + + return node; + } +``` + +There are a lot of bugs in our interpreter; first, there is no alpha conversion, there's a high chance we could have name conflicts within lambda terms. We could use de brujin indexing, but that's outside the scope of this presentation. + +# Further + +Obviously we've only scratched the surface of the lambda calculus and functional programming; I didn't even mention a lot of things I wanted to get to. So here's a list you can study on your own: + +Pattern matching is a fun paradigm and allows one to write much more declarative code. + +The lambda calculus is a "universal model of computation", meaning that any computable function can be expressed in the lambda calculus. This is known as the Church-Turing thesis, and is the basis for the theory of computation. + +I highly recommend reading the paper "Compilation of Lambda-Calculus into Functional Machine Code". You know how we alluded to the "stack" that the Y combinator produced? This paper goes into how we can utilize that stack to compile lambda calculus into machine code on a stack machine by reversing the order through continuation passing style. + +# Extending our interpreter into A Compiler As A Sequence of Transformations + +# Continuation Passing Style ======= -Conclusion -=== +# Conclusions and Comparisons diff --git a/src/components/function_box.tsx b/src/components/function_box.tsx index e1ed497..9f72979 100644 --- a/src/components/function_box.tsx +++ b/src/components/function_box.tsx @@ -72,9 +72,13 @@ export class FunctionBox extends Node { this.padding = props?.padding ?? 100; this.workingText = props?.workingText ?? "👷‍♀️⚙️"; + this.idlingText = props?.idlingText ?? "😴"; this.isChild = props?.isChild ?? false; + if (this.isChild) { + this.idlingText += " | 🧬"; + } this.outputFontSize = props?.outputFontSize ?? 30; this.inputFontSize = props?.inputFontSize ?? 30; @@ -82,80 +86,97 @@ export class FunctionBox extends Node { this.add( - - {range(this.arity).map((i) => ( - - + + + {range(this.arity).map((i) => ( + + + + + ))} + + + + + {this.idlingText} + + + + + + + + + - ))} - - - - - {this.idlingText} - - - - - - - - - - , @@ -252,11 +273,15 @@ export class FunctionBox extends Node { const output = this.function(...this.currentArgs.map((input) => input.val)); switch (typeof output) { case "function": + console.dir(output); + yield this.output().add( , ); diff --git a/src/components/lambda_reducer.tsx b/src/components/lambda_reducer.tsx new file mode 100644 index 0000000..bef8fde --- /dev/null +++ b/src/components/lambda_reducer.tsx @@ -0,0 +1,188 @@ +import { Rect, Node, Txt, Line, NodeProps } from "@motion-canvas/2d"; +import { createRef, all, range, makeRef, DEFAULT } from "@motion-canvas/core"; + +import { + CodeBlock, + insert, + edit, + lines, +} from "@motion-canvas/2d/lib/components/CodeBlock"; +import { Abstraction, Application, LambdaTerm, parse } from "../parser/parser"; +import { + EditOperation, + calculateLevenshteinOperations, +} from "../utils/levenshtein"; + +export interface LambdaReducerProps extends NodeProps { + lambdaTerm?: string; + definitions?: Record; +} + +export class LambdaReducer extends Node { + private readonly codeBlock = createRef(); + private reductions: string[]; + private ast: LambdaTerm; + + public constructor(props?: LambdaReducerProps) { + super({ + ...props, + }); + + const functionDefinitions = props.definitions ?? {}; + let lambdaTerm = props.lambdaTerm ?? "((λ x . x) (λ y . y))"; + while ( + Object.keys(functionDefinitions).some((x) => lambdaTerm.includes(x)) + ) { + lambdaTerm = Object.entries(functionDefinitions).reduce( + (acc, [name, definition]) => { + return acc.replace(new RegExp(name, "g"), definition); + }, + lambdaTerm + ); + } + console.log(lambdaTerm); + + this.reductions = [props.lambdaTerm, lambdaTerm]; + this.ast = parse(lambdaTerm); + this.add( + + ); + } + + private getReductions() { + return this.reductions.filter((x) => x).join("\n=> "); + } + + private isApplication(ast: any): boolean { + return ast.left && ast.right; + } + + private isAbstraction(ast: any): boolean { + return ast.param && ast.body; + } + + private isVariable(ast: any): boolean { + return !!ast.name; + } + + private substitute( + ast: LambdaTerm, + param: string, + replacement: LambdaTerm + ): LambdaTerm { + const node = ast as any; + + if (this.isVariable(node)) { + return node.name === param ? replacement : node; + } else if (this.isApplication(node)) { + const left = this.substitute(node.left, param, replacement); + const right = this.substitute(node.right, param, replacement); + + return { left, right } as Application; + } + + return { + param: node.param, + body: this.substitute(node.body, param, replacement) as Abstraction, + }; + } + + public getCode() { + return this.emitCode(this.ast); + } + + private betaReduceStep(ast: LambdaTerm): LambdaTerm { + const node = ast as any; + + if (this.isApplication(node)) { + const left = node.left as any; + const right = node.right as any; + + if (this.isAbstraction(left)) { + return this.substitute(left.body, left.param.name, right); + } + + return { + left: this.betaReduceStep(left), + right: this.betaReduceStep(right), + } as Application; + } + + if (this.isAbstraction(node)) { + return { + param: node.param, + body: this.betaReduceStep(node.body), + } as Abstraction; + } + + return node; + } + + private emitCode(ast: LambdaTerm): string { + const node = ast as any; + + if (this.isVariable(node)) { + return node.name; + } else if (this.isApplication(node)) { + return `(${this.emitCode(node.left)} ${this.emitCode(node.right)})`; + } else if (this.isAbstraction(node)) { + return `(λ ${node.param.name}.${this.emitCode(node.body)})`; + } + + throw new Error("Invalid lambda term"); + } + + public isDone() { + return ( + this.emitCode(this.betaReduceStep(this.ast)) === this.emitCode(this.ast) + ); + } + + public *step(duration: number) { + yield* this.codeBlock().selection([], 0); + + const old = this.getReductions(); + const next = this.betaReduceStep(this.ast); + const nextCode = this.emitCode(next); + + const operations = calculateLevenshteinOperations( + this.reductions.at(-1), + nextCode + ); + + yield* this.codeBlock().edit(duration)`${old}${insert( + "\n=> " + this.reductions.at(-1) + )}`; + + let code = `this.codeBlock().edit(duration)\`${old}\\n=> `; + + // this is SO FUCKING CURSED + window.editInLambda = edit; + window.insertInLambda = insert; + for (const { operation, diff } of operations) { + const next = `'${diff.new ?? ""}'`; + const old = `'${diff.old ?? ""}'`; + + if (operation === EditOperation.Edit) { + code += `\${editInLambda(${old}, ${next})}`; + continue; + } + if (operation === EditOperation.Insert) { + code += `\${insertInLambda(${next})}`; + continue; + } + code += diff.new ?? ""; + } + code += "`;"; + yield* eval(code); + + yield* this.codeBlock().selection(lines(this.reductions.length), 0.3); + this.reductions.push(nextCode); + this.ast = next; + } +} diff --git a/src/parser/grammar.pegjs b/src/parser/grammar.pegjs new file mode 100644 index 0000000..48a7564 --- /dev/null +++ b/src/parser/grammar.pegjs @@ -0,0 +1,27 @@ +LambdaTerm + = Abstraction + / Application + / Variable + +Application + = LPAREN _? left:LambdaTerm _? right:LambdaTerm _? RPAREN { + return { left, right }; + } + +Abstraction + = LPAREN _? LAMBDA _? param:Variable _? DOT _? body:LambdaTerm _? RPAREN { + return { param, body }; + } + +Variable + = name:([a-zA-Z][A-Z0-9a-z]*) { return { name: name[0] + name[1].join('') }; } + +LAMBDA = "λ" + +DOT = "." + +LPAREN = "(" + +RPAREN = ")" + +_ = (" " / "\n" / "\t" / "\t\n")+ \ No newline at end of file diff --git a/src/parser/parser.ts b/src/parser/parser.ts new file mode 100644 index 0000000..3dcbd6c --- /dev/null +++ b/src/parser/parser.ts @@ -0,0 +1,1282 @@ +/* eslint-disable */ + + + +const peggyParser: {parse: any, SyntaxError: any, DefaultTracer?: any} = // Generated by Peggy 3.0.2. +// +// https://peggyjs.org/ +// @ts-ignore +(function() { +// @ts-ignore + "use strict"; + +// @ts-ignore +function peg$subclass(child, parent) { +// @ts-ignore + function C() { this.constructor = child; } +// @ts-ignore + C.prototype = parent.prototype; +// @ts-ignore + child.prototype = new C(); +} + +// @ts-ignore +function peg$SyntaxError(message, expected, found, location) { +// @ts-ignore + var self = Error.call(this, message); + // istanbul ignore next Check is a necessary evil to support older environments +// @ts-ignore + if (Object.setPrototypeOf) { +// @ts-ignore + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } +// @ts-ignore + self.expected = expected; +// @ts-ignore + self.found = found; +// @ts-ignore + self.location = location; +// @ts-ignore + self.name = "SyntaxError"; +// @ts-ignore + return self; +} + +// @ts-ignore +peg$subclass(peg$SyntaxError, Error); + +// @ts-ignore +function peg$padEnd(str, targetLength, padString) { +// @ts-ignore + padString = padString || " "; +// @ts-ignore + if (str.length > targetLength) { return str; } +// @ts-ignore + targetLength -= str.length; +// @ts-ignore + padString += padString.repeat(targetLength); +// @ts-ignore + return str + padString.slice(0, targetLength); +} + +// @ts-ignore +peg$SyntaxError.prototype.format = function(sources) { +// @ts-ignore + var str = "Error: " + this.message; +// @ts-ignore + if (this.location) { +// @ts-ignore + var src = null; +// @ts-ignore + var k; +// @ts-ignore + for (k = 0; k < sources.length; k++) { +// @ts-ignore + if (sources[k].source === this.location.source) { +// @ts-ignore + src = sources[k].text.split(/\r\n|\n|\r/g); +// @ts-ignore + break; + } + } +// @ts-ignore + var s = this.location.start; +// @ts-ignore + var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) +// @ts-ignore + ? this.location.source.offset(s) +// @ts-ignore + : s; +// @ts-ignore + var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; +// @ts-ignore + if (src) { +// @ts-ignore + var e = this.location.end; +// @ts-ignore + var filler = peg$padEnd("", offset_s.line.toString().length, ' '); +// @ts-ignore + var line = src[s.line - 1]; +// @ts-ignore + var last = s.line === e.line ? e.column : line.length + 1; +// @ts-ignore + var hatLen = (last - s.column) || 1; +// @ts-ignore + str += "\n --> " + loc + "\n" +// @ts-ignore + + filler + " |\n" +// @ts-ignore + + offset_s.line + " | " + line + "\n" +// @ts-ignore + + filler + " | " + peg$padEnd("", s.column - 1, ' ') +// @ts-ignore + + peg$padEnd("", hatLen, "^"); +// @ts-ignore + } else { +// @ts-ignore + str += "\n at " + loc; + } + } +// @ts-ignore + return str; +}; + +// @ts-ignore +peg$SyntaxError.buildMessage = function(expected, found) { +// @ts-ignore + var DESCRIBE_EXPECTATION_FNS = { +// @ts-ignore + literal: function(expectation) { +// @ts-ignore + return "\"" + literalEscape(expectation.text) + "\""; + }, + +// @ts-ignore + class: function(expectation) { +// @ts-ignore + var escapedParts = expectation.parts.map(function(part) { +// @ts-ignore + return Array.isArray(part) +// @ts-ignore + ? classEscape(part[0]) + "-" + classEscape(part[1]) +// @ts-ignore + : classEscape(part); + }); + +// @ts-ignore + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]"; + }, + +// @ts-ignore + any: function() { +// @ts-ignore + return "any character"; + }, + +// @ts-ignore + end: function() { +// @ts-ignore + return "end of input"; + }, + +// @ts-ignore + other: function(expectation) { +// @ts-ignore + return expectation.description; + } + }; + +// @ts-ignore + function hex(ch) { +// @ts-ignore + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + +// @ts-ignore + function literalEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/"/g, "\\\"") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function classEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/\]/g, "\\]") +// @ts-ignore + .replace(/\^/g, "\\^") +// @ts-ignore + .replace(/-/g, "\\-") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function describeExpectation(expectation) { +// @ts-ignore + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + +// @ts-ignore + function describeExpected(expected) { +// @ts-ignore + var descriptions = expected.map(describeExpectation); +// @ts-ignore + var i, j; + +// @ts-ignore + descriptions.sort(); + +// @ts-ignore + if (descriptions.length > 0) { +// @ts-ignore + for (i = 1, j = 1; i < descriptions.length; i++) { +// @ts-ignore + if (descriptions[i - 1] !== descriptions[i]) { +// @ts-ignore + descriptions[j] = descriptions[i]; +// @ts-ignore + j++; + } + } +// @ts-ignore + descriptions.length = j; + } + +// @ts-ignore + switch (descriptions.length) { +// @ts-ignore + case 1: +// @ts-ignore + return descriptions[0]; + +// @ts-ignore + case 2: +// @ts-ignore + return descriptions[0] + " or " + descriptions[1]; + +// @ts-ignore + default: +// @ts-ignore + return descriptions.slice(0, -1).join(", ") +// @ts-ignore + + ", or " +// @ts-ignore + + descriptions[descriptions.length - 1]; + } + } + +// @ts-ignore + function describeFound(found) { +// @ts-ignore + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + +// @ts-ignore + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +// @ts-ignore +function peg$parse(input, options) { +// @ts-ignore + options = options !== undefined ? options : {}; + +// @ts-ignore + var peg$FAILED = {}; +// @ts-ignore + var peg$source = options.grammarSource; + +// @ts-ignore + var peg$startRuleFunctions = { LambdaTerm: peg$parseLambdaTerm }; +// @ts-ignore + var peg$startRuleFunction = peg$parseLambdaTerm; + +// @ts-ignore + var peg$c0 = "\u03BB"; + var peg$c1 = "."; + var peg$c2 = "("; + var peg$c3 = ")"; + var peg$c4 = " "; + var peg$c5 = "\n"; + var peg$c6 = "\t"; + var peg$c7 = "\t\n"; + + var peg$r0 = /^[a-zA-Z]/; + var peg$r1 = /^[A-Z0-9a-z]/; + + var peg$e0 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); + var peg$e1 = peg$classExpectation([["A", "Z"], ["0", "9"], ["a", "z"]], false, false); + var peg$e2 = peg$literalExpectation("\u03BB", false); + var peg$e3 = peg$literalExpectation(".", false); + var peg$e4 = peg$literalExpectation("(", false); + var peg$e5 = peg$literalExpectation(")", false); + var peg$e6 = peg$literalExpectation(" ", false); + var peg$e7 = peg$literalExpectation("\n", false); + var peg$e8 = peg$literalExpectation("\t", false); + var peg$e9 = peg$literalExpectation("\t\n", false); +// @ts-ignore + + var peg$f0 = function(left, right) { +// @ts-ignore + return { left, right }; + };// @ts-ignore + + var peg$f1 = function(param, body) { +// @ts-ignore + return { param, body }; + };// @ts-ignore + + var peg$f2 = function(name) {// @ts-ignore + return { name: name[0] + name[1].join('') }; }; +// @ts-ignore + var peg$currPos = 0; +// @ts-ignore + var peg$savedPos = 0; +// @ts-ignore + var peg$posDetailsCache = [{ line: 1, column: 1 }]; +// @ts-ignore + var peg$maxFailPos = 0; +// @ts-ignore + var peg$maxFailExpected = []; +// @ts-ignore + var peg$silentFails = 0; + +// @ts-ignore + var peg$result; + +// @ts-ignore + if ("startRule" in options) { +// @ts-ignore + if (!(options.startRule in peg$startRuleFunctions)) { +// @ts-ignore + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + +// @ts-ignore + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + +// @ts-ignore + function text() { +// @ts-ignore + return input.substring(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function offset() { +// @ts-ignore + return peg$savedPos; + } + +// @ts-ignore + function range() { +// @ts-ignore + return { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: peg$savedPos, +// @ts-ignore + end: peg$currPos + }; + } + +// @ts-ignore + function location() { +// @ts-ignore + return peg$computeLocation(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function expected(description, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + [peg$otherExpectation(description)], +// @ts-ignore + input.substring(peg$savedPos, peg$currPos), +// @ts-ignore + location + ); + } + +// @ts-ignore + function error(message, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildSimpleError(message, location); + } + +// @ts-ignore + function peg$literalExpectation(text, ignoreCase) { +// @ts-ignore + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$classExpectation(parts, inverted, ignoreCase) { +// @ts-ignore + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$anyExpectation() { +// @ts-ignore + return { type: "any" }; + } + +// @ts-ignore + function peg$endExpectation() { +// @ts-ignore + return { type: "end" }; + } + +// @ts-ignore + function peg$otherExpectation(description) { +// @ts-ignore + return { type: "other", description: description }; + } + +// @ts-ignore + function peg$computePosDetails(pos) { +// @ts-ignore + var details = peg$posDetailsCache[pos]; +// @ts-ignore + var p; + +// @ts-ignore + if (details) { +// @ts-ignore + return details; +// @ts-ignore + } else { +// @ts-ignore + p = pos - 1; +// @ts-ignore + while (!peg$posDetailsCache[p]) { +// @ts-ignore + p--; + } + +// @ts-ignore + details = peg$posDetailsCache[p]; +// @ts-ignore + details = { +// @ts-ignore + line: details.line, +// @ts-ignore + column: details.column + }; + +// @ts-ignore + while (p < pos) { +// @ts-ignore + if (input.charCodeAt(p) === 10) { +// @ts-ignore + details.line++; +// @ts-ignore + details.column = 1; +// @ts-ignore + } else { +// @ts-ignore + details.column++; + } + +// @ts-ignore + p++; + } + +// @ts-ignore + peg$posDetailsCache[pos] = details; + +// @ts-ignore + return details; + } + } + +// @ts-ignore + function peg$computeLocation(startPos, endPos, offset) { +// @ts-ignore + var startPosDetails = peg$computePosDetails(startPos); +// @ts-ignore + var endPosDetails = peg$computePosDetails(endPos); + +// @ts-ignore + var res = { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: { +// @ts-ignore + offset: startPos, +// @ts-ignore + line: startPosDetails.line, +// @ts-ignore + column: startPosDetails.column + }, +// @ts-ignore + end: { +// @ts-ignore + offset: endPos, +// @ts-ignore + line: endPosDetails.line, +// @ts-ignore + column: endPosDetails.column + } + }; +// @ts-ignore + if (offset && peg$source && (typeof peg$source.offset === "function")) { +// @ts-ignore + res.start = peg$source.offset(res.start); +// @ts-ignore + res.end = peg$source.offset(res.end); + } +// @ts-ignore + return res; + } + +// @ts-ignore + function peg$fail(expected) { +// @ts-ignore + if (peg$currPos < peg$maxFailPos) { return; } + +// @ts-ignore + if (peg$currPos > peg$maxFailPos) { +// @ts-ignore + peg$maxFailPos = peg$currPos; +// @ts-ignore + peg$maxFailExpected = []; + } + +// @ts-ignore + peg$maxFailExpected.push(expected); + } + +// @ts-ignore + function peg$buildSimpleError(message, location) { +// @ts-ignore + return new peg$SyntaxError(message, null, null, location); + } + +// @ts-ignore + function peg$buildStructuredError(expected, found, location) { +// @ts-ignore + return new peg$SyntaxError( +// @ts-ignore + peg$SyntaxError.buildMessage(expected, found), +// @ts-ignore + expected, +// @ts-ignore + found, +// @ts-ignore + location + ); + } + +// @ts-ignore + function // @ts-ignore +peg$parseLambdaTerm() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseAbstraction(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseApplication(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseVariable(); + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseApplication() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseLPAREN(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + s3 = peg$parseLambdaTerm(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (s4 === peg$FAILED) { +// @ts-ignore + s4 = null; + } +// @ts-ignore + s5 = peg$parseLambdaTerm(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parse_(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + s7 = peg$parseRPAREN(); +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f0(s3, s5); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAbstraction() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseLPAREN(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + s3 = peg$parseLAMBDA(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (s4 === peg$FAILED) { +// @ts-ignore + s4 = null; + } +// @ts-ignore + s5 = peg$parseVariable(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parse_(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + s7 = peg$parseDOT(); +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + s8 = peg$parse_(); +// @ts-ignore + if (s8 === peg$FAILED) { +// @ts-ignore + s8 = null; + } +// @ts-ignore + s9 = peg$parseLambdaTerm(); +// @ts-ignore + if (s9 !== peg$FAILED) { +// @ts-ignore + s10 = peg$parse_(); +// @ts-ignore + if (s10 === peg$FAILED) { +// @ts-ignore + s10 = null; + } +// @ts-ignore + s11 = peg$parseRPAREN(); +// @ts-ignore + if (s11 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f1(s5, s9); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVariable() { +// @ts-ignore + var s0, s1, s2, s3, s4; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$currPos; +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = []; +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s4 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } +// @ts-ignore + while (s4 !== peg$FAILED) { +// @ts-ignore + s3.push(s4); +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s4 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + } +// @ts-ignore + s2 = [s2, s3]; +// @ts-ignore + s1 = s2; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s1; +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f2(s1); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseLAMBDA() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 955) { +// @ts-ignore + s0 = peg$c0; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseDOT() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s0 = peg$c1; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseLPAREN() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s0 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseRPAREN() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s0 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parse_() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = []; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 32) { +// @ts-ignore + s1 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 10) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 9) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c7) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + } + } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + while (s1 !== peg$FAILED) { +// @ts-ignore + s0.push(s1); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 32) { +// @ts-ignore + s1 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 10) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 9) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c7) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } + } + } + } + } +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + peg$result = peg$startRuleFunction(); + +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos === input.length) { +// @ts-ignore + return peg$result; +// @ts-ignore + } else { +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos < input.length) { +// @ts-ignore + peg$fail(peg$endExpectation()); + } + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + peg$maxFailExpected, +// @ts-ignore + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, +// @ts-ignore + peg$maxFailPos < input.length +// @ts-ignore + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) +// @ts-ignore + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +// @ts-ignore + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})() + +export interface FilePosition { + offset: number; + line: number; + column: number; +} + +export interface FileRange { + start: FilePosition; + end: FilePosition; + source: string; +} + +export interface LiteralExpectation { + type: "literal"; + text: string; + ignoreCase: boolean; +} + +export interface ClassParts extends Array {} + +export interface ClassExpectation { + type: "class"; + parts: ClassParts; + inverted: boolean; + ignoreCase: boolean; +} + +export interface AnyExpectation { + type: "any"; +} + +export interface EndExpectation { + type: "end"; +} + +export interface OtherExpectation { + type: "other"; + description: string; +} + +export type Expectation = LiteralExpectation | ClassExpectation | AnyExpectation | EndExpectation | OtherExpectation; + +declare class _PeggySyntaxError extends Error { + public static buildMessage(expected: Expectation[], found: string | null): string; + public message: string; + public expected: Expectation[]; + public found: string | null; + public location: FileRange; + public name: string; + constructor(message: string, expected: Expectation[], found: string | null, location: FileRange); + format(sources: { + source?: any; + text: string; + }[]): string; +} + +export interface TraceEvent { + type: string; + rule: string; + result?: any; + location: FileRange; + } + +declare class _DefaultTracer { + private indentLevel: number; + public trace(event: TraceEvent): void; +} + +peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; + +export interface ParseOptions { + filename?: string; + startRule?: "LambdaTerm"; + tracer?: any; + [key: string]: any; +} +export type ParseFunction = ( + input: string, + options?: Options + ) => Options extends { startRule: infer StartRule } ? + StartRule extends "LambdaTerm" ? LambdaTerm : LambdaTerm + : LambdaTerm; +export const parse: ParseFunction = peggyParser.parse; + +export const PeggySyntaxError = peggyParser.SyntaxError as typeof _PeggySyntaxError; + +export type PeggySyntaxError = _PeggySyntaxError; + +// These types were autogenerated by ts-pegjs +export type LambdaTerm = Abstraction | Application | Variable; +export type Application = { left: LambdaTerm; right: LambdaTerm }; +export type Abstraction = { param: Variable; body: LambdaTerm }; +export type Variable = { name: string }; +export type LAMBDA = "\u03bb"; +export type DOT = "."; +export type LPAREN = "("; +export type RPAREN = ")"; +export type _ = (" " | "\n" | "\t" | "\t\n")[]; diff --git a/src/project.meta b/src/project.meta index 7c59eb3..4ebbd36 100644 --- a/src/project.meta +++ b/src/project.meta @@ -3,8 +3,8 @@ "shared": { "background": "rgb(30,30,46)", "range": [ - 0, - 33.18333233333333 + 3.5666656666666667, + null ], "size": { "x": 1920, diff --git a/src/scenes/boolean_algebra_lambda.meta b/src/scenes/boolean_algebra_lambda.meta new file mode 100644 index 0000000..385a68c --- /dev/null +++ b/src/scenes/boolean_algebra_lambda.meta @@ -0,0 +1,5 @@ +{ + "version": 0, + "timeEvents": [], + "seed": 2667185663 +} \ No newline at end of file diff --git a/src/scenes/boolean_algebra_lambda.tsx b/src/scenes/boolean_algebra_lambda.tsx new file mode 100644 index 0000000..2639461 --- /dev/null +++ b/src/scenes/boolean_algebra_lambda.tsx @@ -0,0 +1,49 @@ +import { Layout, makeScene2D } from "@motion-canvas/2d"; +import { + Direction, + beginSlide, + createRef, + slideTransition, +} from "@motion-canvas/core"; +import { LambdaReducer } from "../components/lambda_reducer"; +import { baseDefinitions } from "../utils/lambdas"; + +export default makeScene2D(function* (view) { + const lambdaReducer = createRef(); + const layout = createRef(); + + view.add( + + ); + yield* slideTransition(Direction.Right); + yield* beginSlide("Boolean Reductions"); + + for (const term of [ + "((false one) zero)", + "((true one) zero)", + "(((if true) one) zero)", + ]) { + yield* layout().opacity(0, 0.5); + layout().add( + + ); + yield* layout().opacity(1, 0.5); + + yield* beginSlide("Next Reduction " + term); + for (let i = 0; !lambdaReducer().isDone(); i++) { + yield* lambdaReducer().step(0.5); + yield* beginSlide(term + " Next Step " + i); + } + layout().removeChildren(); + } +}); diff --git a/src/scenes/boolean_encoding.meta b/src/scenes/boolean_encoding.meta new file mode 100644 index 0000000..8f4d744 --- /dev/null +++ b/src/scenes/boolean_encoding.meta @@ -0,0 +1,5 @@ +{ + "version": 0, + "timeEvents": [], + "seed": 2394701117 +} \ No newline at end of file diff --git a/src/scenes/boolean_encoding.tsx b/src/scenes/boolean_encoding.tsx new file mode 100644 index 0000000..60a0ea9 --- /dev/null +++ b/src/scenes/boolean_encoding.tsx @@ -0,0 +1,42 @@ +import { Layout, Txt, makeScene2D } from "@motion-canvas/2d"; +import { + Direction, + beginSlide, + createRef, + slideTransition, +} from "@motion-canvas/core"; +import { theme } from "../theme"; + +const boolE = [ + "true = λ t . λ f . t", + "false = λ t . λ f . f", + "if = λ b . λ x . λ y . b x y", + "not = λ b . b false true", + "and = λ b . λ c . b c false", +]; + +export default makeScene2D(function* (view) { + const numerals = createRef(); + + view.add( + + + Boolean Encoding + + + + ); + + yield* slideTransition(Direction.Right); + yield* beginSlide("The Lambda Calculus - Boolean Encoding"); + + for (const bool of boolE) { + yield* numerals().text(numerals().text() + "\n\n" + bool, 1); + yield* beginSlide("boolean - " + bool); + } +}); diff --git a/src/scenes/church_encoding.meta b/src/scenes/church_encoding.meta new file mode 100644 index 0000000..638e825 --- /dev/null +++ b/src/scenes/church_encoding.meta @@ -0,0 +1,5 @@ +{ + "version": 0, + "timeEvents": [], + "seed": 533121975 +} \ No newline at end of file diff --git a/src/scenes/church_encoding.tsx b/src/scenes/church_encoding.tsx new file mode 100644 index 0000000..70bafe4 --- /dev/null +++ b/src/scenes/church_encoding.tsx @@ -0,0 +1,41 @@ +import { Layout, Txt, makeScene2D } from "@motion-canvas/2d"; +import { + Direction, + beginSlide, + createRef, + slideTransition, +} from "@motion-canvas/core"; +import { theme } from "../theme"; + +const churchNumerals = [ + "0 = λ f . λ x . x (no application of f)", + "1 = λ f . λ x . f x (one application of f)", + "2 = λ f . λ x . f (f x) (two applications of f)", + "succ = λ n . λ f . λ x . f ((n f) x)", +]; + +export default makeScene2D(function* (view) { + const numerals = createRef(); + + view.add( + + + Church Encoding + + + + ); + + yield* slideTransition(Direction.Right); + yield* beginSlide("The Lambda Calculus - Church Encoding"); + + for (const numeral of churchNumerals) { + yield* numerals().text(numerals().text() + "\n\n" + numeral, 1); + yield* beginSlide("substitution - " + numeral); + } +}); diff --git a/src/scenes/currying.meta b/src/scenes/currying.meta new file mode 100644 index 0000000..1dd71f1 --- /dev/null +++ b/src/scenes/currying.meta @@ -0,0 +1,5 @@ +{ + "version": 0, + "timeEvents": [], + "seed": 2262306570 +} \ No newline at end of file diff --git a/src/scenes/currying.tsx b/src/scenes/currying.tsx new file mode 100644 index 0000000..01ce24a --- /dev/null +++ b/src/scenes/currying.tsx @@ -0,0 +1,114 @@ +import { Video, Layout, makeScene2D } from "@motion-canvas/2d"; +import { + Direction, + all, + beginSlide, + createRef, + slideTransition, +} from "@motion-canvas/core"; +import { PEOPLE, Person, PersonI } from "../components/person"; +import { birthdayCardFn, valentineCardFn } from "./generalized"; +import { CardI } from "./birthday_letters"; +import { FunctionBox } from "../components/function_box"; +import curry from "../../public/img/curry.mp4"; + +export const cardGeneratorsFor = ( + person: PersonI, +): ((type: string) => () => CardI) => { + const birthdayCardGenerator = () => birthdayCardFn(person); + const valentineCardGenerator = () => valentineCardFn(person); + + const messageType = (type: string) => { + switch (type) { + case "valentine": + return valentineCardGenerator; + case "birthday": + return birthdayCardGenerator; + } + throw new Error(type + " not implemented"); + }; + + return messageType; +}; + +export const cardGeneratorsForSource = `const cardGeneratorsFor = (person: PersonI): ((type: string) => () => CardI) => { + const birthdayCardGenerator = () => birthdayCardFn(person); + const valentineCardGenerator = () => valentineCardFn(person); + + const generatorForType = (type: string) => { + switch (type) { + case "valentine": + return valentineCardGenerator; + case "birthday": + return birthdayCardGenerator; + } + + throw new Error(type + " not implemented"); + }; + + return generatorForType; +};`; + +export default makeScene2D(function* (view) { + const box = createRef(); + const vid = createRef