diff --git a/README b/README.md similarity index 100% rename from README rename to README.md diff --git a/docs/notebooks/0._This_Implementation_of_Joy_in_Python.html b/docs/notebooks/0._This_Implementation_of_Joy_in_Python.html deleted file mode 100644 index decee02..0000000 --- a/docs/notebooks/0._This_Implementation_of_Joy_in_Python.html +++ /dev/null @@ -1,13939 +0,0 @@ - - -
- -This implementation is meant as a tool for exploring the programming model and method of Joy. Python seems like a great implementation language for Joy for several reasons.
-We can lean on the Python immutable types for our basic semantics and types: ints, floats, strings, and tuples, which enforces functional purity. We get garbage collection for free. Compilation via Cython. Glue language with loads of libraries.
- -The main way to interact with the Joy interpreter is through a simple REPL that you start by running the package:
- -$ python -m joy
-Joypy - Copyright © 2017 Simon Forman
-This program comes with ABSOLUTELY NO WARRANTY; for details type "warranty".
-This is free software, and you are welcome to redistribute it
-under certain conditions; type "sharing" for details.
-Type "words" to see a list of all words, and "[<name>] help" to print the
-docs for a word.
-
-
- <-top
-
-joy? _
-
-
-The <-top marker points to the top of the (initially empty) stack. You can enter Joy notation at the prompt and a trace of evaluation will be printed followed by the stack and prompt again:
joy? 23 sqr 18 +
- . 23 sqr 18 +
- 23 . sqr 18 +
- 23 . dup mul 18 +
- 23 23 . mul 18 +
- 529 . 18 +
-529 18 . +
- 547 .
-
-547 <-top
-
-joy?
-
-In Joy, in addition to the types Boolean, integer, float, and string, there is a single sequence type represented by enclosing a sequence of terms in brackets [...]. This sequence type is used to represent both the stack and the expression. It is a cons list made from Python tuples.
import inspect
-import joy.utils.stack
-
-
-print(inspect.getdoc(joy.utils.stack))
-The 0th item in the list will be on the top of the stack and vise versa.
- -joy.utils.stack.list_to_stack([1, 2, 3])
-list(joy.utils.stack.iter_stack((1, (2, (3, ())))))
-This requires reversing the sequence (or iterating backwards) otherwise:
- -stack = ()
-
-for n in [1, 2, 3]:
- stack = n, stack
-
-print(stack)
-print(list(joy.utils.stack.iter_stack(stack)))
-Because Joy lists are made out of Python tuples they are immutable, so all Joy datastructures are purely functional.
- -joy() function.¶The joy() function is extrememly simple. It accepts a stack, an expression, and a dictionary, and it iterates through the expression putting values onto the stack and delegating execution to functions it looks up in the dictionary.
Each function is passed the stack, expression, and dictionary and returns them. Whatever the function returns becomes the new stack, expression, and dictionary. (The dictionary is passed to enable e.g. writing words that let you enter new words into the dictionary at runtime, which nothing does yet and may be a bad idea, and the help command.)
import joy.joy
-
-print(inspect.getsource(joy.joy.joy))
-The joy() function accepts a "viewer" function which it calls on each iteration passing the current stack and expression just before evaluation. This can be used for tracing, breakpoints, retrying after exceptions, or interrupting an evaluation and saving to disk or sending over the network to resume later. The stack and expression together contain all the state of the computation at each step.
TracePrinter.¶A viewer records each step of the evaluation of a Joy program. The TracePrinter has a facility for printing out a trace of the evaluation, one line per step. Each step is aligned to the current interpreter position, signified by a period separating the stack on the left from the pending expression ("continuation") on the right.
One day I thought, What happens if you rewrite Joy to use CSP? I made all the functions accept and return the expression as well as the stack and found that all the combinators could be rewritten to work by modifying the expression rather than making recursive calls to the joy() function.
import joy.parser
-
-print(inspect.getdoc(joy.parser))
-The parser is extremely simple, the undocumented re.Scanner class does most of the tokenizing work and then you just build the tuple structure out of the tokens. There's no Abstract Syntax Tree or anything like that.
print(inspect.getsource(joy.parser._parse))
-That's pretty much all there is to it.
- -joy.parser.text_to_expression('1 2 3 4 5') # A simple sequence.
-joy.parser.text_to_expression('[1 2 3] 4 5') # Three items, the first is a list with three items
-joy.parser.text_to_expression('1 23 ["four" [-5.0] cons] 8888') # A mixed bag. cons is
- # a Symbol, no lookup at
- # parse-time. Haiku docs.
-joy.parser.text_to_expression('[][][][][]') # Five empty lists.
-joy.parser.text_to_expression('[[[[[]]]]]') # Five nested lists.
-The Joy library of functions (aka commands, or "words" after Forth usage) encapsulates all the actual functionality (no pun intended) of the Joy system. There are simple functions such as addition add (or +, the library module supports aliases), and combinators which provide control-flow and higher-order operations.
import joy.library
-
-print(' '.join(sorted(joy.library.initialize())))
-Many of the functions are defined in Python, like dip:
print(inspect.getsource(joy.library.dip))
-Some functions are defined in equations in terms of other functions. When the interpreter executes a definition function that function just pushes its body expression onto the pending expression (the continuation) and returns control to the interpreter.
- -print(joy.library.definitions)
-Currently, there's no function to add new definitions to the dictionary from "within" Joy code itself. Adding new definitions remains a meta-interpreter action. You have to do it yourself, in Python, and wash your hands afterward.
-It would be simple enough to define one, but it would open the door to name binding and break the idea that all state is captured in the stack and expression. There's an implicit standard dictionary that defines the actual semantics of the syntactic stack and expression datastructures (which only contain symbols, not the actual functions. Pickle some and see for yourself.)
-Which brings me to talking about one of my hopes and dreams for this notation: "There should be only one." What I mean is that there should be one universal standard dictionary of commands, and all bespoke work done in a UI for purposes takes place by direct interaction and macros. There would be a Grand Refactoring biannually (two years, not six months, that's semi-annually) where any new definitions factored out of the usage and macros of the previous time, along with new algorithms and such, were entered into the dictionary and posted to e.g. IPFS.
-Code should not burgeon wildly, as it does today. The variety of code should map more-or-less to the well-factored variety of human computably-solvable problems. There shouldn't be dozens of chat apps, JS frameworks, programming languages. It's a waste of time, a fractal "thundering herd" attack on human mentality.
-If you read over the other notebooks you'll see that developing code in Joy is a lot like doing simple mathematics, and the descriptions of the code resemble math papers. The code also works the first time, no bugs. If you have any experience programming at all, you are probably skeptical, as I was, but it seems to work: deriving code mathematically seems to lead to fewer errors.
-But my point now is that this great ratio of textual explanation to wind up with code that consists of a few equations and could fit on an index card is highly desirable. Less code has fewer errors. The structure of Joy engenders a kind of thinking that seems to be very effective for developing structured processes.
-There seems to be an elegance and power to the notation.
- -
-First, import what we need.
- -from joy.joy import run
-from joy.library import initialize
-from joy.utils.stack import stack_to_string
-from joy.utils.pretty_print import TracePrinter
-Define a dictionary, an initial stack, and two helper functions to run Joy code and print results for us.
- -D = initialize()
-S = ()
-
-
-def J(text):
- print(stack_to_string(run(text, S, D)[0]))
-
-
-def V(text):
- tp = TracePrinter()
- run(text, S, D, tp.viewer)
- tp.print_()
-J('23 18 +')
-J('45 30 gcd')
-A viewer records each step of the evaluation of a Joy program. The TracePrinter has a facility for printing out a trace of the evaluation, one line per step. Each step is aligned to the current interpreter position, signified by a period separating the stack on the left from the pending expression ("continuation") on the right. I find these traces beautiful, like a kind of art.
V('23 18 +')
-V('45 30 gcd')
-Here's a longer trace.
- -V('96 27 gcd')
-from notebook_preamble import J, V
-This is what I like to call the functions that just rearrange things on the stack. (One thing I want to mention is that during a hypothetical compilation phase these "stack chatter" words effectively disappear, because we can map the logical stack locations to registers that remain static for the duration of the computation. This remains to be done but it's "off the shelf" technology.)
- -clear¶J('1 2 3 clear')
-dup dupd¶J('1 2 3 dup')
-J('1 2 3 dupd')
-enstacken disenstacken stack unstack¶Replace the stack with a quote of itself.
- -J('1 2 3 enstacken')
-Unpack a list onto the stack.
- -J('4 5 6 [3 2 1] unstack')
-Get the stack on the stack.
- -J('1 2 3 stack')
-Replace the stack with the list on top. -The items appear reversed but they are not, -is on the top of both the list and the stack.
- -J('1 2 3 [4 5 6] disenstacken')
-pop popd popop¶J('1 2 3 pop')
-J('1 2 3 popd')
-J('1 2 3 popop')
-roll< rolldown roll> rollup¶The "down" and "up" refer to the movement of two of the top three items (displacing the third.)
- -J('1 2 3 roll<')
-J('1 2 3 roll>')
-swap¶J('1 2 3 swap')
-tuck over¶J('1 2 3 tuck')
-J('1 2 3 over')
-unit quoted unquoted¶J('1 2 3 unit')
-J('1 2 3 quoted')
-J('1 [2] 3 unquoted')
-V('1 [dup] 3 unquoted') # Unquoting evaluates. Be aware.
-concat swoncat shunt¶J('[1 2 3] [4 5 6] concat')
-J('[1 2 3] [4 5 6] swoncat')
-J('[1 2 3] [4 5 6] shunt')
-cons swons uncons¶J('1 [2 3] cons')
-J('[2 3] 1 swons')
-J('[1 2 3] uncons')
-first second third rest¶J('[1 2 3 4] first')
-J('[1 2 3 4] second')
-J('[1 2 3 4] third')
-J('[1 2 3 4] rest')
-flatten¶J('[[1] [2 [3] 4] [5 6]] flatten')
-getitem at of drop take¶at and getitem are the same function. of == swap at
J('[10 11 12 13 14] 2 getitem')
-J('[1 2 3 4] 0 at')
-J('2 [1 2 3 4] of')
-J('[1 2 3 4] 2 drop')
-J('[1 2 3 4] 2 take') # reverses the order
-reverse could be defines as reverse == dup size take
remove¶J('[1 2 3 1 4] 1 remove')
-reverse¶J('[1 2 3 4] reverse')
-size¶J('[1 1 1 1] size')
-swaack¶"Swap stack" swap the list on the top of the stack for the stack, and put the old stack on top of the new one. Think of it as a context switch. Niether of the lists/stacks change their order.
- -J('1 2 3 [4 5 6] swaack')
-choice select¶J('23 9 1 choice')
-J('23 9 0 choice')
-J('[23 9 7] 1 select') # select is basically getitem, should retire it?
-J('[23 9 7] 0 select')
-zip¶J('[1 2 3] [6 5 4] zip')
-J('[1 2 3] [6 5 4] zip [sum] map')
-+ add¶J('23 9 +')
-- sub¶J('23 9 -')
-* mul¶J('23 9 *')
-/ div floordiv truediv¶J('23 9 /')
-J('23 -9 truediv')
-J('23 9 div')
-J('23 9 floordiv')
-J('23 -9 div')
-J('23 -9 floordiv')
-% mod modulus rem remainder¶J('23 9 %')
-neg¶J('23 neg -5 neg')
-J('2 10 pow')
-sqr sqrt¶J('23 sqr')
-J('23 sqrt')
-++ succ -- pred¶J('1 ++')
-J('1 --')
-<< lshift >> rshift¶J('8 1 <<')
-J('8 1 >>')
-average¶J('[1 2 3 5] average')
-range range_to_zero down_to_zero¶J('5 range')
-J('5 range_to_zero')
-J('5 down_to_zero')
-product¶J('[1 2 3 5] product')
-sum¶J('[1 2 3 5] sum')
-min¶J('[1 2 3 5] min')
-gcd¶J('45 30 gcd')
-least_fraction¶If we represent fractions as a quoted pair of integers [q d] this word reduces them to their ... least common factors or whatever.
- -J('[45 30] least_fraction')
-J('[23 12] least_fraction')
-? truthy¶Get the Boolean value of the item on the top of the stack.
- -J('23 truthy')
-J('[] truthy') # Python semantics.
-J('0 truthy')
-? == dup truthy
-
-V('23 ?')
-J('[] ?')
-J('0 ?')
-& and¶J('23 9 &')
-!= <> ne¶J('23 9 !=')
-The usual suspects:
-< lt<= le = eq> gt>= genotor^ xor¶J('1 1 ^')
-J('1 0 ^')
-help¶J('[help] help')
-parse¶J('[parse] help')
-J('1 "2 [3] dup" parse')
-run¶Evaluate a quoted Joy sequence.
- -J('[1 2 dup + +] run')
-app1 app2 app3¶J('[app1] help')
-J('10 4 [sqr *] app1')
-J('10 3 4 [sqr *] app2')
-J('[app2] help')
-J('10 2 3 4 [sqr *] app3')
-anamorphism¶Given an initial value, a predicate function [P], and a generator function [G], the anamorphism combinator creates a sequence.
n [P] [G] anamorphism
----------------------------
- [...]
-
-
-Example, range:
range == [0 <=] [1 - dup] anamorphism
-
-J('3 [0 <=] [1 - dup] anamorphism')
-branch¶J('3 4 1 [+] [*] branch')
-J('3 4 0 [+] [*] branch')
-cleave¶... x [P] [Q] cleave
-
-
-From the original Joy docs: "The cleave combinator expects two quotations, and below that an item x
-It first executes [P], with x on top, and saves the top result element.
-Then it executes [Q], again with x, and saves the top result.
-Finally it restores the stack to what it was below x and pushes the two
-results P(X) and Q(X)."
Note that P and Q can use items from the stack freely, since the stack (below x) is restored. cleave is a kind of parallel primitive, and it would make sense to create a version that uses, e.g. Python threads or something, to actually run P and Q concurrently. The current implementation of cleave is a definition in terms of app2:
cleave == [i] app2 [popd] dip
-
-J('10 2 [+] [-] cleave')
-dip dipd dipdd¶J('1 2 3 4 5 [+] dip')
-J('1 2 3 4 5 [+] dipd')
-J('1 2 3 4 5 [+] dipdd')
-dupdip¶Expects a quoted program [Q] on the stack and some item under it, dup the item and dip the quoted program under it.
n [Q] dupdip == n Q n
-
-V('23 [++] dupdip *') # N(N + 1)
-genrec primrec¶J('[genrec] help')
-J('3 [1 <=] [] [dup --] [i *] genrec')
-i¶V('1 2 3 [+ +] i')
-ifte¶[predicate] [then] [else] ifte
-
-J('1 2 [1] [+] [*] ifte')
-J('1 2 [0] [+] [*] ifte')
-infra¶V('1 2 3 [4 5 6] [* +] infra')
-loop¶J('[loop] help')
-V('3 dup [1 - dup] loop')
-map pam¶J('10 [1 2 3] [*] map')
-J('10 5 [[*][/][+][-]] pam')
-J('1 2 3 4 5 [+] nullary')
-J('1 2 3 4 5 [+] unary')
-J('1 2 3 4 5 [+] binary') # + has arity 2 so this is technically pointless...
-J('1 2 3 4 5 [+] ternary')
-step¶J('[step] help')
-V('0 [1 2 3] [+] step')
-times¶V('3 2 1 2 [+] times')
-b¶J('[b] help')
-V('1 2 [3] [4] b')
-while¶[predicate] [body] while
-
-J('3 [0 >] [dup --] while')
-x¶J('[x] help')
-V('1 [2] [i 3] x') # Kind of a pointless example.
-void¶Implements Laws of Form arithmetic over quote-only datastructures (that is, datastructures that consist soley of containers, without strings or numbers or anything else.)
- -J('[] void')
-J('[[]] void')
-J('[[][[]]] void')
-J('[[[]][[][]]] void')
-If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
-
-Find the sum of all the multiples of 3 or 5 below 1000.
-
-from notebook_preamble import J, V, define
-Let's create a predicate that returns True if a number is a multiple of 3 or 5 and False otherwise.
define('P [3 % not] dupdip 5 % not or')
-V('80 P')
-Given the predicate function P a suitable program is:
PE1 == 1000 range [P] filter sum
-
-
-This function generates a list of the integers from 0 to 999, filters
-that list by P, and then sums the result.
Logically this is fine, but pragmatically we are doing more work than we -should be; we generate one thousand integers but actually use less than -half of them. A better solution would be to generate just the multiples -we want to sum, and to add them as we go rather than storing them and -adding summing them at the end.
-At first I had the idea to use two counters and increase them by three -and five, respectively. This way we only generate the terms that we -actually want to sum. We have to proceed by incrementing the counter -that is lower, or if they are equal, the three counter, and we have to -take care not to double add numbers like 15 that are multiples of both -three and five.
-This seemed a little clunky, so I tried a different approach.
-Consider the first few terms in the series:
- -3 5 6 9 10 12 15 18 20 21 ...
-
-
-Subtract each number from the one after it (subtracting 0 from 3):
- -3 5 6 9 10 12 15 18 20 21 24 25 27 30 ...
-0 3 5 6 9 10 12 15 18 20 21 24 25 27 ...
--------------------------------------------
-3 2 1 3 1 2 3 3 2 1 3 1 2 3 ...
-
-
-You get this lovely repeating palindromic sequence:
- -3 2 1 3 1 2 3
-
-
-To make a counter that increments by factors of 3 and 5 you just add -these differences to the counter one-by-one in a loop.
-To make use of this sequence to increment a counter and sum terms as we -go we need a function that will accept the sum, the counter, and the next -term to add, and that adds the term to the counter and a copy of the -counter to the running sum. This function will do that:
- -PE1.1 == + [+] dupdip
-
-define('PE1.1 + [+] dupdip')
-V('0 0 3 PE1.1')
-V('0 0 [3 2 1 3 1 2 3] [PE1.1] step')
-So one step through all seven terms brings the counter to 15 and the total to 60.
1000 / 15
-66 * 15
-1000 - 990
-We only want the terms less than 1000.
- -999 - 990
-That means we want to run the full list of numbers sixty-six times to get to 990 and then the first four numbers 3 2 1 3 to get to 999.
- -define('PE1 0 0 66 [[3 2 1 3 1 2 3] [PE1.1] step] times [3 2 1 3] [PE1.1] step pop')
-J('PE1')
-This form uses no extra storage and produces no unused summands. It's -good but there's one more trick we can apply. The list of seven terms -takes up at least seven bytes. But notice that all of the terms are less -than four, and so each can fit in just two bits. We could store all -seven terms in just fourteen bits and use masking and shifts to pick out -each term as we go. This will use less space and save time loading whole -integer terms from the list.
- - 3 2 1 3 1 2 3
-0b 11 10 01 11 01 10 11 == 14811
-
-0b11100111011011
-define('PE1.2 [3 & PE1.1] dupdip 2 >>')
-V('0 0 14811 PE1.2')
-V('3 3 3702 PE1.2')
-V('0 0 14811 7 [PE1.2] times pop')
-And so we have at last:
- -define('PE1 0 0 66 [14811 7 [PE1.2] times pop] times 14811 4 [PE1.2] times popop')
-J('PE1')
-Let's refactor.
- - 14811 7 [PE1.2] times pop
- 14811 4 [PE1.2] times pop
- 14811 n [PE1.2] times pop
-n 14811 swap [PE1.2] times pop
-
-define('PE1.3 14811 swap [PE1.2] times pop')
-Now we can simplify the definition above:
- -define('PE1 0 0 66 [7 PE1.3] times 4 PE1.3 pop')
-J('PE1')
-Here's our joy program all in one place. It doesn't make so much sense, but if you have read through the above description of how it was derived I hope it's clear.
- -PE1.1 == + [+] dupdip
-PE1.2 == [3 & PE1.1] dupdip 2 >>
-PE1.3 == 14811 swap [PE1.2] times pop
-PE1 == 0 0 66 [7 PE1.3] times 4 PE1.3 pop
-
-It's a little clunky iterating sixty-six times though the seven numbers then four more. In the Generator Programs notebook we derive a generator that can be repeatedly driven by the x combinator to produce a stream of the seven numbers repeating over and over again.
define('PE1.terms [0 swap [dup [pop 14811] [] branch [3 &] dupdip 2 >>] dip rest cons]')
-J('PE1.terms 21 [x] times')
-We know from above that we need sixty-six times seven then four more terms to reach up to but not over one thousand.
- -J('7 66 * 4 +')
-J('PE1.terms 466 [x] times pop')
-J('[PE1.terms 466 [x] times pop] run sum')
-Now we can use PE1.1 to accumulate the terms as we go, and then pop the generator and the counter from the stack when we're done, leaving just the sum.
J('0 0 PE1.terms 466 [x [PE1.1] dip] times popop')
-Consider finding the sum of the positive integers less than or equal to ten.
- -J('[10 9 8 7 6 5 4 3 2 1] sum')
-Instead of summing them, observe:
- - 10 9 8 7 6
-+ 1 2 3 4 5
----- -- -- -- --
- 11 11 11 11 11
-
- 11 * 5 = 55
-
-
-From the above example we can deduce that the sum of the first N positive integers is:
- -(N + 1) * N / 2
-
-
-(The formula also works for odd values of N, I'll leave that to you if you want to work it out or you can take my word for it.)
- -define('F dup ++ * 2 floordiv')
-V('10 F')
-We can apply the same reasoning to the PE1 problem.
-Between 0 and 990 inclusive there are sixty-six "blocks" of seven terms each, starting with:
- -[3 5 6 9 10 12 15]
-
-
-And ending with:
- -[978 980 981 984 985 987 990]
-
-
-If we reverse one of these two blocks and sum pairs...
- -J('[3 5 6 9 10 12 15] reverse [978 980 981 984 985 987 990] zip')
-J('[3 5 6 9 10 12 15] reverse [978 980 981 984 985 987 990] zip [sum] map')
-(Interesting that the sequence of seven numbers appears again in the rightmost digit of each term.)
- -J('[ 3 5 6 9 10 12 15] reverse [978 980 981 984 985 987 990] zip [sum] map sum')
-Since there are sixty-six blocks and we are pairing them up, there must be thirty-three pairs, each of which sums to 6945. We also have these additional unpaired terms between 990 and 1000:
- -993 995 996 999
-
-
-So we can give the "sum of all the multiples of 3 or 5 below 1000" like so:
- -J('6945 33 * [993 995 996 999] cons sum')
-It's worth noting, I think, that this same reasoning holds for any two numbers $n$ and $m$ the multiples of which we hope to sum. The multiples would have a cycle of differences of length $k$ and so we could compute the sum of $Nk$ multiples as above.
-The sequence of differences will always be a palidrome. Consider an interval spanning the least common multiple of $n$ and $m$:
- -| | | | | | | |
-| | | | |
-
-
-Here we have 4 and 7, and you can read off the sequence of differences directly from the diagram: 4 3 1 4 2 2 4 1 3 4.
-Geometrically, the actual values of $n$ and $m$ and their lcm don't matter, the pattern they make will always be symmetrical around its midpoint. The same reasoning holds for multiples of more than two numbers.
- -Of course, the simplest joy program for the first Project Euler problem is just:
- -PE1 == 233168
-
-
-Fin.
- -[Given] a sequence of digits (your puzzle input) and find the sum of all digits that match the next digit in the list. The list is circular, so the digit after the last digit is the first digit in the list.
-For example:
-from notebook_preamble import J, V, define
-I'll assume the input is a Joy sequence of integers (as opposed to a string or something else.)
-We might proceed by creating a word that makes a copy of the sequence with the first item moved to the last, and zips it with the original to make a list of pairs, and a another word that adds (one of) each pair to a total if the pair matches.
- -AoC2017.1 == pair_up total_matches
-
-
-Let's derive pair_up:
[a b c] pair_up
--------------------------
- [[a b] [b c] [c a]]
-
-Straightforward (although the order of each pair is reversed, due to the way zip works, but it doesn't matter for this program):
[a b c] dup
-[a b c] [a b c] uncons swap
-[a b c] [b c] a unit concat
-[a b c] [b c a] zip
-[[b a] [c b] [a c]]
-
-define('pair_up dup uncons swap unit concat zip')
-J('[1 2 3] pair_up')
-J('[1 2 2 3] pair_up')
-Now we need to derive total_matches. It will be a step function:
total_matches == 0 swap [F] step
-
-
-Where F will have the pair to work with, and it will basically be a branch or ifte.
total [n m] F
-
-
-It will probably be easier to write if we dequote the pair:
- - total [n m] i F′
-----------------------
- total n m F′
-
-
-Now F′ becomes just:
total n m [=] [pop +] [popop] ifte
-
-
-So:
- -F == i [=] [pop +] [popop] ifte
-
-
-And thus:
- -total_matches == 0 swap [i [=] [pop +] [popop] ifte] step
-
-define('total_matches 0 swap [i [=] [pop +] [popop] ifte] step')
-J('[1 2 3] pair_up total_matches')
-J('[1 2 2 3] pair_up total_matches')
-Now we can define our main program and evaluate it on the examples.
- -define('AoC2017.1 pair_up total_matches')
-J('[1 1 2 2] AoC2017.1')
-J('[1 1 1 1] AoC2017.1')
-J('[1 2 3 4] AoC2017.1')
-J('[9 1 2 1 2 1 2 9] AoC2017.1')
-J('[9 1 2 1 2 1 2 9] AoC2017.1')
- pair_up == dup uncons swap unit concat zip
-total_matches == 0 swap [i [=] [pop +] [popop] ifte] step
-
- AoC2017.1 == pair_up total_matches
-
-
-Now the paired digit is "halfway" round.
- -[a b c d] dup size 2 / [drop] [take reverse] cleave concat zip
-
-J('[1 2 3 4] dup size 2 / [drop] [take reverse] cleave concat zip')
-I realized that each pair is repeated...
- -J('[1 2 3 4] dup size 2 / [drop] [take reverse] cleave zip')
-define('AoC2017.1.extra dup size 2 / [drop] [take reverse] cleave zip swap pop total_matches 2 *')
-J('[1 2 1 2] AoC2017.1.extra')
-J('[1 2 2 1] AoC2017.1.extra')
-J('[1 2 3 4 2 5] AoC2017.1.extra')
-With Joy a great deal of the heuristics from Forth programming carry over nicely. For example, refactoring into small, well-scoped commands with mnemonic names...
- - rotate_seq == uncons swap unit concat
- pair_up == dup rotate_seq zip
- add_if_match == [=] [pop +] [popop] ifte
- total_matches == [i add_if_match] step_zero
-
- AoC2017.1 == pair_up total_matches
-
- half_of_size == dup size 2 /
- split_at == [drop] [take reverse] cleave
- pair_up.extra == half_of_size split_at zip swap pop
-
- AoC2017.1.extra == pair_up.extra total_matches 2 *
-
-For each row, determine the difference between the largest value and the smallest value; the checksum is the sum of all of these differences.
-For example, given the following spreadsheet:
- -5 1 9 5
-7 5 3
-2 4 6 8
-
-
-In this example, the spreadsheet's checksum would be 8 + 4 + 6 = 18.
- -from notebook_preamble import J, V, define
-I'll assume the input is a Joy sequence of sequences of integers.
- -[[5 1 9 5]
- [7 5 3]
- [2 4 6 8]]
-
-
-So, obviously, the initial form will be a step function:
AoC2017.2 == 0 swap [F +] step
-
-This function F must get the max and min of a row of numbers and subtract. We can define a helper function maxmin which does this:
define('maxmin [max] [min] cleave')
-J('[1 2 3] maxmin')
-Then F just does that then subtracts the min from the max:
F == maxmin -
-
-
-So:
- -define('AoC2017.2 [maxmin - +] step_zero')
-J('''
-
-[[5 1 9 5]
- [7 5 3]
- [2 4 6 8]] AoC2017.2
-
-''')
-...find the only two numbers in each row where one evenly divides the other - that is, where the result of the division operation is a whole number. They would like you to find those numbers on each line, divide them, and add up each line's result.
-For example, given the following spreadsheet:
- -5 9 2 8
-9 4 7 3
-3 8 6 5
-
-
-In this example, the sum of the results would be 4 + 3 + 2 = 9.
-What is the sum of each row's result in your puzzle input?
- -J('[5 9 2 8] sort reverse')
-J('[9 8 5 2] uncons [swap [divmod] cons] dupdip')
-[9 8 5 2] uncons [swap [divmod] cons F] dupdip G
- [8 5 2] [9 divmod] F [8 5 2] G
-
-V('[8 5 2] [9 divmod] [uncons swap] dip dup [i not] dip')
-Let's think.
-Given a sorted sequence (from highest to lowest) we want to
-loop I think¶[a b c d] True [Q] loop
-[a b c d] Q [Q] loop
-
-
-Q should either leave the result and False, or the rest and True.
[a b c d] Q
------------------
- result 0
-
- [a b c d] Q
------------------
- [b c d] 1
-
-This suggests that Q should start with:
[a b c d] uncons dup roll<
-[b c d] [b c d] a
-
-
-Now we just have to pop it if we don't need it.
[b c d] [b c d] a [P] [T] [cons] app2 popdd [E] primrec
-[b c d] [b c d] [a P] [a T] [E] primrec
-
-
-w/ Q == [% not] [T] [F] primrec
-
- [a b c d] uncons
- a [b c d] tuck
-[b c d] a [b c d] uncons
-[b c d] a b [c d] roll>
-[b c d] [c d] a b Q
-[b c d] [c d] a b [% not] [T] [F] primrec
-
-[b c d] [c d] a b T
-[b c d] [c d] a b / roll> popop 0
-
-[b c d] [c d] a b F Q
-[b c d] [c d] a b pop swap uncons ... Q
-[b c d] [c d] a swap uncons ... Q
-[b c d] a [c d] uncons ... Q
-[b c d] a c [d] roll> Q
-[b c d] [d] a c Q
-
-Q == [% not] [/ roll> popop 0] [pop swap uncons roll>] primrec
-
-uncons tuck uncons roll> Q
-
-J('[8 5 3 2] 9 [swap] [% not] [cons] app2 popdd')
- [a b c d] uncons
- a [b c d] tuck
-[b c d] a [b c d] [not] [popop 1] [Q] ifte
-
-[b c d] a [] popop 1
-[b c d] 1
-
-[b c d] a [b c d] Q
-
-
- a [...] Q
----------------
- result 0
-
- a [...] Q
----------------
- 1
-
-
-w/ Q == [first % not] [first / 0] [rest [not] [popop 1]] [ifte]
-
-
-
-a [b c d] [first % not] [first / 0] [rest [not] [popop 1]] [ifte]
-a [b c d] first % not
-a b % not
-a%b not
-bool(a%b)
-
-a [b c d] [first % not] [first / 0] [rest [not] [popop 1]] [ifte]
-a [b c d] first / 0
-a b / 0
-a/b 0
-
-a [b c d] [first % not] [first / 0] [rest [not] [popop 1]] [ifte]
-a [b c d] rest [not] [popop 1] [Q] ifte
-a [c d] [not] [popop 1] [Q] ifte
-a [c d] [not] [popop 1] [Q] ifte
-
-a [c d] [not] [popop 1] [Q] ifte
-a [c d] not
-
-a [] popop 1
-1
-
-a [c d] Q
-
-
-uncons tuck [first % not] [first / 0] [rest [not] [popop 1]] [ifte]
-
-First, I made a function G that expects a number and a sequence of candidates and return the result or zero:
n [...] G
----------------
- result
-
- n [...] G
----------------
- 0
-
-
-It's a recursive function that conditionally executes the recursive part of its recursive branch
- -[Pg] [E] [R1 [Pi] [T]] [ifte] genrec
-
-
-The recursive branch is the else-part of the inner ifte:
G == [Pg] [E] [R1 [Pi] [T]] [ifte] genrec
- == [Pg] [E] [R1 [Pi] [T] [G] ifte] ifte
-
-
-But this is in hindsight. Going forward I derived:
- -G == [first % not]
- [first /]
- [rest [not] [popop 0]]
- [ifte] genrec
-
-
-The predicate detects if the n can be evenly divided by the first item in the list. If so, the then-part returns the result. Otherwise, we have:
n [m ...] rest [not] [popop 0] [G] ifte
-n [...] [not] [popop 0] [G] ifte
-
-
-This ifte guards against empty sequences and returns zero in that case, otherwise it executes G.
define('G [first % not] [first /] [rest [not] [popop 0]] [ifte] genrec')
-Now we need a word that uses G on each (head, tail) pair of a sequence until it finds a (non-zero) result. It's going to be designed to work on a stack that has some candidate n, a sequence of possible divisors, and a result that is zero to signal to continue (a non-zero value implies that it is the discovered result):
n [...] p find-result
----------------------------
- result
-
-
-It applies G using nullary because if it fails with one candidate it needs the list to get the next one (the list is otherwise consumed by G.)
find-result == [0 >] [roll> popop] [roll< popop uncons [G] nullary] tailrec
-
-n [...] p [0 >] [roll> popop] [roll< popop uncons [G] nullary] tailrec
-
-
-The base-case is trivial, return the (non-zero) result. The recursive branch...
- -n [...] p roll< popop uncons [G] nullary find-result
-[...] p n popop uncons [G] nullary find-result
-[...] uncons [G] nullary find-result
-m [..] [G] nullary find-result
-m [..] p find-result
-
-
-The puzzle states that the input is well-formed, meaning that we can expect a result before the row sequence empties and so do not need to guard the uncons.
define('find-result [0 >] [roll> popop] [roll< popop uncons [G] nullary] tailrec')
-J('[11 9 8 7 3 2] 0 tuck find-result')
-In order to get the thing started, we need to sort the list in descending order, then prime the find-result function with a dummy candidate value and zero ("continue") flag.
define('prep-row sort reverse 0 tuck')
-Now we can define our program.
- -define('AoC20017.2.extra [prep-row find-result +] step_zero')
-J('''
-
-[[5 9 2 8]
- [9 4 7 3]
- [3 8 6 5]] AoC20017.2.extra
-
-''')
-You come across an experimental new kind of memory stored on an infinite two-dimensional grid.
-Each square on the grid is allocated in a spiral pattern starting at a location marked 1 and then counting up while spiraling outward. For example, the first few squares are allocated like this:
- -17 16 15 14 13
-18 5 4 3 12
-19 6 1 2 11
-20 7 8 9 10
-21 22 23---> ...
-
-
-While this is very space-efficient (no squares are skipped), requested data must be carried back to square 1 (the location of the only access port for this memory system) by programs that can only move up, down, left, or right. They always take the shortest path: the Manhattan Distance between the location of the data and square 1.
-For example:
-How many steps are required to carry the data from the square identified in your puzzle input all the way to the access port?
- -I freely admit that I worked out the program I wanted to write using graph paper and some Python doodles. There's no point in trying to write a Joy program until I'm sure I understand the problem well enough.
-The first thing I did was to write a column of numbers from 1 to n (32 as it happens) and next to them the desired output number, to look for patterns directly:
- -1 0
-2 1
-3 2
-4 1
-5 2
-6 1
-7 2
-8 1
-9 2
-10 3
-11 2
-12 3
-13 4
-14 3
-15 2
-16 3
-17 4
-18 3
-19 2
-20 3
-21 4
-22 3
-23 2
-24 3
-25 4
-26 5
-27 4
-28 3
-29 4
-30 5
-31 6
-32 5
-
-There are four groups repeating for a given "rank", then the pattern enlarges and four groups repeat again, etc.
- - 1 2
- 3 2 3 4
- 5 4 3 4 5 6
- 7 6 5 4 5 6 7 8
-9 8 7 6 5 6 7 8 9 10
-
-
-Four of this pyramid interlock to tile the plane extending from the initial "1" square.
- - 2 3 | 4 5 | 6 7 | 8 9
- 10 11 12 13|14 15 16 17|18 19 20 21|22 23 24 25
-
-
-And so on.
- -We can figure out the pattern for a row of the pyramid at a given "rank" $k$:
-$2k - 1, 2k - 2, ..., k, k + 1, k + 2, ..., 2k$
-or
-$k + (k - 1), k + (k - 2), ..., k, k + 1, k + 2, ..., k + k$
-This shows that the series consists at each place of $k$ plus some number that begins at $k - 1$, decreases to zero, then increases to $k$. Each row has $2k$ members.
- -Let's figure out how, given an index into a row, we can calculate the value there. The index will be from 0 to $k - 1$.
-Let's look at an example, with $k = 4$:
- -0 1 2 3 4 5 6 7
-7 6 5 4 5 6 7 8
-
-k = 4
-Subtract $k$ from the index and take the absolute value:
- -for n in range(2 * k):
- print(abs(n - k),)
-Not quite. Subtract $k - 1$ from the index and take the absolute value:
- -for n in range(2 * k):
- print(abs(n - (k - 1)), end=' ')
-Great, now add $k$...
- -for n in range(2 * k):
- print(abs(n - (k - 1)) + k, end=' ')
-So to write a function that can give us the value of a row at a given index:
- -def row_value(k, i):
- i %= (2 * k) # wrap the index at the row boundary.
- return abs(i - (k - 1)) + k
-k = 5
-for i in range(2 * k):
- print(row_value(k, i), end=' ')
-(I'm leaving out details of how I figured this all out and just giving the relevent bits. It took a little while to zero in of the aspects of the pattern that were important for the task.)
- -Now that we can compute the desired output value for a given rank and the offset (index) into that rank, we need to determine how to find the rank and offset of a number.
-The rank is easy to find by iteratively stripping off the amount already covered by previous ranks until you find the one that brackets the target number. Because each row is $2k$ places and there are $4$ per rank each rank contains $8k$ places. Counting the initial square we have:
-$corner_k = 1 + \sum_{n=1}^k 8n$
-I'm not mathematically sophisticated enough to turn this directly into a formula (but Sympy is, see below.) I'm going to write a simple Python function to iterate and search:
- -def rank_and_offset(n):
- assert n >= 2 # Guard the domain.
- n -= 2 # Subtract two,
- # one for the initial square,
- # and one because we are counting from 1 instead of 0.
- k = 1
- while True:
- m = 8 * k # The number of places total in this rank, 4(2k).
- if n < m:
- return k, n % (2 * k)
- n -= m # Remove this rank's worth.
- k += 1
-for n in range(2, 51):
- print(n, rank_and_offset(n))
-for n in range(2, 51):
- k, i = rank_and_offset(n)
- print(n, row_value(k, i))
-def row_value(k, i):
- return abs(i - (k - 1)) + k
-
-
-def rank_and_offset(n):
- n -= 2 # Subtract two,
- # one for the initial square,
- # and one because we are counting from 1 instead of 0.
- k = 1
- while True:
- m = 8 * k # The number of places total in this rank, 4(2k).
- if n < m:
- return k, n % (2 * k)
- n -= m # Remove this rank's worth.
- k += 1
-
-
-def aoc20173(n):
- if n <= 1:
- return 0
- k, i = rank_and_offset(n)
- return row_value(k, i)
-aoc20173(23)
-aoc20173(23000)
-aoc20173(23000000000000)
-from sympy import floor, lambdify, solve, symbols
-from sympy import init_printing
-init_printing()
-k = symbols('k')
-Since
-$1 + 2 + 3 + ... + N = \frac{N(N + 1)}{2}$
-and
-$\sum_{n=1}^k 8n = 8(\sum_{n=1}^k n) = 8\frac{k(k + 1)}{2}$
-We want:
- -E = 2 + 8 * k * (k + 1) / 2 # For the reason for adding 2 see above.
-
-E
-We can write a function to solve for $k$ given some $n$...
- -def rank_of(n):
- return floor(max(solve(E - n, k))) + 1
-First solve() for $E - n = 0$ which has two solutions (because the equation is quadratic so it has two roots) and since we only care about the larger one we use max() to select it. It will generally not be a nice integer (unless $n$ is the number of an end-corner of a rank) so we take the floor() and add 1 to get the integer rank of $n$. (Taking the ceiling() gives off-by-one errors on the rank boundaries. I don't know why. I'm basically like a monkey doing math here.) =-D
It gives correct answers:
- -for n in (9, 10, 25, 26, 49, 50):
- print(n, rank_of(n))
-And it runs much faster (at least for large numbers):
- -%time rank_of(23000000000000) # Compare runtime with rank_and_offset()!
-%time rank_and_offset(23000000000000)
-After finding the rank you would still have to find the actual value of the rank's first corner and subtract it (plus 2) from the number and compute the offset as above and then the final output, but this overhead is partially shared by the other method, and overshadowed by the time it (the other iterative method) would take for really big inputs.
-The fun thing to do here would be to graph the actual runtime of both methods against each other to find the trade-off point.
- -Sympy is a symbolic math library, and it supports symbolic manipulation of equations. I can put in $y$ (instead of a value) and ask it to solve for $k$.
- -y = symbols('y')
-g, f = solve(E - y, k)
-The equation is quadratic so there are two roots, we are interested in the greater one...
- -g
-f
-Now we can take the floor(), add 1, and lambdify() the equation to get a Python function that calculates the rank directly.
floor(f) + 1
-F = lambdify(y, floor(f) + 1)
-for n in (9, 10, 25, 26, 49, 50):
- print(n, int(F(n)))
-It's pretty fast.
- -%time int(F(23000000000000)) # The clear winner.
-Knowing the equation we could write our own function manually, but the speed is no better.
- -from math import floor as mfloor, sqrt
-
-def mrank_of(n):
- return int(mfloor(sqrt(n - 1) / 2 - 0.5) + 1)
-%time mrank_of(23000000000000)
-Now that we have a fast way to get the rank, we still need to use it to compute the offset into a pyramid row.
- -def offset_of(n, k):
- return (n - 2 + 4 * k * (k - 1)) % (2 * k)
-(Note the sneaky way the sign changes from $k(k + 1)$ to $k(k - 1)$. This is because we want to subract the $(k - 1)$th rank's total places (its own and those of lesser rank) from our $n$ of rank $k$. Substituting $k - 1$ for $k$ in $k(k + 1)$ gives $(k - 1)(k - 1 + 1)$, which of course simplifies to $k(k - 1)$.)
- -offset_of(23000000000000, 2397916)
-So, we can compute the rank, then the offset, then the row value.
- -def rank_of(n):
- return int(mfloor(sqrt(n - 1) / 2 - 0.5) + 1)
-
-
-def offset_of(n, k):
- return (n - 2 + 4 * k * (k - 1)) % (2 * k)
-
-
-def row_value(k, i):
- return abs(i - (k - 1)) + k
-
-
-def aoc20173(n):
- k = rank_of(n)
- i = offset_of(n, k)
- return row_value(k, i)
-aoc20173(23)
-aoc20173(23000)
-aoc20173(23000000000000)
-%time aoc20173(23000000000000000000000000) # Fast for large values.
-At this point I feel confident that I can implement a concise version of this code in Joy. ;-)
- -from notebook_preamble import J, V, define
-rank_of¶ n rank_of
----------------
- k
-
-
-The translation is straightforward.
- -int(floor(sqrt(n - 1) / 2 - 0.5) + 1)
-
-rank_of == -- sqrt 2 / 0.5 - floor ++
-
-define('rank_of -- sqrt 2 / 0.5 - floor ++')
-offset_of¶ n k offset_of
--------------------
- i
-
-(n - 2 + 4 * k * (k - 1)) % (2 * k)
-
-
-A little tricky...
- -n k dup 2 *
-n k k 2 *
-n k k*2 [Q] dip %
-n k Q k*2 %
-
-n k dup --
-n k k --
-n k k-1 4 * * 2 + -
-n k*k-1*4 2 + -
-n k*k-1*4+2 -
-n-k*k-1*4+2
-
-n-k*k-1*4+2 k*2 %
-n-k*k-1*4+2%k*2
-
-
-Ergo:
- -offset_of == dup 2 * [dup -- 4 * * 2 + -] dip %
-
-define('offset_of dup 2 * [dup -- 4 * * 2 + -] dip %')
-row_value¶ k i row_value
--------------------
- n
-
-abs(i - (k - 1)) + k
-
-k i over -- - abs +
-k i k -- - abs +
-k i k-1 - abs +
-k i-k-1 abs +
-k |i-k-1| +
-k+|i-k-1|
-
-define('row_value over -- - abs +')
-aoc2017.3¶ n aoc2017.3
------------------
- m
-
-n dup rank_of
-n k [offset_of] dupdip
-n k offset_of k
-i k swap row_value
-k i row_value
-m
-
-define('aoc2017.3 dup rank_of [offset_of] dupdip swap row_value')
-J('23 aoc2017.3')
-J('23000 aoc2017.3')
-V('23000000000000 aoc2017.3')
- rank_of == -- sqrt 2 / 0.5 - floor ++
-offset_of == dup 2 * [dup -- 4 * * 2 + -] dip %
-row_value == over -- - abs +
-
-aoc2017.3 == dup rank_of [offset_of] dupdip swap row_value
-
-To ensure security, a valid passphrase must contain no duplicate words.
-For example:
-The system's full passphrase list is available as your puzzle input. How many passphrases are valid?
- -from notebook_preamble import J, V, define
-I'll assume the input is a Joy sequence of sequences of integers.
- -[[5 1 9 5]
- [7 5 4 3]
- [2 4 6 8]]
-
-
-So, obviously, the initial form will be a step function:
AoC2017.4 == 0 swap [F +] step
-
-F == [size] [unique size] cleave =
-
-The step_zero combinator includes the 0 swap that would normally open one of these definitions:
J('[step_zero] help')
-AoC2017.4 == [F +] step_zero
-
-define('AoC2017.4 [[size] [unique size] cleave = +] step_zero')
-J('''
-
-[[5 1 9 5]
- [7 5 4 3]
- [2 4 6 8]] AoC2017.4
-
-''')
-...a list of the offsets for each jump. Jumps are relative: -1 moves to the previous instruction, and 2 skips the next one. Start at the first instruction in the list. The goal is to follow the jumps until one leads outside the list.
-In addition, these instructions are a little strange; after each jump, the offset of that instruction increases by 1. So, if you come across an offset of 3, you would move three instructions forward, but change it to a 4 for the next time it is encountered.
-For example, consider the following list of jump offsets:
- -0
-3
-0
-1
--3
-
-
-Positive jumps ("forward") move downward; negative jumps move upward. For legibility in this example, these offset values will be written all on one line, with the current instruction marked in parentheses. The following steps would be taken before an exit is found:
-In this example, the exit is reached in 5 steps.
-How many steps does it take to reach the exit?
- -For now, I'm going to assume a starting state with the size of the sequence pre-computed. We need it to define the exit condition and it is a trivial preamble to generate it. We then need and index and a step-count, which are both initially zero. Then we have the sequence itself, and some recursive function F that does the work.
size index step-count [...] F
------------------------------------
- step-count
-
-F == [P] [T] [R1] [R2] genrec
-
-
-Later on I was thinking about it and the Forth heuristic came to mind, to wit: four things on the stack are kind of much. Immediately I realized that the size properly belongs in the predicate of F! D'oh!
index step-count [...] F
-------------------------------
- step-count
-
-So, let's start by nailing down the predicate:
- -F == [P] [T] [R1] [R2] genrec
- == [P] [T] [R1 [F] R2] ifte
-
-0 0 [0 3 0 1 -3] popop 5 >=
-
-P == popop 5 >=
-
-Now we need the else-part:
- -index step-count [0 3 0 1 -3] roll< popop
-
-E == roll< popop
-
-Last but not least, the recursive branch
- -0 0 [0 3 0 1 -3] R1 [F] R2
-
-
-The R1 function has a big job:
R1 == get the value at index
- increment the value at the index
- add the value gotten to the index
- increment the step count
-
-
-The only tricky thing there is incrementing an integer in the sequence. Joy sequences are not particularly good for random access. We could encode the list of jump offsets in a big integer and use math to do the processing for a good speed-up, but it still wouldn't beat the performance of e.g. a mutable array. This is just one of those places where "plain vanilla" Joypy doesn't shine (in default performance. The legendary Sufficiently-Smart Compiler would of course rewrite this function to use an array "under the hood".)
-In the meantime, I'm going to write a primitive function that just does what we need.
- -from notebook_preamble import D, J, V, define
-from joy.library import SimpleFunctionWrapper
-from joy.utils.stack import list_to_stack
-
-
-@SimpleFunctionWrapper
-def incr_at(stack):
- '''Given a index and a sequence of integers, increment the integer at the index.
-
- E.g.:
-
- 3 [0 1 2 3 4 5] incr_at
- -----------------------------
- [0 1 2 4 4 5]
-
- '''
- sequence, (i, stack) = stack
- mem = []
- while i >= 0:
- term, sequence = sequence
- mem.append(term)
- i -= 1
- mem[-1] += 1
- return list_to_stack(mem, sequence), stack
-
-
-D['incr_at'] = incr_at
-J('3 [0 1 2 3 4 5] incr_at')
-3 0 [0 1 2 3 4] [roll< at] nullary
-3 0 [0 1 2 n 4] n
-
-3 0 [0 1 2 n 4] n [Q] dip
-3 0 [0 1 2 n 4] Q n
-3 0 [0 1 2 n 4] [popd incr_at] unary n
-3 0 [0 1 2 n+1 4] n
-
-3 0 [0 1 2 n+1 4] n [+] cons dipd
-3 0 [0 1 2 n+1 4] [n +] dipd
-3 n + 0 [0 1 2 n+1 4]
-3+n 0 [0 1 2 n+1 4]
-
-3+n 0 [0 1 2 n+1 4] [++] dip
-3+n 1 [0 1 2 n+1 4]
-
-get_value == [roll< at] nullary
-incr_value == [[popd incr_at] unary] dip
-add_value == [+] cons dipd
-incr_step_count == [++] dip
-
-R1 == get_value incr_value add_value incr_step_count
-
-F == [P] [T] [R1] primrec
-
-F == [popop !size! >=] [roll< pop] [get_value incr_value add_value incr_step_count] tailrec
-
-from joy.library import DefinitionWrapper
-
-
-DefinitionWrapper.add_definitions('''
-
- get_value [roll< at] nullary
- incr_value [[popd incr_at] unary] dip
- add_value [+] cons dipd
-incr_step_count [++] dip
-
- AoC2017.5.0 get_value incr_value add_value incr_step_count
-
-''', D)
-from joy.library import DefinitionWrapper
-
-
-DefinitionWrapper.add_definitions('''
-
- get_value [roll< at] nullary
- incr_value [[popd incr_at] unary] dip
- add_value [+] cons dipd
-incr_step_count [++] dip
-
- AoC2017.5.0 get_value incr_value add_value incr_step_count
-
-''', D)
-define('F [popop 5 >=] [roll< popop] [AoC2017.5.0] tailrec')
-J('0 0 [0 3 0 1 -3] F')
-index, and step-count¶We want to go from this to this:
- - [...] AoC2017.5.preamble
-------------------------------
- 0 0 [...] [popop n >=]
-
-
-Where n is the size of the sequence.
The first part is obviously 0 0 roll<, then dup size:
[...] 0 0 roll< dup size
-0 0 [...] n
-
-
-Then:
- -0 0 [...] n [>=] cons [popop] swoncat
-
-
-So:
- -init-index-and-step-count == 0 0 roll<
-prepare-predicate == dup size [>=] cons [popop] swoncat
-
-AoC2017.5.preamble == init-index-and-step-count prepare-predicate
-
-DefinitionWrapper.add_definitions('''
-
-init-index-and-step-count 0 0 roll<
-prepare-predicate dup size [>=] cons [popop] swoncat
-
-AoC2017.5.preamble init-index-and-step-count prepare-predicate
-
-AoC2017.5 AoC2017.5.preamble [roll< popop] [AoC2017.5.0] tailrec
-
-''', D)
-J('[0 3 0 1 -3] AoC2017.5')
- AoC2017.5 == AoC2017.5.preamble [roll< popop] [AoC2017.5.0] primrec
-
- AoC2017.5.0 == get_value incr_value add_value incr_step_count
- AoC2017.5.preamble == init-index-and-step-count prepare-predicate
-
- get_value == [roll< at] nullary
- incr_value == [[popd incr_at] unary] dip
- add_value == [+] cons dipd
- incr_step_count == [++] dip
-
-init-index-and-step-count == 0 0 roll<
- prepare-predicate == dup size [>=] cons [popop] swoncat
-
-This is by far the largest program I have yet written in Joy. Even with the incr_at function it is still a bear. There may be an arrangement of the parameters that would permit more elegant definitions, but it still wouldn't be as efficient as something written in assembly, C, or even Python.
from notebook_preamble import D, J, V, define
-J('[0 2 7 0] dup max')
-from joy.library import SimpleFunctionWrapper
-from joy.utils.stack import list_to_stack
-
-
-@SimpleFunctionWrapper
-def index_of(stack):
- '''Given a sequence and a item, return the index of the item, or -1 if not found.
-
- E.g.:
-
- [a b c] a index_of
- ------------------------
- 0
-
- [a b c] d index_of
- ------------------------
- -1
-
- '''
- item, (sequence, stack) = stack
- i = 0
- while sequence:
- term, sequence = sequence
- if term == item:
- break
- i += 1
- else:
- i = -1
- return i, stack
-
-
-D['index_of'] = index_of
-J('[0 2 7 0] 7 index_of')
-J('[0 2 7 0] 23 index_of')
-Starting at index distribute count "blocks" to the "banks" in the sequence.
[...] count index distribute
-----------------------------
- [...]
-
-
-This seems like it would be a PITA to implement in Joypy...
- -from joy.utils.stack import iter_stack, list_to_stack
-
-
-@SimpleFunctionWrapper
-def distribute(stack):
- '''Starting at index+1 distribute count "blocks" to the "banks" in the sequence.
-
- [...] count index distribute
- ----------------------------
- [...]
-
- '''
- index, (count, (sequence, stack)) = stack
- assert count >= 0
- cheat = list(iter_stack(sequence))
- n = len(cheat)
- assert index < n
- cheat[index] = 0
- while count:
- index += 1
- index %= n
- cheat[index] += 1
- count -= 1
- return list_to_stack(cheat), stack
-
-
-D['distribute'] = distribute
-J('[0 2 7 0] dup max [index_of] nullary distribute')
-J('[2 4 1 2] dup max [index_of] nullary distribute')
-J('[3 1 2 3] dup max [index_of] nullary distribute')
-J('[0 2 3 4] dup max [index_of] nullary distribute')
-J('[1 3 4 1] dup max [index_of] nullary distribute')
-[a F] x
-[a F] a F
-
-[a F] a swap [C] dip rest cons
-a [a F] [C] dip rest cons
-a C [a F] rest cons
-a C [F] cons
-
-w/ C == dup G
-
-a dup G [F] cons
-a a G [F] cons
-
-w/ G == dup max [index_of] nullary distribute
-
-define('direco dip rest cons')
-define('G [direco] cons [swap] swoncat cons')
-define('make_distributor [dup dup max [index_of] nullary distribute] G')
-J('[0 2 7 0] make_distributor 6 [x] times pop')
-First draft:
- -[] [GEN] x [pop index_of 0 >=] [pop size --] [[swons] dip x] tailrec
-
-
-(?)
- -[] [GEN] x [pop index_of 0 >=] [pop size --] [[swons] dip x] tailrec
-[] [...] [GEN] [pop index_of 0 >=] [pop size --] [[swons] dip x] tailrec
-[] [...] [GEN] pop index_of 0 >=
-[] [...] index_of 0 >=
- -1 0 >=
- False
-
-
-Base case
- -[] [...] [GEN] [pop index_of 0 >=] [pop size --] [[swons] dip x] tailrec
-[] [...] [GEN] pop size --
-[] [...] size --
-[] [...] size --
-
-
-A mistake, popop and no need for --
[] [...] [GEN] popop size
-[] size
-n
-
-
-Recursive case
- -[] [...] [GEN] [pop index_of 0 >=] [popop size] [[swons] dip x] tailrec
-[] [...] [GEN] [swons] dip x F
-[] [...] swons [GEN] x F
-[[...]] [GEN] x F
-[[...]] [...] [GEN] F
-
-[[...]] [...] [GEN] F
-
-
-What have we learned?
- -F == [pop index_of 0 >=] [popop size] [[swons] dip x] tailrec
-
-define('count_states [] swap x [pop index_of 0 >=] [popop size] [[swons] dip x] tailrec')
-define('AoC2017.6 make_distributor count_states')
-J('[0 2 7 0] AoC2017.6')
-J('[1 1 1] AoC2017.6')
-J('[8 0 0 0 0 0] AoC2017.6')
-from notebook_preamble import D, J, V, define
-V('23 sqr')
-How would we go about compiling this code (to Python for now)?
-The simplest thing would be to compose the functions from the library:
- -dup, mul = D['dup'], D['mul']
-def sqr(stack, expression, dictionary):
- return mul(*dup(stack, expression, dictionary))
-old_sqr = D['sqr']
-D['sqr'] = sqr
-V('23 sqr')
-It's simple to write a function to emit this kind of crude "compiled" code.
- -def compile_joy(name, expression):
- term, expression = expression
- code = term +'(stack, expression, dictionary)'
- format_ = '%s(*%s)'
- while expression:
- term, expression = expression
- code = format_ % (term, code)
- return '''\
-def %s(stack, expression, dictionary):
- return %s
-''' % (name, code)
-
-
-def compile_joy_definition(defi):
- return compile_joy(defi.name, defi.body)
-print(compile_joy_definition(old_sqr))
-But what about literals?
- -quoted == [unit] dip
-
-unit, dip = D['unit'], D['dip']
-# print compile_joy_definition(D['quoted'])
-# raises
-# TypeError: can only concatenate tuple (not "str") to tuple
-For a program like foo == bar baz 23 99 baq lerp barp we would want something like:
def foo(stack, expression, dictionary):
- stack, expression, dictionary = baz(*bar(stack, expression, dictionary))
- return barp(*lerp(*baq((99, (23, stack)), expression, dictionary)))
-You have to have a little discontinuity when going from a symbol to a literal, because you have to pick out the stack from the arguments to push the literal(s) onto it before you continue chaining function calls.
- -Call-chaining results in code that does too much work. For functions that operate on stacks and only rearrange values, what I like to call "Yin Functions", we can do better.
-We can infer the stack effects of these functions (or "expressions" or "programs") automatically, and the stack effects completely define the semantics of the functions, so we can directly write out a two-line Python function for them. This is already implemented in the joy.utils.types.compile_() function.
from joy.utils.types import compile_, doc_from_stack_effect, infer_string
-from joy.library import SimpleFunctionWrapper
-stack_effects = infer_string('tuck over dup')
-Yin functions have only a single stack effect, they do not branch or loop.
- -for fi, fo in stack_effects:
- print doc_from_stack_effect(fi, fo)
-source = compile_('foo', stack_effects[0])
-All Yin functions can be described in Python as a tuple-unpacking (or "-destructuring") of the stack datastructure followed by building up the new stack structure.
- -print source
-exec compile(source, '__main__', 'single')
-
-D['foo'] = SimpleFunctionWrapper(foo)
-V('23 18 foo')
-There are times when you're deriving a Joy program when you have a stack effect for a Yin function and you need to define it. For example, in the Ordered Binary Trees notebook there is a point where we must derive a function Ee:
[key old_value left right] new_value key [Tree-add] Ee
-------------------------------------------------------------
- [key new_value left right]
-
-
-While it is not hard to come up with this function manually, there is no necessity. This function can be defined (in Python) directly from its stack effect:
- - [a b c d] e a [f] Ee
---------------------------
- [a e c d]
-
-
-(I haven't yet implemented a simple interface for this yet. What follow is an exploration of how to do it.)
- -from joy.parser import text_to_expression
-Ein = '[a b c d] e a [f]' # The terms should be reversed here but I don't realize that until later.
-Eout = '[a e c d]'
-E = '[%s] [%s]' % (Ein, Eout)
-
-print E
-(fi, (fo, _)) = text_to_expression(E)
-fi, fo
-Ein = '[a1 a2 a3 a4] a5 a6 a7'
-Eout = '[a1 a5 a3 a4]'
-E = '[%s] [%s]' % (Ein, Eout)
-
-print E
-(fi, (fo, _)) = text_to_expression(E)
-fi, fo
-def type_vars():
- from joy.library import a1, a2, a3, a4, a5, a6, a7, s0, s1
- return locals()
-
-tv = type_vars()
-tv
-from joy.utils.types import reify
-stack_effect = reify(tv, (fi, fo))
-print doc_from_stack_effect(*stack_effect)
-print stack_effect
-Almost, but what we really want is something like this:
- -stack_effect = eval('(((a1, (a2, (a3, (a4, s1)))), (a5, (a6, (a7, s0)))), ((a1, (a5, (a3, (a4, s1)))), s0))', tv)
-Note the change of () to JoyStackType type variables.
print doc_from_stack_effect(*stack_effect)
-Now we can omit a3 and a4 if we like:
stack_effect = eval('(((a1, (a2, s1)), (a5, (a6, (a7, s0)))), ((a1, (a5, s1)), s0))', tv)
-The right and left parts of the ordered binary tree node are subsumed in the tail of the node's stack/list.
print doc_from_stack_effect(*stack_effect)
-source = compile_('Ee', stack_effect)
-print source
-Oops! The input stack is backwards...
- -stack_effect = eval('((a7, (a6, (a5, ((a1, (a2, s1)), s0)))), ((a1, (a5, s1)), s0))', tv)
-print doc_from_stack_effect(*stack_effect)
-source = compile_('Ee', stack_effect)
-print source
-Compare:
- - [key old_value left right] new_value key [Tree-add] Ee
-------------------------------------------------------------
- [key new_value left right]
-
-eval(compile(source, '__main__', 'single'))
-D['Ee'] = SimpleFunctionWrapper(Ee)
-V('[a b c d] 1 2 [f] Ee')
-
-Consider the compiled code of dup:
def dup(stack):
- (a1, s23) = stack
- return (a1, (a1, s23))
-To compile sqr == dup mul we can compute the stack effect:
stack_effects = infer_string('dup mul')
-for fi, fo in stack_effects:
- print doc_from_stack_effect(fi, fo)
-Then we would want something like this:
- -def sqr(stack):
- (n1, s23) = stack
- n2 = mul(n1, n1)
- return (n2, s23)
-
-
-How about...
- -stack_effects = infer_string('mul mul sub')
-for fi, fo in stack_effects:
- print doc_from_stack_effect(fi, fo)
-def foo(stack):
- (n1, (n2, (n3, (n4, s23)))) = stack
- n5 = mul(n1, n2)
- n6 = mul(n5, n3)
- n7 = sub(n6, n4)
- return (n7, s23)
-
-
-# or
-
-def foo(stack):
- (n1, (n2, (n3, (n4, s23)))) = stack
- n5 = sub(mul(mul(n1, n2), n3), n4)
- return (n5, s23)
-
-stack_effects = infer_string('tuck')
-for fi, fo in stack_effects:
- print doc_from_stack_effect(fi, fo)
-
-First, we need a source of Python identifiers. I'm going to reuse Symbol class for this.
from joy.parser import Symbol
-def _names():
- n = 0
- while True:
- yield Symbol('a' + str(n))
- n += 1
-
-names = _names().next
-Now we need an object that represents a Yang function that accepts two args and return one result (we'll implement other kinds a little later.)
- -class Foo(object):
-
- def __init__(self, name):
- self.name = name
-
- def __call__(self, stack, expression, code):
- in1, (in0, stack) = stack
- out = names()
- code.append(('call', out, self.name, (in0, in1)))
- return (out, stack), expression, code
-A crude "interpreter" that translates expressions of args and Yin and Yang functions into a kind of simple dataflow graph.
- -def I(stack, expression, code):
- while expression:
- term, expression = expression
- if callable(term):
- stack, expression, _ = term(stack, expression, code)
- else:
- stack = term, stack
- code.append(('pop', term))
-
- s = []
- while stack:
- term, stack = stack
- s.insert(0, term)
- if s:
- code.append(('push',) + tuple(s))
- return code
-Something to convert the graph into Python code.
- -strtup = lambda a, b: '(%s, %s)' % (b, a)
-strstk = lambda rest: reduce(strtup, rest, 'stack')
-
-
-def code_gen(code):
- coalesce_pops(code)
- lines = []
- for t in code:
- tag, rest = t[0], t[1:]
-
- if tag == 'pop':
- lines.append(strstk(rest) + ' = stack')
-
- elif tag == 'push':
- lines.append('stack = ' + strstk(rest))
-
- elif tag == 'call':
- #out, name, in_ = rest
- lines.append('%s = %s%s' % rest)
-
- else:
- raise ValueError(tag)
-
- return '\n'.join(' ' + line for line in lines)
-
-
-def coalesce_pops(code):
- index = [i for i, t in enumerate(code) if t[0] == 'pop']
- for start, end in yield_groups(index):
- code[start:end] = \
- [tuple(['pop'] + [t for _, t in code[start:end][::-1]])]
-
-
-def yield_groups(index):
- '''
- Yield slice indices for each group of contiguous ints in the
- index list.
- '''
- k = 0
- for i, (a, b) in enumerate(zip(index, index[1:])):
- if b - a > 1:
- if k != i:
- yield index[k], index[i] + 1
- k = i + 1
- if k < len(index):
- yield index[k], index[-1] + 1
-
-
-def compile_yinyang(name, expression):
- return '''\
-def %s(stack):
-%s
- return stack
-''' % (name, code_gen(I((), expression, [])))
-A few functions to try it with...
- -mul = Foo('mul')
-sub = Foo('sub')
-def import_yin():
- from joy.utils.generated_library import *
- return locals()
-
-yin_dict = {name: SimpleFunctionWrapper(func) for name, func in import_yin().iteritems()}
-
-yin_dict
-
-dup = yin_dict['dup']
-
-#def dup(stack, expression, code):
-# n, stack = stack
-# return (n, (n, stack)), expression
-... and there we are.
- -print compile_yinyang('mul_', (names(), (names(), (mul, ()))))
-e = (names(), (dup, (mul, ())))
-print compile_yinyang('sqr', e)
-e = (names(), (dup, (names(), (sub, (mul, ())))))
-print compile_yinyang('foo', e)
-e = (names(), (names(), (mul, (dup, (sub, (dup, ()))))))
-print compile_yinyang('bar', e)
-e = (names(), (dup, (dup, (mul, (dup, (mul, (mul, ())))))))
-print compile_yinyang('to_the_fifth_power', e)
-
-
-
-
-In 1969 George Spencer-Brown (GSB) published "Laws of Form" which presented a logical system based on a single action, a distinction, that is both an operation and a value. This notebook describes a Python implementation that mimics the Laws of Form notation and uses it to develop a model of computer circuits.
- -See The Markable Mark.
-(()) =
-()() = ()
-
-
-A((B)) = AB
-A() = ()
-A(AB) = A(B)
-
-
-I call these three laws the Bricken Basis after William Bricken who figured out that the third law is complete with the other two. GSB had the first two laws and "Each Way" as the basis. (TODO: Find and include the references for all this.)
-(If anything here is unclear read The Markable Mark. George Burnett-Stuart has done a fantastic job there explaining the Laws of Form.)
- -We can use data structures made solely out of Python frozenset and string objects to represent the forms of the Laws of Form notation. I'm going to use the terms "expression" and "form" interchangably in this document.
class Form(frozenset):
-
- def __str__(self):
- # Because frozenset is immutable, and the contents are all string or frozenset,
- # we can cache the string repr of a form.
- try:
- return self._str
- except AttributeError:
- self._str = '(%s)' % ' '.join(sorted(map(str, self)))
- return self._str
-
- __repr__ = __str__
-
-
-def F(*terms):
- '''Create a Form from terms.'''
- return Form([
- term if isinstance(term, (basestring, Form)) else F(*term)
- for term in terms
- ])
-Define a few variable names.
- -a, b, c = 'abc'
-Some examples of forms.
- -A = F(a, b, c)
-A
-B = F(a, (b, (c,)))
-B
-Forms like a b c must be enclosed in a pair of nested containers like so (( a b c )), this lets us treat them as a single (Python) object without inverting the logical value of the form.
C = F((a, b, c))
-C
-Duplicate terms in a form are automatically removed by frozenset.
F(a, (b,), a, (b,))
-Order is irrelevant, again due to frozenset.
F(b, a, c) == F(a, b, c)
-It's prefectly okay to create forms out of other forms (not just strings.)
- -F(A, (B, (C,)), a)
-Mark = F()
-Mark
-There is no way to represent Void directly in a programming language so we have to use the simplest Void-valued form instead.
- -Void = F(Mark)
-Void
-We can use a Python dict as a context or environment that supplies values (Mark or Void) for the names in a form.
env = dict(a=Mark, b=Mark, c=Mark)
-reify(form, environment) Function¶Given forms with string variable names in them we want to be able to substitute values from an environment. If these values are Mark or Void the result will be a pure arithmentic form.
- -def reify(form, environment):
- if isinstance(form, basestring):
- return environment.get(form, form)
- return Form(reify(inner, environment) for inner in form)
-for form in (A, B, C):
- print form, u'⟶', reify(form, env)
-void(form) Function¶Once the forms have been rendered to pure arithmetic we can use the void() function to find the value of each expression.
def void(form):
- return any(not void(i) for i in form)
-The void() function returns a Boolean value (Python True or False), for convenience let's write a function that returns the Mark or Void value of a form.
def value_of(form, m=Mark, v=Void):
- return (m, v)[void(form)]
-Now we can use the void() function (by way of value_of()) to calculate the base value of each expression structure.
for form in (A, B, C):
- arith = reify(form, env)
- print form, u'⟶', arith, u'⟶', value_of(arith)
-For $n$ variables there are $2^n$ possible assignments of the two values of Mark and Void. If we generate environments that each contain one of the possible assignments of names to the base value we can evaluate an expression containing those names and compute its value.
- -from itertools import product, izip
-
-
-BASE = Void, Mark
-
-
-def environments_of_variables(*variables):
- universe = [BASE] * len(variables)
- for values in product(*universe):
- yield dict(izip(variables, values))
-
-
-envs = list(environments_of_variables(*'abc'))
-
-
-envs
-This is a bit hard to read, so let's define a helper function to convert an environment to a string format.
- -def format_env(env, m='()', v=' '):
- return ' '.join((v, m)[not env[k]] for k in sorted(env))
-
-# Note that Mark is an empty frozenset so in a Boolean context in Python it is False,
-# likewise Void is a set with one member, so Python considers it True in a Boolean context.
-# The `not` in the expression is just to force such a Boolean context, and we compensate
-# by putting `v` in the zero-is-False position in the indexed tuple.
-Now we can print out the environments in a table. Notice that it looks just like a list of the eight three-bit binary numbers.
- -print 'i a b c i in Binary'
-for i, env in enumerate(envs):
- print i, format_env(env, v='--'), '%3s' % (bin(i)[2:],)
-Let's pick one of the expressions and iterate through the environments showing the result of reifying that expression in that environment.
- -print B
-print '-----------'
-for i, env in enumerate(envs):
- e = reify(B, env)
- print i, format_env(env, v='--'), u'⟶', e, u'⟶', value_of(e, m='()', v='')
-Let's render the above as a Truth Table.
- -def truth_table_3(expression):
- print expression
- print ' a b c | Value'
- print '---------+------'
- for E in envs:
- e = reify(expression, E)
- print format_env(E), '|', value_of(e, m='()', v='')
-truth_table_3(B)
-This makes it clear that each expression in Laws of Form calculus is describing a digital Boolean circuit. The names are its inputs and its Void/Mark value is its output. Each boundary is a multi-input NOR gate, known as the Peirce arrow or Quine dagger (See Sheffer stroke and NOR gate.) Instead of two Boolean values there is only one value and non-existance.
- -In order to work with expressions as digital circuits, let's define some helper functions that will create logic circuits out of simpler forms. The names of the functions below reflect the choice of Mark as Boolean True but this is just a convention.
nor = lambda *bits: F(*bits)
-or_ = lambda *bits: F(bits)
-and_ = lambda *bits: Form(F(bit) for bit in bits)
-nand = lambda *bits: nor(and_(*bits))
-nxor = eqiv = lambda a, b: F((a, (b,)), ((a,), b))
-xor = lambda a, b: F(nxor(a, b))
-
-# To build logical expressions with Void as Boolean True use these functions.
-anti_nor = nand
-anti_or = and_
-anti_and = or_
-anti_nand = nor
-anti_eqiv = xor
-anti_xor = eqiv
-Some examples:
- -a, b, c = 'abc'
-
-
-some_expressions = (
- nor(a, b, c),
- or_(a, b, c),
- and_(a, b, c),
- nand(a, b, c),
- xor(a, b),
- eqiv(a, b),
- xor(a, xor(b, c)),
-)
-
-
-for expression in some_expressions:
- print expression
-And let's rewrite the truth_table_3() function to make it work for any number of variables.
def yield_variables_of(expression):
- '''Yield all string members of an expression.'''
- if isinstance(expression, basestring):
- yield expression
- else:
- for inner in expression:
- for leaf in yield_variables_of(inner):
- yield leaf
-
-
-def collect_names(expression):
- '''Return a set of the variables mentioned in an expression.'''
- return set(yield_variables_of(expression))
-
-
-def truth_table(expression):
- '''Print a truth table for an expression.'''
- names = sorted(collect_names(expression))
- header = ' ' + ' '.join(names)
- n = 1 + len(header)
- header += ' | Value'
- print expression
- print header
- print '-' * n + '+------'
- for env in environments_of_variables(*names):
- e = reify(expression, env)
- print format_env(env), '|', ['()', ''][void(e)]
-We can use this truth_table() function to examine the expressions we created above.
truth_table(nor(a, b, c))
-truth_table(or_(a, b, c))
-truth_table(and_(a, b, c))
-truth_table(xor(a, b))
-truth_table(eqiv(a, b))
-truth_table(xor(a, xor(b, c)))
-E1 = and_(
- or_(and_(a, b), and_(b, c), and_(c, a)), # Any two variables...
- nand(a, b, c) # ...but not all three.
-)
-truth_table(E1)
-This is a brute-force SAT solver that doesn't even bother to stop once it's found a solution.
- -Sometimes we will have a function for which we know the behavior (truth table) but not an expression and we want the expression. For example, imagine that we didn't just create the expression for this table:
- - a b c | Value
----------+------
- |
- () |
- () |
- () () | ()
-() |
-() () | ()
-() () | ()
-() () () |
-
-
-To write an expression for this table, first we should understand that each row can be represented as an expression.
- - ⟶ ( a b c )
- () ⟶ ( a b (c))
- () ⟶ ( a (b) c )
- () () ⟶ ( a (b) (c))
-() ⟶ ((a) b c )
-() () ⟶ ((a) b (c))
-() () ⟶ ((a) (b) c )
-() () () ⟶ ((a) (b) (c))
-
-
-Each of the above expressions will be true (Mark-valued) for only one possible combination of the three input variables. For example, let's look at the sixth expression above:
- -e6 = F((a,), b, (c,))
-truth_table(e6)
-To make an expression that is Mark-valued for just certain rows of the table, pick those rows' expressions,
- - () () | ( a (b) (c))
-() () | ((a) b (c))
-() () | ((a) (b) c )
-
-
-And write them down as terms in an OR expression:
- -E = (a(b)(c)) ((a)b(c)) ((a)(b)c)
-
-
-In conventional notation this is called Disjunctive normal form:
- -E = (¬a ∧ b ∧ c) ∨ (a ∧ ¬b ∧ c) ∨ (a ∧ b ∧ ¬c)
-
-
-Here it is in action:
- -e4 = ( a, (b,), (c,))
-e6 = ((a,), b, (c,))
-e7 = ((a,), (b,), c )
-
-E2 = or_(e4, e6, e7)
-
-truth_table(E2)
-Note that the expression E2 above is equivalent to the ealier expression E1 that has the same truth table, in other words:
- -((((((a) (b)) ((b) (c)) ((c) (a))))) ((((a) (b) (c)))))
-
-
-equals
- -(((a (b) (c)) ((a) b (c)) ((a) (b) c)))
-
-
-We can demonstrate this equivalence by evaluating the expression formed by eqiv() from these two.
For every environment (from the set of possible values for the variables) if both expressions have the same value when evaluated then the eqiv() of those expressions will be Mark-valued (true in our chosen context.)
truth_table(eqiv(E1, E2))
-The truth table above shows that the equivalence expression is true (Mark-valued by our current convention) for all possible assignments of Mark/Void to the three variables a, b, and c. This indicates that the expression is a tautology.
If you have two binary digits ("bits") and you are interested in the (binary) sum of these digits you will need two circuits, one for the "ones place" and one for the "twos place" or "carry bit".
-Consider:
- -a b | c s
-----+----
-0 0 | 0 0
-0 1 | 0 1
-1 0 | 0 1
-1 1 | 1 0
-
-
-Treating each output column ('c' for carry, 's' for sum) as a single expression, it's easy to see that the carry bit is just AND and the sum bit is just XOR of the two input bits.
- -a, b = 'ab'
-
-
-half_bit_adder = {
- 'Sum': xor(a, b),
- 'Carry': and_(a, b),
-}
-
-
-for name, expr in half_bit_adder.items():
- print name
- truth_table(expr)
- print
-In order to add two multi-bit binary numbers we need adder circuits that are designed to work with three input bits: the two bits to add together and a carry bit from the previous addition:
- - a b Cin Sum Cout
- 0 0 0 | 0 0
- 0 0 1 | 1 0
- 0 1 0 | 1 0
- 0 1 1 | 0 1
- 1 0 0 | 1 0
- 1 0 1 | 0 1
- 1 1 0 | 0 1
- 1 1 1 | 1 1
-
-
-Looking back at our table of three-variable expressions:
- - ⟶ ( a b c )
- () ⟶ ( a b (c))
- () ⟶ ( a (b) c )
- () () ⟶ ( a (b) (c))
-() ⟶ ((a) b c )
-() () ⟶ ((a) b (c))
-() () ⟶ ((a) (b) c )
-() () () ⟶ ((a) (b) (c))
-
-
-We can easily determine expressions for sum and carry:
- -Sum = (a b (c)) (a (b) c) ((a) b c) ((a) (b) (c))
-
-Cout = (a (b) (c)) ((a) b (c)) ((a) (b) c) ((a) (b) (c))
-
-Sum = F(( (a, b, (c,)), (a, (b,), c), ((a,), b, c), ((a,), (b,), (c,)) ),)
-Carry = F(( (a, (b,), (c,)), ((a,), b, (c,)), ((a,), (b,), c), ((a,), (b,), (c,)) ),)
-print 'Sum'
-truth_table(Sum)
-print
-print 'Carry'
-truth_table(Carry)
-Let's make a full_bit_adder() function that can define new expressions in terms of variables (or expressions) passed into it.
def full_bit_adder(a, b, c):
- return (
- F(( (a, b, (c,)), (a, (b,), c), ((a,), b, c), ((a,), (b,), (c,)) ),),
- F(( (a, (b,), (c,)), ((a,), b, (c,)), ((a,), (b,), c), ((a,), (b,), (c,)) ),),
- )
-Now we can chain it to make a set of circuits that define together an eight-bit adder circuit with carry.
- -sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-sum4, cout = full_bit_adder('a4', 'b4', cout)
-sum5, cout = full_bit_adder('a5', 'b5', cout)
-sum6, cout = full_bit_adder('a6', 'b6', cout)
-sum7, cout = full_bit_adder('a7', 'b7', cout)
-Unfortunately, the sizes of the resulting expression explode:
- -map(len, map(str, (sum0, sum1, sum2, sum3, sum4, sum5, sum6, sum7, cout)))
-We could also use the definitions from the Wikipedia article:
- -S = A ⊕ B ⊕ C
-Cout = (A ⋅ B) + (Cin ⋅ (A ⊕ B))
-
-def full_bit_adder(a, b, c):
- return (
- xor(xor(a, b), c),
- or_(and_(a, b), and_(c, xor(a, b))),
- )
-sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-print 'Sum'
-truth_table(sum0)
-print
-print 'Carry'
-truth_table(cout)
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-sum4, cout = full_bit_adder('a4', 'b4', cout)
-sum5, cout = full_bit_adder('a5', 'b5', cout)
-sum6, cout = full_bit_adder('a6', 'b6', cout)
-sum7, cout = full_bit_adder('a7', 'b7', cout)
-The sizes of these expression are much more tractable:
- -map(len, map(str, (sum0, sum1, sum2, sum3, sum4, sum5, sum6, sum7, cout)))
-The Form Python datastructure is based on frozenset so duplicate terms are automatically removed and order of terms is irrelevant just as we would prefer. But we want to be able to automatically simplify forms beyond just that. Ideally, we would like a function that applies the rules of the calculus automatically:
A((B)) = AB
-A() = ()
-A(AB) = A(B)
-
-
-I'm going to specify the behaviour of the desired function in a unittest.
- -import unittest
-Let's deal with three easy cases first: string, the Mark, and the Void. The simplify() function should just return them unchanged.
class UnwrapTest0(unittest.TestCase):
-
- def testMark(self):
- self.assertEqual(Mark, simplify(Mark))
-
- def testVoid(self):
- self.assertEqual(Void, simplify(Void))
-
- def testLeaf(self):
- self.assertEqual('a', simplify('a'))
-
-
-def simplify(form):
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'UnwrapTest0'], exit=False)
-(a)¶A single string in a form (a) should also be returned unchanged:
class UnwrapTest1(unittest.TestCase):
-
- def testNegatedLeaf(self):
- a = nor('a')
- self.assertEqual(a, simplify(a))
-
-
-def simplify(form):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- # Let's just recurse.
- return Form(simplify(inner) for inner in form)
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'UnwrapTest1'], exit=False)
-So far, so good. But what about ((a))? This should be returned as just a. And ((a b)) should remain ((a b)) because we can't represent just a b as a single Python object, so we have to retain the outer pair of containers to hold them without inverting the Mark/Void value (if we just used one container.)
class UnwrapTest2(unittest.TestCase):
-
- def testUnwrapLeaf(self):
- '''((a)) = a'''
- a = or_('a')
- self.assertEqual('a', simplify(a))
-
- def testDoNotUnwrapTwoLeaves(self):
- '''((a b)) = ((a b))'''
- a = or_('a', 'b')
- self.assertEqual(a, simplify(a))
-
-
-def simplify(form):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- # Let's just recurse.
- result = Form(simplify(inner) for inner in form)
-
- # Check for ((a)) and return just a.
- # If there is more than one item in the inner container ((a b..))
- # then we must keep the outer containers.
- if len(result) == 1:
- inner, = result # inner = (a)
- if isinstance(inner, Form) and len(inner) == 1:
- a, = inner
- return a
-
- return result
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'UnwrapTest2'], exit=False)
-Does it work for (((a))) = (a) and ((((a)))) = a and so on?
class UnwrapTest3(unittest.TestCase):
-
- def testMultiUnwrapLeaf(self):
- A = 'a'
- B = nor(A)
- a = nor(B)
- self.assertEqual(A, simplify(a))
- a = nor(a)
- self.assertEqual(B, simplify(a))
- a = nor(a)
- self.assertEqual(A, simplify(a))
- a = nor(a)
- self.assertEqual(B, simplify(a))
- a = nor(a)
- self.assertEqual(A, simplify(a))
- a = nor(a)
- self.assertEqual(B, simplify(a))
-
- def testMultiDoNotUnwrapTwoLeaves(self):
- e = F('a', 'b')
- f = a = nor(e)
- self.assertEqual(f, simplify(a))
- a = nor(a)
- self.assertEqual(e, simplify(a))
- a = nor(a)
- self.assertEqual(f, simplify(a))
- a = nor(a)
- self.assertEqual(e, simplify(a))
- a = nor(a)
- self.assertEqual(f, simplify(a))
- a = nor(a)
- self.assertEqual(e, simplify(a))
-
-# Technically, several of the tests above are redundant,
-# I'm not willing to figure out the right point ot stop
-# right now, so I just do extra tests.
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'UnwrapTest3'], exit=False)
-But now let's trick our function, it can't handle (a ((b c))) = (a b c) yet. This is going to require an auxiliary helper function that is similar to simplify() but that yields terms into an outer context.
class UnwrapTest4(unittest.TestCase):
-
- def testMultiUnwrapLeaf(self):
- a, b, c = 'abc'
- f = F(a,((b, c),))
- e = F(a, b, c)
- self.assertEqual(e, simplify(f))
-
- def testMulti_blah_Leaf(self):
- a, b, c = 'abc'
- f = F(a,(((b, c),),),)
- e = F(a, (b, c))
- self.assertEqual(e, simplify(f))
-
- def testMulti_blah_blah_Leaf(self):
- a, b, c, d = 'abcd'
- f = F(a,((((b, c),), d),))
- e = F(a, b, c, d)
- self.assertEqual(e, simplify(f))
-
-
-def simplify(form):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- result = []
- for inner in simplify_gen(form): # Use the generator instead of recursing into simplify().
- result.append(inner)
- result = Form(result)
-
- # Check for ((a)) and return just a.
- # If there is more than one item in the inner container ((a b..))
- # then we must keep the outer containers.
- if len(result) == 1:
- inner, = result # inner = (a)
- if isinstance(inner, Form):
- if len(inner) == 1:
- a, = inner
- return a
- else:
- # len(inner) cannot be 0, because that means form is Void
- # and would already have been returned.
- assert len(inner) > 1, repr(inner)
-
- # What to do here?
- # We cannot yield the items in inner into the containing context
- # because we don't have it (or even know if it exists.)
- # Therefore we need a different simplify() generator function that yields
- # the simplified contents of a form, and we have to call that instead
- # of recurring on simplify() above.
- pass
-
-
- return result
-
-
-def simplify_gen(form):
-
- for inner in form:
-
- inner = simplify(inner)
- # Now inner is simplified, except for ((a b...)) which simplify() can't handle.
-
- # Three easy cases, strings, Mark, or Void.
- if isinstance(inner, basestring):
- yield inner
- continue
-
- if inner == Mark:
- yield inner
- assert False # The simplify() function will not keep iterating after this.
- return # Partial implementation of ()A = ().
-
- if inner == Void:
- continue # Omit Void. Implementation of (()) = .
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # yielded above.)
-
- # Check for ((...)) and return just ... .
- if len(inner) > 1: # (foo bar)
- yield inner
- continue
-
- assert len(inner) == 1, repr(inner) # Just in case...
-
- inner_inner, = inner
- if isinstance(inner_inner, Form): # inner_inner = (...)
- for inner_inner_inner in inner_inner:
- yield inner_inner_inner
- continue
-
- #else: # inner_inner = foo ; inner = (foo)
-
- yield inner
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'UnwrapTest4'], exit=False)
-If the Mark occurs in a sub-form it should Occlude all sibling sub-forms, rendering its container form Void.
- -class MarkTest0(unittest.TestCase):
-
- def testMarkOccludes0(self):
- a, b, c = 'abc'
- f = F(a, (), b, c)
- self.assertEqual(Void, simplify(f))
-
- def testMarkOccludes1(self):
- a, b, c = 'abc'
- f = F(a, (b, c, ()))
- e = F(a)
- self.assertEqual(e, simplify(f))
-
- def testMarkOccludes2(self):
- a, b, c = 'abc'
- f = F(a, (b, ((), c)))
- e = F(a, (b,))
- self.assertEqual(e, simplify(f))
-
-
-def simplify(form):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- result = []
- for inner in simplify_gen(form):
- if inner == Mark:
- return Void # Discard any other inner forms, form is Void.
- result.append(inner)
- result = Form(result)
-
- # Check for ((a)) and return just a.
- # If there is more than one item in the inner container ((a b..))
- # then we must keep the outer containers.
- if len(result) == 1:
- inner, = result # inner = (a)
- if isinstance(inner, Form):
- if len(inner) == 1:
- a, = inner
- return a
-
- return result
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'MarkTest0'], exit=False)
-So we have (()) = -- and ()A = () what about A(AB) = A(B)?
class PervadeTest0(unittest.TestCase):
-
- def testPervade0(self):
- a = 'a'
- f = F(a, (a,))
- self.assertEqual(Void, simplify(f))
-
- def testPervade1(self):
- a = 'a'
- f = F(((a,),), (a,))
- self.assertEqual(Void, simplify(f))
-
- def testPervade2(self):
- a = 'a'
- b = 'b'
- f = F(a, (b, (a,)))
- e = F(a)
- self.assertEqual(e, simplify(f))
-
-
-def simplify(form, exclude=None):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- if exclude is None:
- exclude = set()
-
- new_stuff = form - exclude
- exclude = exclude | new_stuff
-
- result = []
- for inner in simplify_gen(new_stuff, exclude):
- if inner == Mark:
- return Void # Discard any other inner forms, form is Void.
- result.append(inner)
- result = Form(result)
-
- # Check for ((a)) and return just a.
- # If there is more than one item in the inner container ((a b..))
- # then we must keep the outer containers.
- if len(result) == 1:
- inner, = result # inner = (a)
- if isinstance(inner, Form):
- if len(inner) == 1:
- a, = inner
- return a
-
- return result
-
-
-def simplify_gen(form, exclude):
-
- for inner in form:
-
- inner = simplify(inner, exclude)
- # Now inner is simplified, except for ((a b...)) which simplify() can't handle.
-
- # Three easy cases, strings, Mark, or Void.
- if isinstance(inner, basestring):
- yield inner
- continue
-
- if inner == Mark:
- yield inner
- assert False # The simplify() function will not keep iterating after this.
- return # Partial implementation of ()A = ().
-
- if inner == Void:
- continue # Omit Void. Implementation of (()) = .
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # yielded above.)
-
- # Check for ((...)) and return just ... .
- if len(inner) > 1: # (foo bar)
- yield inner
- continue
-
- assert len(inner) == 1, repr(inner) # Just in case...
-
- inner_inner, = inner
- if isinstance(inner_inner, Form): # inner_inner = (...)
- for inner_inner_inner in inner_inner:
- yield inner_inner_inner
- continue
-
- #else: # inner_inner = foo ; inner = (foo)
-
- yield inner
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'PervadeTest0'], exit=False)
-TODO set up Hypothesis to generate test cases...
- -# Run ALL the tests!
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored'], exit=False)
-truth_table(F((a, b), (a, (b,))))
-The form says, "if b then a else a". I'll come back to the interpretation of "Each-Way" as an if-then-else statement later.
The thing to note here is that the value for a can be a whole expression which appears twice in the new form: once next to b and once next to (b).
In the first case we can remove any occurances of b from the a next to it
b (...(b c (d ...)))
-b (...( c (d ...)))
-
-
-and in the second case we can change any occurances of b to the Mark.
(b)(...(b c (d ...)))
-(b)((b)(b c (d ...)))
-(b)(...(b (b) c (d ...)))
-(b)(...(b ( ) c (d ...)))
-(b)(...( ( ) ))
-(b)(... )
-
-
-We can send (b) into the form until it reaches and b, at which point b(b) becomes () and sweeps out any siblings rendering its containing form Void.
For the first case we can use simplify() and pass in b as a member of the exclude set.
A = F(a, (b, (c,)))
-A
-simplify(A, {b})
-with_mark¶In the second case (b)...b... = (b)...()... we can modify the simplify() function to accept a name that it should treat as Mark-valued.
class MarkitTest0(unittest.TestCase):
-
- def testMarkit0(self):
- a, b, c = 'abc'
- f = F(a, (b, (c,)))
- e = F(a)
- self.assertEqual(e, simplify(f, with_mark=b))
-
-
-def simplify(form, exclude=None, with_mark=None):
-
- # Three easy cases, for strings, Mark, or Void, just return it.
- if isinstance(form, basestring) or form in BASE:
- return form
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # returned above.)
-
- if exclude is None:
- exclude = set()
-
- new_stuff = form - exclude
- exclude = exclude | new_stuff
-
- result = []
- for inner in simplify_gen(new_stuff, exclude, with_mark):
- if inner == Mark or inner == with_mark:
- return Void # Discard any other inner forms, form is Void.
- result.append(inner)
- result = Form(result)
-
- # Check for ((a)) and return just a.
- # If there is more than one item in the inner container ((a b..))
- # then we must keep the outer containers.
- if len(result) == 1:
- inner, = result # inner = (a)
- if isinstance(inner, Form):
- if len(inner) == 1:
- a, = inner
- return a
-
- return result
-
-
-def simplify_gen(form, exclude, with_mark):
-
- for inner in form:
-
- inner = simplify(inner, exclude, with_mark)
- # Now inner is simplified, except for ((a b...)) which simplify() can't handle.
-
- # Three easy cases, strings, Mark, or Void.
- if isinstance(inner, basestring):
- yield inner
- continue
-
- if inner == Mark or inner == with_mark:
- yield Mark
- assert False # The simplify() function will not keep iterating after this.
- return # Partial implementation of ()A = ().
-
- if inner == Void:
- continue # Omit Void. Implementation of (()) = .
-
- # We know it's a Form and it's not empty (else it would be the Mark and
- # yielded above.)
-
- # Check for ((...)) and return just ... .
- if len(inner) > 1: # (foo bar)
- yield inner
- continue
-
- assert len(inner) == 1, repr(inner) # Just in case...
-
- inner_inner, = inner
- if isinstance(inner_inner, Form): # inner_inner = (...)
- for inner_inner_inner in inner_inner:
- if inner_inner_inner == with_mark:
- yield Mark
- assert False # The simplify() function will not keep iterating after this.
- return # Never reached, could delete this line.
- yield inner_inner_inner
- continue
-
- #else: # inner_inner = foo ; inner = (foo)
-
- yield inner
-
-
-if __name__ == '__main__':
- unittest.main(argv=['ignored', 'MarkitTest0'], exit=False)
-simplify(A, with_mark=b)
-Now we can create a new form that is equivalent to A.
def each_way(form, name):
- return simplify(F(
- ( name , form),
- ((name,), simplify(form, with_mark=name)),
- ))
-
-Ab = each_way(A, b)
-
-print A, '=', Ab
-In this particular case the original form A was so simple that the new version Ab is actually a bit larger. With a large expression to start with the form after simplification would (usually) be smaller.
Recall our original expressions for the sum and carry bits of a full-bit adder circuit, derived from the truth tables:
- -Sum = F(( (a, b, (c,)), (a, (b,), c), ((a,), b, c), ((a,), (b,), (c,)) ),)
-Carry = F(( (a, (b,), (c,)), ((a,), b, (c,)), ((a,), (b,), c), ((a,), (b,), (c,)) ),)
-Sum
-Carry
-And the expressions derived from the definitions on Wikipedia:
- -Sum, Carry = full_bit_adder(a, b, c)
-Sum
-Carry
-We can use the each_way() function to look for simpler equivalent forms by, for example, iterating though the names and trying it with each. Try the following cells with both versions of the Sum and Carry above.
print Sum
-for name in (a, b, c, a, b, c):
- Sum = each_way(Sum, name)
- print Sum
-print Carry
-for name in (a, b, c, a, b, c):
- Carry = each_way(Carry, name)
- print Carry
-Let's redefine the full_bit_adder() function with the smallest version of each above.
def full_bit_adder(a, b, c):
- '''From the truth table.'''
- return (
- F(( (a, b, (c,)), (a, (b,), c), ((a,), b, c), ((a,), (b,), (c,)) ),),
- F(( (a, (b,), (c,)), ((a,), b, (c,)), ((a,), (b,), c), ((a,), (b,), (c,)) ),),
- )
-# Sizes: [63, 327, 1383, 5607, 22503, 90087, 360423, 1441767, 1441773]
-def full_bit_adder(a, b, c):
- '''Simplest forms from above.'''
- return (
- F( (((b,), (c,)), (a,), (b, c)), (((b,), c), ((c,), b), a) ),
- F( (((a,), (c,)), b), ((b,), a, c) ),
- )
-# Sizes: [57, 177, 417, 897, 1857, 3777, 7617, 15297, 7653]
-def full_bit_adder(a, b, c):
- '''Based on the definitions from Wikipedia.'''
- return (
- xor(xor(a, b), c), # ((((((((a) b) ((b) a)))) c) (((((a) b) ((b) a))) (c))))
- or_(and_(a, b), and_(c, xor(a, b))), # ((((((((a) b) ((b) a)))) (c)) ((a) (b))))
- )
-# Sizes: [67, 159, 251, 343, 435, 527, 619, 711, 371]
-def full_bit_adder(a, b, c):
- '''Based on the definitions from Wikipedia.'''
- return (
- simplify(xor(xor(a, b), c)),
- simplify(or_(and_(a, b), and_(c, xor(a, b)))),
- )
-# Sizes: [59, 135, 211, 287, 363, 439, 515, 591, 311]
-In this case, the version from the definitions does much better than the other two.
- -Sum, Carry = full_bit_adder(a, b, c)
-
-truth_table(Sum)
-print
-truth_table(Carry)
-print
-
-sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-sum4, cout = full_bit_adder('a4', 'b4', cout)
-sum5, cout = full_bit_adder('a5', 'b5', cout)
-sum6, cout = full_bit_adder('a6', 'b6', cout)
-sum7, cout = full_bit_adder('a7', 'b7', cout)
-
-print map(len, map(str, (sum0, sum1, sum2, sum3, sum4, sum5, sum6, sum7, cout)))
-This is something of an Interlude, we aren't going to use it below, but it's too cool to omit mention.
-We can use the simplify() function to create a more efficient SAT solver along the lines of the DPLL algorithm.
It works by selecting a name from the form, and simplifying the form with that name first as Void then as Mark, then recursing with the new form and the next name. If the resulting simplified form becomes the Mark then our choices (of assigning Void or Mark to the names selected so far) constitute a "solution" to the original form. That is, if we reify() the form with the environment returned by the dpll() function the result will be Mark-valued.
def dpll(E, partial=None, unit=True):
- if partial is None:
- partial = {}
- else:
- partial = partial.copy() # so we can backtrack later..
-
- if isinstance(E, basestring):
- partial[E] = Mark
- return partial
-
- if unit:
- E = assign_unit_clauses(E, partial)
-
- if not E:
- return partial
-
- if Mark in E:
- return
-
- v = next_symbol_of(E)
-
- partial[v] = Void
-
- res = dpll(simplify(E, {v}), partial, unit)
- if res is not None:
- return res
-
- partial[v] = Mark
-
- return dpll(simplify(E, with_mark=v), partial, unit)
-
-
-def assign_unit_clauses(E, partial):
- '''
- Find and assign values to an "unit clauses" in the form, simplifying as you go.
- A unit clause is a bare name or a negated name: a or (a), for these clauses we
- can set them to Void or Mark, respectively, to contibute to making their containing
- Form the Mark.
- '''
- on, off, E = find_units(E)
- while on or off:
- while on:
- if on & off: return Void
- term = first_of(on)
- partial[term] = Mark
- ON, OFF, E = find_units(simplify(E, with_mark=term))
- on |= ON
- on.remove(term)
- off |= OFF
- while off:
- if on & off: return Void
- term = first_of(off)
- partial[term] = Void
- ON, OFF, E = find_units(simplify(E, {term}))
- off |= OFF
- off.remove(term)
- on |= ON
- return E
-
-
-def next_symbol_of(E):
- if isinstance(E, basestring):
- return E
- for it in E:
- return next_symbol_of(it)
- raise Exception("no more symbols")
-
-
-def find_units(E):
- '''
- Return two sets and a possibly-reduced E. The literals in the first
- set must be Void and those in the second must be set Mark to have the
- entire expression become Void.
- '''
- on, off, poly = set(), set(), set()
- for clause in E:
- if isinstance(clause, basestring):
- off.add(clause)
- continue
- if len(clause) != 1:
- poly.add(clause)
- continue
- (n,) = clause # Unwrap one layer of containment.
- if isinstance(n, basestring):
- on.add(n)
- else:
- poly.add(clause)
- return on, off, Form(poly)
-
-
-# Return any item from a form.
-first_of = lambda form: next(iter(form))
-A = F(a, (b, (c,)))
-truth_table(A)
-solution = dpll(A)
-arith = reify(A, solution)
-print
-print 'A solution:', solution
-print
-print 'Reifies to', arith, u'⟶', value_of(arith)
-dpll_iter()¶We can write a generator version of the function that keeps looking for solutions after the first.
- -def dpll_iter(E, partial=None, unit=True):
- if partial is None:
- partial = {}
- else:
- partial = partial.copy() # so we can backtrack later..
-
- if isinstance(E, basestring):
- partial[E] = Mark
- yield partial
- return
-
- if unit:
- E = assign_unit_clauses(E, partial)
-
- if not E:
- yield partial
- return
-
- if Mark in E:
- return
-
- v = next_symbol_of(E)
-
- partial[v] = Void
-
- for res in dpll_iter(simplify(E, {v}), partial, unit):
- yield res
-
- partial[v] = Mark
-
- for res in dpll_iter(simplify(E, with_mark=v), partial, unit):
- yield res
-for solution in dpll_iter(A):
- print (solution)
-Sum, Carry = full_bit_adder(a, b, c)
-for solution in dpll_iter(Sum, unit=False):
- r = reify(Sum, solution)
- print (solution), r, '=', simplify(r)
-for solution in dpll_iter(Carry, unit=False):
- r = reify(Carry, solution)
- print (solution), r, '=', simplify(r)
-Notice that the reified form still has c in it but that doesn't prevent the simplify() function from reducing the form to the Mark. This should be the case for all solutions generated by the dpll_iter() function.
for solution in dpll_iter(cout):
- print simplify(reify(cout, solution)),
-Interesting! Some solutions do not simplify() completely in one go. The form (((a5) a5)) is Mark-valued:
(((a5) a5))
-((( ) a5))
-((( ) ))
-( )
-()
-
-E = F(((a,), a))
-print E
-print simplify(E)
-Something to keep in mind.
- -sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-Put the circuit expressions into a handy dictionary, and we are ready to add some numbers.
- -CIRCUITS = {
- 'sum0': sum0,
- 'sum1': sum1,
- 'sum2': sum2,
- 'sum3': sum3,
- 'cout': cout,
-}
-A bunch of crufty junk to print out a nice truth table with the columns arranged to make it (relatively) easy to see the addition.
- -def stringy_env(env):
- return {
- k: value_of(e, v='--', m='()')
- for k, e in env.items()
- }
-
-INPUTs = ('Cin', 'a3', 'a2', 'a1', 'a0', 'b3', 'b2', 'b1', 'b0')
-OUTPUTs = ('cout', 'sum3', 'sum2', 'sum1', 'sum0')
-
-format_string = '%(' + ')s %('.join(INPUTs) + ')s | %(' + ')s %('.join(OUTPUTs) + ')s'
-
-
-print 'Ci|a3 a2 a1 a0|b3 b2 b1 b0 | Co s3 s2 s1 s0'
-results = {}
-for env in environments_of_variables(*INPUTs):
-
- for name, expression in CIRCUITS.items():
- results[name] = value_of(reify(expression, env))
-
- env.update(results)
- print format_string % stringy_env(env)
-That was a bit steep, let's formalize it and make it a little easier to work with.
-First let's have a register of named values:
- -R = {name: Void for name in 'Cin a3 a2 a1 a0 b3 b2 b1 b0 Cout'.split()}
-Let's have a program of named expressions that give new values when evaluated in terms of the current values in R (this is just the same CIRCUITS, but feeding back the results into the "b" bits):
P = {
- 'b0': sum0,
- 'b1': sum1,
- 'b2': sum2,
- 'b3': sum3,
- 'Cout': cout,
-}
-One cycle of the machine means to evaluate each named expression in the program with the current values in the register.
- -make_reify_reducer = lambda env: lambda form: value_of(reify(form, env))
-
-
-def cycle(program, register):
- rr = make_reify_reducer(register)
- return {bit: rr(expression) for bit, expression in program.iteritems()}
-With all the register values at "zero" (Void) nothing happens.
- -R.update(cycle(P, R))
-R
-Let's make a nice display function to inspect our little adder computer.
- -def show_as_int(*names):
- '''
- Return a function that converts a sequence of
- named bits (as Void/Mark values) into a integer.
- '''
- def inner(register):
- i, n = 0, 1
- for name in names:
- if not register[name]:
- i += n
- n <<= 1
- return i
- return inner
-a_register = show_as_int('a0', 'a1', 'a2', 'a3')
-b_register = show_as_int('b0', 'b1', 'b2', 'b3')
-
-
-def show_computer_state(R):
- print 'a: %-3i b: %-3i Cin: %-3i Cout: %-3i' % (
- a_register(R),
- b_register(R),
- int(not R['Cin']),
- int(not R['Cout']),
- )
-show_computer_state(R)
-Let's set one bit to true (Mark-valued in the chosen convention. We could have Void be true but we would have to form the circuit expressions differently.)
- -R['a0'] = Mark
-Now let's count to twenty.
- -for _ in range(20):
- show_computer_state(R)
- R.update(cycle(P, R))
-You can see that at the sixteenth step the "Cout" carry bit is true and the count cycles back to zero.
- -# Reset the register.
-R = {name: Void for name in 'Cin a3 a2 a1 a0 b3 b2 b1 b0 Cout'.split()}
-
-# Count by three.
-R['a0'] = R['a1'] = Mark
-
-# Print out twenty cycles.
-for _ in range(20):
- show_computer_state(R)
- R.update(cycle(P, R))
-You can see that the "b" bits are indeed counting by threes: 0, 3, 6, 9, 12, 15 & carry, 2, 5, 8, 11, 14 & carry, 1, 4, 7, 10, 13 & carry, 0, 3, 6, 9, ...
-This is my basic model for computation: A register, a program, and a cycle function. Note that reducing the form on each cycle isn't necessary, we can run the cycles and just reify() without reducing and we get new circuits that define bits in terms of the register values N cycles in the past.
# Universe
-U = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
-
-
-# Register.
-R = {name: Void for name in U}
-
-
-# A program to XOR each bit with its neighbor, wrapping at the edge.
-P = {
- name: xor(name, U[(U.index(name) + 1) % len(U)])
- for name in U
-}
-
-
-def show(reg):
- '''Simple visualization of the register.'''
- print ''.join('.0'[not reg[name]] for name in U)
-
-
-# Set one "bit" in the register.
-R[U[-1]] = Mark
-
-
-# Run through some cycles.
-for _ in range(100):
- show(R)
- R.update(cycle(P, R))
-Before building larger "computers" I want to switch to a more efficient implementation based on a register as a set of names that are currently Mark-valued, and a set_solve() function that evaluates a form in terms of such a set, and assuming all other names are Void-valued.
def set_solve(form, marks):
- '''
- Given a form and a set of names that are Marks assume all other names
- in the form are Void and reduce to basic value (Mark or Void.)
- '''
- return (
- (Void, Mark)[form in marks]
- if isinstance(form, basestring)
- else _set_solve(form, marks)
- )
-
-
-def _set_solve(form, marks):
- for inner in form:
- if isinstance(inner, basestring):
- if inner in marks:
- return Void
- continue
- if not _set_solve(inner, marks): # Mark
- return Void
- return Mark
-A = F(a, (b, (c,)))
-print A
-print set_solve(A, {a})
-print set_solve(A, {b})
-print set_solve(A, {c})
-To calculate the new R first collect all the names in R that are not mentioned in P (and so cannot be -set to Void by it) then add the names evaluated by solving P's expressions with the marks in R.
- -def set_cycle(R, P):
- return R.difference(P).union(
- signal
- for signal, expression in P.iteritems()
- if not set_solve(expression, R)
- )
-# Universe
-U = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
-
-
-# Register.
-R = {U[-1]}
-
-
-# A program to XOR each bit with its neighbor, wrapping at the edge.
-P = {
- name: xor(name, U[(U.index(name) + 1) % len(U)])
- for name in U
-}
-
-
-def show(reg):
- '''Simple visualization of the register.'''
- print ''.join('.0'[name in reg] for name in U)
-
-
-# Run through some cycles.
-for _ in range(100):
- show(R)
- R = set_cycle(R, P)
-def set_show_as_int(*names):
- def inner(register):
- i, n = 0, 1
- for name in names:
- if name in register:
- i += n
- n <<= 1
- return i
- return inner
-def ifte(predicate, true, false):
- return F(
- ((predicate,), true),
- ( predicate , false),
- )
-
-
-E = ifte(a, b, c)
-
-
-truth_table(E)
-If a is Mark-valued the value of the whole form is that of b, but if a is Void-valued the value of the whole form is that of c.
w/ a = ()
-
-((( a) b) ( a c))
-(((()) b) (() c))
-(( b) (() ))
-(( b) )
- b
-
-w/ a =
-
-(((a) b) (a c))
-((( ) b) ( c))
-((( ) ) ( c))
-( ( c))
- c
-
-def flip_flop(q, reset, set_):
- return F(reset, (q, set_))
-
-q, r, s = 'qrs'
-E = flip_flop(q, r, s)
-truth_table(E)
-This is a form that can be used in a circuit to "remember" a value.
- -w/ r = ()
-
-((q s) r)
-((q s) ())
-( ())
-
-w/ s = (), r = ___
-
-((q s) r)
-((q ()) )
-(( ()) )
-( )
-
-w/ s = ___, r = ___
-
-((q s) r)
-((q ) )
- q
-
-
-If both are Void then the form is just q, if r is Mark then the form is Void, otherwise if s is Mark the form becomes Mark. This is called a "flip-flop" circuit, and it comprises a simple machine to remember one bit.
Consider a simple computer:
- -U = q, r, s = 'qrs'
-
-P = {
- q: flip_flop(q, r, s),
- r: Void,
- s: Void,
-}
-
-R = set() # Initially all signals are off, Void-valued.
-bools_of = lambda universe: lambda register: (name in register for name in universe)
-
-
-def simple_show(universe):
- B = bools_of(universe)
- def _shower(register):
- print ''.join('.0'[b] for b in B(register))
- return _shower
-show = simple_show(U)
-show(R)
-def SET(R):
- R |= {s}
- show(R)
- return set_cycle(R, P)
-
-
-def RESET(R):
- R |= {r}
- show(R)
- return set_cycle(R, P)
-print U
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-
-R = SET(R)
-
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-
-R = SET(R)
-
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-
-R = RESET(R)
-
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-
-R = RESET(R)
-
-show(R) ; R = set_cycle(R, P)
-show(R) ; R = set_cycle(R, P)
-You can see that q is stable unless s or r set or reset it.
We can use the system we have developed so far to build addressable RAM.
- -# Width in bits of the DATA registers.
-WIDTH = 2
-
-# Width in bits of the ADDR registers.
-LENGTH = 3 # Actual length 2**LENGTH
-P = {}
-We'll assume a single WRITE bit that sets a RAM location determined by the ADDR sub-register to the contents of the DATA sub-register.
WRITE = 'WRITE'
-# Address register.
-ADDR = ['addr_%i' % i for i in range(LENGTH)]
-ADDR
-# Data register.
-DATA = ['a%i' % i for i in range(WIDTH)]
-DATA
-We can recognize which RAM location we want to address by using the expressions for each row from above to create a predicate for each location that depends on the ADDR bits.
def make_ram(program, addrs, data, width, write):
- RAM = []
- for addr, env in enumerate(environments_of_variables(*addrs)):
-
- addr_predicate = F((name,) if env[name] else name for name in env)
-
- predicate = and_(write, addr_predicate)
-
- for bit in range(width):
- ram = 'RAM_%i_%i' % (addr, bit)
- RAM.append(ram)
- program[ram] = simplify(ifte(predicate, data[bit], ram))
-
- return RAM
-RAM = make_ram(P, ADDR, DATA, WIDTH, WRITE)
-RAM
-P
-E = P['RAM_0_0']
-E
-Note that if the WRITE bit is unset then each RAM bit is just set to itself.
simplify(E, exclude={'WRITE'})
-But if the WRITE bit is set then each RAM location still depends on the -per-location-predicate.
simplify(E, with_mark='WRITE')
-def make_accumulator(program, addrs, data, width, read, alts):
-
- addr_predicates = [
- F((name,) if env[name] else name for name in env)
- for env in environments_of_variables(*addrs)
- ]
-
- for bit in range(width):
- a = data[bit]
- alt = alts[bit]
- foo = Void
- for addr, predicate in enumerate(addr_predicates):
- ram = 'RAM_%i_%i' % (addr, bit)
- foo = (ifte(predicate, ram, foo))
- program[a] = ifte(read, foo, alt)
-p = {}
-make_accumulator(p, ADDR, DATA, WIDTH, 'READ', DATA)
-p
-E = p[DATA[0]]
-E
-simplify(E, with_mark='READ')
-simplify(E, {'READ'})
-each_way(E, 'a0')
-
-
-
-each_way(E, 'WRITE')
-each_way(E, 'addr_0')
-each_way(E, 'a0')
-simplify(each_way(E, 'WRITE'), with_mark='WRITE')
-
-, Sorting Networks for routing, more basic functions.
-Eventually: Orchestration with Joy.
- -The rule A(AB) = A(B) is the powerhouse of LoF.
w/ A = ()
- - A(AB) = A(B)
-()(()B) = ()(B)
- () = ()
-
-
-w/ A =
- - A(AB) = A(B)
- (B) = (B)
-
-
-
-Be aware of the recursive nature of this rule:
- -A(...(...(A B)))
-A(.A.(...(A B)))
-A(.A.(.A.(A B)))
-A(.A.(.A.( B)))
-A(.A.(...( B)))
-A(...(...( B)))
-
-
-There is this too:
- -(A)(...(...(... A B)))
-(A)((A)(...(... A B)))
-(A)((A)((A)(... A B)))
-(A)((A)((A)((A) A B)))
-(A)((A)((A)(( ) A B)))
-(A)((A)(...(( ) )))
-(A)(...(... ))
-
-
-Summarized:
- -(A)(...(...(... A )))
-(A)(...(...(... () )))
-(A)(...(... ))
-
-Given a string form of an arithmetic expression (in other words a string that consists of only balanced pairs of parentheses) we can reduce it to its Mark/Void value by substitution to a fixed-point.
- -# Translate the LoF Arithmetic rules to string substitution rules.
-reduce_string = lambda s: (
- s
- .replace('()()', '()')
- .replace('(())', '')
-)
-def to_fixed_point(initial_value, F, limit=10000):
- '''Do value = F(value) until value == F(value).'''
-
- next_value = F(initial_value)
-
- while next_value != initial_value:
-
- if not limit: # A safety mechanism. Bail out after N iterations.
- raise RuntimeError('Reached limit of allowed iterations without finding fixed point.')
- limit -= 1
-
- initial_value = next_value
- next_value = F(initial_value)
-
- return initial_value
-from operator import add
-
-
-def dyck_language(left, right=0, t='', A='(', B=')', op=add):
- '''Yield balanced pairs of A and B.'''
- if not (left or right):
- yield t
- return
-
- if left > 0:
- for i in dyck_language(left - 1, right + 1, op(t, A), A, B, op):
- yield i
-
- if right > 0:
- for i in dyck_language(left, right - 1, op(t, B), A, B, op):
- yield i
-for s in dyck_language(5):
- print s, u'⟶', to_fixed_point(s, reduce_string)
-void() and mark()¶The void() and mark() functions can be defined recursively in terms of each other. Note that void() uses any() while mark() uses all(). These functions implement a depth-first search. If we used versions of any() and all() that evaluated their arguments in parallel void() could return after the True result while mark() depends on all terms's results so its runtime will be bound by term with the greatest runtime.
def void(form):
- return any(mark(i) for i in form)
-
-def mark(form):
- return all(void(i) for i in form)
-
-for form in BASE:
- for func in (void, mark):
- print form, 'is', func.__name__, '?', func(form)
-The Void and the Mark are not Boolean true and false. Rather they correspond to non-existance and existance. When translating a traditional logical statement into Laws of Form the first thing we must do is choose which mapping we would like to use: true = Mark or true = Void. The notation works the same way once the translation is made, so the only real criteria for choosing is which gives the smaller form.
-If you examine the truth tables for the basic forms above in light of this, you can see that each defines two logical functions depending on whether you treat Void as true and Mark as false, or Void as false and Mark as true.
-For example, the juxtaposition of two terms next to each other a b corresponds to OR if Mark is true, and to AND if Void is true. Likewise, the form ((a)(b)) is AND if Mark is true and OR if Void is true.
Consider:
- -(A ∧ ¬B) ∨ (C ∧ D)
-
-
-(This reads "(A and not B) or (C and D)" in case you have a hard time remembering what the symbols mean like I do.)
-If we choose Mark to be true then the form is:
- -((A) B) ((C)(D))
-
-
-If we choose Void to be true then the form is:
- -((A (B)) (C D))
-
-
-As I said above, the notation works the same way either way, so once the translation is made you can forget about the Boolean true/false and just work with the Laws of Form rules. Logic is containers.
-Consider also the De Morgan dual of the original statement:
- -¬((¬A ∨ B) ∧ (¬C ∨ ¬D))
-
-
-If we choose Mark to be true then the form is:
- -(( ((A) B) ((C)(D)) ))
-
-
-The outer pair of containers can be deleted leaving the same form as above:
- -((A) B) ((C)(D))
-
-
-Likewise, if we choose Void to be true then the form is:
- -((((A)) (B)) (((C)) ((D))))
-
-
-Again, A((B)) => AB reduces this form to the same one above:
- -((A (B)) (C D))
-
-
-In the Laws of Form there are no De Morgan Dual statements. If you translate a logic statement and its dual into Laws of Form notation they both reduce to the same form.
-To me this is a clear indication that the Laws of Form are superior to the traditional notation involving ¬ ∨ ∧, etc. LoF replaces all the symbols with just names and boundaries. The Laws of Form are not dualistic, they work directly in terms of existance and non-existance. Duality only comes in when you interpret the forms as Boolean statements and have to pick a mapping.
from collections import Counter
-
-histo = Counter(yield_variables_of(cout))
-histo
-#import pprint as pp
-
-
-#E = cout
-#for var, count in histo.most_common():
-# print 'stan', var, count
-# E = to_fixed_point(simplify(standardize(E, var)), simplify)
-# print len(str(E))
-# pp.pprint(dict(Counter(yield_variables_of(E))))
-# print '------'
-Rather than manually calling standard_form() let's define a function that reduces a form to a (hopefully) smaller equivalent form by going through all the variables in the form and using standard_form() with each. Along with clean and unwrap we can drive an expression to a fixed point.
def STAN(form):
- for var, _ in Counter(yield_variables_of(form)).most_common():
- form = to_fixed_point(simplify(standardize(form, var)), simplify)
- return form
-sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-
-map(len, map(str, (sum0, sum1, sum2, sum3, cout)))
-sum0, cout = full_bit_adder('a0', 'b0', 'Cin')
-sum0 = to_fixed_point(sum0, STAN)
-cout = to_fixed_point(cout, STAN)
-
-sum1, cout = full_bit_adder('a1', 'b1', cout)
-sum1 = to_fixed_point(sum1, STAN)
-cout = to_fixed_point(cout, STAN)
-
-sum2, cout = full_bit_adder('a2', 'b2', cout)
-sum2 = to_fixed_point(sum2, STAN)
-cout = to_fixed_point(cout, STAN)
-
-sum3, cout = full_bit_adder('a3', 'b3', cout)
-sum3 = to_fixed_point(sum3, STAN)
-cout = to_fixed_point(cout, STAN)
-
-
-map(len, map(str, (sum0, sum1, sum2, sum3, cout)))
-It would be useful and fun to write a simple search algorithm that tried different ways to reduce a form to see if it could find particulaly compact versions.
-Let's generate the expressions for the next two output bits, and the carry bit.
- -The sum3 bit expression is pretty big.
sum3
-But it's only about 1/9th of size of the previous version (which was 9261.)
- -len(str(sum3))
-
-
-
-
-
-
-Let's simplify the first one manually just for fun:
- -(((((())) (())) ((()))))
- (( ) ) ( )
- ( )
-
-
-Sure enough, it reduces to Mark after just a few applications of the rule (()) = __ (the underscores indicates the absence of any value, aka Void.) We could also just delete variables that are Void in the original expression:
((((a)b)(c)))
- (( ) )( )
- ( )
-
-
-C = F((a, b))
-for form in (A, B, C):
- arth = reify(form, env)
- print form, u'⟶', arth, u'⟶', value_of(arth)
-
-
-
-print A
-Aa = simplify(A, {a})
-print a, Aa
-Aab = simplify(Aa, {b})
-print a, b, Aab
-Aabc = simplify(Aab, {c})
-print a, b, c, Aabc
-print a, Aa
-Aab = simplify(Aa, with_mark=b)
-print a, F(b), Aab
-print a, Aa
-Aac = simplify(Aa, with_mark=c)
-print a, F(c), Aac
-print a, Aa
-Aac = simplify(Aa, {c})
-print a, c, Aac
-
-
-
-
-from collections import Counter
-
-histo = Counter(yield_variables_of(sum7))
-histo
-len(str(sum7))
-Immediately we can just call simplify() until it stops shinking the expression.
s7 = simplify(sum7)
-len(str(s7))
-s7 = simplify(s7)
-len(str(s7))
-Once was enough (we should consider adding a call to simplify() in the full_bit_adder() function.)
Let's try using each_way() with the most common names in the form.
s7 = simplify(each_way(s7, 'a0'))
-len(str(s7))
-s7 = simplify(s7)
-len(str(s7))
-Counter(yield_variables_of(s7))
-s7 = sum7
-
-#for name, count in histo.most_common():
-# s7 = simplify(each_way(s7, name))
-# print len(str(s7))
-
-def super_simple(form):
- return to_fixed_point(form, simplify)
-len(str(sum7))
-s7 = super_simple(sum7)
-len(str(s7))
-s7 = sum7
-
-#for name, count in histo.most_common():
-# s7 = super_simple(each_way(s7, name))
-# print len(str(s7))
-
-
-
-print ' '.join(name[:2] for name in sorted(R))
-for _ in range(20):
- print format_env(R), '=', b_register(R)
- R.update(cycle(P, R))
-
-
-
-
-Legend:
- -∧ intersection
-∨ union
-∘ concatenation (see below)
-¬ complement
-ϕ empty set (aka ∅)
-λ singleton set containing just the empty string
-I set of all letters in alphabet
-
-Derivative of a set R of strings and a string a:
∂a(R)
-
-∂a(a) → λ
-∂a(λ) → ϕ
-∂a(ϕ) → ϕ
-∂a(¬a) → ϕ
-∂a(R*) → ∂a(R)∘R*
-∂a(¬R) → ¬∂a(R)
-∂a(R∘S) → ∂a(R)∘S ∨ δ(R)∘∂a(S)
-∂a(R ∧ S) → ∂a(R) ∧ ∂a(S)
-∂a(R ∨ S) → ∂a(R) ∨ ∂a(S)
-
-∂ab(R) = ∂b(∂a(R))
-
-Auxiliary predicate function δ (I call it nully) returns either λ if λ ⊆ R or ϕ otherwise:
δ(a) → ϕ
-δ(λ) → λ
-δ(ϕ) → ϕ
-δ(R*) → λ
-δ(¬R) δ(R)≟ϕ → λ
-δ(¬R) δ(R)≟λ → ϕ
-δ(R∘S) → δ(R) ∧ δ(S)
-δ(R ∧ S) → δ(R) ∧ δ(S)
-δ(R ∨ S) → δ(R) ∨ δ(S)
-
-Some rules we will use later for "compaction":
- -R ∧ ϕ = ϕ ∧ R = ϕ
-
-R ∧ I = I ∧ R = R
-
-R ∨ ϕ = ϕ ∨ R = R
-
-R ∨ I = I ∨ R = I
-
-R∘ϕ = ϕ∘R = ϕ
-
-R∘λ = λ∘R = R
-
-Concatination of sets: for two sets A and B the set A∘B is defined as:
-{a∘b for a in A for b in B}
-E.g.:
-{'a', 'b'}∘{'c', 'd'} → {'ac', 'ad', 'bc', 'bd'}
- -from functools import partial as curry
-from itertools import product
-ϕ and λ¶The empty set and the set of just the empty string.
- -phi = frozenset() # ϕ
-y = frozenset({''}) # λ
-I'm only going to use two symbols (at first) becaase this is enough to illustrate the algorithm and because you can represent any other alphabet with two symbols (if you had to.)
-I chose the names O and l (uppercase "o" and lowercase "L") to look like 0 and 1 (zero and one) respectively.
syms = O, l = frozenset({'0'}), frozenset({'1'})
-To represent REs in Python I'm going to use tagged tuples. A regular expression is one of:
- -O
-l
-(KSTAR, R)
-(NOT, R)
-(AND, R, S)
-(CONS, R, S)
-(OR, R, S)
-
-
-Where R and S stand for regular expressions.
AND, CONS, KSTAR, NOT, OR = 'and cons * not or'.split() # Tags are just strings.
-Because they are formed of frozenset, tuple and str objects only, these datastructures are immutable.
def stringy(re):
- '''
- Return a nice string repr for a regular expression datastructure.
- '''
- if re == I: return '.'
- if re in syms: return next(iter(re))
- if re == y: return '^'
- if re == phi: return 'X'
-
- assert isinstance(re, tuple), repr(re)
- tag = re[0]
-
- if tag == KSTAR:
- body = stringy(re[1])
- if not body: return body
- if len(body) > 1: return '(' + body + ")*"
- return body + '*'
-
- if tag == NOT:
- body = stringy(re[1])
- if not body: return body
- if len(body) > 1: return '(' + body + ")'"
- return body + "'"
-
- r, s = stringy(re[1]), stringy(re[2])
- if tag == CONS: return r + s
- if tag == OR: return '%s | %s' % (r, s)
- if tag == AND: return '(%s) & (%s)' % (r, s)
-
- raise ValueError
-I = (KSTAR, (OR, O, l))
-print stringy(I)
-(.111.) & (.01 + 11*)'¶The example expression from Brzozowski:
- -(.111.) & (.01 + 11*)'
- a & (b + c)'
-
-
-Note that it contains one of everything.
- -a = (CONS, I, (CONS, l, (CONS, l, (CONS, l, I))))
-b = (CONS, I, (CONS, O, l))
-c = (CONS, l, (KSTAR, l))
-it = (AND, a, (NOT, (OR, b, c)))
-print stringy(it)
-nully()¶Let's get that auxiliary predicate function δ out of the way.
def nully(R):
- '''
- δ - Return λ if λ ⊆ R otherwise ϕ.
- '''
-
- # δ(a) → ϕ
- # δ(ϕ) → ϕ
- if R in syms or R == phi:
- return phi
-
- # δ(λ) → λ
- if R == y:
- return y
-
- tag = R[0]
-
- # δ(R*) → λ
- if tag == KSTAR:
- return y
-
- # δ(¬R) δ(R)≟ϕ → λ
- # δ(¬R) δ(R)≟λ → ϕ
- if tag == NOT:
- return phi if nully(R[1]) else y
-
- # δ(R∘S) → δ(R) ∧ δ(S)
- # δ(R ∧ S) → δ(R) ∧ δ(S)
- # δ(R ∨ S) → δ(R) ∨ δ(S)
- r, s = nully(R[1]), nully(R[2])
- return r & s if tag in {AND, CONS} else r | s
-This is the straightforward version with no "compaction". -It works fine, but does waaaay too much work because the -expressions grow each derivation.
- -def D(symbol):
-
- def derv(R):
-
- # ∂a(a) → λ
- if R == {symbol}:
- return y
-
- # ∂a(λ) → ϕ
- # ∂a(ϕ) → ϕ
- # ∂a(¬a) → ϕ
- if R == y or R == phi or R in syms:
- return phi
-
- tag = R[0]
-
- # ∂a(R*) → ∂a(R)∘R*
- if tag == KSTAR:
- return (CONS, derv(R[1]), R)
-
- # ∂a(¬R) → ¬∂a(R)
- if tag == NOT:
- return (NOT, derv(R[1]))
-
- r, s = R[1:]
-
- # ∂a(R∘S) → ∂a(R)∘S ∨ δ(R)∘∂a(S)
- if tag == CONS:
- A = (CONS, derv(r), s) # A = ∂a(R)∘S
- # A ∨ δ(R) ∘ ∂a(S)
- # A ∨ λ ∘ ∂a(S) → A ∨ ∂a(S)
- # A ∨ ϕ ∘ ∂a(S) → A ∨ ϕ → A
- return (OR, A, derv(s)) if nully(r) else A
-
- # ∂a(R ∧ S) → ∂a(R) ∧ ∂a(S)
- # ∂a(R ∨ S) → ∂a(R) ∨ ∂a(S)
- return (tag, derv(r), derv(s))
-
- return derv
-def _compaction_rule(relation, one, zero, a, b):
- return (
- b if a == one else # R*1 = 1*R = R
- a if b == one else
- zero if a == zero or b == zero else # R*0 = 0*R = 0
- (relation, a, b)
- )
-An elegant symmetry.
- -# R ∧ I = I ∧ R = R
-# R ∧ ϕ = ϕ ∧ R = ϕ
-_and = curry(_compaction_rule, AND, I, phi)
-
-# R ∨ ϕ = ϕ ∨ R = R
-# R ∨ I = I ∨ R = I
-_or = curry(_compaction_rule, OR, phi, I)
-
-# R∘λ = λ∘R = R
-# R∘ϕ = ϕ∘R = ϕ
-_cons = curry(_compaction_rule, CONS, y, phi)
-We can save re-processing by remembering results we have already computed. RE datastructures are immutable and the derv() functions are pure so this is fine.
class Memo(object):
-
- def __init__(self, f):
- self.f = f
- self.calls = self.hits = 0
- self.mem = {}
-
- def __call__(self, key):
- self.calls += 1
- try:
- result = self.mem[key]
- self.hits += 1
- except KeyError:
- result = self.mem[key] = self.f(key)
- return result
-This version uses the rules above to perform compaction. It keeps the expressions from growing too large.
- -def D_compaction(symbol):
-
- @Memo
- def derv(R):
-
- # ∂a(a) → λ
- if R == {symbol}:
- return y
-
- # ∂a(λ) → ϕ
- # ∂a(ϕ) → ϕ
- # ∂a(¬a) → ϕ
- if R == y or R == phi or R in syms:
- return phi
-
- tag = R[0]
-
- # ∂a(R*) → ∂a(R)∘R*
- if tag == KSTAR:
- return _cons(derv(R[1]), R)
-
- # ∂a(¬R) → ¬∂a(R)
- if tag == NOT:
- return (NOT, derv(R[1]))
-
- r, s = R[1:]
-
- # ∂a(R∘S) → ∂a(R)∘S ∨ δ(R)∘∂a(S)
- if tag == CONS:
- A = _cons(derv(r), s) # A = ∂a(r)∘s
- # A ∨ δ(R) ∘ ∂a(S)
- # A ∨ λ ∘ ∂a(S) → A ∨ ∂a(S)
- # A ∨ ϕ ∘ ∂a(S) → A ∨ ϕ → A
- return _or(A, derv(s)) if nully(r) else A
-
- # ∂a(R ∧ S) → ∂a(R) ∧ ∂a(S)
- # ∂a(R ∨ S) → ∂a(R) ∨ ∂a(S)
- dr, ds = derv(r), derv(s)
- return _and(dr, ds) if tag == AND else _or(dr, ds)
-
- return derv
-(FIXME: redo.)
- -o, z = D_compaction('0'), D_compaction('1')
-REs = set()
-N = 5
-names = list(product(*(N * [(0, 1)])))
-dervs = list(product(*(N * [(o, z)])))
-for name, ds in zip(names, dervs):
- R = it
- ds = list(ds)
- while ds:
- R = ds.pop()(R)
- if R == phi or R == I:
- break
- REs.add(R)
-
-print stringy(it) ; print
-print o.hits, '/', o.calls
-print z.hits, '/', z.calls
-print
-for s in sorted(map(stringy, REs), key=lambda n: (len(n), n)):
- print s
-Should match:
- -(.111.) & ((.01 | 11*)')
-
-92 / 122
-92 / 122
-
-(.01 )'
-(.01 | 1 )'
-(.01 | ^ )'
-(.01 | 1*)'
-(.111.) & ((.01 | 1 )')
-(.111. | 11.) & ((.01 | ^ )')
-(.111. | 11.) & ((.01 | 1*)')
-(.111. | 11. | 1.) & ((.01 )')
-(.111. | 11. | 1.) & ((.01 | 1*)')
-
-We could parse larger alphabets by defining patterns for e.g. each byte of the ASCII code. Or we can generalize this code. If you study the code above you'll see that we never use the "set-ness" of the symbols O and l. The only time Python set operators (& and |) appear is in the nully() function, and there they operate on (recursively computed) outputs of that function, never O and l.
What if we try:
- -(OR, O, l)
-
-∂1((OR, O, l))
- ∂a(R ∨ S) → ∂a(R) ∨ ∂a(S)
-∂1(O) ∨ ∂1(l)
- ∂a(¬a) → ϕ
-ϕ ∨ ∂1(l)
- ∂a(a) → λ
-ϕ ∨ λ
- ϕ ∨ R = R
-λ
-
-
-And compare it to:
- -{'0', '1')
-
-∂1({'0', '1'))
- ∂a(R ∨ S) → ∂a(R) ∨ ∂a(S)
-∂1({'0')) ∨ ∂1({'1'))
- ∂a(¬a) → ϕ
-ϕ ∨ ∂1({'1'))
- ∂a(a) → λ
-ϕ ∨ λ
- ϕ ∨ R = R
-λ
-
-
-This suggests that we should be able to alter the functions above to detect sets and deal with them appropriately. Exercise for the Reader for now.
- -We can drive the regular expressions to flesh out the underlying state machine transition table.
- -.111. & (.01 + 11*)'
-
-
-Says, "Three or more 1's and not ending in 01 nor composed of all 1's."
-Start at a and follow the transition arrows according to their labels. Accepting states have a double outline. (Graphic generated with Dot from Graphviz.) You'll see that only paths that lead to one of the accepting states will match the regular expression. All other paths will terminate at one of the non-accepting states.
There's a happy path to g along 111:
a→c→e→g
-
-
-After you reach g you're stuck there eating 1's until you see a 0, which takes you to the i→j→i|i→j→h→i "trap". You can't reach any other states from those two loops.
If you see a 0 before you see 111 you will reach b, which forms another "trap" with d and f. The only way out is another happy path along 111 to h:
b→d→f→h
-
-
-Once you have reached h you can see as many 1's or as many 0' in a row and still be either still at h (for 1's) or move to i (for 0's). If you find yourself at i you can see as many 0's, or repetitions of 10, as there are, but if you see just a 1 you move to j.
So how do we get the state machine from the regular expression?
-It turns out that each RE is effectively a state, and each arrow points to the derivative RE in respect to the arrow's symbol.
-If we label the initial RE a, we can say:
a --0--> ∂0(a)
-a --1--> ∂1(a)
-
-
-And so on, each new unique RE is a new state in the FSM table.
- -Here are the derived REs at each state:
- -a = (.111.) & ((.01 | 11*)')
-b = (.111.) & ((.01 | 1)')
-c = (.111. | 11.) & ((.01 | 1*)')
-d = (.111. | 11.) & ((.01 | ^)')
-e = (.111. | 11. | 1.) & ((.01 | 1*)')
-f = (.111. | 11. | 1.) & ((.01)')
-g = (.01 | 1*)'
-h = (.01)'
-i = (.01 | 1)'
-j = (.01 | ^)'
-
-
-You can see the one-way nature of the g state and the hij "trap" in the way that the .111. on the left-hand side of the & disappears once it has been matched.
from collections import defaultdict
-from pprint import pprint
-from string import ascii_lowercase
-d0, d1 = D_compaction('0'), D_compaction('1')
-explore()¶def explore(re):
-
- # Don't have more than 26 states...
- names = defaultdict(iter(ascii_lowercase).next)
-
- table, accepting = dict(), set()
-
- to_check = {re}
- while to_check:
-
- re = to_check.pop()
- state_name = names[re]
-
- if (state_name, 0) in table:
- continue
-
- if nully(re):
- accepting.add(state_name)
-
- o, i = d0(re), d1(re)
- table[state_name, 0] = names[o] ; to_check.add(o)
- table[state_name, 1] = names[i] ; to_check.add(i)
-
- return table, accepting
-table, accepting = explore(it)
-table
-accepting
-Once we have the FSM table and the set of accepting states we can generate the diagram above.
- -_template = '''\
-digraph finite_state_machine {
- rankdir=LR;
- size="8,5"
- node [shape = doublecircle]; %s;
- node [shape = circle];
-%s
-}
-'''
-
-def link(fr, nm, label):
- return ' %s -> %s [ label = "%s" ];' % (fr, nm, label)
-
-
-def make_graph(table, accepting):
- return _template % (
- ' '.join(accepting),
- '\n'.join(
- link(from_, to, char)
- for (from_, char), (to) in sorted(table.iteritems())
- )
- )
-print make_graph(table, accepting)
-There are lots of FSM libraries already. Once you have the state transition table they should all be straightforward to use. State Machine code is very simple. Just for fun, here is an implementation in Python that imitates what "compiled" FSM code might look like in an "unrolled" form. Most FSM code uses a little driver loop and a table datastructure, the code below instead acts like JMP instructions ("jump", or GOTO in higher-level-but-still-low-level languages) to hard-code the information in the table into a little patch of branches.
- -Python has no GOTO statement but we can fake it with a "trampoline" function.
- -def trampoline(input_, jump_from, accepting):
- I = iter(input_)
- while True:
- try:
- bounce_to = jump_from(I)
- except StopIteration:
- return jump_from in accepting
- jump_from = bounce_to
-Little helpers to process the iterator of our data (a "stream" of "1" and "0" characters, not bits.)
- -getch = lambda I: int(next(I))
-
-
-def _1(I):
- '''Loop on ones.'''
- while getch(I): pass
-
-
-def _0(I):
- '''Loop on zeros.'''
- while not getch(I): pass
-With those preliminaries out of the way, from the state table of .111. & (.01 + 11*)' we can immediately write down state machine code. (You have to imagine that these are GOTO statements in C or branches in assembly and that the state names are branch destination labels.)
a = lambda I: c if getch(I) else b
-b = lambda I: _0(I) or d
-c = lambda I: e if getch(I) else b
-d = lambda I: f if getch(I) else b
-e = lambda I: g if getch(I) else b
-f = lambda I: h if getch(I) else b
-g = lambda I: _1(I) or i
-h = lambda I: _1(I) or i
-i = lambda I: _0(I) or j
-j = lambda I: h if getch(I) else i
-Note that the implementations of h and g are identical ergo h = g and we could eliminate one in the code but h is an accepting state and g isn't.
def acceptable(input_):
- return trampoline(input_, a, {h, i})
-for n in range(2**5):
- s = bin(n)[2:]
- print '%05s' % s, acceptable(s)
-(UNFINISHED) -Brzozowski also shewed how to go from the state machine to strings and expressions...
- -Each of these states is just a name for a Brzozowskian RE, and so, other than the initial state a, they can can be described in terms of the derivative-with-respect-to-N of some other state/RE:
c = d1(a)
-b = d0(a)
-b = d0(c)
-...
-i = d0(j)
-j = d1(i)
-
-
-Consider:
- -c = d1(a)
-b = d0(c)
-
-
-Substituting:
- -b = d0(d1(a))
-
-
-Unwrapping:
- -b = d10(a)
-
-
-'''
- -j = d1(d0(j))
-
-
-Unwrapping:
- -j = d1(d0(j)) = d01(j)
-
-
-We have a loop or "fixed point".
- -j = d01(j) = d0101(j) = d010101(j) = ...
-
-
-hmm...
- -j = (01)*
-
-x to Generate Values¶Cf. jp-reprod.html
- -from notebook_preamble import J, V, define
-Consider the x combinator:
x == dup i
-
-We can apply it to a quoted program consisting of some value a and some function B:
[a B] x
-[a B] a B
-
-Let B function swap the a with the quote and run some function C on it to generate a new value b:
B == swap [C] dip
-
-[a B] a B
-[a B] a swap [C] dip
-a [a B] [C] dip
-a C [a B]
-b [a B]
-
-Now discard the quoted a with rest then cons b:
b [a B] rest cons
-b [B] cons
-[b B]
-
-Altogether, this is the definition of B:
B == swap [C] dip rest cons
-
-We can make a generator for the Natural numbers (0, 1, 2, ...) by using 0 for a and [dup ++] for [C]:
[0 swap [dup ++] dip rest cons]
-
-
-Let's try it:
- -V('[0 swap [dup ++] dip rest cons] x')
-After one application of x the quoted program contains 1 and 0 is below it on the stack.
J('[0 swap [dup ++] dip rest cons] x x x x x pop')
-direco¶define('direco == dip rest cons')
-V('[0 swap [dup ++] direco] x')
-We want to define a function that accepts a and [C] and builds our quoted program:
a [C] G
--------------------------
- [a swap [C] direco]
-
-Working in reverse:
- -[a swap [C] direco] cons
-a [swap [C] direco] concat
-a [swap] [[C] direco] swap
-a [[C] direco] [swap]
-a [C] [direco] cons [swap]
-
-
-Reading from the bottom up:
- -G == [direco] cons [swap] swap concat cons
-G == [direco] cons [swap] swoncat cons
-
-define('G == [direco] cons [swap] swoncat cons')
-Let's try it out:
- -J('0 [dup ++] G')
-J('0 [dup ++] G x x x pop')
-J('1 [dup 1 <<] G x x x x x x x x x pop')
-[x] times¶If we have one of these quoted programs we can drive it using times with the x combinator.
J('23 [dup ++] G 5 [x] times')
-Look at the treatment of the Project Euler Problem One in the "Developing a Program" notebook and you'll see that we might be interested in generating an endless cycle of:
- -3 2 1 3 1 2 3
-
-
-To do this we want to encode the numbers as pairs of bits in a single int:
- - 3 2 1 3 1 2 3
-0b 11 10 01 11 01 10 11 == 14811
-
-
-And pick them off by masking with 3 (binary 11) and then shifting the int right two bits.
- -define('PE1.1 == dup [3 &] dip 2 >>')
-V('14811 PE1.1')
-If we plug 14811 and [PE1.1] into our generator form...
J('14811 [PE1.1] G')
-...we get a generator that works for seven cycles before it reaches zero:
- -J('[14811 swap [PE1.1] direco] 7 [x] times')
-We need a function that checks if the int has reached zero and resets it if so.
- -define('PE1.1.check == dup [pop 14811] [] branch')
-J('14811 [PE1.1.check PE1.1] G')
-J('[14811 swap [PE1.1.check PE1.1] direco] 21 [x] times')
-(It would be more efficient to reset the int every seven cycles but that's a little beyond the scope of this article. This solution does extra work, but not much, and we're not using it "in production" as they say.)
- -In the PE1 problem we are asked to sum all the multiples of three and five less than 1000. It's worked out that we need to use all seven numbers sixty-six times and then four more.
- -J('7 66 * 4 +')
-If we drive our generator 466 times and sum the stack we get 999.
- -J('[14811 swap [PE1.1.check PE1.1] direco] 466 [x] times')
-J('[14811 swap [PE1.1.check PE1.1] direco] 466 [x] times pop enstacken sum')
-define('PE1.2 == + dup [+] dip')
-Now we can add PE1.2 to the quoted program given to G.
J('0 0 0 [PE1.1.check PE1.1] G 466 [x [PE1.2] dip] times popop')
-The obvious first thing to do is just add b and a:
[b a F] b a +
-[b a F] b+a
-
-From here we want to arrive at:
- -b [b+a b F]
-
-Let's start with swons:
[b a F] b+a swons
-[b+a b a F]
-
-Considering this quote as a stack:
- -F a b b+a
-
-We want to get it to:
- -F b b+a b
-
-So:
- -F a b b+a popdd over
-F b b+a b
-
-And therefore:
- -[b+a b a F] [popdd over] infra
-[b b+a b F]
-
-But we can just use cons to carry b+a into the quote:
[b a F] b+a [popdd over] cons infra
-[b a F] [b+a popdd over] infra
-[b b+a b F]
-
-Lastly:
- -[b b+a b F] uncons
-b [b+a b F]
-
-Putting it all together:
- -F == + [popdd over] cons infra uncons
-fib_gen == [1 1 F]
-
-define('fib == + [popdd over] cons infra uncons')
-define('fib_gen == [1 1 fib]')
-J('fib_gen 10 [x] times')
--By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.
-
Now that we have a generator for the Fibonacci sequence, we need a function that adds a term in the sequence to a sum if it is even, and pops it otherwise.
define('PE2.1 == dup 2 % [+] [pop] branch')
-And a predicate function that detects when the terms in the series "exceed four million".
- -define('>4M == 4000000 >')
-Now it's straightforward to define PE2 as a recursive function that generates terms in the Fibonacci sequence until they exceed four million and sums the even ones.
define('PE2 == 0 fib_gen x [pop >4M] [popop] [[PE2.1] dip x] primrec')
-J('PE2')
-Here's the collected program definitions:
- -fib == + swons [popdd over] infra uncons
-fib_gen == [1 1 fib]
-
-even == dup 2 %
->4M == 4000000 >
-
-PE2.1 == even [+] [pop] branch
-PE2 == 0 fib_gen x [pop >4M] [popop] [[PE2.1] dip x] primrec
-
-Using o for odd and e for even:
o + o = e
-e + e = e
-o + e = o
-
-
-So the Fibonacci sequence considered in terms of just parity would be:
- -o o e o o e o o e o o e o o e o o e
-1 1 2 3 5 8 . . .
-
-
-Every third term is even.
- -J('[1 0 fib] x x x') # To start the sequence with 1 1 2 3 instead of 1 2 3.
-Drive the generator three times and popop the two odd terms.
J('[1 0 fib] x x x [popop] dipd')
-define('PE2.2 == x x x [popop] dipd')
-J('[1 0 fib] 10 [PE2.2] times')
-Replace x with our new driver function PE2.2 and start our fib generator at 1 0.
J('0 [1 0 fib] PE2.2 [pop >4M] [popop] [[PE2.1] dip PE2.2] primrec')
-You would probably start with a special version of G, and perhaps modifications to the default x?
define('codireco == cons dip rest cons')
-V('[0 [dup ++] codireco] x')
-define('G == [codireco] cons cons')
-J('230 [dup ++] G 5 [x] times pop')
-A hylomorphism H :: A -> B converts a value of type A into a value of type B by means of:
G :: A -> (A, B)F :: (B, B) -> BP :: A -> Bool to detect the base casec :: BIt may be helpful to see this function implemented in imperative Python code.
- -def hylomorphism(c, F, P, G):
- '''Return a hylomorphism function H.'''
-
- def H(a):
- if P(a):
- result = c
- else:
- b, aa = G(a)
- result = F(b, H(aa))
- return result
-
- return H
-As a concrete example let's use a function that, given a positive integer, returns the sum of all positive integers less than that one. (In this case the types A and B are both int.)
range() and sum()¶r = range(10)
-r
-sum(r)
-range_sum = lambda n: sum(range(n))
-range_sum(10)
-G = lambda n: (n - 1, n - 1)
-F = lambda a, b: a + b
-P = lambda n: n <= 1
-
-H = hylomorphism(0, F, P, G)
-H(10)
-If you were to run the above code in a debugger and check out the call stack you would find that the variable b in each call to H() is storing the intermediate values as H() recurses. This is what was meant by "call stack in the form of a cons list".
from notebook_preamble import D, DefinitionWrapper, J, V, define
-We can define a combinator hylomorphism that will make a hylomorphism combinator H from constituent parts.
H == c [F] [P] [G] hylomorphism
-
-
-The function H is recursive, so we start with ifte and set the else-part to
-some function J that will contain a quoted copy of H. (The then-part just
-discards the leftover a and replaces it with the base case value c.)
H == [P] [pop c] [J] ifte
-
-
-The else-part J gets just the argument a on the stack.
a J
-a G The first thing to do is use the generator G
-aa b which produces b and a new aa
-aa b [H] dip we recur with H on the new aa
-aa H b F and run F on the result.
-
-
-This gives us a definition for J.
J == G [H] dip F
-
-
-Plug it in and convert to genrec.
- -H == [P] [pop c] [G [H] dip F] ifte
-H == [P] [pop c] [G] [dip F] genrec
-
-
-This is the form of a hylomorphism in Joy, which nicely illustrates that -it is a simple specialization of the general recursion combinator.
- -H == [P] [pop c] [G] [dip F] genrec
-
-hylomorphism¶Now we just need to derive a definition that builds the genrec arguments
-out of the pieces given to the hylomorphism combinator.
H == [P] [pop c] [G] [dip F] genrec
- [P] [c] [pop] swoncat [G] [F] [dip] swoncat genrec
- [P] c unit [pop] swoncat [G] [F] [dip] swoncat genrec
- [P] c [G] [F] [unit [pop] swoncat] dipd [dip] swoncat genrec
-
-
-Working in reverse:
-swoncat twice to decouple [c] and [F].unit to dequote c.dipd to untangle [unit [pop] swoncat] from the givens.At this point all of the arguments (givens) to the hylomorphism are to the left so we have
-a definition for hylomorphism:
hylomorphism == [unit [pop] swoncat] dipd [dip] swoncat genrec
-
-
-The order of parameters is different than the one we started with but -that hardly matters, you can rearrange them or just supply them in the -expected order.
- -[P] c [G] [F] hylomorphism == H
-
-define('hylomorphism == [unit [pop] swoncat] dipd [dip] swoncat genrec')
-Demonstrate summing a range of integers from 0 to n-1.
-[P] is [0 <=]c is 0[G] is [1 - dup][F] is [+]So to sum the positive integers less than five we can do this.
- -V('5 [0 <=] 0 [1 - dup] [+] hylomorphism')
-An anamorphism can be defined as a hylomorphism that uses [] for c and
-swons for F.
[P] [G] anamorphism == [P] [] [G] [swons] hylomorphism == A
-
-
-This allows us to define an anamorphism combinator in terms of -the hylomorphism combinator.
- -[] swap [swons] hylomorphism == anamorphism
-
-
-Partial evaluation gives us a "pre-cooked" form.
- -[P] [G] . anamorphism
-[P] [G] . [] swap [swons] hylomorphism
-[P] [G] [] . swap [swons] hylomorphism
-[P] [] [G] . [swons] hylomorphism
-[P] [] [G] [swons] . hylomorphism
-[P] [] [G] [swons] . [unit [pop] swoncat] dipd [dip] swoncat genrec
-[P] [] [G] [swons] [unit [pop] swoncat] . dipd [dip] swoncat genrec
-[P] [] . unit [pop] swoncat [G] [swons] [dip] swoncat genrec
-[P] [[]] [pop] . swoncat [G] [swons] [dip] swoncat genrec
-[P] [pop []] [G] [swons] [dip] . swoncat genrec
-
-[P] [pop []] [G] [dip swons] genrec
-
-
-(We could also have just substituted for c and F in the definition of H.)
H == [P] [pop c ] [G] [dip F ] genrec
-A == [P] [pop []] [G] [dip swons] genrec
-
-
-The partial evaluation is overkill in this case but it serves as a -reminder that this sort of program specialization can, in many cases, be -carried out automatically.)
-Untangle [G] from [pop []] using swap.
[P] [G] [pop []] swap [dip swons] genrec
-
-
-All of the arguments to anamorphism are to the left, so we have a definition for it.
anamorphism == [pop []] swap [dip swons] genrec
-
-
-An example of an anamorphism is the range function.
- -range == [0 <=] [1 - dup] anamorphism
-
-A catamorphism can be defined as a hylomorphism that uses [uncons swap] for [G]
-and [[] =] for the predicate [P].
c [F] catamorphism == [[] =] c [uncons swap] [F] hylomorphism == C
-
-
-This allows us to define a catamorphism combinator in terms of
-the hylomorphism combinator.
[[] =] roll> [uncons swap] swap hylomorphism == catamorphism
-
-
-Partial evaluation doesn't help much.
- -c [F] . catamorphism
-c [F] . [[] =] roll> [uncons swap] swap hylomorphism
-c [F] [[] =] . roll> [uncons swap] swap hylomorphism
-[[] =] c [F] [uncons swap] . swap hylomorphism
-[[] =] c [uncons swap] [F] . hylomorphism
-[[] =] c [uncons swap] [F] [unit [pop] swoncat] . dipd [dip] swoncat genrec
-[[] =] c . unit [pop] swoncat [uncons swap] [F] [dip] swoncat genrec
-[[] =] [c] [pop] . swoncat [uncons swap] [F] [dip] swoncat genrec
-[[] =] [pop c] [uncons swap] [F] [dip] . swoncat genrec
-[[] =] [pop c] [uncons swap] [dip F] genrec
-
-
-Because the arguments to catamorphism have to be prepared (unlike the arguments -to anamorphism, which only need to be rearranged slightly) there isn't much point -to "pre-cooking" the definition.
- -catamorphism == [[] =] roll> [uncons swap] swap hylomorphism
-
-
-An example of a catamorphism is the sum function.
- -sum == 0 [+] catamorphism
-
-I'm not sure exactly how to translate the "Fusion Law" for catamorphisms into Joy.
-I know that a map composed with a cata can be expressed as a new cata:
[F] map b [B] cata == b [F B] cata
-
-
-But this isn't the one described in "Bananas...". That's more like:
-A cata composed with some function can be expressed as some other cata:
- -b [B] catamorphism F == c [C] catamorphism
-
-
-Given:
- -b F == c
-
-...
-
-B F == [F] dip C
-
-...
-
-b[B]cata F == c[C]cata
-
-F(B(head, tail)) == C(head, F(tail))
-
-1 [2 3] B F 1 [2 3] F C
-
-
-b F == c
-B F == F C
-
-b [B] catamorphism F == c [C] catamorphism
-b [B] catamorphism F == b F [C] catamorphism
-
-...
-
-
-Or maybe,
- -[F] map b [B] cata == c [C] cata ???
-
-[F] map b [B] cata == b [F B] cata I think this is generally true, unless F consumes stack items
- instead of just transforming TOS. Of course, there's always [F] unary.
-b [F] unary [[F] unary B] cata
-
-[10 *] map 0 swap [+] step == 0 swap [10 * +] step
-
-
-
-For example:
- -F == 10 *
-b == 0
-B == +
-c == 0
-C == F +
-
-b F == c
-0 10 * == 0
-
-B F == [F] dip C
-+ 10 * == [10 *] dip F +
-+ 10 * == [10 *] dip 10 * +
-
-n m + 10 * == 10(n+m)
-
-n m [10 *] dip 10 * +
-n 10 * m 10 * +
-10n m 10 * +
-10n 10m +
-10n+10m
-
-10n+10m = 10(n+m)
-
-
-Ergo:
- -0 [+] catamorphism 10 * == 0 [10 * +] catamorphism
-
-step combinator will usually be better to use than catamorphism.¶sum == 0 swap [+] step
-sum == 0 [+] catamorphism
-
-Here is (part of) the payoff.
-An anamorphism followed by (composed with) a -catamorphism is a hylomorphism, with the advantage that the hylomorphism -does not create the intermediate list structure. The values are stored in -either the call stack, for those implementations that use one, or in the pending -expression ("continuation") for the Joypy interpreter. They still have to -be somewhere, converting from an anamorphism and catamorphism to a hylomorphism -just prevents using additional storage and doing additional processing.
- - range == [0 <=] [1 - dup] anamorphism
- sum == 0 [+] catamorphism
-
-range sum == [0 <=] [1 - dup] anamorphism 0 [+] catamorphism
- == [0 <=] 0 [1 - dup] [+] hylomorphism
-
-
-We can let the hylomorphism combinator build range_sum for us or just
-substitute ourselves.
H == [P] [pop c] [G] [dip F] genrec
-range_sum == [0 <=] [pop 0] [1 - dup] [dip +] genrec
-
-defs = '''
-anamorphism == [pop []] swap [dip swons] genrec
-hylomorphism == [unit [pop] swoncat] dipd [dip] swoncat genrec
-catamorphism == [[] =] roll> [uncons swap] swap hylomorphism
-range == [0 <=] [1 - dup] anamorphism
-sum == 0 [+] catamorphism
-range_sum == [0 <=] 0 [1 - dup] [+] hylomorphism
-'''
-
-DefinitionWrapper.add_definitions(defs, D)
-J('10 range')
-J('[9 8 7 6 5 4 3 2 1 0] sum')
-V('10 range sum')
-V('10 range_sum')
-A paramorphism P :: B -> A is a recursion combinator that uses dup on intermediate values.
n swap [P] [pop] [[F] dupdip G] primrec
-
-
-With
-n :: A is the "identity" for F (like 1 for multiplication, 0 for addition)F :: (A, B) -> AG :: B -> B generates the next B value.P :: B -> Bool detects the end of the series.For Factorial function (types A and B are both integer):
n == 1
-F == *
-G == --
-P == 1 <=
-
-define('factorial == 1 swap [1 <=] [pop] [[*] dupdip --] primrec')
-Try it with input 3 (omitting evaluation of predicate):
- -3 1 swap [1 <=] [pop] [[*] dupdip --] primrec
-1 3 [1 <=] [pop] [[*] dupdip --] primrec
-
-1 3 [*] dupdip --
-1 3 * 3 --
-3 3 --
-3 2
-
-3 2 [*] dupdip --
-3 2 * 2 --
-6 2 --
-6 1
-
-6 1 [1 <=] [pop] [[*] dupdip --] primrec
-
-6 1 pop
-6
-
-J('3 factorial')
-paramorphism from the form above.¶n swap [P] [pop] [[F] dupdip G] primrec
-
-n swap [P] [pop] [[F] dupdip G] primrec
-n [P] [swap] dip [pop] [[F] dupdip G] primrec
-n [P] [[F] dupdip G] [[swap] dip [pop]] dip primrec
-n [P] [F] [dupdip G] cons [[swap] dip [pop]] dip primrec
-n [P] [F] [G] [dupdip] swoncat cons [[swap] dip [pop]] dip primrec
-
-paramorphism == [dupdip] swoncat cons [[swap] dip [pop]] dip primrec
-
-define('paramorphism == [dupdip] swoncat cons [[swap] dip [pop]] dip primrec')
-define('factorial == 1 [1 <=] [*] [--] paramorphism')
-J('3 factorial')
-tails¶An example of a paramorphism for lists given in the "Bananas..." paper is tails which returns the list of "tails" of a list.
[1 2 3] tails == [[] [3] [2 3]]
-
-
-Using paramorphism we would write:
n == []
-F == rest swons
-G == rest
-P == not
-
-tails == [] [not] [rest swons] [rest] paramorphism
-
-define('tails == [] [not] [rest swons] [rest] paramorphism')
-J('[1 2 3] tails')
-J('25 range tails [popop] infra [sum] map')
-J('25 range [range_sum] map')
-rest¶Right before the recursion begins we have:
- -[] [1 2 3] [not] [pop] [[rest swons] dupdip rest] primrec
-
-
-But we might prefer to factor rest in the quote:
[] [1 2 3] [not] [pop] [rest [swons] dupdip] primrec
-
-
-There's no way to do that with the paramorphism combinator as defined. We would have to write and use a slightly different recursion combinator that accepted an additional "preprocessor" function [H] and built:
n swap [P] [pop] [H [F] dupdip G] primrec
-
-
-Or just write it out manually. This is yet another place where the sufficiently smart compiler will one day automatically refactor the code. We could write a paramorphism combinator that checked [F] and [G] for common prefix and extracted it.
Our story so far...
-F :: (B, B) -> BP :: A -> Bool to detect the base casec :: Bw/ G :: A -> (A, B)
-
-H == [P ] [pop c ] [G ] [dip F ] genrec
-A == [P ] [pop []] [G ] [dip swons] genrec
-C == [[] =] [pop c ] [uncons swap] [dip F ] genrec
-
-
-w/ G :: B -> B
-
-P == c swap [P ] [pop] [[F ] dupdip G ] primrec
-? == [] swap [P ] [pop] [[swons] dupdip G ] primrec
-? == c swap [[] =] [pop] [[F ] dupdip uncons swap] primrec
-
-There are at least four kinds of recursive combinator, depending on two choices. The first choice is whether the combiner function should be evaluated during the recursion or pushed into the pending expression to be "collapsed" at the end. The second choice is whether the combiner needs to operate on the current value of the datastructure or the generator's output.
- -H == [P] [pop c] [G ] [dip F] genrec
-H == c swap [P] [pop] [G [F] dip ] [i] genrec
-H == [P] [pop c] [ [G] dupdip ] [dip F] genrec
-H == c swap [P] [pop] [ [F] dupdip G] [i] genrec
-
-
-Consider:
- -... a G [H] dip F w/ a G == a' b
-... c a G [F] dip H a G == b a'
-... a [G] dupdip [H] dip F a G == a'
-... c a [F] dupdip G H a G == a'
-
-H == [P] [pop c] [G] [dip F] genrec
-
-
-Iterate n times.
- -... a [P] [pop c] [G] [dip F] genrec
-... a G [H] dip F
-... a' b [H] dip F
-... a' H b F
-... a' G [H] dip F b F
-... a'' b [H] dip F b F
-... a'' H b F b F
-... a'' G [H] dip F b F b F
-... a''' b [H] dip F b F b F
-... a''' H b F b F b F
-... a''' pop c b F b F b F
-... c b F b F b F
-
-
-This form builds up a continuation that contains the intermediate results along with the pending combiner functions. When the base case is reached the last term is replaced by the identity value c and the continuation "collapses" into the final result.
- -When you can start with the identity value c on the stack and the combiner can operate as you go, using the intermediate results immediately rather than queuing them up, use this form. An important difference is that the generator function must return its results in the reverse order.
- -H == c swap [P] [pop] [G [F] dip] primrec
-
-... c a G [F] dip H
-... c b a' [F] dip H
-... c b F a' H
-... c b F a' G [F] dip H
-... c b F b a'' [F] dip H
-... c b F b F a'' H
-... c b F b F a'' G [F] dip H
-... c b F b F b a''' [F] dip H
-... c b F b F b F a''' H
-... c b F b F b F a''' pop
-... c b F b F b F
-
-
-The end line here is the same as for above, but only because we didn't evaluate F when it normally would have been.
If the combiner and the generator both need to work on the current value then dup must be used at some point, and the generator must produce one item instead of two (the b is instead the duplicate of a.)
H == [P] [pop c] [[G] dupdip] [dip F] genrec
-
-... a [G] dupdip [H] dip F
-... a G a [H] dip F
-... a' a [H] dip F
-... a' H a F
-... a' [G] dupdip [H] dip F a F
-... a' G a' [H] dip F a F
-... a'' a' [H] dip F a F
-... a'' H a' F a F
-... a'' [G] dupdip [H] dip F a' F a F
-... a'' G a'' [H] dip F a' F a F
-... a''' a'' [H] dip F a' F a F
-... a''' H a'' F a' F a F
-... a''' pop c a'' F a' F a F
-... c a'' F a' F a F
-
-And, last but not least, if you can combine as you go, starting with c, and the combiner needs to work on the current item, this is the form:
- -W == c swap [P] [pop] [[F] dupdip G] primrec
-
-... a c swap [P] [pop] [[F] dupdip G] primrec
-... c a [P] [pop] [[F] dupdip G] primrec
-... c a [F] dupdip G W
-... c a F a G W
-... c a F a' W
-... c a F a' [F] dupdip G W
-... c a F a' F a' G W
-... c a F a' F a'' W
-... c a F a' F a'' [F] dupdip G W
-... c a F a' F a'' F a'' G W
-... c a F a' F a'' F a''' W
-... c a F a' F a'' F a''' pop
-... c a F a' F a'' F
-
-Each of the four variations above can be specialized to ana- and catamorphic forms.
- -def WTFmorphism(c, F, P, G):
- '''Return a hylomorphism function H.'''
-
- def H(a, d=c):
- if P(a):
- result = d
- else:
- a, b = G(a)
- result = H(a, F(d, b))
- return result
-
- return H
-F = lambda a, b: a + b
-P = lambda n: n <= 1
-G = lambda n: (n - 1, n - 1)
-
-wtf = WTFmorphism(0, F, P, G)
-
-print wtf(5)
-H == [P ] [pop c ] [G ] [dip F ] genrec
-
-DefinitionWrapper.add_definitions('''
-P == 1 <=
-Ga == -- dup
-Gb == --
-c == 0
-F == +
-''', D)
-V('[1 2 3] [[] =] [pop []] [uncons swap] [dip swons] genrec')
-V('3 [P] [pop c] [Ga] [dip F] genrec')
-V('3 [P] [pop []] [Ga] [dip swons] genrec')
-V('[2 1] [[] =] [pop c ] [uncons swap] [dip F] genrec')
-|[ (c, F), (G, P) ]| == (|c, F|) • [(G, P)]
-
-
-"Bananas, Lenses, & Barbed Wire"
- -(|...|) [(...)] [<...>]
-
-
-I think they are having slightly too much fun with the symbols.
-"Too much is always better than not enough."
- -Let's use the Newton-Raphson method for finding the root of an equation to write a function that can compute the square root of a number.
- - -from notebook_preamble import J, V, define
-To make a generator that generates successive approximations let’s start by assuming an initial approximation and then derive the function that computes the next approximation:
- - a F
----------
- a'
-
-This is the equation for computing the next approximate value of the square root:
-$a_{i+1} = \frac{(a_i+\frac{n}{a_i})}{2}$
- -a n over / + 2 /
-a n a / + 2 /
-a n/a + 2 /
-a+n/a 2 /
-(a+n/a)/2
-
-
-The function we want has the argument n in it:
F == n over / + 2 /
-
-Our generator would be created by:
- -a [dup F] make_generator
-
-
-With n as part of the function F, but n is the input to the sqrt function we’re writing. If we let 1 be the initial approximation:
- -1 n 1 / + 2 /
-1 n/1 + 2 /
-1 n + 2 /
-n+1 2 /
-(n+1)/2
-
-
-The generator can be written as:
- -23 1 swap [over / + 2 /] cons [dup] swoncat make_generator
-1 23 [over / + 2 /] cons [dup] swoncat make_generator
-1 [23 over / + 2 /] [dup] swoncat make_generator
-1 [dup 23 over / + 2 /] make_generator
-
-define('gsra 1 swap [over / + 2 /] cons [dup] swoncat make_generator')
-J('23 gsra')
-Let's drive the generator a few time (with the x combinator) and square the approximation to see how well it works...
J('23 gsra 6 [x popd] times first sqr')
-From "Why Functional Programming Matters" by John Hughes:
--The remainder of a square root finder is a function within, which takes a tolerance and a list of approximations and looks down the list for two successive approximations that differ by no more than the given tolerance.
-
(And note that by “list” he means a lazily-evaluated list.)
-Using the output [a G] of the above generator for square root approximations, and further assuming that the first term a has been generated already and epsilon ε is handy on the stack...
a [b G] ε within
----------------------- a b - abs ε <=
- b
-
-
- a [b G] ε within
----------------------- a b - abs ε >
- b [c G] ε within
-
-a [b G] ε [first - abs] dip <=
-a [b G] first - abs ε <=
-a b - abs ε <=
-a-b abs ε <=
-abs(a-b) ε <=
-(abs(a-b)<=ε)
-
-define('_within_P [first - abs] dip <=')
-a [b G] ε roll< popop first
- [b G] ε a popop first
- [b G] first
- b
-
-define('_within_B roll< popop first')
-a [b G] ε R0 [within] R1
-
-
-x combinator to generate next term from G.within with i (it is a "tail-recursive" function.)Pretty straightforward:
- -a [b G] ε R0 [within] R1
-a [b G] ε [popd x] dip [within] i
-a [b G] popd x ε [within] i
- [b G] x ε [within] i
-b [c G] ε [within] i
-b [c G] ε within
-
-b [c G] ε within
-
-define('_within_R [popd x] dip')
-The recursive function we have defined so far needs a slight preamble: x to prime the generator and the epsilon value to use:
[a G] x ε ...
-a [b G] ε ...
-
-define('within x 0.000000001 [_within_P] [_within_B] [_within_R] tailrec')
-define('sqrt gsra within')
-Try it out...
- -J('36 sqrt')
-J('23 sqrt')
-Check it.
- -4.795831523312719**2
-from math import sqrt
-
-sqrt(23)
-Although any expression in Joy can be considered to describe a tree with the quotes as compound nodes and the non-quote values as leaf nodes, in this page I want to talk about ordered binary trees and how to make and use them.
-The basic structure, in a crude type notation, is:
- -Tree :: [] | [key value Tree Tree]
-
-
-That says that a Tree is either the empty quote [] or a quote with four items: a key, a value, and two Trees representing the left and right branches of the tree.
We're going to derive some recursive functions to work with such datastructures:
- -Tree-add
-Tree-delete
-Tree-get
-Tree-iter
-Tree-iter-order
-
-
-Once these functions are defined we have a new "type" to work with, and the Sufficiently Smart Compiler can be modified to use an optimized implementation under the hood. (Where does the "type" come from? It has a contingent existence predicated on the disciplined use of these functions on otherwise undistinguished Joy datastructures.)
- -from notebook_preamble import D, J, V, define, DefinitionWrapper
-Let's consider adding nodes to a Tree structure.
- - Tree value key Tree-add
------------------------------
- Tree′
-
-If the current node is [] then you just return [key value [] []]:
Tree-add == [popop not] [[pop] dipd Tree-new] [R0] [R1] genrec
-
-
-Tree-new¶Where Tree-new is defined as:
value key Tree-new
-------------------------
- [key value [] []]
-
-
-Example:
- -value key swap [[] []] cons cons
-key value [[] []] cons cons
-key [value [] []] cons
- [key value [] []]
-
-
-Definition:
- -Tree-new == swap [[] []] cons cons
-
-define('Tree-new == swap [[] []] cons cons')
-J('"v" "k" Tree-new')
-(As an implementation detail, the [[] []] literal used in the definition of Tree-new will be reused to supply the constant tail for all new nodes produced by it. This is one of those cases where you get amortized storage "for free" by using persistent datastructures. Because the tail, which is ((), ((), ())) in Python, is immutable and embedded in the definition body for Tree-new, all new nodes can reuse it as their own tail without fear that some other code somewhere will change it.)
We now have to derive R0 and R1, consider:
[key_n value_n left right] value key R0 [Tree-add] R1
-
-
-In this case, there are three possibilites: the key can be greater or less than or equal to the node's key. In two of those cases we will need to apply a copy of Tree-add, so R0 is pretty much out of the picture.
[R0] == []
-
-[key_n value_n left right] value key [BTree-add] R1
-
-
-The first thing we need to do is compare the the key we're adding to the node key and branch accordingly:
[key_n value_n left right] value key [BTree-add] [P] [T] [E] ifte
-
-
-That would suggest something like:
- -[key_n value_n left right] value key [BTree-add] P
-[key_n value_n left right] value key [BTree-add] pop roll> pop first >
-[key_n value_n left right] value key roll> pop first >
-key [key_n value_n left right] value roll> pop first >
-key key_n >
-Boolean
-
-
-Let's abstract the predicate just a little to let us specify the comparison operator:
- -P > == pop roll> pop first >
-P < == pop roll> pop first <
-P == pop roll> pop first
-
-define('P == pop roll> pop first')
-J('["old_key" 23 [] []] 17 "new_key" ["..."] P')
-Here the parentheses are meant to signify that the expression is not literal, the code in the parentheses is meant to have been evaluated:
- - [key_n value_n left right] value key [Tree-add] T
--------------------------------------------------------
- [key_n value_n left (Tree-add key value right)]
-
-So how do we do this? We're going to want to use infra on some function K that has the key and value to work with, as well as the quoted copy of Tree-add to apply somehow. Considering the node as a stack:
right left value_n key_n value key [Tree-add] K
------------------------------------------------------
- right value key Tree-add left value_n key_n
-
-
-Pretty easy:
- -right left value_n key_n value key [Tree-add] cons cons dipdd
-right left value_n key_n [value key Tree-add] dipdd
-right value key Tree-add left value_n key_n
-
-
-So:
- -K == cons cons dipdd
-
-
-Looking at it from the point-of-view of the node as node again:
- -[key_n value_n left right] [value key [Tree-add] K] infra
-
-
-Expand K and evaluate a little:
[key_n value_n left right] [value key [Tree-add] K] infra
-[key_n value_n left right] [value key [Tree-add] cons cons dipdd] infra
-[key_n value_n left right] [[value key Tree-add] dipdd] infra
-
-
-Then, working backwards:
- -[key_n value_n left right] [[value key Tree-add] dipdd] infra
-[key_n value_n left right] [value key Tree-add] [dipdd] cons infra
-[key_n value_n left right] value key [Tree-add] cons cons [dipdd] cons infra
-
-And so T is just:
T == cons cons [dipdd] cons infra
-
-define('T == cons cons [dipdd] cons infra')
-J('["old_k" "old_value" "left" "right"] "new_value" "new_key" ["Tree-add"] T')
-This is very very similar to the above:
- -[key_n value_n left right] value key [Tree-add] E
-[key_n value_n left right] value key [Tree-add] [P <] [Te] [Ee] ifte
-
-define('E == [P <] [Te] [Ee] ifte')
-In this case Te works that same as T but on the left child tree instead of the right, so the only difference is that it must use dipd instead of dipdd:
Te == cons cons [dipd] cons infra
-
-define('Te == cons cons [dipd] cons infra')
-J('["old_k" "old_value" "left" "right"] "new_value" "new_key" ["Tree-add"] Te')
-This means we must find:
- - [key old_value left right] new_value key [Tree-add] Ee
-------------------------------------------------------------
- [key new_value left right]
-
-This is another easy one:
- -Ee == pop swap roll< rest rest cons cons
-
-Example:
- -[key old_value left right] new_value key [Tree-add] pop swap roll< rest rest cons cons
-[key old_value left right] new_value key swap roll< rest rest cons cons
-[key old_value left right] key new_value roll< rest rest cons cons
-key new_value [key old_value left right] rest rest cons cons
-key new_value [ left right] cons cons
- [key new_value left right]
-
-define('Ee == pop swap roll< rest rest cons cons')
-J('["k" "old_value" "left" "right"] "new_value" "k" ["Tree-add"] Ee')
-Tree-add¶Tree-add == [popop not] [[pop] dipd Tree-new] [] [[P >] [T] [E] ifte] genrec
-
-
-Putting it all together:
- -Tree-new == swap [[] []] cons cons
-P == pop roll> pop first
-T == cons cons [dipdd] cons infra
-Te == cons cons [dipd] cons infra
-Ee == pop swap roll< rest rest cons cons
-E == [P <] [Te] [Ee] ifte
-R == [P >] [T] [E] ifte
-
-Tree-add == [popop not] [[pop] dipd Tree-new] [] [R] genrec
-
-define('Tree-add == [popop not] [[pop] dipd Tree-new] [] [[P >] [T] [E] ifte] genrec')
-J('[] 23 "b" Tree-add') # Initial
-J('["b" 23 [] []] 88 "c" Tree-add') # Greater than
-J('["b" 23 [] []] 88 "a" Tree-add') # Less than
-J('["b" 23 [] []] 88 "b" Tree-add') # Equal to
-J('[] 23 "b" Tree-add 88 "a" Tree-add 44 "c" Tree-add') # Series.
-J('[] [[23 "b"] [88 "a"] [44 "c"]] [i Tree-add] step')
-cmp combinator¶Instead of mucking about with nested ifte combinators let's use cmp which takes two values and three quoted programs on the stack and runs one of the three depending on the results of comparing the two values:
a b [G] [E] [L] cmp
-------------------------- a > b
- G
-
- a b [G] [E] [L] cmp
-------------------------- a = b
- E
-
- a b [G] [E] [L] cmp
-------------------------- a < b
- L
-
-J("1 0 ['G'] ['E'] ['L'] cmp")
-J("1 1 ['G'] ['E'] ['L'] cmp")
-J("0 1 ['G'] ['E'] ['L'] cmp")
-Tree-add¶We need a new non-destructive predicate P:
[node_key node_value left right] value key [Tree-add] P
-------------------------------------------------------------------------
- [node_key node_value left right] value key [Tree-add] key node_key
-
-Let's start with over to get a copy of the key and then apply some function Q with the nullary combinator so it can dig out the node key (by throwing everything else away):
P == over [Q] nullary
-
-[node_key node_value left right] value key [Tree-add] over [Q] nullary
-[node_key node_value left right] value key [Tree-add] key [Q] nullary
-
-And Q would be:
Q == popop popop first
-
-[node_key node_value left right] value key [Tree-add] key Q
-[node_key node_value left right] value key [Tree-add] key popop popop first
-[node_key node_value left right] value key popop first
-[node_key node_value left right] first
- node_key
-
-Or just:
- -P == over [popop popop first] nullary
-
-define('P == over [popop popop first] nullary')
-Using cmp to simplify our code above at R1:
[node_key node_value left right] value key [Tree-add] R1
-[node_key node_value left right] value key [Tree-add] P [T] [E] [Te] cmp
-
-The line above becomes one of the three lines below:
- -[node_key node_value left right] value key [Tree-add] T
-[node_key node_value left right] value key [Tree-add] E
-[node_key node_value left right] value key [Tree-add] Te
-
-The definition is a little longer but, I think, more elegant and easier to understand:
- -Tree-add == [popop not] [[pop] dipd Tree-new] [] [P [T] [Ee] [Te] cmp] genrec
-
-define('Tree-add == [popop not] [[pop] dipd Tree-new] [] [P [T] [Ee] [Te] cmp] genrec')
-J('[] 23 "b" Tree-add 88 "a" Tree-add 44 "c" Tree-add') # Still works.
-Let's take a crack at writing a function that can recursively iterate or traverse these trees.
- -[]¶The stopping predicate just has to detect the empty list:
- -Tree-iter == [not] [E] [R0] [R1] genrec
-
-
-And since there's nothing at this node, we just pop it:
Tree-iter == [not] [pop] [R0] [R1] genrec
-
-[key value left right]¶Now we need to figure out R0 and R1:
Tree-iter == [not] [pop] [R0] [R1] genrec
- == [not] [pop] [R0 [Tree-iter] R1] ifte
-
-
-Let's look at it in situ:
- -[key value left right] R0 [Tree-iter] R1
-
-R0 is almost certainly going to use dup to make a copy of the node and then dip on some function to process the copy with it:
[key value left right] [F] dupdip [Tree-iter] R1
-[key value left right] F [key value left right] [Tree-iter] R1
-
-
-For example, if we're getting all the keys F would be first:
R0 == [first] dupdip
-
-[key value left right] [first] dupdip [Tree-iter] R1
-[key value left right] first [key value left right] [Tree-iter] R1
-key [key value left right] [Tree-iter] R1
-
-Now R1 needs to apply [Tree-iter] to left and right. If we drop the key and value from the node using rest twice we are left with an interesting situation:
key [key value left right] [Tree-iter] R1
-key [key value left right] [Tree-iter] [rest rest] dip
-key [key value left right] rest rest [Tree-iter]
-key [left right] [Tree-iter]
-
-
-Hmm, will step do?
key [left right] [Tree-iter] step
-key left Tree-iter [right] [Tree-iter] step
-key left-keys [right] [Tree-iter] step
-key left-keys right Tree-iter
-key left-keys right-keys
-
-
-Neat. So:
- -R1 == [rest rest] dip step
-
-We have:
- -Tree-iter == [not] [pop] [[F] dupdip] [[rest rest] dip step] genrec
-
-
-When I was reading this over I realized rest rest could go in R0:
Tree-iter == [not] [pop] [[F] dupdip rest rest] [step] genrec
-
-
-(And [step] genrec is such a cool and suggestive combinator!)
F per-node processing function.¶ [F] Tree-iter
-------------------------------------------------------
- [not] [pop] [[F] dupdip rest rest] [step] genrec
-
-
-Working backward:
- -[not] [pop] [[F] dupdip rest rest] [step] genrec
-[not] [pop] [F] [dupdip rest rest] cons [step] genrec
-[F] [not] [pop] roll< [dupdip rest rest] cons [step] genrec
-
-Tree-iter¶Tree-iter == [not] [pop] roll< [dupdip rest rest] cons [step] genrec
-
-define('Tree-iter == [not] [pop] roll< [dupdip rest rest] cons [step] genrec')
-J('[] [foo] Tree-iter') # It doesn't matter what F is as it won't be used.
-J("['b' 23 ['a' 88 [] []] ['c' 44 [] []]] [first] Tree-iter")
-J("['b' 23 ['a' 88 [] []] ['c' 44 [] []]] [second] Tree-iter")
-We can use this to make a set-like datastructure by just setting values to e.g. 0 and ignoring them. It's set-like in that duplicate items added to it will only occur once within it, and we can query it in $O(\log_2 N)$ time.
- -J('[] [3 9 5 2 8 6 7 8 4] [0 swap Tree-add] step')
-define('to_set == [] swap [0 swap Tree-add] step')
-J('[3 9 5 2 8 6 7 8 4] to_set')
-And with that we can write a little program unique to remove duplicate items from a list.
define('unique == [to_set [first] Tree-iter] cons run')
-J('[3 9 3 5 2 9 8 8 8 6 2 7 8 4 3] unique') # Filter duplicate items.
-Tree-iter that does In-Order Traversal¶If you look back to the non-empty case of the Tree-iter function we can design a variant that first processes the left child, then the current node, then the right child. This will allow us to traverse the tree in sort order.
Tree-iter-order == [not] [pop] [R0] [R1] genrec
-
-
-To define R0 and R1 it helps to look at them as they will appear when they run:
[key value left right] R0 [BTree-iter-order] R1
-
-Staring at this for a bit suggests dup third to start:
[key value left right] R0 [Tree-iter-order] R1
-[key value left right] dup third [Tree-iter-order] R1
-[key value left right] left [Tree-iter-order] R1
-
-Now maybe:
- -[key value left right] left [Tree-iter-order] [cons dip] dupdip
-[key value left right] left [Tree-iter-order] cons dip [Tree-iter-order]
-[key value left right] [left Tree-iter-order] dip [Tree-iter-order]
-left Tree-iter-order [key value left right] [Tree-iter-order]
-
-So far, so good. Now we need to process the current node's values:
- -left Tree-iter-order [key value left right] [Tree-iter-order] [[F] dupdip] dip
-left Tree-iter-order [key value left right] [F] dupdip [Tree-iter-order]
-left Tree-iter-order [key value left right] F [key value left right] [Tree-iter-order]
-
-
-If F needs items from the stack below the left stuff it should have cons'd them before beginning maybe? For functions like first it works fine as-is.
left Tree-iter-order [key value left right] first [key value left right] [Tree-iter-order]
-left Tree-iter-order key [key value left right] [Tree-iter-order]
-
-First ditch the rest of the node and get the right child:
- -left Tree-iter-order key [key value left right] [Tree-iter-order] [rest rest rest first] dip
-left Tree-iter-order key right [Tree-iter-order]
-
-
-Then, of course, we just need i to run Tree-iter-order on the right side:
left Tree-iter-order key right [Tree-iter-order] i
-left Tree-iter-order key right Tree-iter-order
-
-Tree-iter-order¶The result is a little awkward:
- -R1 == [cons dip] dupdip [[F] dupdip] dip [rest rest rest first] dip i
-
-
-Let's do a little semantic factoring:
- -fourth == rest rest rest first
-
-proc_left == [cons dip] dupdip
-proc_current == [[F] dupdip] dip
-proc_right == [fourth] dip i
-
-Tree-iter-order == [not] [pop] [dup third] [proc_left proc_current proc_right] genrec
-
-
-Now we can sort sequences.
- -#define('Tree-iter-order == [not] [pop] [dup third] [[cons dip] dupdip [[first] dupdip] dip [rest rest rest first] dip i] genrec')
-
-
-DefinitionWrapper.add_definitions('''
-
-fourth == rest rest rest first
-
-proc_left == [cons dip] dupdip
-proc_current == [[first] dupdip] dip
-proc_right == [fourth] dip i
-
-Tree-iter-order == [not] [pop] [dup third] [proc_left proc_current proc_right] genrec
-
-''', D)
-J('[3 9 5 2 8 6 7 8 4] to_set Tree-iter-order')
-Parameterizing the [F] function is left as an exercise for the reader.
Let's derive a function that accepts a tree and a key and returns the value associated with that key.
- - tree key Tree-get
------------------------
- value
-
-
-But what do we do if the key isn't in the tree? In Python we might raise a KeyError but I'd like to avoid exceptions in Joy if possible, and here I think it's possible. (Division by zero is an example of where I think it's probably better to let Python crash Joy. Sometimes the machinery fails and you have to "stop the line", I think.)
Let's pass the buck to the caller by making the base case a given, you have to decide for yourself what [E] should be.
tree key [E] Tree-get
----------------------------- key in tree
- value
-
- tree key [E] Tree-get
----------------------------- key not in tree
- [] key E
-
-[]¶As before, the stopping predicate just has to detect the empty list:
- -Tree-get == [pop not] [E] [R0] [R1] genrec
-
-
-So we define:
- -Tree-get == [pop not] swap [R0] [R1] genrec
-
-
-Note that this Tree-get creates a slightly different function than itself and that function does the actual recursion. This kind of higher-level programming is unusual in most languages but natural in Joy.
tree key [E] [pop not] swap [R0] [R1] genrec
-tree key [pop not] [E] [R0] [R1] genrec
-
-
-The anonymous specialized recursive function that will do the real work.
- -[pop not] [E] [R0] [R1] genrec
-
-[key value left right]¶Now we need to figure out R0 and R1:
[key value left right] key R0 [BTree-get] R1
-
-
-We want to compare the search key with the key in the node, and if they are the same return the value, otherwise recur on one of the child nodes. So it's very similar to the above funtion, with [R0] == [] and R1 == P [T>] [E] [T<] cmp:
[key value left right] key [BTree-get] P [T>] [E] [T<] cmp
-
-P == over [get-node-key] nullary
-get-node-key == pop popop first
-
-
-The only difference is that get-node-key does one less pop because there's no value to discard.
Now we have to derive the branches:
- -[key_n value_n left right] key [BTree-get] T>
-[key_n value_n left right] key [BTree-get] E
-[key_n value_n left right] key [BTree-get] T<
-
-The cases of T> and T< are similar to above but instead of using infra we have to discard the rest of the structure:
[key_n value_n left right] key [BTree-get] T>
----------------------------------------------------
- right key BTree-get
-
-
-And:
- - [key_n value_n left right] key [BTree-get] T<
----------------------------------------------------
- left key BTree-get
-
-
-So:
- -T> == [fourth] dipd i
-T< == [third] dipd i
-
-
-E.g.:
- -[key_n value_n left right] key [BTree-get] [fourth] dipd i
-[key_n value_n left right] fourth key [BTree-get] i
- right key [BTree-get] i
- right key BTree-get
-
-Return the node's value:
- -[key_n value_n left right] key [BTree-get] E == value_n
-
-E == popop second
-
-Tree-get¶So:
- -fourth == rest rest rest first
-get-node-key == pop popop first
-P == over [get-node-key] nullary
-T> == [fourth] dipd i
-T< == [third] dipd i
-E == popop second
-
-Tree-get == [pop not] swap [] [P [T>] [E] [T<] cmp] genrec
-
-# I don't want to deal with name conflicts with the above so I'm inlining everything here.
-# The original Joy system has "hide" which is a meta-command which allows you to use named
-# definitions that are only in scope for a given definition. I don't want to implement
-# that (yet) so...
-
-
-define('''
-Tree-get == [pop not] swap [] [
- over [pop popop first] nullary
- [[fourth] dipd i]
- [popop second]
- [[third] dipd i]
- cmp
- ] genrec
-''')
-J('["gary" 23 [] []] "mike" [popd " not in tree" +] Tree-get')
-J('["gary" 23 [] []] "gary" [popop "err"] Tree-get')
-J('''
-
- [] [[0 'a'] [1 'b'] [2 'c']] [i Tree-add] step
-
- 'c' [popop 'not found'] Tree-get
-
-''')
-J('''
-
- [] [[0 'a'] [1 'b'] [2 'c']] [i Tree-add] step
-
- 'd' [popop 'not found'] Tree-get
-
-''')
-Now let's write a function that can return a tree datastructure with a key, value pair deleted:
- - tree key Tree-delete
----------------------------
- tree
-
-
-If the key is not in tree it just returns the tree unchanged.
- -Now we get to figure out the recursive case. We need the node's key to compare and we need to carry the key into recursive branches. Let D be shorthand for Tree-Delete:
D == Tree-Delete == [pop not] [pop] [R0] [R1] genrec
-
-[node_key node_value left right] key R0 [D] R1
-[node_key node_value left right] key over first swap dup [D] cons R1′
-[node_key node_value left right] key [...] first swap dup [D] cons R1′
-[node_key node_value left right] key node_key swap dup [D] cons R1′
-[node_key node_value left right] node_key key dup [D] cons R1′
-[node_key node_value left right] node_key key key [D] cons R1′
-[node_key node_value left right] node_key key [key D] R1′
-
-And then:
- -[node_key node_value left right] node_key key [key D] R1′
-[node_key node_value left right] node_key key [key D] roll> [T>] [E] [T<] cmp
-[node_key node_value left right] node_key key [key D] roll> [T>] [E] [T<] cmp
-[node_key node_value left right] [key D] node_key key [T>] [E] [T<] cmp
-
-So:
- -R0 == over first swap dup
-R1 == cons roll> [T>] [E] [T<] cmp
-
-The last line above:
- -[node_key node_value left right] [key D] node_key key [T>] [E] [T<] cmp
-
-
-Then becomes one of these three:
- -[node_key node_value left right] [key D] T>
-[node_key node_value left right] [key D] E
-[node_key node_value left right] [key D] T<
-
- [node_key node_value left right] [F] T>
--------------------------------------------------
- [node_key node_value (left F) right]
-
-
- [node_key node_value left right] [F] T<
--------------------------------------------------
- [node_key node_value left (right F)]
-
-First, treating the node as a stack:
- -right left node_value node_key [key D] dipd
-right left key D node_value node_key
-right left' node_value node_key
-
-Ergo:
- -[node_key node_value left right] [key D] [dipd] cons infra
-
-So:
- -T> == [dipd] cons infra
-T< == [dipdd] cons infra
-
-We have found the node in the tree where key equals node_key. We need to replace the current node with something
[node_key node_value left right] [key D] E
-------------------------------------------------
- tree
-
-
-We have to handle three cases, so let's use cond.
[]¶The first two cases are symmetrical: if we only have one non-empty child node return it. If both child nodes are empty return an empty node.
- -E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [default]
-] cond
-
-If both child nodes are non-empty, we find the highest node in our lower sub-tree, take its key and value to replace (delete) our own, then get rid of it by recursively calling delete() on our lower sub-node with our new key.
-(We could also find the lowest node in our higher sub-tree and take its key and value and delete it. I only implemented one of these two symmetrical options. Over a lot of deletions this might make the tree more unbalanced. Oh well.)
- -The initial structure of the default function:
- -default == [E′] cons infra
-
-[node_key node_value left right] [key D] default
-[node_key node_value left right] [key D] [E′] cons infra
-[node_key node_value left right] [[key D] E′] infra
-
-right left node_value node_key [key D] E′
-
-First things first, we no longer need this node's key and value:
- -right left node_value node_key [key D] roll> popop E″
-right left [key D] node_value node_key popop E″
-right left [key D] E″
-
-right left [key D] E″
-
-Ditch the key:
- -right left [key D] rest E‴
-right left [D] E‴
-
-Find the right-most node:
- -right left [D] [dup W] dip E⁗
-right left dup W [D] E⁗
-right left left W [D] E⁗
-
-Consider:
- -left W
-
-We know left is not empty:
- -[L_key L_value L_left L_right] W
-
-We want to keep extracting the right node as long as it is not empty:
- -W.rightmost == [P] [B] while
-
-left W.rightmost W′
-
-The predicate:
- -[L_key L_value L_left L_right] P
-[L_key L_value L_left L_right] fourth
- L_right
-
-
-This can run on [] so must be guarded:
?fourth == [] [fourth] [] ifte
-
-( - if_not_empty == [] swap [] ifte - ?fourth == [fourth] if_not_empty -)
- -The body is just fourth:
left [?fourth] [fourth] while W′
-rightest W′
-
-So:
- -W.rightmost == [?fourth] [fourth] while
-
-We know rightest is not empty:
- -[R_key R_value R_left R_right] W′
-[R_key R_value R_left R_right] W′
-[R_key R_value R_left R_right] uncons uncons pop
-R_key [R_value R_left R_right] uncons pop
-R_key R_value [R_left R_right] pop
-R_key R_value
-
-So:
- -W == [?fourth] [fourth] while uncons uncons pop
-
-And:
- -right left left W [D] E⁗
-right left R_key R_value [D] E⁗
-
-Final stretch. We want to end up with something like:
- -right left [R_key D] i R_value R_key
-right left R_key D R_value R_key
-right left′ R_value R_key
-
-If we adjust our definition of W to include over at the end:
W == [fourth] [fourth] while uncons uncons pop over
-
-That will give us:
- -right left R_key R_value R_key [D] E⁗
-
-right left R_key R_value R_key [D] cons dipd E⁗′
-right left R_key R_value [R_key D] dipd E⁗′
-right left R_key D R_key R_value E⁗′
-right left′ R_key R_value E⁗′
-right left′ R_key R_value swap
-right left′ R_value R_key
-
-So:
- -E′ == roll> popop E″
-
-E″ == rest E‴
-
-E‴ == [dup W] dip E⁗
-
-E⁗ == cons dipdd swap
-
-Substituting:
- -W == [fourth] [fourth] while uncons uncons pop over
-E′ == roll> popop rest [dup W] dip cons dipd swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E′] cons infra]
-] cond
-
-Minor rearrangement, move dup into W:
W == dup [fourth] [fourth] while uncons uncons pop over
-E′ == roll> popop rest [W] dip cons dipd swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E′] cons infra]
-] cond
-
-W.rightmost == [fourth] [fourth] while
-W.unpack == uncons uncons pop
-W == dup W.rightmost W.unpack over
-E.clear_stuff == roll> popop rest
-E.delete == cons dipd
-E.0 == E.clear_stuff [W] dip E.delete swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E.0] cons infra]
-] cond
-T> == [dipd] cons infra
-T< == [dipdd] cons infra
-R0 == over first swap dup
-R1 == cons roll> [T>] [E] [T<] cmp
-BTree-Delete == [pop not] swap [R0] [R1] genrec
-
-
-By the standards of the code I've written so far, this is a huge Joy program.
- -DefinitionWrapper.add_definitions('''
-first_two == uncons uncons pop
-fourth == rest rest rest first
-?fourth == [] [fourth] [] ifte
-W.rightmost == [?fourth] [fourth] while
-E.clear_stuff == roll> popop rest
-E.delete == cons dipd
-W == dup W.rightmost first_two over
-E.0 == E.clear_stuff [W] dip E.delete swap
-E == [[[pop third not] pop fourth] [[pop fourth not] pop third] [[E.0] cons infra]] cond
-T> == [dipd] cons infra
-T< == [dipdd] cons infra
-R0 == over first swap dup
-R1 == cons roll> [T>] [E] [T<] cmp
-Tree-Delete == [pop not] [pop] [R0] [R1] genrec
-''', D)
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'c' Tree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'b' Tree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'a' Tree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'der' Tree-Delete ")
-J('[] [4 2 3 1 6 7 5 ] [0 swap Tree-add] step')
-J("[4 0 [2 0 [1 0 [] []] [3 0 [] []]] [6 0 [5 0 [] []] [7 0 [] []]]] 3 Tree-Delete ")
-J("[4 0 [2 0 [1 0 [] []] [3 0 [] []]] [6 0 [5 0 [] []] [7 0 [] []]]] 4 Tree-Delete ")
-fourth == rest_two rest first
-?fourth == [] [fourth] [] ifte
-first_two == uncons uncons pop
-ccons == cons cons
-cinf == cons infra
-rest_two == rest rest
-
-_Tree_T> == [dipd] cinf
-_Tree_T< == [dipdd] cinf
-
-_Tree_add_P == over [popop popop first] nullary
-_Tree_add_T> == ccons _Tree_T<
-_Tree_add_T< == ccons _Tree_T>
-_Tree_add_Ee == pop swap roll< rest_two ccons
-_Tree_add_R == _Tree_add_P [_Tree_add_T>] [_Tree_add_Ee] [_Tree_add_T<] cmp
-_Tree_add_E == [pop] dipd Tree-new
-
-_Tree_iter_order_left == [cons dip] dupdip
-_Tree_iter_order_current == [[F] dupdip] dip
-_Tree_iter_order_right == [fourth] dip i
-_Tree_iter_order_R == _Tree_iter_order_left _Tree_iter_order_current _Tree_iter_order_right
-
-_Tree_get_P == over [pop popop first] nullary
-_Tree_get_T> == [fourth] dipd i
-_Tree_get_T< == [third] dipd i
-_Tree_get_E == popop second
-_Tree_get_R == _Tree_get_P [_Tree_get_T>] [_Tree_get_E] [_Tree_get_T<] cmp
-
-_Tree_delete_rightmost == [?fourth] [fourth] while
-_Tree_delete_clear_stuff == roll> popop rest
-_Tree_delete_del == dip cons dipd swap
-_Tree_delete_W == dup _Tree_delete_rightmost first_two over
-_Tree_delete_E.0 == _Tree_delete_clear_stuff [_Tree_delete_W] _Tree_delete_del
-_Tree_delete_E == [[[pop third not] pop fourth] [[pop fourth not] pop third] [[_Tree_delete_E.0] cinf]] cond
-_Tree_delete_R0 == over first swap dup
-_Tree_delete_R1 == cons roll> [_Tree_T>] [_Tree_delete_E] [_Tree_T<] cmp
-
-Tree-new == swap [[] []] ccons
-Tree-add == [popop not] [_Tree_add_E] [] [_Tree_add_R] genrec
-Tree-iter == [not] [pop] roll< [dupdip rest_two] cons [step] genrec
-Tree-iter-order == [not] [pop] [dup third] [_Tree_iter_order_R] genrec
-Tree-get == [pop not] swap [] [_Tree_get_R] genrec
-Tree-delete == [pop not] [pop] [_Tree_delete_R0] [_Tree_delete_R1] genrec
-
-from notebook_preamble import J, V, define
- -b ± sqrt(b^2 - 4 * a * c)
---------------------------------
- 2 * a
-
-$\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$
- -This math translates to Joy code in a straightforward manner. We are going to use named variables to keep track of the arguments, then write a definition without them.
- --b¶b neg
-
-sqrt(b^2 - 4 * a * c)¶b sqr 4 a c * * - sqrt
-
-/2a¶a 2 * /
-
-±¶There is a function pm that accepts two values on the stack and replaces them with their sum and difference.
pm == [+] [-] cleave popdd
-
-b neg b sqr 4 a c * * - sqrt pm a 2 * [/] cons app2
-
-
-We use app2 to compute both roots by using a quoted program [2a /] built with cons.
Working backwards we use dip and dipd to extract the code from the variables:
b neg b sqr 4 a c * * - sqrt pm a 2 * [/] cons app2
-b [neg] dupdip sqr 4 a c * * - sqrt pm a 2 * [/] cons app2
-b a c [[neg] dupdip sqr 4] dipd * * - sqrt pm a 2 * [/] cons app2
-b a c a [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2
-b a c over [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2
-
-
-The three arguments are to the left, so we can "chop off" everything to the right and say it's the definition of the quadratic function:
define('quadratic == over [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2')
-Let's try it out:
- -J('3 1 1 quadratic')
-If you look at the Joy evaluation trace you can see that the first few lines are the dip and dipd combinators building the main program by incorporating the values on the stack. Then that program runs and you get the results. This is pretty typical of Joy code.
V('-5 1 4 quadratic')
-
-from notebook_preamble import D, DefinitionWrapper, J, V, define
-This article describes the genrec combinator, how to use it, and several generic specializations.
[if] [then] [rec1] [rec2] genrec
----------------------------------------------------------------------
- [if] [then] [rec1 [[if] [then] [rec1] [rec2] genrec] rec2] ifte
-
-
-
-From "Recursion Theory and Joy" (j05cmp.html) by Manfred von Thun:
-- -"The genrec combinator takes four program parameters in addition to -whatever data parameters it needs. Fourth from the top is an if-part, -followed by a then-part. If the if-part yields true, then the then-part -is executed and the combinator terminates. The other two parameters are -the rec1-part and the rec2-part. If the if-part yields false, the -rec1-part is executed. Following that the four program parameters and -the combinator are again pushed onto the stack bundled up in a quoted -form. Then the rec2-part is executed, where it will find the bundled -form. Typically it will then execute the bundled form, either with i or -with app2, or some other combinator."
-
The way to design one of these is to fix your base case and
-test and then treat R1 and R2 as an else-part "sandwiching"
-a quotation of the whole function.
For example, given a (general recursive) function F:
F == [I] [T] [R1] [R2] genrec
- == [I] [T] [R1 [F] R2] ifte
-
-
-If the [I] predicate is false you must derive R1 and R2 from:
... R1 [F] R2
-
-
-Set the stack arguments in front and figure out what R1 and R2
-have to do to apply the quoted [F] in the proper way.
Primitive recursive functions are those where R2 == i.
P == [I] [T] [R] primrec
- == [I] [T] [R [P] i] ifte
- == [I] [T] [R P] ifte
-
-A hylomorphism is a recursive function H :: A -> C that converts a value of type A into a value of type C by means of:
G :: A -> (B, A)F :: (B, C) -> CP :: A -> Bool to detect the base casec :: CIt may be helpful to see this function implemented in imperative Python code.
- -def hylomorphism(c, F, P, G):
- '''Return a hylomorphism function H.'''
-
- def H(a):
- if P(a):
- result = c
- else:
- b, aa = G(a)
- result = F(b, H(aa)) # b is stored in the stack frame during recursive call to H().
- return result
-
- return H
-Cf. "Bananas, Lenses, & Barbed Wire"
-Note that during evaluation of H() the intermediate b values are stored in the Python call stack. This is what is meant by "call stack in the form of a cons list".
We can define a combinator hylomorphism that will make a hylomorphism combinator H from constituent parts.
H == [P] c [G] [F] hylomorphism
-
-
-The function H is recursive, so we start with ifte and set the else-part to
-some function J that will contain a quoted copy of H. (The then-part just
-discards the leftover a and replaces it with the base case value c.)
H == [P] [pop c] [J] ifte
-
-
-The else-part J gets just the argument a on the stack.
a J
-a G The first thing to do is use the generator G
-aa b which produces b and a new aa
-aa b [H] dip we recur with H on the new aa
-aa H b F and run F on the result.
-
-
-This gives us a definition for J.
J == G [H] dip F
-
-
-Plug it in and convert to genrec.
- -H == [P] [pop c] [G [H] dip F] ifte
-H == [P] [pop c] [G] [dip F] genrec
-
-
-This is the form of a hylomorphism in Joy, which nicely illustrates that -it is a simple specialization of the general recursion combinator.
- -H == [P] c [G] [F] hylomorphism == [P] [pop c] [G] [dip F] genrec
-
-hylomorphism combinator¶Now we just need to derive a definition that builds the genrec arguments
-out of the pieces given to the hylomorphism combinator.
[P] c [G] [F] hylomorphism
-------------------------------------------
- [P] [pop c] [G] [dip F] genrec
-
-
-Working in reverse:
-swoncat twice to decouple [c] and [F].unit to dequote c.dipd to untangle [unit [pop] swoncat] from the givens.So:
- -H == [P] [pop c] [G] [dip F] genrec
- [P] [c] [pop] swoncat [G] [F] [dip] swoncat genrec
- [P] c unit [pop] swoncat [G] [F] [dip] swoncat genrec
- [P] c [G] [F] [unit [pop] swoncat] dipd [dip] swoncat genrec
-
-
-At this point all of the arguments (givens) to the hylomorphism are to the left so we have
-a definition for hylomorphism:
hylomorphism == [unit [pop] swoncat] dipd [dip] swoncat genrec
-
-define('hylomorphism == [unit [pop] swoncat] dipd [dip] swoncat genrec')
-Let's write a function that, given a positive integer, returns the sum of all positive integers less than that one. (In this case the types A, B and C are all int.)
To sum a range of integers from 0 to n - 1:
-[P] is [1 <=]c is 0[G] is [-- dup][F] is [+]define('triangular_number == [1 <=] 0 [-- dup] [+] hylomorphism')
-Let's try it:
- -J('5 triangular_number')
-J('[0 1 2 3 4 5 6] [triangular_number] map')
-There are at least four kinds of recursive combinator, depending on two choices. The first choice is whether the combiner function F should be evaluated during the recursion or pushed into the pending expression to be "collapsed" at the end. The second choice is whether the combiner needs to operate on the current value of the datastructure or the generator's output, in other words, whether F or G should run first in the recursive branch.
H1 == [P] [pop c] [G ] [dip F] genrec
-H2 == c swap [P] [pop] [G [F] dip ] [i] genrec
-H3 == [P] [pop c] [ [G] dupdip ] [dip F] genrec
-H4 == c swap [P] [pop] [ [F] dupdip G] [i] genrec
-
-
-The working of the generator function G differs slightly for each. Consider the recursive branches:
... a G [H1] dip F w/ a G == a′ b
-
-... c a G [F] dip H2 a G == b a′
-
-... a [G] dupdip [H3] dip F a G == a′
-
-... c a [F] dupdip G H4 a G == a′
-
-
-The following four sections illustrate how these work, omitting the predicate evaluation.
- -H1¶H1 == [P] [pop c] [G] [dip F] genrec
-
-
-Iterate n times.
- -... a G [H1] dip F
-... a′ b [H1] dip F
-... a′ H1 b F
-... a′ G [H1] dip F b F
-... a″ b′ [H1] dip F b F
-... a″ H1 b′ F b F
-... a″ G [H1] dip F b′ F b F
-... a‴ b″ [H1] dip F b′ F b F
-... a‴ H1 b″ F b′ F b F
-... a‴ pop c b″ F b′ F b F
-... c b″ F b′ F b F
-... d b′ F b F
-... d′ b F
-... d″
-
-
-This form builds up a pending expression (continuation) that contains the intermediate results along with the pending combiner functions. When the base case is reached the last term is replaced by the identity value c and the continuation "collapses" into the final result using the combiner F.
H2¶When you can start with the identity value c on the stack and the combiner F can operate as you go using the intermediate results immediately rather than queuing them up, use this form. An important difference is that the generator function must return its results in the reverse order.
H2 == c swap [P] [pop] [G [F] dip] primrec
-
-... c a G [F] dip H2
-... c b a′ [F] dip H2
-... c b F a′ H2
-... d a′ H2
-... d a′ G [F] dip H2
-... d b′ a″ [F] dip H2
-... d b′ F a″ H2
-... d′ a″ H2
-... d′ a″ G [F] dip H2
-... d′ b″ a‴ [F] dip H2
-... d′ b″ F a‴ H2
-... d″ a‴ H2
-... d″ a‴ pop
-... d″
-
-H3¶If you examine the traces above you'll see that the combiner F only gets to operate on the results of G, it never "sees" the first value a. If the combiner and the generator both need to work on the current value then dup must be used, and the generator must produce one item instead of two (the b is instead the duplicate of a.)
H3 == [P] [pop c] [[G] dupdip] [dip F] genrec
-
-... a [G] dupdip [H3] dip F
-... a G a [H3] dip F
-... a′ a [H3] dip F
-... a′ H3 a F
-... a′ [G] dupdip [H3] dip F a F
-... a′ G a′ [H3] dip F a F
-... a″ a′ [H3] dip F a F
-... a″ H3 a′ F a F
-... a″ [G] dupdip [H3] dip F a′ F a F
-... a″ G a″ [H3] dip F a′ F a F
-... a‴ a″ [H3] dip F a′ F a F
-... a‴ H3 a″ F a′ F a F
-... a‴ pop c a″ F a′ F a F
-... c a″ F a′ F a F
-... d a′ F a F
-... d′ a F
-... d″
-
-H4¶And, last but not least, if you can combine as you go, starting with c, and the combiner F needs to work on the current item, this is the form:
H4 == c swap [P] [pop] [[F] dupdip G] primrec
-
-... c a [F] dupdip G H4
-... c a F a G H4
-... d a G H4
-... d a′ H4
-... d a′ [F] dupdip G H4
-... d a′ F a′ G H4
-... d′ a′ G H4
-... d′ a″ H4
-... d′ a″ [F] dupdip G H4
-... d′ a″ F a″ G H4
-... d″ a″ G H4
-... d″ a‴ H4
-... d″ a‴ pop
-... d″
-
-An anamorphism can be defined as a hylomorphism that uses [] for c and
-swons for F. An anamorphic function builds a list of values.
A == [P] [] [G] [swons] hylomorphism
-
-range et. al.¶An example of an anamorphism is the range function which generates the list of integers from 0 to n - 1 given n.
Each of the above variations can be used to make four slightly different range functions.
range with H1¶H1 == [P] [pop c] [G] [dip F] genrec
- == [0 <=] [pop []] [-- dup] [dip swons] genrec
-
-define('range == [0 <=] [] [-- dup] [swons] hylomorphism')
-J('5 range')
-range with H2¶H2 == c swap [P] [pop] [G [F] dip] primrec
- == [] swap [0 <=] [pop] [-- dup [swons] dip] primrec
-
-define('range_reverse == [] swap [0 <=] [pop] [-- dup [swons] dip] primrec')
-J('5 range_reverse')
-range with H3¶H3 == [P] [pop c] [[G] dupdip] [dip F] genrec
- == [0 <=] [pop []] [[--] dupdip] [dip swons] genrec
-
-define('ranger == [0 <=] [pop []] [[--] dupdip] [dip swons] genrec')
-J('5 ranger')
-range with H4¶H4 == c swap [P] [pop] [[F] dupdip G ] primrec
- == [] swap [0 <=] [pop] [[swons] dupdip --] primrec
-
-define('ranger_reverse == [] swap [0 <=] [pop] [[swons] dupdip --] primrec')
-J('5 ranger_reverse')
-Hopefully this illustrates the workings of the variations. For more insight you can run the cells using the V() function instead of the J() function to get a trace of the Joy evaluation.
A catamorphism can be defined as a hylomorphism that uses [uncons swap] for [G]
-and [[] =] (or just [not]) for the predicate [P]. A catamorphic function tears down a list term-by-term and makes some new value.
C == [not] c [uncons swap] [F] hylomorphism
-
-define('swuncons == uncons swap') # Awkward name.
-An example of a catamorphism is the sum function.
- -sum == [not] 0 [swuncons] [+] hylomorphism
-
-define('sum == [not] 0 [swuncons] [+] hylomorphism')
-J('[5 4 3 2 1] sum')
-step combinator¶The step combinator will usually be better to use than catamorphism.
J('[step] help')
-define('sum == 0 swap [+] step')
-J('[5 4 3 2 1] sum')
-For the Factorial function:
- -H4 == c swap [P] [pop] [[F] dupdip G] primrec
-
-
-With:
- -c == 1
-F == *
-G == --
-P == 1 <=
-
-define('factorial == 1 swap [1 <=] [pop] [[*] dupdip --] primrec')
-J('5 factorial')
-tails¶An example of a paramorphism for lists given in the "Bananas..." paper is tails which returns the list of "tails" of a list.
[1 2 3] tails
---------------------
- [[] [3] [2 3]]
-
-We can build as we go, and we want F to run after G, so we use pattern H2:
H2 == c swap [P] [pop] [G [F] dip] primrec
-
-We would use:
- -c == []
-F == swons
-G == rest dup
-P == not
-
-define('tails == [] swap [not] [pop] [rest dup [swons] dip] primrec')
-J('[1 2 3] tails')
-Our story so far...
-H == [P ] [pop c ] [G ] [dip F ] genrec
-A == [P ] [pop []] [G ] [dip swap cons] genrec
-C == [not] [pop c ] [uncons swap] [dip F ] genrec
-
-
-P == c swap [P ] [pop] [[F ] dupdip G ] primrec
-? == [] swap [P ] [pop] [[swap cons] dupdip G ] primrec
-? == c swap [not] [pop] [[F ] dupdip uncons swap] primrec
-
-|[ (c, F), (G, P) ]| == (|c, F|) • [(G, P)]
-
-
-"Bananas, Lenses, & Barbed Wire"
- -(|...|) [(...)] [<...>]
-
-
-I think they are having slightly too much fun with the symbols. However, "Too much is always better than not enough."
- -For now, there is no way to define new functions from within the Joy language. All functions (and the interpreter) all accept and return a dictionary parameter (in addition to the stack and expression) so that we can implement e.g. a function that adds new functions to the dictionary. However, there's no function that does that. Adding a new function to the dictionary is a meta-interpreter action, you have to do it in Python, not Joy.
- -from notebook_preamble import D, J, V
-V('[23 18] average')
-size with a Python version¶Both sum and size each convert a sequence to a single value.
sum == 0 swap [+] step
-size == 0 swap [pop ++] step
-
-An efficient sum function is already in the library. But for size we can use a “compiled” version hand-written in Python to speed up evaluation and make the trace more readable.
from joy.library import SimpleFunctionWrapper
-from joy.utils.stack import iter_stack
-
-
-@SimpleFunctionWrapper
-def size(stack):
- '''Return the size of the sequence on the stack.'''
- sequence, stack = stack
- n = 0
- for _ in iter_stack(sequence):
- n += 1
- return n, stack
-Now we replace the old version in the dictionary with the new version, and re-evaluate the expression.
- -D['size'] = size
-You can see that size now executes in a single step.
V('[23 18] average')
-Here is the example of Joy code from the README file:
[[[abs]ii <=][[<>][pop !-]||]&&][[!-][[++]][[--]]ifte dip][[pop !-][--][++]ifte]ifte
-
-
-It might seem unreadable but with a little familiarity it becomes just as -legible as any other notation. Some layout helps:
- -[ [[abs] ii <=]
- [
- [<>] [pop !-] ||
- ] &&
-]
-[[ !-] [[++]] [[--]] ifte dip]
-[[pop !-] [--] [++] ifte ]
-ifte
-
-
-This function accepts two integers on the stack and increments or -decrements one of them such that the new pair of numbers is the next -coordinate pair in a square spiral (like the kind used to construct an -Ulam Spiral).
- -It's adapted from the original code on StackOverflow:
-- -If all you're trying to do is generate the first N points in the spiral -(without the original problem's constraint of masking to an N x M -region), the code becomes very simple:
-
void spiral(const int N)
-{
- int x = 0;
- int y = 0;
- for(int i = 0; i < N; ++i)
- {
- cout << x << '\t' << y << '\n';
- if(abs(x) <= abs(y) && (x != y || x >= 0))
- x += ((y >= 0) ? 1 : -1);
- else
- y += ((x >= 0) ? -1 : 1);
- }
-}
-
-I'm going to make a function that take two ints (x and y) and
-generates the next pair, we'll turn it into a generator later using the
-x combinator.
We need a function that computes abs(x) <= abs(y), we can use ii to
-apply abs to both values and then compare them
-with <=:
[abs] ii <=
-
-[_p [abs] ii <=] inscribe
-clear 23 -18
-23 -18-
[_p] trace
-23 -18 • _p - 23 -18 • [abs] ii <= -23 -18 [abs] • ii <= - 23 • abs -18 abs <= - 23 • -18 abs <= - 23 -18 • abs <= - 23 18 • <= - false • - -false-
clear
-I've defined two short-circuiting Boolean combinators && and || that
-each accept two quoted predicate programs, run the first, and
-conditionally run the second only if required (to compute the final
-Boolean value). They run their predicate arguments nullary.
[&& [nullary] cons [nullary [false]] dip branch] inscribe
-[|| [nullary] cons [nullary] dip [true] branch] inscribe
-clear
-[true] [false] &&
-false-
clear
-[false] [true] &&
-false-
clear
-[true] [false] ||
-true-
clear
-[false] [true] ||
-true-
clear
-Given those, we can define x != y || x >= 0 as:
_a == [!=] [pop 0 >=] ||
-
-[_a [!=] [pop 0 >=] ||] inscribe
-And (abs(x) <= abs(y) && (x != y || x >= 0)) as:
_b == [_p] [_a] &&
-
-[_b [_p] [_a] &&] inscribe
-It's a little rough, but, as I say, with a little familiarity it becomes -legible.
- -clear 23 -18
-23 -18-
[_b] trace
-23 -18 • _b - 23 -18 • [_p] [_a] && - 23 -18 [_p] • [_a] && - 23 -18 [_p] [_a] • && - 23 -18 [_p] [_a] • [nullary] cons [nullary [false]] dip branch - 23 -18 [_p] [_a] [nullary] • cons [nullary [false]] dip branch - 23 -18 [_p] [[_a] nullary] • [nullary [false]] dip branch -23 -18 [_p] [[_a] nullary] [nullary [false]] • dip branch - 23 -18 [_p] • nullary [false] [[_a] nullary] branch - 23 -18 [_p] • [stack] dinfrirst [false] [[_a] nullary] branch - 23 -18 [_p] [stack] • dinfrirst [false] [[_a] nullary] branch - 23 -18 [_p] [stack] • dip infrst [false] [[_a] nullary] branch - 23 -18 • stack [_p] infrst [false] [[_a] nullary] branch - 23 -18 [-18 23] • [_p] infrst [false] [[_a] nullary] branch - 23 -18 [-18 23] [_p] • infrst [false] [[_a] nullary] branch - 23 -18 [-18 23] [_p] • infra first [false] [[_a] nullary] branch - 23 -18 • _p [-18 23] swaack first [false] [[_a] nullary] branch - 23 -18 • [abs] ii <= [-18 23] swaack first [false] [[_a] nullary] branch - 23 -18 [abs] • ii <= [-18 23] swaack first [false] [[_a] nullary] branch - 23 • abs -18 abs <= [-18 23] swaack first [false] [[_a] nullary] branch - 23 • -18 abs <= [-18 23] swaack first [false] [[_a] nullary] branch - 23 -18 • abs <= [-18 23] swaack first [false] [[_a] nullary] branch - 23 18 • <= [-18 23] swaack first [false] [[_a] nullary] branch - false • [-18 23] swaack first [false] [[_a] nullary] branch - false [-18 23] • swaack first [false] [[_a] nullary] branch - 23 -18 [false] • first [false] [[_a] nullary] branch - 23 -18 false • [false] [[_a] nullary] branch - 23 -18 false [false] • [[_a] nullary] branch - 23 -18 false [false] [[_a] nullary] • branch - 23 -18 • false - 23 -18 false • - -23 -18 false-
clear
-Turning to the branches of the main if statement:
x += ((y >= 0) ? 1 : -1);
-
-
-Rewrite as a hybrid (pseudo-code) ifte expression:
[y >= 0] [x += 1] [X -= 1] ifte
-
-
-Change each C phrase to Joy code:
- -[0 >=] [[++] dip] [[--] dip] ifte
-
-
-Factor out the dip from each branch:
- -[0 >=] [[++]] [[--]] ifte dip
-
-
-Similar logic applies to the other branch:
- -y += ((x >= 0) ? -1 : 1);
-
-[x >= 0] [y -= 1] [y += 1] ifte
-
-[pop 0 >=] [--] [++] ifte
-
-We can assemble the three functions we just defined in quotes and give
-them them to the ifte combinator. With some arrangement to show off
-the symmetry of the two branches, we have:
[[[abs] ii <=] [[<>] [pop !-] ||] &&]
-[[ !-] [[++]] [[--]] ifte dip]
-[[pop !-] [--] [++] ifte ]
-ifte
-
-[spiral_next
-
-[_b]
-[[ !-] [[++]] [[--]] ifte dip]
-[[pop !-] [--] [++] ifte ]
-ifte
-
-] inscribe
-As I was writing this up I realized that, since the && combinator
-doesn't consume the stack (below its quoted args), I can unquote the
-predicate, swap the branches, and use the branch combinator instead of
-ifte:
[[abs] ii <=] [[<>] [pop !-] ||] &&
-[[pop !-] [--] [++] ifte ]
-[[ !-] [[++]] [[--]] ifte dip]
-branch
-
-Let's try it out:
- -clear 0 0
-0 0-
spiral_next
-1 0-
spiral_next
-1 -1-
spiral_next
-0 -1-
spiral_next
--1 -1-
spiral_next
--1 0-
spiral_next
--1 1-
spiral_next
-0 1-
spiral_next
-1 1-
spiral_next
-2 1-
spiral_next
-2 0-
spiral_next
-2 -1-
spiral_next
-2 -2-
spiral_next
-1 -2-
spiral_next
-0 -2-
spiral_next
--1 -2-
x¶It can be used with the x combinator to make a kind of generator for -spiral square coordinates.
-We can use codireco to make a generator
codireco == cons dip rest cons
-
-
-It will look like this:
- -[value [F] codireco]
-
-
-Here's a trace of how it works:
- -clear
-
-[0 [dup ++] codireco] [x] trace
-[0 [dup ++] codireco] • x - [0 [dup ++] codireco] • 0 [dup ++] codireco - [0 [dup ++] codireco] 0 • [dup ++] codireco -[0 [dup ++] codireco] 0 [dup ++] • codireco -[0 [dup ++] codireco] 0 [dup ++] • codi reco -[0 [dup ++] codireco] 0 [dup ++] • cons dip reco -[0 [dup ++] codireco] [0 dup ++] • dip reco - • 0 dup ++ [0 [dup ++] codireco] reco - 0 • dup ++ [0 [dup ++] codireco] reco - 0 0 • ++ [0 [dup ++] codireco] reco - 0 1 • [0 [dup ++] codireco] reco - 0 1 [0 [dup ++] codireco] • reco - 0 1 [0 [dup ++] codireco] • rest cons - 0 1 [[dup ++] codireco] • cons - 0 [1 [dup ++] codireco] • - -0 [1 [dup ++] codireco]-
clear
-But first we have to change the spiral_next function to work on a
-quoted pair of integers, and leave a copy of the pair on the stack.
-From:
y x spiral_next
----------------------
- y' x'
-
-
-to:
- - [x y] [spiral_next] infra
--------------------------------
- [x' y']
-
-[0 0] [spiral_next] infra
-[0 1]-
So our generator is:
- -[[x y] [dup [spiral_next] infra] codireco]
-
-
-Or rather:
- -[[0 0] [dup [spiral_next] infra] codireco]
-
-
-There is a function make_generator that will build the generator for us
-out of the value and stepper function:
[0 0] [dup [spiral_next] infra] make_generator
-----------------------------------------------------
- [[0 0] [dup [spiral_next] infra] codireco]
-
-clear
-Here it is in action:
- -[0 0] [dup [spiral_next] infra] make_generator x x x x pop
-[0 0] [0 1] [-1 1] [-1 0]-
Four x combinators, four pairs of coordinates.
Or you can leave out dup and let the value stay in the generator until you want it:
clear
-
-[0 0] [[spiral_next] infra] make_generator 50 [x] times first
-[2 4]-
So that's an example of Joy code. It's a straightforward translation of -the original. It's a little long for a single definition, you might -break it up like so:
- -_spn_Pa == [abs] ii <=
-_spn_Pb == [!=] [pop 0 >=] ||
-_spn_P == [_spn_Pa] [_spn_Pb] &&
-
-_spn_T == [ !-] [[++]] [[--]] ifte dip
-_spn_E == [pop !-] [--] [++] ifte
-
-spiral_next == _spn_P [_spn_E] [_spn_T] branch
-
-
-This way it's easy to see that the function is a branch with two -quasi-symmetrical paths.
-We then used this function to make a simple generator of coordinate
-pairs, where the next pair in the series can be generated at any time by
-using the x combinator on the generator (which is just a quoted
-expression containing a copy of the current pair and the "stepper
-function" to generate the next pair from that.)
All definite actions (computer program) can be defined by four fundamental patterns of combination:
-Do one thing after another. In joy this is represented by putting two symbols together, juxtaposition:
- -foo bar
-
-
-Operations have inputs and outputs. The outputs of foo must be compatible in "arity", type, and shape with the inputs of bar.
Do one thing or another.
- -boolean [F] [T] branch
-
-
- t [F] [T] branch
-----------------------
- T
-
-
- f [F] [T] branch
-----------------------
- F
-
-
-branch == unit cons swap pick i
-
-boolean [F] [T] branch
-boolean [F] [T] unit cons swap pick i
-boolean [F] [[T]] cons swap pick i
-boolean [[F] [T]] swap pick i
-[[F] [T]] boolean pick i
-[F-or-T] i
-
-Given some branch function G:
G == [F] [T] branch
-
-
-Used in a sequence like so:
- -foo G bar
-
-
-The inputs and outputs of F and T must be compatible with the outputs for foo and the inputs of bar, respectively.
foo F bar
-
-foo T bar
-
-ifte¶Often it will be easier on the programmer to write branching code with the predicate specified in a quote. The ifte combinator provides this (T for "then" and E for "else"):
[P] [T] [E] ifte
-
-
-Defined in terms of branch:
ifte == [nullary not] dip branch
-
-
-
-In this case, P must be compatible with the stack and return a Boolean value, and T and E both must be compatible with the preceeding and following functions, as described above for F and T. (Note that in the current implementation we are depending on Python for the underlying semantics, so the Boolean value doesn't have to be Boolean because Python's rules for "truthiness" will be used to evaluate it. I reflect this in the structure of the stack effect comment of branch, it will only accept Boolean values, and in the definition of ifte above by including not in the quote, which also has the effect that the subject quotes are in the proper order for branch.)
Do one thing zero or more times.
- -boolean [Q] loop
-
-
- t [Q] loop
-----------------
- Q [Q] loop
-
-
- ... f [Q] loop
---------------------
- ...
-
-
-The loop combinator generates a copy of itself in the true branch. This is the hallmark of recursive defintions. In Thun there is no equivalent to conventional loops. (There is, however, the x combinator, defined as x == dup i, which permits recursive constructs that do not need to be directly self-referential, unlike loop and genrec.)
loop == [] swap [dup dip loop] cons branch
-
-boolean [Q] loop
-boolean [Q] [] swap [dup dip loop] cons branch
-boolean [] [Q] [dup dip loop] cons branch
-boolean [] [[Q] dup dip loop] branch
-
-
-In action the false branch does nothing while the true branch does:
- -t [] [[Q] dup dip loop] branch
- [Q] dup dip loop
- [Q] [Q] dip loop
- Q [Q] loop
-
-Because loop expects and consumes a Boolean value, the Q function must be compatible with the previous stack and itself with a boolean flag for the next iteration:
Q == G b
-
-Q [Q] loop
-G b [Q] loop
-G Q [Q] loop
-G G b [Q] loop
-G G Q [Q] loop
-G G G b [Q] loop
-G G G
-
-while¶Keep doing B while some predicate P is true. This is convenient as the predicate function is made nullary automatically and the body function can be designed without regard to leaving a Boolean flag for the next iteration:
[P] [B] while
---------------------------------------
- [P] nullary [B [P] nullary] loop
-
-
-while == swap [nullary] cons dup dipd concat loop
-
-
-[P] [B] while
-[P] [B] swap [nullary] cons dup dipd concat loop
-[B] [P] [nullary] cons dup dipd concat loop
-[B] [[P] nullary] dup dipd concat loop
-[B] [[P] nullary] [[P] nullary] dipd concat loop
-[P] nullary [B] [[P] nullary] concat loop
-[P] nullary [B [P] nullary] loop
-
-The parallel operation indicates that two (or more) functions do not interfere with each other and so can run in parallel. The main difficulty in this sort of thing is orchestrating the recombining ("join" or "wait") of the results of the functions after they finish.
-The current implementaions and the following definitions are not actually parallel (yet), but there is no reason they couldn't be reimplemented in terms of e.g. Python threads. I am not concerned with performance of the system just yet, only the elegance of the code it allows us to write.
- -cleave¶Joy has a few parallel combinators, the main one being cleave:
... x [A] [B] cleave
----------------------------------------------------------
- ... [x ...] [A] infra first [x ...] [B] infra first
----------------------------------------------------------
- ... a b
-
-
-The cleave combinator expects a value and two quotes and it executes each quote in "separate universes" such that neither can affect the other, then it takes the first item from the stack in each universe and replaces the value and quotes with their respective results.
(I think this corresponds to the "fork" operator, the little upward-pointed triangle, that takes two functions A :: x -> a and B :: x -> b and returns a function F :: x -> (a, b), in Conal Elliott's "Compiling to Categories" paper, et. al.)
Just a thought, if you cleave two jobs and one requires more time to finish than the other you'd like to be able to assign resources accordingly so that they both finish at the same time.
There are also app2 and app3 which run a single quote on more than one value:
... y x [Q] app2
- ---------------------------------------------------------
- ... [y ...] [Q] infra first [x ...] [Q] infra first
-
-
- ... z y x [Q] app3
- ---------------------------------
- ... [z ...] [Q] infra first
- [y ...] [Q] infra first
- [x ...] [Q] infra first
-
-
-Because the quoted program can be i we can define cleave in terms of app2:
cleave == [i] app2 [popd] dip
-
-(I'm not sure why cleave was specified to take that value, I may make a combinator that does the same thing but without expecting a value.)
clv == [i] app2
-
- [A] [B] clv
-------------------
- a b
-
-map¶The common map function in Joy should also be though of as a parallel operator:
[a b c ...] [Q] map
-
-
-There is no reason why the implementation of map couldn't distribute the Q function over e.g. a pool of worker CPUs.
pam¶One of my favorite combinators, the pam combinator is just:
pam == [i] map
-
-
-This can be used to run any number of programs separately on the current stack and combine their (first) outputs in a result list.
- - [[A] [B] [C] ...] [i] map
--------------------------------
- [ a b c ...]
-
-The cleave operators and others all have pretty brutal join semantics: everything works and we always wait for every sub-computation. We can imagine a few different potentially useful patterns of "joining" results from parallel combinators.
Thinking about variations of pam there could be one that only returns the first result of the first-to-finish sub-program, or the stack could be replaced by its output stack.
The other sub-programs would be cancelled.
- -Also known as "Futures" or "Promises" (by everybody else. "Fulinators" is what I was going to call them when I was thinking about implementing them in Thun.)
-The runtime could be amended to permit "thunks" representing the results of in-progress computations to be left on the stack and picked up by subsequent functions. These would themselves be able to leave behind more "thunks", the values of which depend on the eventual resolution of the values of the previous thunks.
-In this way you can create "chains" (and more complex shapes) out of normal-looking code that consist of a kind of call-graph interspersed with "asyncronous" ... events?
-In any case, until I can find a rigorous theory that shows that this sort of thing works perfectly in Joy code I'm not going to worry about it. (And I think the Categories can deal with it anyhow? Incremental evaluation, yeah?)
- -Although any expression in Joy can be considered to describe a tree with the quotes as compound nodes and the non-quote values as leaf nodes, in this page I want to talk about ordered binary trees and how to make and use them.
-The basic structure, in a crude type notation, is:
- -BTree :: [] | [key value BTree BTree]
-
-
-That says that a BTree is either the empty quote [] or a quote with four items: a key, a value, and two BTrees representing the left and right branches of the tree.
Let's take a crack at writing a function that can recursively iterate or traverse these trees.
- -[]¶The stopping predicate just has to detect the empty list:
- -BTree-iter == [not] [E] [R0] [R1] genrec
-
-
-And since there's nothing at this node, we just pop it:
BTree-iter == [not] [pop] [R0] [R1] genrec
-
-[key value left right]¶Now we need to figure out R0 and R1:
BTree-iter == [not] [pop] [R0] [R1] genrec
- == [not] [pop] [R0 [BTree-iter] R1] ifte
-
-
-Let's look at it in situ:
- -[key value left right] R0 [BTree-iter] R1
-
-R0 is almost certainly going to use dup to make a copy of the node and then dip on some function to process the copy with it:
[key value left right] [F] dupdip [BTree-iter] R1
-[key value left right] F [key value left right] [BTree-iter] R1
-
-
-For example, if we're getting all the keys F would be first:
R0 == [first] dupdip
-
-[key value left right] [first] dupdip [BTree-iter] R1
-[key value left right] first [key value left right] [BTree-iter] R1
-key [key value left right] [BTree-iter] R1
-
-Now R1 needs to apply [BTree-iter] to left and right. If we drop the key and value from the node using rest twice we are left with an interesting situation:
key [key value left right] [BTree-iter] R1
-key [key value left right] [BTree-iter] [rest rest] dip
-key [key value left right] rest rest [BTree-iter]
-key [left right] [BTree-iter]
-
-
-Hmm, will step do?
key [left right] [BTree-iter] step
-key left BTree-iter [right] [BTree-iter] step
-key left-keys [right] [BTree-iter] step
-key left-keys right BTree-iter
-key left-keys right-keys
-
-
-Wow. So:
- -R1 == [rest rest] dip step
-
-We have:
- -BTree-iter == [not] [pop] [[F] dupdip] [[rest rest] dip step] genrec
-
-
-When I was reading this over I realized rest rest could go in R0:
BTree-iter == [not] [pop] [[F] dupdip rest rest] [step] genrec
-
-
-(And [step] genrec is such a cool and suggestive combinator!)
F per-node processing function.¶[F] BTree-iter == [not] [pop] [[F] dupdip rest rest] [step] genrec
-
-
-Working backward:
- -[not] [pop] [[F] dupdip rest rest] [step] genrec
-[not] [pop] [F] [dupdip rest rest] cons [step] genrec
-[F] [not] [pop] roll< [dupdip rest rest] cons [step] genrec
-
-
-Ergo:
- -BTree-iter == [not] [pop] roll< [dupdip rest rest] cons [step] genrec
-
-from notebook_preamble import D, J, V, define, DefinitionWrapper
-define('BTree-iter == [not] [pop] roll< [dupdip rest rest] cons [step] genrec')
-J('[] [23] BTree-iter') # It doesn't matter what F is as it won't be used.
-J('["tommy" 23 [] []] [first] BTree-iter')
-J('["tommy" 23 ["richard" 48 [] []] ["jenny" 18 [] []]] [first] BTree-iter')
-J('["tommy" 23 ["richard" 48 [] []] ["jenny" 18 [] []]] [second] BTree-iter')
-Let's consider adding nodes to a BTree structure.
- -BTree value key BTree-add == BTree
-
-If the current node is [] then you just return [key value [] []]:
BTree-add == [popop not] [[pop] dipd BTree-new] [R0] [R1] genrec
-
-
-Where BTree-new is:
value key BTree-new == [key value [] []]
-
-value key swap [[] []] cons cons
-key value [[] []] cons cons
-key [value [] []] cons
- [key value [] []]
-
-BTree-new == swap [[] []] cons cons
-
-define('BTree-new == swap [[] []] cons cons')
-V('"v" "k" BTree-new')
-(As an implementation detail, the [[] []] literal used in the definition of BTree-new will be reused to supply the constant tail for all new nodes produced by it. This is one of those cases where you get amortized storage "for free" by using persistent datastructures. Because the tail, which is ((), ((), ())) in Python, is immutable and embedded in the definition body for BTree-new, all new nodes can reuse it as their own tail without fear that some other code somewhere will change it.)
We now have to derive R0 and R1, consider:
[key_n value_n left right] value key R0 [BTree-add] R1
-
-
-In this case, there are three possibilites: the key can be greater or less than or equal to the node's key. In two of those cases we will need to apply a copy of BTree-add, so R0 is pretty much out of the picture.
[R0] == []
-
-The first thing we need to do is compare the the key we're adding to see if it is greater than the node key and branch accordingly, although in this case it's easier to write a destructive predicate and then use ifte to apply it nullary:
[key_n value_n left right] value key [BTree-add] R1
-[key_n value_n left right] value key [BTree-add] [P >] [T] [E] ifte
-
-[key_n value_n left right] value key [BTree-add] P >
-[key_n value_n left right] value key [BTree-add] pop roll> pop first >
-[key_n value_n left right] value key roll> pop first >
-key [key_n value_n left right] value roll> pop first >
-key key_n >
-Boolean
-
-P > == pop roll> pop first >
-P < == pop roll> pop first <
-P == pop roll> pop first
-
-define('P == pop roll> pop first')
-V('["k" "v" [] []] "vv" "kk" [0] P >')
-Here the parantheses are meant to signify that the right-hand side (RHS) is not literal, the code in the parentheses is meant to have been evaluated:
- -[key_n value_n left right] value key [BTree-add] T == [key_n value_n left (BTree-add key value right)]
-
-infra on K.¶So how do we do this? We know we're going to want to use infra on some function K that has the key and value to work with, as well as the quoted copy of BTree-add to apply somehow:
right left value_n key_n value key [BTree-add] K
- ...
-right value key BTree-add left value_n key_n
-
-
-Pretty easy:
- -right left value_n key_n value key [BTree-add] cons cons dipdd
-right left value_n key_n [value key BTree-add] dipdd
-right value key BTree-add left value_n key_n
-
-
-So:
- -K == cons cons dipdd
-
-
-And:
- -[key_n value_n left right] [value key [BTree-add] K] infra
-
-
-T.¶So now we're at getting from this to this:
- -[key_n value_n left right] value key [BTree-add] T
- ...
-[key_n value_n left right] [value key [BTree-add] K] infra
-
-
-And so T is just:
value key [BTree-add] T == [value key [BTree-add] K] infra
- T == [ K] cons cons cons infra
-
-define('K == cons cons dipdd')
-define('T == [K] cons cons cons infra')
-V('"r" "l" "v" "k" "vv" "kk" [0] K')
-V('["k" "v" "l" "r"] "vv" "kk" [0] T')
-This is very very similar to the above:
- -[key_n value_n left right] value key [BTree-add] E
-[key_n value_n left right] value key [BTree-add] [P <] [Te] [Ee] ifte
-
-
-In this case Te works that same as T but on the left child tree instead of the right, so the only difference is that it must use dipd instead of dipdd:
Te == [cons cons dipd] cons cons cons infra
-
-
-This suggests an alternate factorization:
- -ccons == cons cons
-T == [ccons dipdd] ccons cons infra
-Te == [ccons dipd] ccons cons infra
-
-
-But whatever.
- -define('Te == [cons cons dipd] cons cons cons infra')
-V('["k" "v" "l" "r"] "vv" "kk" [0] Te')
-This means we must find:
- -[key_n value_n left right] value key [BTree-add] Ee
- ...
-[key value left right]
-
-
-This is another easy one:
- -Ee == pop swap roll< rest rest cons cons
-
-[key_n value_n left right] value key [BTree-add] pop swap roll< rest rest cons cons
-[key_n value_n left right] value key swap roll< rest rest cons cons
-[key_n value_n left right] key value roll< rest rest cons cons
-key value [key_n value_n left right] rest rest cons cons
-key value [ left right] cons cons
- [key value left right]
-
-define('Ee == pop swap roll< rest rest cons cons')
-V('["k" "v" "l" "r"] "vv" "k" [0] Ee')
-define('E == [P <] [Te] [Ee] ifte')
-BTree-add¶BTree-add == [popop not] [[pop] dipd BTree-new] [] [[P >] [T] [E] ifte] genrec
-
-
-Putting it all together:
- -BTree-new == swap [[] []] cons cons
-P == pop roll> pop first
-T == [cons cons dipdd] cons cons cons infra
-Te == [cons cons dipd] cons cons cons infra
-Ee == pop swap roll< rest rest cons cons
-E == [P <] [Te] [Ee] ifte
-
-BTree-add == [popop not] [[pop] dipd BTree-new] [] [[P >] [T] [E] ifte] genrec
-
-define('BTree-add == [popop not] [[pop] dipd BTree-new] [] [[P >] [T] [E] ifte] genrec')
-J('[] 23 "b" BTree-add') # Initial
-J('["b" 23 [] []] 88 "c" BTree-add') # Less than
-J('["b" 23 [] []] 88 "a" BTree-add') # Greater than
-J('["b" 23 [] []] 88 "b" BTree-add') # Equal to
-J('[] 23 "a" BTree-add 88 "b" BTree-add 44 "c" BTree-add') # Series.
-We can use this to make a set-like datastructure by just setting values to e.g. 0 and ignoring them. It's set-like in that duplicate items added to it will only occur once within it, and we can query it in $O(\log_2 N)$ time.
- -J('[] [3 9 5 2 8 6 7 8 4] [0 swap BTree-add] step')
-define('to_set == [] swap [0 swap BTree-add] step')
-J('[3 9 5 2 8 6 7 8 4] to_set')
-And with that we can write a little program to remove duplicate items from a list.
- -define('unique == [to_set [first] BTree-iter] cons run')
-J('[3 9 3 5 2 9 8 8 8 6 2 7 8 4 3] unique') # Filter duplicate items.
-cmp combinator¶Instead of all this mucking about with nested ifte let's just go whole hog and define cmp which takes two values and three quoted programs on the stack and runs one of the three depending on the results of comparing the two values:
a b [G] [E] [L] cmp
-------------------------- a > b
- G
-
- a b [G] [E] [L] cmp
-------------------------- a = b
- E
-
- a b [G] [E] [L] cmp
-------------------------- a < b
- L
-
-
-We need a new non-destructive predicate P:
[key_n value_n left right] value key [BTree-add] P
-[key_n value_n left right] value key [BTree-add] over [Q] nullary
-[key_n value_n left right] value key [BTree-add] key [Q] nullary
-[key_n value_n left right] value key [BTree-add] key Q
-[key_n value_n left right] value key [BTree-add] key popop popop first
-[key_n value_n left right] value key popop first
-[key_n value_n left right] first
- key_n
-[key_n value_n left right] value key [BTree-add] key [Q] nullary
-[key_n value_n left right] value key [BTree-add] key key_n
-
-P == over [popop popop first] nullary
-
-
-Here are the definitions again, pruned and renamed in some cases:
- -BTree-new == swap [[] []] cons cons
-P == over [popop popop first] nullary
-T> == [cons cons dipdd] cons cons cons infra
-T< == [cons cons dipd] cons cons cons infra
-E == pop swap roll< rest rest cons cons
-
-
-Using cmp to simplify our code above at R1:
[key_n value_n left right] value key [BTree-add] R1
-[key_n value_n left right] value key [BTree-add] P [T>] [E] [T<] cmp
-
-
-The line above becomes one of the three lines below:
- -[key_n value_n left right] value key [BTree-add] T>
-[key_n value_n left right] value key [BTree-add] E
-[key_n value_n left right] value key [BTree-add] T<
-
-
-The definition is a little longer but, I think, more elegant and easier to understand:
- -BTree-add == [popop not] [[pop] dipd BTree-new] [] [P [T>] [E] [T<] cmp] genrec
-
-from joy.library import FunctionWrapper
-from joy.utils.stack import concat
-from notebook_preamble import D
-
-
-@FunctionWrapper
-def cmp_(stack, expression, dictionary):
- '''
- cmp takes two values and three quoted programs on the stack and runs
- one of the three depending on the results of comparing the two values:
-
- a b [G] [E] [L] cmp
- ------------------------- a > b
- G
-
- a b [G] [E] [L] cmp
- ------------------------- a = b
- E
-
- a b [G] [E] [L] cmp
- ------------------------- a < b
- L
- '''
- L, (E, (G, (b, (a, stack)))) = stack
- expression = concat(G if a > b else L if a < b else E, expression)
- return stack, expression, dictionary
-
-
-D['cmp'] = cmp_
-from joy.library import FunctionWrapper, S_ifte
-
-
-@FunctionWrapper
-def cond(stack, expression, dictionary):
- '''
- like a case statement; works by rewriting into a chain of ifte.
-
- [..[[Bi] Ti]..[D]] -> ...
-
-
- [[[B0] T0] [[B1] T1] [D]] cond
- -----------------------------------------
- [B0] [T0] [[B1] [T1] [D] ifte] ifte
-
- '''
- conditions, stack = stack
- if conditions:
- expression = _cond(conditions, expression)
- try:
- # Attempt to preload the args to first ifte.
- (P, (T, (E, expression))) = expression
- except ValueError:
- # If, for any reason, the argument to cond should happen to contain
- # only the default clause then this optimization will fail.
- pass
- else:
- stack = (E, (T, (P, stack)))
- return stack, expression, dictionary
-
-
-def _cond(conditions, expression):
- (clause, rest) = conditions
- if not rest: # clause is [D]
- return clause
- P, T = clause
- return (P, (T, (_cond(rest, ()), (S_ifte, expression))))
-
-
-
-D['cond'] = cond
-J("1 0 ['G'] ['E'] ['L'] cmp")
-J("1 1 ['G'] ['E'] ['L'] cmp")
-J("0 1 ['G'] ['E'] ['L'] cmp")
-from joy.library import DefinitionWrapper
-
-
-DefinitionWrapper.add_definitions('''
-
-P == over [popop popop first] nullary
-T> == [cons cons dipdd] cons cons cons infra
-T< == [cons cons dipd] cons cons cons infra
-E == pop swap roll< rest rest cons cons
-
-BTree-add == [popop not] [[pop] dipd BTree-new] [] [P [T>] [E] [T<] cmp] genrec
-
-''', D)
-J('[] 23 "b" BTree-add') # Initial
-J('["b" 23 [] []] 88 "c" BTree-add') # Less than
-J('["b" 23 [] []] 88 "a" BTree-add') # Greater than
-J('["b" 23 [] []] 88 "b" BTree-add') # Equal to
-J('[] 23 "a" BTree-add 88 "b" BTree-add 44 "c" BTree-add') # Series.
-It may seem silly, but a big part of programming in Forth (and therefore in Joy) is the idea of small, highly-factored definitions. If you choose names carefully the resulting definitions can take on a semantic role.
- -get-node-key == popop popop first
-remove-key-and-value-from-node == rest rest
-pack-key-and-value == cons cons
-prep-new-key-and-value == pop swap roll<
-pack-and-apply == [pack-key-and-value] swoncat cons pack-key-and-value infra
-
-BTree-new == swap [[] []] pack-key-and-value
-P == over [get-node-key] nullary
-T> == [dipdd] pack-and-apply
-T< == [dipd] pack-and-apply
-E == prep-new-key-and-value remove-key-and-value-from-node pack-key-and-value
-
-BTree-iter that does In-Order Traversal¶If you look back to the non-empty case of the BTree-iter function we can design a varient that first processes the left child, then the current node, then the right child. This will allow us to traverse the tree in sort order.
BTree-iter-order == [not] [pop] [R0 [BTree-iter] R1] ifte
-
-
-To define R0 and R1 it helps to look at them as they will appear when they run:
[key value left right] R0 [BTree-iter-order] R1
-
-Staring at this for a bit suggests dup third to start:
[key value left right] R0 [BTree-iter-order] R1
-[key value left right] dup third [BTree-iter-order] R1
-[key value left right] left [BTree-iter-order] R1
-
-
-Now maybe:
- -[key value left right] left [BTree-iter-order] [cons dip] dupdip
-[key value left right] left [BTree-iter-order] cons dip [BTree-iter-order]
-[key value left right] [left BTree-iter-order] dip [BTree-iter-order]
-left BTree-iter-order [key value left right] [BTree-iter-order]
-
-So far, so good. Now we need to process the current node's values:
- -left BTree-iter-order [key value left right] [BTree-iter-order] [[F] dupdip] dip
-left BTree-iter-order [key value left right] [F] dupdip [BTree-iter-order]
-left BTree-iter-order [key value left right] F [key value left right] [BTree-iter-order]
-
-
-If F needs items from the stack below the left stuff it should have cons'd them before beginning maybe? For functions like first it works fine as-is.
left BTree-iter-order [key value left right] first [key value left right] [BTree-iter-order]
-left BTree-iter-order key [key value left right] [BTree-iter-order]
-
-First ditch the rest of the node and get the right child:
- -left BTree-iter-order key [key value left right] [BTree-iter-order] [rest rest rest first] dip
-left BTree-iter-order key right [BTree-iter-order]
-
-
-Then, of course, we just need i to run BTree-iter-order on the right side:
left BTree-iter-order key right [BTree-iter-order] i
-left BTree-iter-order key right BTree-iter-order
-
-BTree-iter-order¶The result is a little awkward:
- -R1 == [cons dip] dupdip [[F] dupdip] dip [rest rest rest first] dip i
-
-
-Let's do a little semantic factoring:
- -fourth == rest rest rest first
-
-proc_left == [cons dip] dupdip
-proc_current == [[F] dupdip] dip
-proc_right == [fourth] dip i
-
-BTree-iter-order == [not] [pop] [dup third] [proc_left proc_current proc_right] genrec
-
-
-Now we can sort sequences.
- -define('BTree-iter-order == [not] [pop] [dup third] [[cons dip] dupdip [[first] dupdip] dip [rest rest rest first] dip i] genrec')
-J('[3 9 5 2 8 6 7 8 4] to_set BTree-iter-order')
-Let's derive a function that accepts a tree and a key and returns the value associated with that key.
- - tree key BTree-get
-------------------------
- value
-
-[]¶As before, the stopping predicate just has to detect the empty list:
- -BTree-get == [pop not] [E] [R0] [R1] genrec
-
-
-But what do we do if the key isn't in the tree? In Python we might raise a KeyError but I'd like to avoid exceptions in Joy if possible, and here I think it's possible. (Division by zero is an example of where I think it's probably better to let Python crash Joy. Sometimes the machinery fails and you have to "stop the line", methinks.)
Let's pass the buck to the caller by making the base case a given, you have to decide for yourself what [E] should be.
tree key [E] BTree-get
----------------------------- key in tree
- value
-
- tree key [E] BTree-get
----------------------------- key not in tree
- tree key E
-
-
-Now we define:
- -BTree-get == [pop not] swap [R0] [R1] genrec
-
-
-Note that this BTree-get creates a slightly different function than itself and that function does the actual recursion. This kind of higher-level programming is unusual in most languages but natural in Joy.
tree key [E] [pop not] swap [R0] [R1] genrec
-tree key [pop not] [E] [R0] [R1] genrec
-
-
-The anonymous specialized recursive function that will do the real work.
- -[pop not] [E] [R0] [R1] genrec
-
-[key value left right]¶Now we need to figure out R0 and R1:
[key value left right] key R0 [BTree-get] R1
-
-
-We want to compare the search key with the key in the node, and if they are the same return the value and if they differ then recurse on one of the child nodes. So it's very similar to the above funtion, with [R0] == [] and R1 == P [T>] [E] [T<] cmp:
[key value left right] key [BTree-get] P [T>] [E] [T<] cmp
-
-
-So:
- -get-node-key == pop popop first
-P == over [get-node-key] nullary
-
-
-The only difference is that get-node-key does one less pop because there's no value to discard. Now we have to derive the branches:
[key_n value_n left right] key [BTree-get] T>
-[key_n value_n left right] key [BTree-get] E
-[key_n value_n left right] key [BTree-get] T<
-
-
-The cases of T> and T< are similar to above but instead of using infra we have to discard the rest of the structure:
[key_n value_n left right] key [BTree-get] T> == right key BTree-get
-[key_n value_n left right] key [BTree-get] T< == left key BTree-get
-
-
-So:
- -T> == [fourth] dipd i
-T< == [third] dipd i
-
-
-E.g.:
- -[key_n value_n left right] key [BTree-get] [fourth] dipd i
-[key_n value_n left right] fourth key [BTree-get] i
- right key [BTree-get] i
- right key BTree-get
-
-
-And:
- -[key_n value_n left right] key [BTree-get] E == value_n
-
-E == popop second
-
-
-So:
- -fourth == rest rest rest first
-get-node-key == pop popop first
-P == over [get-node-key] nullary
-T> == [fourth] dipd i
-T< == [third] dipd i
-E == popop second
-
-BTree-get == [pop not] swap [] [P [T>] [E] [T<] cmp] genrec
-
-# I don't want to deal with name conflicts with the above so I'm inlining everything here.
-# The original Joy system has "hide" which is a meta-command which allows you to use named
-# definitions that are only in scope for a given definition. I don't want to implement
-# that (yet) so...
-
-
-define('''
-BTree-get == [pop not] swap [] [
- over [pop popop first] nullary
- [[rest rest rest first] dipd i]
- [popop second]
- [[third] dipd i]
- cmp
- ] genrec
-''')
-J('[] "gary" [popop "err"] BTree-get')
-J('["gary" 23 [] []] "gary" [popop "err"] BTree-get')
-J('''
-
- [] [[0 'a'] [1 'b'] [2 'c']] [i BTree-add] step
-
- 'c' [popop 'not found'] BTree-get
-
-''')
-Now let's write a function that can return a tree datastructure with a key, value pair deleted:
- - tree key BTree-delete
----------------------------
- tree
-
-
-
-If the key is not in tree it just returns the tree unchanged.
- -So:
- -BTree-Delete == [pop not] swap [R0] [R1] genrec
-
- [Er] BTree-delete
--------------------------------------
- [pop not] [Er] [R0] [R1] genrec
-
-[n_key n_value left right] [BTree-get]
-[n_key n_value left right] [BTree-get] E
-[n_key n_value left right] [BTree-get] T<
-
-Now we get to figure out the recursive case:
- -w/ D == [pop not] [Er] [R0] [R1] genrec
-
-[node_key node_value left right] key R0 [D] R1
-[node_key node_value left right] key over first swap dup [D] R1
-[node_key node_value left right] node_key key key [D] R1
-
-
-And then:
- -[node_key node_value left right] node_key key key [D] R1
-[node_key node_value left right] node_key key key [D] cons roll> [T>] [E] [T<] cmp
-[node_key node_value left right] node_key key [key D] roll> [T>] [E] [T<] cmp
-[node_key node_value left right] [key D] node_key key [T>] [E] [T<] cmp
-
-Now this:;
- -[node_key node_value left right] [key D] node_key key [T>] [E] [T<] cmp
-
-
-Becomes one of these three:;
- -[node_key node_value left right] [key D] T>
-[node_key node_value left right] [key D] E
-[node_key node_value left right] [key D] T<
-
- [node_key node_value left right] [key D] T>
--------------------------------------------------
- [node_key node_value left key D right]
-
-First:
- -right left node_value node_key [key D] dipd
-right left key D node_value node_key
-right left' node_value node_key
-
-Ergo:
- -[node_key node_value left right] [key D] [dipd] cons infra
-
-So:
- -T> == [dipd] cons infra
-T< == [dipdd] cons infra
-
-[node_key node_value left right] [key D] E
-
-
-We have to handle three cases, so let's use cond.
The first two cases are symmetrical, if we only have one non-empty child node return it.
- -E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [default]
-] cond
-
-(If both child nodes are empty return an empty node.)
- -The initial structure of the default function:
- -default == [E'] cons infra
-
-[node_key node_value left right] [key D] default
-[node_key node_value left right] [key D] [E'] cons infra
-[node_key node_value left right] [[key D] E'] infra
-
-right left node_value node_key [key D] E'
-
-If both child nodes are non-empty, we find the highest node in our lower sub-tree, take its key and value to replace (delete) our own, then get rid of it by recursively calling delete() on our lower sub-node with our new key.
-(We could also find the lowest node in our higher sub-tree and take its key and value and delete it. I only implemented one of these two symmetrical options. Over a lot of deletions this might make the tree more unbalanced. Oh well.)
- -First things first, we no longer need this node's key and value:
- -right left node_value node_key [key D] roll> popop E''
-right left [key D] node_value node_key popop E''
-right left [key D] E''
-
-Then we have to we find the highest (right-most) node in our lower (left) sub-tree:
- -right left [key D] E''
-
-Ditch the key:
- -right left [key D] rest E'''
-right left [D] E'''
-
-Find the right-most node:
- -right left [D] [dup W] dip E''''
-right left dup W [D] E''''
-right left left W [D] E''''
-
-Consider:
- -left W
-
-We know left is not empty:
- -[L_key L_value L_left L_right] W
-
-We want to keep extracting the right node as long as it is not empty:
- -left [P] [B] while W'
-
-The predicate:
- -[L_key L_value L_left L_right] P
-[L_key L_value L_left L_right] fourth
- L_right
-
-
-(This has a bug, can run on [] so must be guarded:
if_not_empty == [] swap [] ifte
-?fourth == [fourth] if_not_empty
-W.rightmost == [?fourth] [fourth] while
-
-The body is also fourth:
left [fourth] [fourth] while W'
-rightest W'
-
-We know rightest is not empty:
- -[R_key R_value R_left R_right] W'
-[R_key R_value R_left R_right] uncons uncons pop
-R_key [R_value R_left R_right] uncons pop
-R_key R_value [R_left R_right] pop
-R_key R_value
-
-So:
- -W == [fourth] [fourth] while uncons uncons pop
-
-And:
- -right left left W [D] E''''
-right left R_key R_value [D] E''''
-
-Final stretch. We want to end up with something like:
- -right left [R_key D] i R_value R_key
-right left R_key D R_value R_key
-right left' R_value R_key
-
-If we adjust our definition of W to include over at the end:
W == [fourth] [fourth] while uncons uncons pop over
-
-That will give us:
- -right left R_key R_value R_key [D] E''''
-
-right left R_key R_value R_key [D] cons dipdd E'''''
-right left R_key R_value [R_key D] dipdd E'''''
-right left R_key D R_key R_value E'''''
-right left' R_key R_value E'''''
-right left' R_key R_value swap
-right left' R_value R_key
-
-So:
- -E' == roll> popop E''
-
-E'' == rest E'''
-
-E''' == [dup W] dip E''''
-
-E'''' == cons dipdd swap
-
-Substituting:
- -W == [fourth] [fourth] while uncons uncons pop over
-E' == roll> popop rest [dup W] dip cons dipdd swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E'] cons infra]
-] cond
-
-Minor rearrangement:
- -W == dup [fourth] [fourth] while uncons uncons pop over
-E' == roll> popop rest [W] dip cons dipdd swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E'] cons infra]
-] cond
-
-W.rightmost == [fourth] [fourth] while
-W.unpack == uncons uncons pop
-E.clear_stuff == roll> popop rest
-E.delete == cons dipdd
-W == dup W.rightmost W.unpack over
-E.0 == E.clear_stuff [W] dip E.delete swap
-E == [
- [[pop third not] pop fourth]
- [[pop fourth not] pop third]
- [[E.0] cons infra]
-] cond
-T> == [dipd] cons infra
-T< == [dipdd] cons infra
-R0 == over first swap dup
-R1 == cons roll> [T>] [E] [T<] cmp
-BTree-Delete == [pop not] swap [R0] [R1] genrec
-
-
-By the standards of the code I've written so far, this is a huge Joy program.
- -DefinitionWrapper.add_definitions('''
-first_two == uncons uncons pop
-fourth == rest rest rest first
-?fourth == [] [fourth] [] ifte
-W.rightmost == [?fourth] [fourth] while
-E.clear_stuff == roll> popop rest
-E.delete == cons dipdd
-W == dup W.rightmost first_two over
-E.0 == E.clear_stuff [W] dip E.delete swap
-E == [[[pop third not] pop fourth] [[pop fourth not] pop third] [[E.0] cons infra]] cond
-T> == [dipd] cons infra
-T< == [dipdd] cons infra
-R0 == over first swap dup
-R1 == cons roll> [T>] [E] [T<] cmp
-BTree-Delete == [pop not] swap [R0] [R1] genrec''', D)
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'c' ['Er'] BTree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'b' ['Er'] BTree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'a' ['Er'] BTree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'der' ['Er'] BTree-Delete ")
-J("['a' 23 [] ['b' 88 [] ['c' 44 [] []]]] 'der' [pop] BTree-Delete ")
-One bug, I forgot to put not in the first two clauses of the cond.
The behavior of the [Er] function should maybe be different: either just silently fail, or maybe implement some sort of function that can grab the pending expression up to a sentinel value or something, allowing for a kind of "except"-ish control-flow?
Then, once we have add, get, and delete we can see about abstracting them.
- -Let's consider a tree structure, similar to one described "Why functional programming matters" by John Hughes, that consists of a node value and a sequence of zero or more child trees. (The asterisk is meant to indicate the Kleene star.)
- -tree = [] | [node [tree*]]
-
-treestep¶In the spirit of step we are going to define a combinator treestep which expects a tree and three additional items: a base-case value z, and two quoted programs [C] and [N].
tree z [C] [N] treestep
-
-
-If the current tree node is empty then just leave z on the stack in lieu:
[] z [C] [N] treestep
----------------------------
- z
-
-
-Otherwise, evaluate N on the node value, map the whole function (abbreviated here as k) over the child trees recursively, and then combine the result with C.
[node [tree*]] z [C] [N] treestep
---------------------------------------- w/ K == z [C] [N] treestep
- node N [tree*] [K] map C
-
-Since this is a recursive function, we can begin to derive it by finding the ifte stage that genrec will produce. The predicate and base-case functions are trivial, so we just have to derive J.
K == [not] [pop z] [J] ifte
-
-
-The behavior of J is to accept a (non-empty) tree node and arrive at the desired outcome.
[node [tree*]] J
-------------------------------
- node N [tree*] [K] map C
-
-
-So J will have some form like:
J == .. [N] .. [K] .. [C] ..
-
-
-Let's dive in. First, unquote the node and dip N.
[node [tree*]] i [N] dip
- node [tree*] [N] dip
-node N [tree*]
-
-
-Next, map K over teh child trees and combine with C.
node N [tree*] [K] map C
-node N [tree*] [K] map C
-node N [K.tree*] C
-
-
-So:
- -J == i [N] dip [K] map C
-
-
-Plug it in and convert to genrec:
K == [not] [pop z] [i [N] dip [K] map C] ifte
-K == [not] [pop z] [i [N] dip] [map C] genrec
-
-[not] [pop z] [i [N] dip] [map C] genrec
-
-[not] [pop z] [i [N] dip] [map C] genrec
-[not] [z] [pop] swoncat [i [N] dip] [map C] genrec
-[not] z unit [pop] swoncat [i [N] dip] [map C] genrec
-z [not] swap unit [pop] swoncat [i [N] dip] [map C] genrec
- \ .........TS0............./
- \/
-z TS0 [i [N] dip] [map C] genrec
-z [i [N] dip] [TS0] dip [map C] genrec
-z [[N] dip] [i] swoncat [TS0] dip [map C] genrec
-z [N] [dip] cons [i] swoncat [TS0] dip [map C] genrec
- \ ......TS1........./
- \/
-z [N] TS1 [TS0] dip [map C] genrec
-z [N] [map C] [TS1 [TS0] dip] dip genrec
-z [N] [C] [map] swoncat [TS1 [TS0] dip] dip genrec
-z [C] [N] swap [map] swoncat [TS1 [TS0] dip] dip genrec
-
-
-The givens are all to the left so we have our definition.
- -treestep¶ TS0 == [not] swap unit [pop] swoncat
- TS1 == [dip] cons [i] swoncat
-treestep == swap [map] swoncat [TS1 [TS0] dip] dip genrec
-
-DefinitionWrapper.add_definitions('''
-
- TS0 == [not] swap unit [pop] swoncat
- TS1 == [dip] cons [i] swoncat
-treestep == swap [map] swoncat [TS1 [TS0] dip] dip genrec
-
-''', D)
- [] 0 [C] [N] treestep
----------------------------
- 0
-
-
- [n [tree*]] 0 [sum +] [] treestep
- --------------------------------------------------
- n [tree*] [0 [sum +] [] treestep] map sum +
-
-J('[] 0 [sum +] [] treestep')
-J('[23 []] 0 [sum +] [] treestep')
-J('[23 [[2 []] [3 []]]] 0 [sum +] [] treestep')
-Let's simplify the tree datastructure definition slightly by just letting the children be the rest of the tree:
tree = [] | [node tree*]
-
-The J function changes slightly.
[node tree*] J
-------------------------------
- node N [tree*] [K] map C
-
-
-[node tree*] uncons [N] dip [K] map C
-node [tree*] [N] dip [K] map C
-node N [tree*] [K] map C
-node N [tree*] [K] map C
-node N [K.tree*] C
-
-J == uncons [N] dip [K] map C
-
-K == [not] [pop z] [uncons [N] dip] [map C] genrec
-
-define('TS1 == [dip] cons [uncons] swoncat') # We only need to redefine one word.
-J('[23 [2] [3]] 0 [sum +] [] treestep')
-J('[23 [2 [8] [9]] [3] [4 []]] 0 [sum +] [] treestep')
-I think these trees seem a little easier to read.
- -BTree = [] | [[key value] left right]
-
-What kind of functions can we write for this with our treestep? The pattern for processing a non-empty node is:
node N [tree*] [K] map C
-
-
-Plugging in our BTree structure:
- -[key value] N [left right] [K] map C
-
-
-[key value] uncons pop [left right] [K] map i
-key [value] pop [left right] [K] map i
-key [left right] [K] map i
-key [lkey rkey ] i
-key lkey rkey
-
-J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] 23 [i] [uncons pop] treestep')
-Doesn't work because map extracts the first item of whatever its mapped function produces. We have to return a list, rather than depositing our results directly on the stack.
[key value] N [left right] [K] map C
-
-[key value] first [left right] [K] map flatten cons
-key [left right] [K] map flatten cons
-key [[lk] [rk] ] flatten cons
-key [ lk rk ] cons
- [key lk rk ]
-
-
-So:
- -[] [flatten cons] [first] treestep
-
-J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [] [flatten cons] [first] treestep')
-There we go.
-treestep.¶From here:
- -key [[lk] [rk]] C
-key [[lk] [rk]] i
-key [lk] [rk] roll<
-[lk] [rk] key swons concat
-[lk] [key rk] concat
-[lk key rk]
-
-
-So:
- -[] [i roll< swons concat] [first] treestep
-
-J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [] [i roll< swons concat] [uncons pop] treestep')
-Let's reexamine:
- -[key value left right] R0 [BTree-iter-order] R1
- ...
-left BTree-iter-order key value F right BTree-iter-order
-
-
-[key value left right] unstack swap
- key value left right swap
- key value right left
-
-key value right left [BTree-iter-order] [cons dipdd] dupdip
-key value right left [BTree-iter-order] cons dipdd [BTree-iter-order]
-key value right [left BTree-iter-order] dipdd [BTree-iter-order]
-left BTree-iter-order key value right [BTree-iter-order]
-
-left BTree-iter-order key value right [F] dip [BTree-iter-order]
-left BTree-iter-order key value F right [BTree-iter-order] i
-left BTree-iter-order key value F right BTree-iter-order
-
-
-So:
- -R0 == unstack swap
-R1 == [cons dipdd [F] dip] dupdip i
-
-[key value left right] R0 [BTree-iter-order] R1
-[key value left right] unstack swap [BTree-iter-order] [cons dipdd [F] dip] dupdip i
- key value right left [BTree-iter-order] [cons dipdd [F] dip] dupdip i
-
- key value right left [BTree-iter-order] cons dipdd [F] dip [BTree-iter-order] i
- key value right [left BTree-iter-order] dipdd [F] dip [BTree-iter-order] i
- left BTree-iter-order key value right [F] dip [BTree-iter-order] i
- left BTree-iter-order key value F right [BTree-iter-order] i
- left BTree-iter-order key value F right BTree-iter-order
-
-
-BTree-iter-order == [not] [pop] [unstack swap] [[cons dipdd [F] dip] dupdip i] genrec
-
-cons cons¶cons2 == cons cons
-
-
-Refactoring:
- -BTree-new == swap [[] []] cons2
-T == [cons2 dipdd] cons2 cons infra
-Te == [cons2 dipd] cons2 cons infra
-Ee == pop swap roll< rest rest cons2
-
-
-It's used a lot because it's tied to the fact that there are two "data items" in each node. This point to a more general factorization that would render a combinator that could work for other geometries of trees.
- -A general form for tree data with N children per node:
- -[[data] [child0] ... [childN-1]]
-
-
-Suggests a general form of recursive iterator, but I have to go walk the dogs at the mo'.
-For a given structure, you would have a structure of operator functions and sort of merge them and run them, possibly in a different order (pre- post- in- y'know). The Cn functions could all be the same and use the step trick if the children nodes are all of the right kind. If they are heterogeneous then we need a way to get the different Cn into the structure in the right order. If I understand correctly, the "Bananas..." paper shows how to do this automatically from a type description. They present, if I have it right, a tiny machine that accepts some sort of algebraic data type description and returns a function that can recusre over it, I think.
[data.. [c0] [c1] ... [cN]] [F C0 C1 ... CN] infil
---------------------------------------------------------
- data F [c0] C0 [c1] C1 ... [cN] CN
-
-[F] a parameter.¶We can generalize to a sort of pure form:
- -BTree-iter == [not] [pop] [[F]] [R1] genrec
- == [not] [pop] [[F] [BTree-iter] R1] ifte
-
-
-Putting [F] to the left as a given:
[F] unit [not] [pop] roll< [R1] genrec
-[[F]] [not] [pop] roll< [R1] genrec
- [not] [pop] [[F]] [R1] genrec
-
-
-Let's us define a parameterized form:
- -BTree-iter == unit [not] [pop] roll< [R1] genrec
-
-
-So in the general case of non-empty nodes:
- -[key value left right] [F] [BTree-iter] R1
-
-
-We just define R1 to do whatever it has to to process the node. For example:
[key value left right] [F] [BTree-iter] R1
- ...
-key value F left BTree-iter right BTree-iter
-left BTree-iter key value F right BTree-iter
-left BTree-iter right BTree-iter key value F
-
-
-Pre-, ??-, post-order traversals.
- -[key value left right] uncons uncons
- key value [left right]
-
-
-For pre- and post-order we can use the step trick:
[left right] [BTree-iter] step
- ...
-left BTree-iter right BTree-iter
-
-
-We worked out one scheme for ?in-order? traversal above, but maybe we can do better?
- -[key value left right] [F] [BTree-iter] [unstack] dipd
-[key value left right] unstack [F] [BTree-iter]
- key value left right [F] [BTree-iter]
-
-key value left right [F] [BTree-iter] R1.1
-
-
-Hmm...
- -key value left right [F] [BTree-iter] tuck
-key value left right [BTree-iter] [F] [BTree-iter]
-
-
-[key value left right] [F] [BTree-iter] [unstack [roll>] dip] dipd
-[key value left right] unstack [roll>] dip [F] [BTree-iter]
- key value left right [roll>] dip [F] [BTree-iter]
- key value left roll> right [F] [BTree-iter]
- left key value right [F] [BTree-iter]
-
-left key value right [F] [BTree-iter] tuck foo
-left key value right [BTree-iter] [F] [BTree-iter] foo
- ...
-left BTree-iter key value F right BTree-iter
-
-
-We could just let [R1] be a parameter too, for maximum flexibility.
If I understand it correctly, the "Bananas..." paper talks about a way to build the processor function automatically from the description of the type. I think if we came up with an elegant way for the Joy code to express that, it would be cool. In Joypy the definitions can be circular because lookup happens at evaluation, not parsing. E.g.:
- -A == ... B ...
-B == ... A ...
-
-
-That's fine. Circular datastructures can't be made though.
- -
-
-
-
-
-
-
-
-
-
-
-
-treestep¶Let's consider a tree structure, similar to one described "Why functional programming matters" by John Hughes, that consists of a node value followed by zero or more child trees. (The asterisk is meant to indicate the Kleene star.)
- -tree = [] | [node tree*]
-
-In the spirit of step we are going to define a combinator treestep which expects a tree and three additional items: a base-case function [B], and two quoted programs [N] and [C].
tree [B] [N] [C] treestep
-
-If the current tree node is empty then just execute B:
[] [B] [N] [C] treestep
----------------------------
- [] B
-
-Otherwise, evaluate N on the node value, map the whole function (abbreviated here as K) over the child trees recursively, and then combine the result with C.
[node tree*] [B] [N] [C] treestep
---------------------------------------- w/ K == [B] [N] [C] treestep
- node N [tree*] [K] map C
-
-
-(Later on we'll experiment with making map part of C so you can use other combinators.)
We can begin to derive it by finding the ifte stage that genrec will produce.
K == [not] [B] [R0] [R1] genrec
- == [not] [B] [R0 [K] R1] ifte
-
-
-So we just have to derive J:
J == R0 [K] R1
-
-The behavior of J is to accept a (non-empty) tree node and arrive at the desired outcome.
[node tree*] J
-------------------------------
- node N [tree*] [K] map C
-
-So J will have some form like:
J == ... [N] ... [K] ... [C] ...
-
-Let's dive in. First, unquote the node and dip N.
[node tree*] uncons [N] dip
-node [tree*] [N] dip
-node N [tree*]
-
-Next, map K over the child trees and combine with C.
node N [tree*] [K] map C
-node N [tree*] [K] map C
-node N [K.tree*] C
-
-So:
- -J == uncons [N] dip [K] map C
-
-Plug it in and convert to genrec:
K == [not] [B] [J ] ifte
- == [not] [B] [uncons [N] dip [K] map C] ifte
- == [not] [B] [uncons [N] dip] [map C] genrec
-
-Working backwards:
- -[not] [B] [uncons [N] dip] [map C] genrec
-[B] [not] swap [uncons [N] dip] [map C] genrec
-[B] [uncons [N] dip] [[not] swap] dip [map C] genrec
- ^^^^^^^^^^^^^^^^
-[B] [[N] dip] [uncons] swoncat [[not] swap] dip [map C] genrec
-[B] [N] [dip] cons [uncons] swoncat [[not] swap] dip [map C] genrec
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Extract a couple of auxiliary definitions:
- -TS.0 == [[not] swap] dip
-TS.1 == [dip] cons [uncons] swoncat
-
-[B] [N] TS.1 TS.0 [map C] genrec
-[B] [N] [map C] [TS.1 TS.0] dip genrec
-[B] [N] [C] [map] swoncat [TS.1 TS.0] dip genrec
-
-
-The givens are all to the left so we have our definition.
- -Working backwards:
- -[not] [B] [uncons [N] dip] [map C] genrec
-[not] [B] [N] [dip] cons [uncons] swoncat [map C] genrec
-[B] [N] [not] roll> [dip] cons [uncons] swoncat [map C] genrec
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-treestep¶from notebook_preamble import D, J, V, define, DefinitionWrapper
-DefinitionWrapper.add_definitions('''
-
- _treestep_0 == [[not] swap] dip
- _treestep_1 == [dip] cons [uncons] swoncat
- treegrind == [_treestep_1 _treestep_0] dip genrec
- treestep == [map] swoncat treegrind
-
-''', D)
-Consider trees, the nodes of which are integers. We can find the sum of all nodes in a tree with this function:
- -sumtree == [pop 0] [] [sum +] treestep
-
-define('sumtree == [pop 0] [] [sum +] treestep')
-Running this function on an empty tree value gives zero:
- - [] [pop 0] [] [sum +] treestep
-------------------------------------
- 0
-
-J('[] sumtree') # Empty tree.
-Running it on a non-empty node:
- -[n tree*] [pop 0] [] [sum +] treestep
-n [tree*] [[pop 0] [] [sum +] treestep] map sum +
-n [ ... ] sum +
-n m +
-n+m
-
-J('[23] sumtree') # No child trees.
-J('[23 []] sumtree') # Child tree, empty.
-J('[23 [2 [4]] [3]] sumtree') # Non-empty child trees.
-J('[23 [2 [8] [9]] [3] [4 []]] sumtree') # Etc...
-J('[23 [2 [8] [9]] [3] [4 []]] [pop 0] [] [cons sum] treestep') # Alternate "spelling".
-J('[23 [2 [8] [9]] [3] [4 []]] [] [pop 23] [cons] treestep') # Replace each node.
-J('[23 [2 [8] [9]] [3] [4 []]] [] [pop 1] [cons] treestep')
-J('[23 [2 [8] [9]] [3] [4 []]] [] [pop 1] [cons] treestep sumtree')
-J('[23 [2 [8] [9]] [3] [4 []]] [pop 0] [pop 1] [sum +] treestep') # Combine replace and sum into one function.
-J('[4 [3 [] [7]]] [pop 0] [pop 1] [sum +] treestep') # Combine replace and sum into one function.
-treestep.¶Tree = [] | [[key value] left right]
-
-What kind of functions can we write for this with our treestep?
The pattern for processing a non-empty node is:
- -node N [tree*] [K] map C
-
-
-Plugging in our BTree structure:
- -[key value] N [left right] [K] map C
-
-[key value] first [left right] [K] map i
-key [value] [left right] [K] map i
-key [left right] [K] map i
-key [lkey rkey ] i
-key lkey rkey
-
-This doesn't quite work:
- -J('[[3 0] [[2 0] [][]] [[9 0] [[5 0] [[4 0] [][]] [[8 0] [[6 0] [] [[7 0] [][]]][]]][]]] ["B"] [first] [i] treestep')
-Doesn't work because map extracts the first item of whatever its mapped function produces. We have to return a list, rather than depositing our results directly on the stack.
[key value] N [left right] [K] map C
-
-[key value] first [left right] [K] map flatten cons
-key [left right] [K] map flatten cons
-key [[lk] [rk] ] flatten cons
-key [ lk rk ] cons
- [key lk rk ]
-
-
-So:
- -[] [first] [flatten cons] treestep
-
-J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [] [first] [flatten cons] treestep')
-There we go.
- -From here:
- -key [[lk] [rk]] C
-key [[lk] [rk]] i
-key [lk] [rk] roll<
-[lk] [rk] key swons concat
-[lk] [key rk] concat
-[lk key rk]
-
-
-So:
- -[] [i roll< swons concat] [first] treestep
-
-J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [] [uncons pop] [i roll< swons concat] treestep')
-treegrind?¶The treegrind function doesn't include the map combinator, so the [C] function must arrange to use some combinator on the quoted recursive copy [K]. With this function, the pattern for processing a non-empty node is:
node N [tree*] [K] C
-
-
-Plugging in our BTree structure:
- -[key value] N [left right] [K] C
-
-J('[["key" "value"] ["left"] ["right"] ] ["B"] ["N"] ["C"] treegrind')
-treegrind with step¶Iteration through the nodes
- -J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [pop] ["N"] [step] treegrind')
-Sum the nodes' keys.
- -J('0 [[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [pop] [first +] [step] treegrind')
-Rebuild the tree using map (imitating treestep.)
J('[[3 0] [[2 0] [] []] [[9 0] [[5 0] [[4 0] [] []] [[8 0] [[6 0] [] [[7 0] [] []]] []]] []]] [] [[100 +] infra] [map cons] treegrind')
-We'll start by saying that the base-case (the key is not in the tree) is user defined, and the per-node function is just the query key literal:
- -[B] [query_key] [C] treegrind
-
-This means we just have to define C from:
[key value] query_key [left right] [K] C
-
-Let's try cmp:
C == P [T>] [E] [T<] cmp
-
-[key value] query_key [left right] [K] P [T>] [E] [T<] cmp
-
-P¶Seems pretty easy (we must preserve the value in case the keys are equal):
- -[key value] query_key [left right] [K] P
-[key value] query_key [left right] [K] roll<
-[key value] [left right] [K] query_key [roll< uncons swap] dip
-
-[key value] [left right] [K] roll< uncons swap query_key
-[left right] [K] [key value] uncons swap query_key
-[left right] [K] key [value] swap query_key
-[left right] [K] [value] key query_key
-
-P == roll< [roll< uncons swap] dip
-
-
-(Possibly with a swap at the end? Or just swap T< and T>.)
So now:
- -[left right] [K] [value] key query_key [T>] [E] [T<] cmp
-
-
-Becomes one of these three:
- -[left right] [K] [value] T>
-[left right] [K] [value] E
-[left right] [K] [value] T<
-
-T< and T>¶T< == pop [first] dip i
-T> == pop [second] dip i
-
-T> == pop [first] dip i
-T< == pop [second] dip i
-E == roll> popop first
-P == roll< [roll< uncons swap] dip
-
-Tree-get == [P [T>] [E] [T<] cmp] treegrind
-
-
-To me, that seems simpler than the genrec version.
DefinitionWrapper.add_definitions('''
-
- T> == pop [first] dip i
- T< == pop [second] dip i
- E == roll> popop first
- P == roll< [roll< uncons swap] dip
-
- Tree-get == [P [T>] [E] [T<] cmp] treegrind
-
-''', D)
-J('''\
-
-[[3 13] [[2 12] [] []] [[9 19] [[5 15] [[4 14] [] []] [[8 18] [[6 16] [] [[7 17] [] []]] []]] []]]
-
-[] [5] Tree-get
-
-''')
-J('''\
-
-[[3 13] [[2 12] [] []] [[9 19] [[5 15] [[4 14] [] []] [[8 18] [[6 16] [] [[7 17] [] []]] []]] []]]
-
-[pop "nope"] [25] Tree-get
-
-''')
-import logging, sys
-
-logging.basicConfig(
- format='%(message)s',
- stream=sys.stdout,
- level=logging.INFO,
- )
-from joy.utils.types import (
- doc_from_stack_effect,
- infer,
- reify,
- unify,
- FUNCTIONS,
- JoyTypeError,
-)
-D = FUNCTIONS.copy()
-del D['product']
-globals().update(D)
-fi, fo = infer(pop, swap, rolldown, rrest, ccons)[0]
-print doc_from_stack_effect(fi, fo)
-from joy.parser import text_to_expression
-from joy.utils.stack import stack_to_string
-e = text_to_expression('0 1 2 [3 4]') # reverse order
-print stack_to_string(e)
-u = unify(e, fi)[0]
-u
-g = reify(u, (fi, fo))
-print doc_from_stack_effect(*g)
-e = text_to_expression('[2 3]')
-u = unify(e, fo)[0] # output side, not input side
-u
-g = reify(u, (fi, fo))
-print doc_from_stack_effect(*g)
-fi, fo = infer(dup, mul)[0]
-e = text_to_expression('"two"')
-print stack_to_string(e)
-try:
- unify(e, fi)
-except JoyTypeError, err:
- print err
-This notebook presents a simple type inferencer for Joy code. It can infer the stack effect of most Joy expressions. It's built largely by means of existing ideas and research. (A great overview of the existing knowledge is a talk "Type Inference in Stack-Based Programming Languages" given by Rob Kleffner on or about 2017-03-10 as part of a course on the history of programming languages.)
-The notebook starts with a simple inferencer based on the work of Jaanus Pöial which we then progressively elaborate to cover more Joy semantics. Along the way we write a simple "compiler" that emits Python code for what I like to call Yin functions. (Yin functions are those that only rearrange values in stacks, as opposed to Yang functions that actually work on the values themselves.)
- -"Typing Tools for Typeless Stack Languages" by Jaanus Pöial
- -@INPROCEEDINGS{Pöial06typingtools,
- author = {Jaanus Pöial},
- title = {Typing tools for typeless stack languages},
- booktitle = {In 23rd Euro-Forth Conference},
- year = {2006},
- pages = {40--46}
-}
-
-This rule deals with functions (and literals) that put items on the stack (-- d):
(a -- b)∘(-- d)
----------------------
- (a -- b d)
-
-This rule deals with functions that consume items from the stack (a --):
(a --)∘(c -- d)
----------------------
- (c a -- d)
-
-The third rule is actually two rules. These two rules deal with composing functions when the second one will consume one of items the first one produces. The two types must be unified or a type conflict declared.
- - (a -- b t[i])∘(c u[j] -- d) t <= u (t is subtype of u)
--------------------------------
- (a -- b )∘(c -- d) t[i] == t[k] == u[j]
- ^
-
- (a -- b t[i])∘(c u[j] -- d) u <= t (u is subtype of t)
--------------------------------
- (a -- b )∘(c -- d) t[i] == u[k] == u[j]
-
-Let's work through some examples by hand to develop an intuition for the algorithm.
- -There's a function in one of the other notebooks.
- -F == pop swap roll< rest rest cons cons
-
-
-It's all "stack chatter" and list manipulation so we should be able to deduce its type.
- -Joy function types will be represented by Forth-style stack effect comments. I'm going to use numbers instead of names to keep track of the stack arguments. (A little bit like De Bruijn index, at least it reminds me of them):
- -pop (1 --)
-
-swap (1 2 -- 2 1)
-
-roll< (1 2 3 -- 2 3 1)
-
-
-These commands alter the stack but don't "look at" the values so these numbers represent an "Any type".
- -pop swap¶(1 --) (1 2 -- 2 1)
-
-
-Here we encounter a complication. The argument numbers need to be made unique among both sides. For this let's change pop to use 0:
(0 --) (1 2 -- 2 1)
-
-
-Following the second rule:
- -(1 2 0 -- 2 1)
-
-pop∘swap roll<¶(1 2 0 -- 2 1) (1 2 3 -- 2 3 1)
-
-
-Let's re-label them:
- -(1a 2a 0a -- 2a 1a) (1b 2b 3b -- 2b 3b 1b)
-
-Now we follow the rules.
-We must unify 1a and 3b, and 2a and 2b, replacing the terms in the forms:
(1a 2a 0a -- 2a 1a) (1b 2b 3b -- 2b 3b 1b)
- w/ {1a: 3b}
-(3b 2a 0a -- 2a ) (1b 2b -- 2b 3b 1b)
- w/ {2a: 2b}
-(3b 2b 0a -- ) (1b -- 2b 3b 1b)
-
-
-Here we must apply the second rule:
- - (3b 2b 0a --) (1b -- 2b 3b 1b)
------------------------------------
- (1b 3b 2b 0a -- 2b 3b 1b)
-
-
-Now we de-label the type, uh, labels:
- -(1b 3b 2b 0a -- 2b 3b 1b)
-
-w/ {
- 1b: 1,
- 3b: 2,
- 2b: 3,
- 0a: 0,
- }
-
-(1 2 3 0 -- 3 2 1)
-
-
-And now we have the stack effect comment for pop∘swap∘roll<.
pop∘swap∘roll<¶The simplest way to "compile" this function would be something like:
- -def poswrd(s, e, d):
- return rolldown(*swap(*pop(s, e, d)))
-However, internally this function would still be allocating tuples (stack cells) and doing other unnecesssary work.
-Looking ahead for a moment, from the stack effect comment:
- -(1 2 3 0 -- 3 2 1)
-
-
-We should be able to directly write out a Python function like:
- -def poswrd(stack):
- (_, (a, (b, (c, stack)))) = stack
- return (c, (b, (a, stack)))
-This eliminates the internal work of the first version. Because this function only rearranges the stack and doesn't do any actual processing on the stack items themselves all the information needed to implement it is in the stack effect comment.
- -These are slightly tricky.
- -rest ( [1 ...] -- [...] )
-
-cons ( 1 [...] -- [1 ...] )
-
-pop∘swap∘roll< rest¶(1 2 3 0 -- 3 2 1) ([1 ...] -- [...])
-
-
-Re-label (instead of adding left and right tags I'm just taking the next available index number for the right-side stack effect comment):
- -(1 2 3 0 -- 3 2 1) ([4 ...] -- [...])
-
-
-Unify and update:
- -(1 2 3 0 -- 3 2 1) ([4 ...] -- [...])
- w/ {1: [4 ...]}
-([4 ...] 2 3 0 -- 3 2 ) ( -- [...])
-
-
-Apply the first rule:
- - ([4 ...] 2 3 0 -- 3 2) (-- [...])
----------------------------------------
- ([4 ...] 2 3 0 -- 3 2 [...])
-
-
-And there we are.
- -pop∘swap∘roll<∘rest rest¶Let's do it again.
- -([4 ...] 2 3 0 -- 3 2 [...]) ([1 ...] -- [...])
-
-
-Re-label (the tails of the lists on each side each get their own label):
- -([4 .0.] 2 3 0 -- 3 2 [.0.]) ([5 .1.] -- [.1.])
-
-
-Unify and update (note the opening square brackets have been omited in the substitution dict, this is deliberate and I'll explain below):
- -([4 .0.] 2 3 0 -- 3 2 [.0.] ) ([5 .1.] -- [.1.])
- w/ { .0.] : 5 .1.] }
-([4 5 .1.] 2 3 0 -- 3 2 [5 .1.]) ([5 .1.] -- [.1.])
-
-How do we find .0.] in [4 .0.] and replace it with 5 .1.] getting the result [4 5 .1.]? This might seem hard, but because the underlying structure of the Joy list is a cons-list in Python it's actually pretty easy. I'll explain below.
Next we unify and find our two terms are the same already: [5 .1.]:
([4 5 .1.] 2 3 0 -- 3 2 [5 .1.]) ([5 .1.] -- [.1.])
-
-
-Giving us:
- -([4 5 .1.] 2 3 0 -- 3 2) (-- [.1.])
-
-
-From here we apply the first rule and get:
- -([4 5 .1.] 2 3 0 -- 3 2 [.1.])
-
-
-Cleaning up the labels:
- -([4 5 ...] 2 3 1 -- 3 2 [...])
-
-
-This is the stack effect of pop∘swap∘roll<∘rest∘rest.
pop∘swap∘roll<∘rest∘rest cons¶([4 5 ...] 2 3 1 -- 3 2 [...]) (1 [...] -- [1 ...])
-
-
-Re-label:
- -([4 5 .1.] 2 3 1 -- 3 2 [.1.]) (6 [.2.] -- [6 .2.])
-
-
-Unify:
- -([4 5 .1.] 2 3 1 -- 3 2 [.1.]) (6 [.2.] -- [6 .2.])
- w/ { .1.] : .2.] }
-([4 5 .2.] 2 3 1 -- 3 2 ) (6 -- [6 .2.])
- w/ {2: 6}
-([4 5 .2.] 6 3 1 -- 3 ) ( -- [6 .2.])
-
-
-First rule:
- -([4 5 .2.] 6 3 1 -- 3 [6 .2.])
-
-
-Re-label:
- -([4 5 ...] 2 3 1 -- 3 [2 ...])
-
-
-Done.
- -pop∘swap∘roll<∘rest∘rest∘cons cons¶One more time.
- -([4 5 ...] 2 3 1 -- 3 [2 ...]) (1 [...] -- [1 ...])
-
-
-Re-label:
- -([4 5 .1.] 2 3 1 -- 3 [2 .1.]) (6 [.2.] -- [6 .2.])
-
-
-Unify:
- -([4 5 .1.] 2 3 1 -- 3 [2 .1.]) (6 [.2.] -- [6 .2.] )
- w/ { .2.] : 2 .1.] }
-([4 5 .1.] 2 3 1 -- 3 ) (6 -- [6 2 .1.])
- w/ {3: 6}
-([4 5 .1.] 2 6 1 -- ) ( -- [6 2 .1.])
-
-
-First or second rule:
- -([4 5 .1.] 2 6 1 -- [6 2 .1.])
-
-
-Clean up the labels:
- -([4 5 ...] 2 3 1 -- [3 2 ...])
-
-
-And there you have it, the stack effect for pop∘swap∘roll<∘rest∘rest∘cons∘cons.
([4 5 ...] 2 3 1 -- [3 2 ...])
-
-From this stack effect comment it should be possible to construct the following Python code:
- -def F(stack):
- (_, (d, (c, ((a, (b, S0)), stack)))) = stack
- return (d, (c, S0)), stack
-I'm going to use pairs of tuples of type descriptors, which will be integers or tuples of type descriptors:
- -roll_dn = (1, 2, 3), (2, 3, 1)
-
-pop = (1,), ()
-
-swap = (1, 2), (2, 1)
-compose()¶def compose(f, g):
-
- (f_in, f_out), (g_in, g_out) = f, g
-
- # First rule.
- #
- # (a -- b) (-- d)
- # ---------------------
- # (a -- b d)
-
- if not g_in:
-
- fg_in, fg_out = f_in, f_out + g_out
-
- # Second rule.
- #
- # (a --) (c -- d)
- # ---------------------
- # (c a -- d)
-
- elif not f_out:
-
- fg_in, fg_out = g_in + f_in, g_out
-
- else: # Unify, update, recur.
-
- fo, gi = f_out[-1], g_in[-1]
-
- s = unify(gi, fo)
-
- if s == False: # s can also be the empty dict, which is ok.
- raise TypeError('Cannot unify %r and %r.' % (fo, gi))
-
- f_g = (f_in, f_out[:-1]), (g_in[:-1], g_out)
-
- if s: f_g = update(s, f_g)
-
- fg_in, fg_out = compose(*f_g)
-
- return fg_in, fg_out
-unify()¶def unify(u, v, s=None):
- if s is None:
- s = {}
-
- if isinstance(u, int):
- s[u] = v
- elif isinstance(v, int):
- s[v] = u
- else:
- s = False
-
- return s
-update()¶def update(s, term):
- if not isinstance(term, tuple):
- return s.get(term, term)
- return tuple(update(s, inner) for inner in term)
-relabel()¶def relabel(left, right):
- return left, _1000(right)
-
-def _1000(right):
- if not isinstance(right, tuple):
- return 1000 + right
- return tuple(_1000(n) for n in right)
-
-relabel(pop, swap)
-delabel()¶def delabel(f):
- s = {u: i for i, u in enumerate(sorted(_unique(f)))}
- return update(s, f)
-
-def _unique(f, seen=None):
- if seen is None:
- seen = set()
- if not isinstance(f, tuple):
- seen.add(f)
- else:
- for inner in f:
- _unique(inner, seen)
- return seen
-
-delabel(relabel(pop, swap))
-C()¶At last we put it all together in a function C() that accepts two stack effect comments and returns their composition (or raises and exception if they can't be composed due to type conflicts.)
def C(f, g):
- f, g = relabel(f, g)
- fg = compose(f, g)
- return delabel(fg)
-Let's try it out.
- -C(pop, swap)
-C(C(pop, swap), roll_dn)
-C(swap, roll_dn)
-C(pop, C(swap, roll_dn))
-poswrd = reduce(C, (pop, swap, roll_dn))
-poswrd
-Here's that trick to represent functions like rest and cons that manipulate stacks. We use a cons-list of tuples and give the tails their own numbers. Then everything above already works.
rest = ((1, 2),), (2,)
-
-cons = (1, 2), ((1, 2),)
-C(poswrd, rest)
-Compare this to the stack effect comment we wrote above:
- -(( (3, 4), 1, 2, 0 ), ( 2, 1, 4 ))
-( [4 ...] 2 3 0 -- 3 2 [...])
-
-
-The translation table, if you will, would be:
- -{
-3: 4,
-4: ...],
-1: 2,
-2: 3,
-0: 0,
-}
-
-F = reduce(C, (pop, swap, roll_dn, rest, rest, cons, cons))
-
-F
-Compare with the stack effect comment and you can see it works fine:
- -([4 5 ...] 2 3 1 -- [3 2 ...])
- 3 4 5 1 2 0 2 1 5
-
-cons and uncons¶However, if we try to compose e.g. cons and uncons it won't work:
uncons = ((1, 2),), (1, 2)
-try:
- C(cons, uncons)
-except Exception, e:
- print e
-unify() version 2¶The problem is that the unify() function as written doesn't handle the case when both terms are tuples. We just have to add a clause to deal with this recursively:
def unify(u, v, s=None):
- if s is None:
- s = {}
- elif s:
- u = update(s, u)
- v = update(s, v)
-
- if isinstance(u, int):
- s[u] = v
-
- elif isinstance(v, int):
- s[v] = u
-
- elif isinstance(u, tuple) and isinstance(v, tuple):
-
- if len(u) != 2 or len(v) != 2:
- # Not a type error, caller passed in a bad value.
- raise ValueError(repr((u, v))) # FIXME this message sucks.
-
- (a, b), (c, d) = u, v
- s = unify(a, c, s)
- if s != False:
- s = unify(b, d, s)
- else:
- s = False
-
- return s
-C(cons, uncons)
-Now consider the Python function we would like to derive:
- -def F_python(stack):
- (_, (d, (c, ((a, (b, S0)), stack)))) = stack
- return (d, (c, S0)), stack
-And compare it to the input stack effect comment tuple we just computed:
- -F[0]
-The stack-de-structuring tuple has nearly the same form as our input stack effect comment tuple, just in the reverse order:
- -(_, (d, (c, ((a, (b, S0)), stack))))
-
-
-Remove the punctuation:
- - _ d c (a, (b, S0))
-
-
-Reverse the order and compare:
- - (a, (b, S0)) c d _
-((3, (4, 5 )), 1, 2, 0)
-
-
-Eh?
- -And the return tuple
- -F[1]
-is similar to the output stack effect comment tuple:
- -((d, (c, S0)), stack)
-((2, (1, 5 )), )
-
-
-This should make it pretty easy to write a Python function that accepts the stack effect comment tuples and returns a new Python function (either as a string of code or a function object ready to use) that performs the semantics of that Joy function (described by the stack effect.)
- -We want to substitute Python identifiers for the integers. I'm going to repurpose joy.parser.Symbol class for this:
from collections import defaultdict
-from joy.parser import Symbol
-
-
-def _names_for():
- I = iter(xrange(1000))
- return lambda: Symbol('a%i' % next(I))
-
-
-def identifiers(term, s=None):
- if s is None:
- s = defaultdict(_names_for())
- if isinstance(term, int):
- return s[term]
- return tuple(identifiers(inner, s) for inner in term)
-doc_from_stack_effect()¶As a convenience I've implemented a function to convert the Python stack effect comment tuples to reasonable text format. There are some details in how this code works that related to stuff later in the notebook, so you should skip it for now and read it later if you're interested.
- -def doc_from_stack_effect(inputs, outputs):
- return '(%s--%s)' % (
- ' '.join(map(_to_str, inputs + ('',))),
- ' '.join(map(_to_str, ('',) + outputs))
- )
-
-
-def _to_str(term):
- if not isinstance(term, tuple):
- try:
- t = term.prefix == 's'
- except AttributeError:
- return str(term)
- return '[.%i.]' % term.number if t else str(term)
-
- a = []
- while term and isinstance(term, tuple):
- item, term = term
- a.append(_to_str(item))
-
- try:
- n = term.number
- except AttributeError:
- n = term
- else:
- if term.prefix != 's':
- raise ValueError('Stack label: %s' % (term,))
-
- a.append('.%s.' % (n,))
- return '[%s]' % ' '.join(a)
-compile_()¶Now we can write a compiler function to emit Python source code. (The underscore suffix distiguishes it from the built-in compile() function.)
def compile_(name, f, doc=None):
- if doc is None:
- doc = doc_from_stack_effect(*f)
- inputs, outputs = identifiers(f)
- i = o = Symbol('stack')
- for term in inputs:
- i = term, i
- for term in outputs:
- o = term, o
- return '''def %s(stack):
- """%s"""
- %s = stack
- return %s''' % (name, doc, i, o)
-Here it is in action:
- -source = compile_('F', F)
-
-print source
-Compare:
- -def F_python(stack):
- (_, (d, (c, ((a, (b, S0)), stack)))) = stack
- return ((d, (c, S0)), stack)
-Next steps:
- -L = {}
-
-eval(compile(source, '__main__', 'single'), {}, L)
-
-L['F']
-Let's try it out:
- -from notebook_preamble import D, J, V
-from joy.library import SimpleFunctionWrapper
-D['F'] = SimpleFunctionWrapper(L['F'])
-J('[4 5 ...] 2 3 1 F')
-With this, we have a partial Joy compiler that works on the subset of Joy functions that manipulate stacks (both what I call "stack chatter" and the ones that manipulate stacks on the stack.)
-I'm probably going to modify the definition wrapper code to detect definitions that can be compiled by this partial compiler and do it automatically. It might be a reasonable idea to detect sequences of compilable functions in definitions that have uncompilable functions in them and just compile those. However, if your library is well-factored this might be less helpful.
- -We can use compile_() to generate many primitives in the library from their stack effect comments:
def defs():
-
- rolldown = (1, 2, 3), (2, 3, 1)
-
- rollup = (1, 2, 3), (3, 1, 2)
-
- pop = (1,), ()
-
- swap = (1, 2), (2, 1)
-
- rest = ((1, 2),), (2,)
-
- rrest = C(rest, rest)
-
- cons = (1, 2), ((1, 2),)
-
- uncons = ((1, 2),), (1, 2)
-
- swons = C(swap, cons)
-
- return locals()
-for name, stack_effect_comment in sorted(defs().items()):
- print
- print compile_(name, stack_effect_comment)
- print
-So far we have dealt with types of functions, those dealing with simple stack manipulation. Let's extend our machinery to deal with types of arguments.
- -Consider the definition of sqr:
sqr == dup mul
-
-
-
-The dup function accepts one anything and returns two of that:
dup (1 -- 1 1)
-
-
-And mul accepts two "numbers" (we're ignoring ints vs. floats vs. complex, etc., for now) and returns just one:
mul (n n -- n)
-
-
-So we're composing:
- -(1 -- 1 1)∘(n n -- n)
-
-
-The rules say we unify 1 with n:
(1 -- 1 1)∘(n n -- n)
---------------------------- w/ {1: n}
- (1 -- 1 )∘(n -- n)
-
-
-This involves detecting that "Any type" arguments can accept "numbers". If we were composing these functions the other way round this is still the case:
- - (n n -- n)∘(1 -- 1 1)
---------------------------- w/ {1: n}
- (n n -- )∘( -- n n)
-
-
-The important thing here is that the mapping is going the same way in both cases, from the "any" integer to the number
- -We should also mind that the number that mul produces is not (necessarily) the same as either of its inputs, which are not (necessarily) the same as each other:
mul (n2 n1 -- n3)
-
-
- (1 -- 1 1)∘(n2 n1 -- n3)
--------------------------------- w/ {1: n2}
- (n2 -- n2 )∘(n2 -- n3)
-
-
- (n2 n1 -- n3)∘(1 -- 1 1 )
--------------------------------- w/ {1: n3}
- (n2 n1 -- )∘( -- n3 n3)
-
-So we need separate domains of "any" numbers and "number" numbers, and we need to be able to ask the order of these domains. Now the notes on the right side of rule three make more sense, eh?
- - (a -- b t[i])∘(c u[j] -- d) t <= u (t is subtype of u)
--------------------------------
- (a -- b )∘(c -- d) t[i] == t[k] == u[j]
- ^
-
- (a -- b t[i])∘(c u[j] -- d) u <= t (u is subtype of t)
--------------------------------
- (a -- b )∘(c -- d) t[i] == u[k] == u[j]
-
-
-The indices i, k, and j are the number part of our labels and t and u are the domains.
By creative use of Python's "double underscore" methods we can define a Python class hierarchy of Joy types and use the issubclass() method to establish domain ordering, as well as other handy behaviour that will make it fairly easy to reuse most of the code above.
class AnyJoyType(object):
-
- prefix = 'a'
-
- def __init__(self, number):
- self.number = number
-
- def __repr__(self):
- return self.prefix + str(self.number)
-
- def __eq__(self, other):
- return (
- isinstance(other, self.__class__)
- and other.prefix == self.prefix
- and other.number == self.number
- )
-
- def __ge__(self, other):
- return issubclass(other.__class__, self.__class__)
-
- def __add__(self, other):
- return self.__class__(self.number + other)
- __radd__ = __add__
-
- def __hash__(self):
- return hash(repr(self))
-
-
-class NumberJoyType(AnyJoyType): prefix = 'n'
-class FloatJoyType(NumberJoyType): prefix = 'f'
-class IntJoyType(FloatJoyType): prefix = 'i'
-
-
-class StackJoyType(AnyJoyType):
- prefix = 's'
-
-
-_R = range(10)
-A = map(AnyJoyType, _R)
-N = map(NumberJoyType, _R)
-S = map(StackJoyType, _R)
-Mess with it a little:
- -from itertools import permutations
-"Any" types can be specialized to numbers and stacks, but not vice versa:
- -for a, b in permutations((A[0], N[0], S[0]), 2):
- print a, '>=', b, '->', a >= b
-Our crude Numerical Tower of numbers > floats > integers works as well (but we're not going to use it yet):
- -for a, b in permutations((A[0], N[0], FloatJoyType(0), IntJoyType(0)), 2):
- print a, '>=', b, '->', a >= b
-sqr¶dup = (A[1],), (A[1], A[1])
-
-mul = (N[1], N[2]), (N[3],)
-dup
-mul
-Re-labeling still works fine:
- -foo = relabel(dup, mul)
-
-foo
-delabel() version 2¶The delabel() function needs an overhaul. It now has to keep track of how many labels of each domain it has "seen".
from collections import Counter
-
-
-def delabel(f, seen=None, c=None):
- if seen is None:
- assert c is None
- seen, c = {}, Counter()
-
- try:
- return seen[f]
- except KeyError:
- pass
-
- if not isinstance(f, tuple):
- seen[f] = f.__class__(c[f.prefix] + 1)
- c[f.prefix] += 1
- return seen[f]
-
- return tuple(delabel(inner, seen, c) for inner in f)
-delabel(foo)
-unify() version 3¶def unify(u, v, s=None):
- if s is None:
- s = {}
- elif s:
- u = update(s, u)
- v = update(s, v)
-
- if u == v:
- return s
-
- if isinstance(u, AnyJoyType) and isinstance(v, AnyJoyType):
- if u >= v:
- s[u] = v
- return s
- if v >= u:
- s[v] = u
- return s
- raise TypeError('Cannot unify %r and %r.' % (u, v))
-
- if isinstance(u, tuple) and isinstance(v, tuple):
- if len(u) != len(v) != 2:
- raise TypeError(repr((u, v)))
- for uu, vv in zip(u, v):
- s = unify(uu, vv, s)
- if s == False: # (instead of a substitution dict.)
- break
- return s
-
- if isinstance(v, tuple):
- if not stacky(u):
- raise TypeError('Cannot unify %r and %r.' % (u, v))
- s[u] = v
- return s
-
- if isinstance(u, tuple):
- if not stacky(v):
- raise TypeError('Cannot unify %r and %r.' % (v, u))
- s[v] = u
- return s
-
- return False
-
-
-def stacky(thing):
- return thing.__class__ in {AnyJoyType, StackJoyType}
-Rewrite the stack effect comments:
- -def defs():
-
- rolldown = (A[1], A[2], A[3]), (A[2], A[3], A[1])
-
- rollup = (A[1], A[2], A[3]), (A[3], A[1], A[2])
-
- pop = (A[1],), ()
-
- popop = (A[2], A[1],), ()
-
- popd = (A[2], A[1],), (A[1],)
-
- popdd = (A[3], A[2], A[1],), (A[2], A[1],)
-
- swap = (A[1], A[2]), (A[2], A[1])
-
- rest = ((A[1], S[1]),), (S[1],)
-
- rrest = C(rest, rest)
-
- cons = (A[1], S[1]), ((A[1], S[1]),)
-
- ccons = C(cons, cons)
-
- uncons = ((A[1], S[1]),), (A[1], S[1])
-
- swons = C(swap, cons)
-
- dup = (A[1],), (A[1], A[1])
-
- dupd = (A[2], A[1]), (A[2], A[2], A[1])
-
- mul = (N[1], N[2]), (N[3],)
-
- sqrt = C(dup, mul)
-
- first = ((A[1], S[1]),), (A[1],)
-
- second = C(rest, first)
-
- third = C(rest, second)
-
- tuck = (A[2], A[1]), (A[1], A[2], A[1])
-
- over = (A[2], A[1]), (A[2], A[1], A[2])
-
- succ = pred = (N[1],), (N[2],)
-
- divmod_ = pm = (N[2], N[1]), (N[4], N[3])
-
- return locals()
-DEFS = defs()
-for name, stack_effect_comment in sorted(DEFS.items()):
- print name, '=', doc_from_stack_effect(*stack_effect_comment)
-globals().update(DEFS)
-dup and mul¶C(dup, mul)
-Revisit the F function, works fine.
F = reduce(C, (pop, swap, rolldown, rest, rest, cons, cons))
-F
-print doc_from_stack_effect(*F)
-Some otherwise inefficient functions are no longer to be feared. We can also get the effect of combinators in some limited cases.
- -def neato(*funcs):
- print doc_from_stack_effect(*reduce(C, funcs))
-# e.g. [swap] dip
-neato(rollup, swap, rolldown)
-# e.g. [popop] dipd
-neato(popdd, rolldown, pop)
-# Reverse the order of the top three items.
-neato(rollup, swap)
-compile_() version 2¶Because the type labels represent themselves as valid Python identifiers the compile_() function doesn't need to generate them anymore:
def compile_(name, f, doc=None):
- inputs, outputs = f
- if doc is None:
- doc = doc_from_stack_effect(inputs, outputs)
- i = o = Symbol('stack')
- for term in inputs:
- i = term, i
- for term in outputs:
- o = term, o
- return '''def %s(stack):
- """%s"""
- %s = stack
- return %s''' % (name, doc, i, o)
-print compile_('F', F)
-But it cannot magically create new functions that involve e.g. math and such. Note that this is not a sqr function implementation:
print compile_('sqr', C(dup, mul))
-(Eventually I should come back around to this becuase it's not tooo difficult to exend this code to be able to compile e.g. n2 = mul(n1, n1) for mul with the right variable names and insert it in the right place. It requires a little more support from the library functions, in that we need to know to call mul() the Python function for mul the Joy function, but since most of the math functions (at least) are already wrappers it should be straightforward.)
compilable()¶The functions that can be compiled are the ones that have only AnyJoyType and StackJoyType labels in their stack effect comments. We can write a function to check that:
from itertools import imap
-
-
-def compilable(f):
- return isinstance(f, tuple) and all(imap(compilable, f)) or stacky(f)
-for name, stack_effect_comment in sorted(defs().items()):
- if compilable(stack_effect_comment):
- print name, '=', doc_from_stack_effect(*stack_effect_comment)
-Consider the stack function which grabs the whole stack, quotes it, and puts it on itself:
stack (... -- ... [...] )
-stack (... a -- ... a [a ...] )
-stack (... b a -- ... b a [a b ...])
-
-
-We would like to represent this in Python somehow. -To do this we use a simple, elegant trick.
- -stack S -- ( S, S)
-stack (a, S) -- ( (a, S), (a, S))
-stack (a, (b, S)) -- ( (a, (b, S)), (a, (b, S)))
-
-
-Instead of representing the stack effect comments as a single tuple (with N items in it) we use the same cons-list structure to hold the sequence and unify() the whole comments.
stack∘uncons¶Let's try composing stack and uncons. We want this result:
stack∘uncons (... a -- ... a a [...])
-
-The stack effects are:
- -stack = S -- (S, S)
-
-uncons = ((a, Z), S) -- (Z, (a, S))
-
-Unifying:
- - S -- (S, S) ∘ ((a, Z), S) -- (Z, (a, S ))
- w/ { S: (a, Z) }
-(a, Z) -- ∘ -- (Z, (a, (a, Z)))
-
-
-So:
- -stack∘uncons == (a, Z) -- (Z, (a, (a, Z)))
-
-
-It works.
- -stack∘uncons∘uncons¶Let's try stack∘uncons∘uncons:
(a, S ) -- (S, (a, (a, S ))) ∘ ((b, Z), S` ) -- (Z, (b, S` ))
-
- w/ { S: (b, Z) }
-
-(a, (b, Z)) -- ((b, Z), (a, (a, (b, Z)))) ∘ ((b, Z), S` ) -- (Z, (b, S` ))
-
- w/ { S`: (a, (a, (b, Z))) }
-
-(a, (b, Z)) -- ((b, Z), (a, (a, (b, Z)))) ∘ ((b, Z), (a, (a, (b, Z)))) -- (Z, (b, (a, (a, (b, Z)))))
-
-(a, (b, Z)) -- (Z, (b, (a, (a, (b, Z)))))
-
-
-It works.
- -compose() version 2¶This function has to be modified to use the new datastructures and it is no longer recursive, instead recursion happens as part of unification. Further, the first and second of Pöial's rules are now handled automatically by the unification algorithm. (One easy way to see this is that now an empty stack effect comment is represented by a StackJoyType instance which is not "falsey" and so neither of the first two rules' if clauses will ever be True. Later on I change the "truthiness" of StackJoyType to false to let e.g. joy.utils.stack.concat work with our stack effect comment cons-list tuples.)
def compose(f, g):
- (f_in, f_out), (g_in, g_out) = f, g
- s = unify(g_in, f_out)
- if s == False: # s can also be the empty dict, which is ok.
- raise TypeError('Cannot unify %r and %r.' % (f_out, g_in))
- return update(s, (f_in, g_out))
-I don't want to rewrite all the defs myself, so I'll write a little conversion function instead. This is programmer's laziness.
- -def sequence_to_stack(seq, stack=StackJoyType(23)):
- for item in seq: stack = item, stack
- return stack
-
-NEW_DEFS = {
- name: (sequence_to_stack(i), sequence_to_stack(o))
- for name, (i, o) in DEFS.iteritems()
-}
-NEW_DEFS['stack'] = S[0], (S[0], S[0])
-NEW_DEFS['swaack'] = (S[1], S[0]), (S[0], S[1])
-globals().update(NEW_DEFS)
-C(stack, uncons)
-reduce(C, (stack, uncons, uncons))
-The display function should be changed too.
- -doc_from_stack_effect() version 2¶Clunky junk, but it will suffice for now.
- -def doc_from_stack_effect(inputs, outputs):
- switch = [False] # Do we need to display the '...' for the rest of the main stack?
- i, o = _f(inputs, switch), _f(outputs, switch)
- if switch[0]:
- i.append('...')
- o.append('...')
- return '(%s--%s)' % (
- ' '.join(reversed([''] + i)),
- ' '.join(reversed(o + [''])),
- )
-
-
-def _f(term, switch):
- a = []
- while term and isinstance(term, tuple):
- item, term = term
- a.append(item)
- assert isinstance(term, StackJoyType), repr(term)
- a = [_to_str(i, term, switch) for i in a]
- return a
-
-
-def _to_str(term, stack, switch):
- if not isinstance(term, tuple):
- if term == stack:
- switch[0] = True
- return '[...]'
- return (
- '[.%i.]' % term.number
- if isinstance(term, StackJoyType)
- else str(term)
- )
-
- a = []
- while term and isinstance(term, tuple):
- item, term = term
- a.append(_to_str(item, stack, switch))
- assert isinstance(term, StackJoyType), repr(term)
- if term == stack:
- switch[0] = True
- end = '...'
- else:
- end = '.%i.' % term.number
- a.append(end)
- return '[%s]' % ' '.join(a)
-for name, stack_effect_comment in sorted(NEW_DEFS.items()):
- print name, '=', doc_from_stack_effect(*stack_effect_comment)
-print ; print doc_from_stack_effect(*stack)
-print ; print doc_from_stack_effect(*C(stack, uncons))
-print ; print doc_from_stack_effect(*reduce(C, (stack, uncons, uncons)))
-print ; print doc_from_stack_effect(*reduce(C, (stack, uncons, cons)))
-print doc_from_stack_effect(*C(ccons, stack))
-Q = C(ccons, stack)
-
-Q
-compile_() version 3¶This makes the compile_() function pretty simple as the stack effect comments are now already in the form needed for the Python code:
def compile_(name, f, doc=None):
- i, o = f
- if doc is None:
- doc = doc_from_stack_effect(i, o)
- return '''def %s(stack):
- """%s"""
- %s = stack
- return %s''' % (name, doc, i, o)
-print compile_('Q', Q)
-
-
-
-
-
-unstack = (S[1], S[0]), S[1]
-enstacken = S[0], (S[0], S[1])
-print doc_from_stack_effect(*unstack)
-print doc_from_stack_effect(*enstacken)
-print doc_from_stack_effect(*C(cons, unstack))
-print doc_from_stack_effect(*C(cons, enstacken))
-C(cons, unstack)
-
-...
- -class IntJoyType(NumberJoyType): prefix = 'i'
-
-
-F = map(FloatJoyType, _R)
-I = map(IntJoyType, _R)
-muls = [
- ((I[2], (I[1], S[0])), (I[3], S[0])),
- ((F[2], (I[1], S[0])), (F[3], S[0])),
- ((I[2], (F[1], S[0])), (F[3], S[0])),
- ((F[2], (F[1], S[0])), (F[3], S[0])),
-]
-for f in muls:
- print doc_from_stack_effect(*f)
-for f in muls:
- try:
- e = C(dup, f)
- except TypeError:
- continue
- print doc_from_stack_effect(*dup), doc_from_stack_effect(*f), doc_from_stack_effect(*e)
-from itertools import product
-
-
-def meta_compose(F, G):
- for f, g in product(F, G):
- try:
- yield C(f, g)
- except TypeError:
- pass
-
-
-def MC(F, G):
- return sorted(set(meta_compose(F, G)))
-for f in MC([dup], [mul]):
- print doc_from_stack_effect(*f)
-for f in MC([dup], muls):
- print doc_from_stack_effect(*f)
-We can borrow a trick from Brzozowski's Derivatives of Regular Expressions to invent a new type of type variable, a "sequence type" (I think this is what they mean in the literature by that term...) or "Kleene Star" type. I'm going to represent it as a type letter and the asterix, so a sequence of zero or more AnyJoyType variables would be:
A*
-
-The A* works by splitting the universe into two alternate histories:
A* -> 0 | A A*
-
-
-The Kleene star variable disappears in one universe, and in the other it turns into an AnyJoyType variable followed by itself again. We have to return all universes (represented by their substitution dicts, the "unifiers") that don't lead to type conflicts.
Consider unifying two stacks (the lowercase letters are any type variables of the kinds we have defined so far):
- -[a A* b .0.] U [c d .1.]
- w/ {c: a}
-[ A* b .0.] U [ d .1.]
-
-Now we have to split universes to unify A*. In the first universe it disappears:
[b .0.] U [d .1.]
- w/ {d: b, .1.: .0.}
- [] U []
-
-While in the second it spawns an A, which we will label e:
[e A* b .0.] U [d .1.]
- w/ {d: e}
-[ A* b .0.] U [ .1.]
- w/ {.1.: A* b .0.}
-[ A* b .0.] U [ A* b .0.]
-
-Giving us two unifiers:
- -{c: a, d: b, .1.: .0.}
-{c: a, d: e, .1.: A* b .0.}
-
-class KleeneStar(object):
-
- kind = AnyJoyType
-
- def __init__(self, number):
- self.number = number
- self.count = 0
- self.prefix = repr(self)
-
- def __repr__(self):
- return '%s%i*' % (self.kind.prefix, self.number)
-
- def another(self):
- self.count += 1
- return self.kind(10000 * self.number + self.count)
-
- def __eq__(self, other):
- return (
- isinstance(other, self.__class__)
- and other.number == self.number
- )
-
- def __ge__(self, other):
- return self.kind >= other.kind
-
- def __add__(self, other):
- return self.__class__(self.number + other)
- __radd__ = __add__
-
- def __hash__(self):
- return hash(repr(self))
-
-class AnyStarJoyType(KleeneStar): kind = AnyJoyType
-class NumberStarJoyType(KleeneStar): kind = NumberJoyType
-#class FloatStarJoyType(KleeneStar): kind = FloatJoyType
-#class IntStarJoyType(KleeneStar): kind = IntJoyType
-class StackStarJoyType(KleeneStar): kind = StackJoyType
-
-
-As = map(AnyStarJoyType, _R)
-Ns = map(NumberStarJoyType, _R)
-Ss = map(StackStarJoyType, _R)
-unify() version 4¶Can now return multiple results...
- -def unify(u, v, s=None):
- if s is None:
- s = {}
- elif s:
- u = update(s, u)
- v = update(s, v)
-
- if u == v:
- return s,
-
- if isinstance(u, AnyJoyType) and isinstance(v, AnyJoyType):
- if u >= v:
- s[u] = v
- return s,
- if v >= u:
- s[v] = u
- return s,
- raise TypeError('Cannot unify %r and %r.' % (u, v))
-
- if isinstance(u, tuple) and isinstance(v, tuple):
- if len(u) != len(v) != 2:
- raise TypeError(repr((u, v)))
-
- a, b = v
- if isinstance(a, KleeneStar):
- # Two universes, in one the Kleene star disappears and unification
- # continues without it...
- s0 = unify(u, b)
-
- # In the other it spawns a new variable.
- s1 = unify(u, (a.another(), v))
-
- t = s0 + s1
- for sn in t:
- sn.update(s)
- return t
-
- a, b = u
- if isinstance(a, KleeneStar):
- s0 = unify(v, b)
- s1 = unify(v, (a.another(), u))
- t = s0 + s1
- for sn in t:
- sn.update(s)
- return t
-
- ses = unify(u[0], v[0], s)
- results = ()
- for sn in ses:
- results += unify(u[1], v[1], sn)
- return results
-
- if isinstance(v, tuple):
- if not stacky(u):
- raise TypeError('Cannot unify %r and %r.' % (u, v))
- s[u] = v
- return s,
-
- if isinstance(u, tuple):
- if not stacky(v):
- raise TypeError('Cannot unify %r and %r.' % (v, u))
- s[v] = u
- return s,
-
- return ()
-
-
-def stacky(thing):
- return thing.__class__ in {AnyJoyType, StackJoyType}
-a = (As[1], S[1])
-a
-b = (A[1], S[2])
-b
-for result in unify(b, a):
- print result, '->', update(result, a), update(result, b)
-for result in unify(a, b):
- print result, '->', update(result, a), update(result, b)
-(a1*, s1) [a1*] (a1, s2) [a1]
-
-(a1*, (a1, s2)) [a1* a1] (a1, s2) [a1]
-
-(a1*, s1) [a1*] (a2, (a1*, s1)) [a2 a1*]
-
-sum_ = ((Ns[1], S[1]), S[0]), (N[0], S[0])
-
-print doc_from_stack_effect(*sum_)
-f = (N[1], (N[2], (N[3], S[1]))), S[0]
-
-print doc_from_stack_effect(S[0], f)
-for result in unify(sum_[0], f):
- print result, '->', update(result, sum_[1])
-compose() version 3¶This function has to be modified to yield multiple results.
- -def compose(f, g):
- (f_in, f_out), (g_in, g_out) = f, g
- s = unify(g_in, f_out)
- if not s:
- raise TypeError('Cannot unify %r and %r.' % (f_out, g_in))
- for result in s:
- yield update(result, (f_in, g_out))
-
-def meta_compose(F, G):
- for f, g in product(F, G):
- try:
- for result in C(f, g):
- yield result
- except TypeError:
- pass
-
-
-def C(f, g):
- f, g = relabel(f, g)
- for fg in compose(f, g):
- yield delabel(fg)
-for f in MC([dup], muls):
- print doc_from_stack_effect(*f)
-for f in MC([dup], [sum_]):
- print doc_from_stack_effect(*f)
-for f in MC([cons], [sum_]):
- print doc_from_stack_effect(*f)
-sum_ = (((N[1], (Ns[1], S[1])), S[0]), (N[0], S[0]))
-print doc_from_stack_effect(*cons),
-print doc_from_stack_effect(*sum_),
-
-for f in MC([cons], [sum_]):
- print doc_from_stack_effect(*f)
-a = (A[4], (As[1], (A[3], S[1])))
-a
-b = (A[1], (A[2], S[2]))
-b
-for result in unify(b, a):
- print result
-for result in unify(a, b):
- print result
-In order to compute the stack effect of combinators you kinda have to have the quoted programs they expect available. In the most general case, the i combinator, you can't say anything about its stack effect other than it expects one quote:
i (... [.1.] -- ... .1.)
-
-
-Or
- -i (... [A* .1.] -- ... A*)
-
-
-Consider the type of:
- -[cons] dip
-
-
-Obviously it would be:
- -(a1 [..1] a2 -- [a1 ..1] a2)
-
-
-dip itself could have:
(a1 [..1] -- ... then what?
-
-
-
-Without any information about the contents of the quote we can't say much about the result.
- -I think there's a way forward. If we convert our list (of terms we are composing) into a stack structure we can use it as a Joy expression, then we can treat the output half of a function's stack effect comment as a Joy interpreter stack, and just execute combinators directly. We can hybridize the compostition function with an interpreter to evaluate combinators, compose non-combinator functions, and put type variables on the stack. For combinators like branch that can have more than one stack effect we have to "split universes" again and return both.
We need a type variable for Joy functions that can go in our expressions and be used by the hybrid inferencer/interpreter. They have to store a name and a list of stack effects.
- -class FunctionJoyType(AnyJoyType):
-
- def __init__(self, name, sec, number):
- self.name = name
- self.stack_effects = sec
- self.number = number
-
- def __add__(self, other):
- return self
- __radd__ = __add__
-
- def __repr__(self):
- return self.name
-For non-combinator functions the stack effects list contains stack effect comments (represented by pairs of cons-lists as described above.)
- -class SymbolJoyType(FunctionJoyType):
- prefix = 'F'
-For combinators the list contains Python functions.
- -class CombinatorJoyType(FunctionJoyType):
-
- prefix = 'C'
-
- def __init__(self, name, sec, number, expect=None):
- super(CombinatorJoyType, self).__init__(name, sec, number)
- self.expect = expect
-
- def enter_guard(self, f):
- if self.expect is None:
- return f
- g = self.expect, self.expect
- new_f = list(compose(f, g, ()))
- assert len(new_f) == 1, repr(new_f)
- return new_f[0][1]
-For simple combinators that have only one effect (like dip) you only need one function and it can be the combinator itself.
import joy.library
-
-dip = CombinatorJoyType('dip', [joy.library.dip], 23)
-For combinators that can have more than one effect (like branch) you have to write functions that each implement the action of one of the effects.
def branch_true(stack, expression, dictionary):
- (then, (else_, (flag, stack))) = stack
- return stack, concat(then, expression), dictionary
-
-def branch_false(stack, expression, dictionary):
- (then, (else_, (flag, stack))) = stack
- return stack, concat(else_, expression), dictionary
-
-branch = CombinatorJoyType('branch', [branch_true, branch_false], 100)
-You can also provide an optional stack effect, input-side only, that will then be used as an identity function (that accepts and returns stacks that match the "guard" stack effect) which will be used to guard against type mismatches going into the evaluation of the combinator.
- -infer()¶With those in place, we can define a function that accepts a sequence of Joy type variables, including ones representing functions (not just values), and attempts to grind out all the possible stack effects of that expression.
-One tricky thing is that type variables in the expression have to be updated along with the stack effects after doing unification or we risk losing useful information. This was a straightforward, if awkward, modification to the call structure of meta_compose() et. al.
ID = S[0], S[0] # Identity function.
-
-
-def infer(*expression):
- return sorted(set(_infer(list_to_stack(expression))))
-
-
-def _infer(e, F=ID):
- _log_it(e, F)
- if not e:
- return [F]
-
- n, e = e
-
- if isinstance(n, SymbolJoyType):
- eFG = meta_compose([F], n.stack_effects, e)
- res = flatten(_infer(e, Fn) for e, Fn in eFG)
-
- elif isinstance(n, CombinatorJoyType):
- fi, fo = n.enter_guard(F)
- res = flatten(_interpret(f, fi, fo, e) for f in n.stack_effects)
-
- elif isinstance(n, Symbol):
- assert n not in FUNCTIONS, repr(n)
- func = joy.library._dictionary[n]
- res = _interpret(func, F[0], F[1], e)
-
- else:
- fi, fo = F
- res = _infer(e, (fi, (n, fo)))
-
- return res
-
-
-def _interpret(f, fi, fo, e):
- new_fo, ee, _ = f(fo, e, {})
- ee = update(FUNCTIONS, ee) # Fix Symbols.
- new_F = fi, new_fo
- return _infer(ee, new_F)
-
-
-def _log_it(e, F):
- _log.info(
- u'%3i %s ∘ %s',
- len(inspect_stack()),
- doc_from_stack_effect(*F),
- expression_to_string(e),
- )
-And that brings us to current Work-In-Progress. The mixed-mode inferencer/interpreter infer() function seems to work well. There are details I should document, and the rest of the code in the types module (FIXME link to its docs here!) should be explained... There is cruft to convert the definitions in DEFS to the new SymbolJoyType objects, and some combinators. Here is an example of output from the current code :
1/0 # (Don't try to run this cell! It's not going to work. This is "read only" code heh..)
-
-logging.basicConfig(format='%(message)s', stream=sys.stdout, level=logging.INFO)
-
-globals().update(FUNCTIONS)
-
-h = infer((pred, s2), (mul, s3), (div, s4), (nullary, (bool, s5)), dipd, branch)
-
-print '-' * 40
-
-for fi, fo in h:
- print doc_from_stack_effect(fi, fo)
-The numbers at the start of the lines are the current depth of the Python call stack. They're followed by the current computed stack effect (initialized to ID) then the pending expression (the inference of the stack effect of which is the whole object of the current example.)
In this example we are implementing (and inferring) ifte as [nullary bool] dipd branch which shows off a lot of the current implementation in action.
7 (--) ∘ [pred] [mul] [div] [nullary bool] dipd branch
- 8 (-- [pred ...2]) ∘ [mul] [div] [nullary bool] dipd branch
- 9 (-- [pred ...2] [mul ...3]) ∘ [div] [nullary bool] dipd branch
- 10 (-- [pred ...2] [mul ...3] [div ...4]) ∘ [nullary bool] dipd branch
- 11 (-- [pred ...2] [mul ...3] [div ...4] [nullary bool ...5]) ∘ dipd branch
- 15 (-- [pred ...5]) ∘ nullary bool [mul] [div] branch
- 19 (-- [pred ...2]) ∘ [stack] dinfrirst bool [mul] [div] branch
- 20 (-- [pred ...2] [stack ]) ∘ dinfrirst bool [mul] [div] branch
- 22 (-- [pred ...2] [stack ]) ∘ dip infra first bool [mul] [div] branch
- 26 (--) ∘ stack [pred] infra first bool [mul] [div] branch
- 29 (... -- ... [...]) ∘ [pred] infra first bool [mul] [div] branch
- 30 (... -- ... [...] [pred ...1]) ∘ infra first bool [mul] [div] branch
- 34 (--) ∘ pred s1 swaack first bool [mul] [div] branch
- 37 (n1 -- n2) ∘ [n1] swaack first bool [mul] [div] branch
- 38 (... n1 -- ... n2 [n1 ...]) ∘ swaack first bool [mul] [div] branch
- 41 (... n1 -- ... n1 [n2 ...]) ∘ first bool [mul] [div] branch
- 44 (n1 -- n1 n2) ∘ bool [mul] [div] branch
- 47 (n1 -- n1 b1) ∘ [mul] [div] branch
- 48 (n1 -- n1 b1 [mul ...1]) ∘ [div] branch
- 49 (n1 -- n1 b1 [mul ...1] [div ...2]) ∘ branch
- 53 (n1 -- n1) ∘ div
- 56 (f2 f1 -- f3) ∘
- 56 (i1 f1 -- f2) ∘
- 56 (f1 i1 -- f2) ∘
- 56 (i2 i1 -- f1) ∘
- 53 (n1 -- n1) ∘ mul
- 56 (f2 f1 -- f3) ∘
- 56 (i1 f1 -- f2) ∘
- 56 (f1 i1 -- f2) ∘
- 56 (i2 i1 -- i3) ∘
-----------------------------------------
-(f2 f1 -- f3)
-(i1 f1 -- f2)
-(f1 i1 -- f2)
-(i2 i1 -- f1)
-(i2 i1 -- i3)
-
-We built a simple type inferencer, and a kind of crude "compiler" for a subset of Joy functions. Then we built a more powerful inferencer that actually does some evaluation and explores branching code paths
- -Work remains to be done:
-loop and genrec, etc..For type checking to work the type label classes have to be modified to let T >= t succeed, where e.g. T is IntJoyType and t is int. If you do that you can take advantage of the logical relational nature of the stack effect comments to "compute in reverse" as it were. There's a working demo of this at the end of the types module. But if you're interested in all that you should just use Prolog!
Anyhow, type checking is a few easy steps away.
- -def _ge(self, other):
- return (issubclass(other.__class__, self.__class__)
- or hasattr(self, 'accept')
- and isinstance(other, self.accept))
-
-AnyJoyType.__ge__ = _ge
-AnyJoyType.accept = tuple, int, float, long, str, unicode, bool, Symbol
-StackJoyType.accept = tuple
-This notebook is about using the "zipper" with joy datastructures. See the Zipper wikipedia entry or the original paper: "FUNCTIONAL PEARL The Zipper" by Gérard Huet
-Given a datastructure on the stack we can navigate through it, modify it, and rebuild it using the "zipper" technique.
- -from notebook_preamble import J, V, define
-J('[1 [2 [3 4 25 6] 7] 8]')
-Zippers work by keeping track of the current item, the already-seen items, and the yet-to-be seen items as you traverse a datastructure (the datastructure used to keep track of these items is the zipper.)
-In Joy we can do this with the following words:
- -z-down == [] swap uncons swap
-z-up == swons swap shunt
-z-right == [swons] cons dip uncons swap
-z-left == swons [uncons swap] dip swap
-
-
-Let's use them to change 25 into 625. The first time a word is used I show the trace so you can see how it works. If we were going to use these a lot it would make sense to write Python versions for efficiency, but see below.
- -define('z-down == [] swap uncons swap')
-define('z-up == swons swap shunt')
-define('z-right == [swons] cons dip uncons swap')
-define('z-left == swons [uncons swap] dip swap')
-V('[1 [2 [3 4 25 6] 7] 8] z-down')
-V('[] [[2 [3 4 25 6] 7] 8] 1 z-right')
-J('[1] [8] [2 [3 4 25 6] 7] z-down')
-J('[1] [8] [] [[3 4 25 6] 7] 2 z-right')
-J('[1] [8] [2] [7] [3 4 25 6] z-down')
-J('[1] [8] [2] [7] [] [4 25 6] 3 z-right')
-J('[1] [8] [2] [7] [3] [25 6] 4 z-right')
-J('[1] [8] [2] [7] [4 3] [6] 25 sqr')
-V('[1] [8] [2] [7] [4 3] [6] 625 z-up')
-J('[1] [8] [2] [7] [3 4 625 6] z-up')
-J('[1] [8] [2 [3 4 625 6] 7] z-up')
-dip and infra¶In Joy we have the dip and infra combinators which can "target" or "address" any particular item in a Joy tree structure.
V('[1 [2 [3 4 25 6] 7] 8] [[[[[[sqr] dipd] infra] dip] infra] dip] infra')
-If you read the trace carefully you'll see that about half of it is the dip and infra combinators de-quoting programs and "digging" into the subject datastructure. Instead of maintaining temporary results on the stack they are pushed into the pending expression (continuation). When sqr has run the rest of the pending expression rebuilds the datastructure.
Z¶Imagine a function Z that accepts a sequence of dip and infra combinators, a quoted program [Q], and a datastructure to work on. It would effectively execute the quoted program as if it had been embedded in a nested series of quoted programs, e.g.:
[...] [Q] [dip dip infra dip infra dip infra] Z
--------------------------------------------------------------
- [...] [[[[[[[Q] dip] dip] infra] dip] infra] dip] infra
-
-
-The Z function isn't hard to make.
define('Z == [[] cons cons] step i')
-Here it is in action in a simplified scenario.
- -V('1 [2 3 4] Z')
-And here it is doing the main thing.
- -J('[1 [2 [3 4 25 6] 7] 8] [sqr] [dip dip infra dip infra dip infra] Z')
-Because we are only using two combinators we could replace the list with a string made from only two characters.
- - [...] [Q] 'ddididi' Zstr
--------------------------------------------------------------
- [...] [[[[[[[Q] dip] dip] infra] dip] infra] dip] infra
-
-
-The string can be considered a name or address for an item in the subject datastructure.
-It's easy to read off (in reverse) the right sequence of "d" and "i" from the subject datastructure:
- -[ n [ n [ n n x ...
-i d i d i d d Bingo!
-
-
-from notebook_preamble import D, DefinitionWrapper, J, V, define
-scan in terms of a reduction.¶- -Problem I. The reduction operator
-/of APL takes some binary operator⨁on its left and a vectorxof values on its right. The meaning of⨁/xforx = [a b ... z]is the valuea⨁b⨁...⨁z. For this to be well-defined in the absence of brackets, the operation⨁has to be associative. Now there is another operator\of APL calledscan. Its effect is closely related to reduction in that we have:
⨁\x = [a a⨁b a⨁b⨁c ... a⨁b⨁...⨁z]
-
-
-- -The problem is to find some definition of
-scanas a reduction. In other words, we have to find some functionfand an operator⨂so that
⨁\x = f(a)⨂f(b)⨂...⨂f(z)
-
-Ignoring the exact requirements (finding f and ⨂) can we implement scan as a hylomorphism in Joy?
Looking at the forms of hylomorphism, H3 is the one to use:
H3¶If the combiner and the generator both need to work on the current value then dup must be used, and the generator must produce one item instead of two (the b is instead the duplicate of a.)
H3 == [P] [pop c] [[G] dupdip] [dip F] genrec
-
-... a [G] dupdip [H3] dip F
-... a G a [H3] dip F
-... a′ a [H3] dip F
-... a′ H3 a F
-... a′ [G] dupdip [H3] dip F a F
-... a′ G a′ [H3] dip F a F
-... a″ a′ [H3] dip F a F
-... a″ H3 a′ F a F
-... a″ [G] dupdip [H3] dip F a′ F a F
-... a″ G a″ [H3] dip F a′ F a F
-... a‴ a″ [H3] dip F a′ F a F
-... a‴ H3 a″ F a′ F a F
-... a‴ pop c a″ F a′ F a F
-... c a″ F a′ F a F
-... d a′ F a F
-... d′ a F
-... d″
-
-We're building a list of values so this is an "anamorphism". (An anamorphism uses [] for c and swons for F.)
scan == [P] [pop []] [[G] dupdip] [dip swons] genrec
-
-
-Convert to ifte:
scan == [P] [pop []] [[G] dupdip [scan] dip swons] ifte
-
-On the recursive branch [G] dupdip doesn't cut it:
[1 2 3] [G] dupdip [scan] dip swons
-[1 2 3] G [1 2 3] [scan] dip swons
-
-first¶At this point, we want the copy of [1 2 3] to just be 1, so we use first.
scan == [P] [pop []] [[G] dupdip first] [dip swons] genrec
-
-[1 2 3] [G] dupdip first [scan] dip swons
-[1 2 3] G [1 2 3] first [scan] dip swons
-[1 2 3] G 1 [scan] dip swons
-
-G applies ⨁¶Now what does G have to do? Just apply ⨁ to the first two terms in the list.
[1 2 3] G
-[1 2 3] [⨁] infra
-[1 2 3] [+] infra
-[3 3]
-
-P¶Which tells us that the predicate [P] must guard against lists with less that two items in them:
P == size 1 <=
-
-Let's see what we've got so far:
- -scan == [P ] [pop []] [[G] dupdip first] [dip swons] genrec
-scan == [size 1 <=] [pop []] [[[F] infra] dupdip first] [dip swons] genrec
-
-This works to a point, but it throws away the last term:
- -J('[1 2 3] [size 1 <=] [pop []] [[[+] infra] dupdip first] [dip swons] genrec')
-Hmm... Let's take out the pop for a sec...
J('[1 2 3] [size 1 <=] [[]] [[[+] infra] dupdip first] [dip swons] genrec')
-That leaves the last item in our list, then it puts an empty list on the stack and swons's the new terms onto that. If we leave out that empty list, they will be swons'd onto that list that already has the last item.
J('[1 2 3] [size 1 <=] [] [[[+] infra] dupdip first] [dip swons] genrec')
-⨁¶So we have:
- -[⨁] scan == [size 1 <=] [] [[[⨁] infra] dupdip first] [dip swons] genrec
-
-
-Trivially:
- - == [size 1 <=] [] [[[⨁] infra] dupdip first] [dip swons] genrec
- == [[[⨁] infra] dupdip first] [size 1 <=] [] roll< [dip swons] genrec
- == [[⨁] infra] [dupdip first] cons [size 1 <=] [] roll< [dip swons] genrec
- == [⨁] [infra] cons [dupdip first] cons [size 1 <=] [] roll< [dip swons] genrec
-
-
-And so:
- -scan == [infra] cons [dupdip first] cons [size 1 <=] [] roll< [dip swons] genrec
-
-define('scan [infra] cons [dupdip first] cons [size 1 <=] [] roll< [dip swons] genrec')
-J('[1 2 3 4] [+] scan')
-J('[1 2 3 4] [*] scan')
-J('[1 2 3 4 5 6 7] [neg +] scan')
-- -Define a line to be a sequence of characters not containing the newline character. It is easy to define a function
-Unlinesthat converts a non-empty sequence of lines into a sequence of characters by inserting newline characters between every two lines.Since
-Unlinesis injective, the functionLines, which converts a sequence of characters into a sequence of lines by splitting on newline characters, can be specified as the inverse ofUnlines.The problem, just as in Problem 1. is to find a definition by reduction of the function
-Lines.
Unlines = uncons ['\n' swap + +] step
-
-J('["hello" "world"] uncons ["\n" swap + +] step')
-Again ignoring the actual task let's just derive Lines:
"abc\nefg\nhij" Lines
----------------------------
- ["abc" "efg" "hij"]
-
-Instead of P == [size 1 <=] we want ["\n" in], and for the base-case of a string with no newlines in it we want to use unit:
Lines == ["\n" in] [unit] [R0] [dip swons] genrec
-Lines == ["\n" in] [unit] [R0 [Lines] dip swons] ifte
-
-Derive R0:
"a \n b" R0 [Lines] dip swons
-"a \n b" split-at-newline swap [Lines] dip swons
-"a " " b" swap [Lines] dip swons
-" b" "a " [Lines] dip swons
-" b" Lines "a " swons
-[" b"] "a " swons
-["a " " b"]
-
-So:
- -R0 == split-at-newline swap
-
-Lines == ["\n" in] [unit] [split-at-newline swap] [dip swons] genrec
-
-This is all good and well, but in the paper many interesting laws and properties are discussed. Am I missing the point?
- -0 [a b c d] [F] step == 0 [a b] [F] step 0 [c d] [F] step concat
-
-
-For associative function F and a "unit" element for that function, here represented by 0.
For functions that don't have a "unit" we can fake it (the example is given of infinity for the min(a, b) function.) We can also use:
safe_step == [size 1 <=] [] [uncons [F] step] ifte
-
-
-Or:
- -safe_step == [pop size 1 <=] [pop] [[uncons] dip step] ifte
-
- [a b c] [F] safe_step
----------------------------
- a [b c] [F] step
-
-
-To limit F to working on pairs of terms from its domain.
import sympy
-
-from joy.joy import run
-from joy.library import UnaryBuiltinWrapper
-from joy.utils.pretty_print import TracePrinter
-from joy.utils.stack import list_to_stack
-
-from notebook_preamble import D, J, V, define
-sympy.init_printing()
-The SymPy package provides a powerful and elegant "thunk" object that can take the place of a numeric value in calculations and "record" the operations performed on it.
-We can create some of these objects and put them on the Joy stack:
- -stack = list_to_stack(sympy.symbols('c a b'))
-If we evaluate the quadratic program
over [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2
-
-
-The SypPy Symbols will become the symbolic expression of the math operations. Unfortunately, the library sqrt function doesn't work with the SymPy objects:
viewer = TracePrinter()
-try:
- run('over [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2', stack, D, viewer.viewer)
-except Exception, e:
- print e
-viewer.print_()
-We can pick out that first symbolic expression obect from the Joy stack:
- -S, E = viewer.history[-1]
-q = S[0]
-q
-The Python math.sqrt() function causes the "can't convert expression to float" exception but sympy.sqrt() does not:
sympy.sqrt(q)
-sympy.sqrt¶This is easy to fix.
- -D['sqrt'] = UnaryBuiltinWrapper(sympy.sqrt)
-Now it works just fine.
- -(root1, (root2, _)) = run('over [[[neg] dupdip sqr 4] dipd * * - sqrt pm] dip 2 * [/] cons app2', stack, D)[0]
-root1
-root2
-At some point I will probably make an optional library of Joy wrappers for SymPy functions, and either load it automatically if SymPy installation is available or have a CLI switch or something. There's a huge amount of incredibly useful stuff and I don't see why Joy shouldn't expose another interface for using it. (As an example, the symbolic expressions can be "lambdafied" into very fast versions, i.e. a function that takes a, b, and c and computes the value of the root using just low-level fast code, bypassing Joy and Python. Also, Numpy, &c.)
Starting with the example from Partial Computation of Programs
-by Yoshihiko Futamura of a function to compute u to the kth power:
def F(u, k):
- z = 1
- while k != 0:
- if odd(k):
- z = z * u
- k = k / 2
- u = u * u
- return z
-Partial evaluation with k = 5:
def F5(u):
- z = 1 * u
- u = u * u
- u = u * u
- z = z * u
- return z
-Translate F(u, k) to Joy
u k 1 # z = 1
- [pop] [Fw] while # the while statement
- popopd # discard u k, "return" z
-
-What's Fw?
- -u k z [pop odd] [Ft] [] ifte # the if statement
- [2 //] dip # k = k / 2 floordiv
- [sqr] dipd # u = u * u
-
- [[sqr] dip 2 //] dip # We can merge last two lines.
-
-Helper function Ft (to compute z = z * u).
- - u k z Ft
----------------
- u k u*z
-
-
-Ft == [over] dip *
-
-Putting it together:
- -Ft == [over] dip *
-Fb == [[sqr] dip 2 //] dip
-Fw == [pop odd] [Ft] [] ifte Fb
- F == 1 [pop] [Fw] while popopd
-
-define('odd == 2 %')
-define('Ft == [over] dip *')
-define('Fb == [[sqr] dip 2 //] dip')
-define('Fw == [pop odd] [Ft] [] ifte Fb')
-define('F == 1 [pop] [Fw] while popopd')
-Try it out:
- -J('2 5 F')
-In order to elide the tests let's define special versions of while and ifte:
from joy.joy import joy
-from joy.library import FunctionWrapper
-from joy.parser import Symbol
-from joy.utils.stack import concat
-
-
-S_while = Symbol('while')
-
-
-@FunctionWrapper
-def while_(S, expression, dictionary):
- '''[if] [body] while'''
- (body, (if_, stack)) = S
- if joy(stack, if_, dictionary)[0][0]:
- expression = concat(body, (if_, (body, (S_while, expression))))
- return stack, expression, dictionary
-
-
-@FunctionWrapper
-def ifte(stack, expression, dictionary):
- '''[if] [then] [else] ifte'''
- (else_, (then, (if_, stack))) = stack
- if_res = joy(stack, if_, dictionary)[0][0]
- quote = then if if_res else else_
- expression = concat(quote, expression)
- return (stack, expression, dictionary)
-
-
-D['ifte'] = ifte
-D['while'] = while_
-And with a SymPy symbol for the u argument:
stack = list_to_stack([5, sympy.symbols('u')])
-viewer = TracePrinter()
-try:
- (result, _) = run('F', stack, D, viewer.viewer)[0]
-except Exception, e:
- print e
-viewer.print_()
-result
-Let's try partial evaluation by hand and use a "stronger" thunk.
-Caret underscoring indicates terms that form thunks. When an arg is unavailable for a computation we just postpone it until the arg becomes available and in the meantime treat the pending computation as one unit.
- - u 5 . F
- u 5 . 1 [pop] [Fw] while popopd
- u 5 1 . [pop] [Fw] while popopd
- u 5 1 [pop] . [Fw] while popopd
- u 5 1 [pop] [Fw] . while popopd
- u 5 1 . Fw [pop] [Fw] while popopd
- u 5 1 . [pop odd] [Ft] [] ifte Fb [pop] [Fw] while popopd
- u 5 1 [pop odd] . [Ft] [] ifte Fb [pop] [Fw] while popopd
- u 5 1 [pop odd] [Ft] . [] ifte Fb [pop] [Fw] while popopd
- u 5 1 [pop odd] [Ft] [] . ifte Fb [pop] [Fw] while popopd
- u 5 1 . Ft Fb [pop] [Fw] while popopd
- u 5 1 . [over] dip * Fb [pop] [Fw] while popopd
- u 5 1 [over] . dip * Fb [pop] [Fw] while popopd
- u 5 . over 1 * Fb [pop] [Fw] while popopd
- u 5 u . 1 * Fb [pop] [Fw] while popopd
- u 5 u 1 . * Fb [pop] [Fw] while popopd
- u 5 u . Fb [pop] [Fw] while popopd
- u 5 u . [[sqr] dip 2 //] dip [pop] [Fw] while popopd
- u 5 u [[sqr] dip 2 //] . dip [pop] [Fw] while popopd
- u 5 . [sqr] dip 2 // u [pop] [Fw] while popopd
- u 5 [sqr] . dip 2 // u [pop] [Fw] while popopd
- u . sqr 5 2 // u [pop] [Fw] while popopd
- u . dup mul 5 2 // u [pop] [Fw] while popopd
- u dup * . 5 2 // u [pop] [Fw] while popopd
- ^^^^^^^
-
- u dup * 2 u [pop] [Fw] . while popopd
- u dup * 2 u . Fw [pop] [Fw] while popopd
- u dup * 2 u . [pop odd] [Ft] [] ifte Fb [pop] [Fw] while popopd
- u dup * 2 u [pop odd] . [Ft] [] ifte Fb [pop] [Fw] while popopd
- u dup * 2 u [pop odd] [Ft] . [] ifte Fb [pop] [Fw] while popopd
- u dup * 2 u [pop odd] [Ft] [] . ifte Fb [pop] [Fw] while popopd
- u dup * 2 u . Fb [pop] [Fw] while popopd
- u dup * 2 u . [[sqr] dip 2 //] dip [pop] [Fw] while popopd
- u dup * 2 u [[sqr] dip 2 //] . dip [pop] [Fw] while popopd
- u dup * 2 . [sqr] dip 2 // u [pop] [Fw] while popopd
- u dup * 2 [sqr] . dip 2 // u [pop] [Fw] while popopd
- u dup * . sqr 2 2 // u [pop] [Fw] while popopd
- u dup * . dup mul 2 2 // u [pop] [Fw] while popopd
- u dup * dup * . 2 2 // u [pop] [Fw] while popopd
- ^^^^^^^^^^^^^
-
-w/ K == u dup * dup *
K 1 u [pop] [Fw] . while popopd
- K 1 u . Fw [pop] [Fw] while popopd
- K 1 u . [pop odd] [Ft] [] ifte Fb [pop] [Fw] while popopd
- K 1 u [pop odd] . [Ft] [] ifte Fb [pop] [Fw] while popopd
- K 1 u [pop odd] [Ft] . [] ifte Fb [pop] [Fw] while popopd
- K 1 u [pop odd] [Ft] [] . ifte Fb [pop] [Fw] while popopd
- K 1 u . Ft Fb [pop] [Fw] while popopd
- K 1 u . [over] dip * Fb [pop] [Fw] while popopd
- K 1 u [over] . dip * Fb [pop] [Fw] while popopd
- K 1 . over u * Fb [pop] [Fw] while popopd
- K 1 K . u * Fb [pop] [Fw] while popopd
- K 1 K u . * Fb [pop] [Fw] while popopd
- K 1 K u * . Fb [pop] [Fw] while popopd
- ^^^^^
-
-w/ L == K u *
K 1 L . Fb [pop] [Fw] while popopd
- K 1 L . [[sqr] dip 2 //] dip [pop] [Fw] while popopd
- K 1 L [[sqr] dip 2 //] . dip [pop] [Fw] while popopd
- K 1 . [sqr] dip 2 // L [pop] [Fw] while popopd
- K 1 [sqr] . dip 2 // L [pop] [Fw] while popopd
- K . sqr 1 2 // L [pop] [Fw] while popopd
- K . dup mul 1 2 // L [pop] [Fw] while popopd
- K K . mul 1 2 // L [pop] [Fw] while popopd
- K K * . 1 2 // L [pop] [Fw] while popopd
- ^^^^^
- K K * . 1 2 // L [pop] [Fw] while popopd
- K K * 1 . 2 // L [pop] [Fw] while popopd
- K K * 1 2 . // L [pop] [Fw] while popopd
- K K * 0 . L [pop] [Fw] while popopd
- K K * 0 L . [pop] [Fw] while popopd
- K K * 0 L [pop] . [Fw] while popopd
- K K * 0 L [pop] [Fw] . while popopd
- ^^^^^
- K K * 0 L . popopd
- L .
-
-So:
- -K == u dup * dup *
-L == K u *
-
-
-Our result "thunk" would be:
- -u dup * dup * u *
-
-
-Mechanically, you could do:
- -u dup * dup * u *
-u u [dup * dup *] dip *
-u dup [dup * dup *] dip *
-
-
-F5 == dup [dup * dup *] dip *
-
-
-But we can swap the two arguments to the final * to get all mentions of u to the left:
u u dup * dup * *
-
-
-
-Then de-duplicate "u":
- -u dup dup * dup * *
-
-
-
-To arrive at a startlingly elegant form for F5:
- -F5 == dup dup * dup * *
-
-stack = list_to_stack([sympy.symbols('u')])
-viewer = TracePrinter()
-try:
- (result, _) = run('dup dup * dup * *', stack, D, viewer.viewer)[0]
-except Exception, e:
- print e
-viewer.print_()
-result
-I'm not sure how to implement these kinds of thunks. I think you have to have support in the interpreter, or you have to modify all of the functions like dup to check for thunks in their inputs.
Working on the compiler, from this:
- -dup dup * dup * *
-
-
-We can already generate:
- ----------------------------------
-(a0, stack) = stack
-a1 = mul(a0, a0)
-a2 = mul(a1, a1)
-a3 = mul(a2, a0)
-stack = (a3, stack)
----------------------------------
-
-
-This is pretty old stuff... (E.g. from 1999, M. Anton Ertl Compilation of Stack-Based Languages he goes a lot further for Forth.)
- -
-by Arthur Nunes-Harwitt
- - -def m(x, y): return x * y
-
-print m(2, 3)
-def m(x): return lambda y: x * y
-
-print m(2)(3)
-def m(x): return "lambda y: %(x)s * y" % locals()
-
-print m(2)
-print eval(m(2))(3)
-In Joy:
- -m == [*] cons
-
-3 2 m i
-3 2 [*] cons i
-3 [2 *] i
-3 2 *
-6
-
-def p(n, b): # original
- return 1 if n == 0 else b * p(n - 1, b)
-
-
-def p(n): # curried
- return lambda b: 1 if n == 0 else b * p(n - 1, b)
-
-
-def p(n): # quoted
- return "lambda b: 1 if %(n)s == 0 else b * p(%(n)s - 1, b)" % locals()
-
-
-print p(3)
-Original
- -p == [0 =] [popop 1] [-- over] [dip *] genrec
-
-b n p
-b n [0 =] [popop 1] [-- over [p] dip *]
-
-b n -- over [p] dip *
-b n-1 over [p] dip *
-b n-1 b [p] dip *
-b n-1 p b *
-
-curried, quoted
- - n p
----------------------------------------------
- [[n 0 =] [pop 1] [dup n --] [*] genrec]
-
-def p(n): # lambda lowered
- return (
- lambda b: 1
- if n == 0 else
- lambda b: b * p(n - 1, b)
- )
-
-
-def p(n): # lambda lowered quoted
- return (
- "lambda b: 1"
- if n == 0 else
- "lambda b: b * p(%(n)s - 1, b)" % locals()
- )
-
-print p(3)
-p == [0 =] [[pop 1]] [ [-- [dup] dip p *] cons ]ifte
-
-
-3 p
-3 [-- [dup] dip p *] cons
-[3 -- [dup] dip p *]
-
-def p(n): # expression lifted
- if n == 0:
- return lambda b: 1
- f = p(n - 1)
- return lambda b: b * f(b)
-
-
-print p(3)(2)
-def p(n): # quoted
- if n == 0:
- return "lambda b: 1"
- f = p(n - 1)
- return "lambda b: b * (%(f)s)(b)" % locals()
-
-print p(3)
-print eval(p(3))(2)
-p == [0 =] [pop [pop 1]] [-- p [dupdip *] cons] ifte
-
-
-3 p
-3 -- p [dupdip *] cons
-2 p [dupdip *] cons
-2 -- p [dupdip *] cons [dupdip *] cons
-1 p [dupdip *] cons [dupdip *] cons
-1 -- p [dupdip *] cons [dupdip *] cons [dupdip *] cons
-0 p [dupdip *] cons [dupdip *] cons [dupdip *] cons
-0 pop [pop 1] [dupdip *] cons [dupdip *] cons [dupdip *] cons
-[pop 1] [dupdip *] cons [dupdip *] cons [dupdip *] cons
-...
-[[[[pop 1] dupdip *] dupdip *] dupdip *]
-
-
-2 [[[[pop 1] dupdip *] dupdip *] dupdip *] i
-2 [[[pop 1] dupdip *] dupdip *] dupdip *
-2 [[pop 1] dupdip *] dupdip * 2 *
-2 [pop 1] dupdip * 2 * 2 *
-2 pop 1 2 * 2 * 2 *
- 1 2 * 2 * 2 *
-
-
-
-p == [0 =] [pop [pop 1]] [-- p [dupdip *] cons] ifte
-p == [0 =] [pop [pop 1]] [-- [p] i [dupdip *] cons] ifte
-p == [0 =] [pop [pop 1]] [--] [i [dupdip *] cons] genrec
-
-define('p == [0 =] [pop [pop 1]] [--] [i [dupdip *] cons] genrec')
-J('3 p')
-V('2 [[[[pop 1] dupdip *] dupdip *] dupdip *] i')
-stack = list_to_stack([sympy.symbols('u')])
-(result, s) = run('p i', stack, D)[0]
-result
-From this:
- -p == [0 =] [pop pop 1] [-- over] [dip *] genrec
-
-
-To this:
- -p == [0 =] [pop [pop 1]] [--] [i [dupdip *] cons] genrec
-
-F():¶def odd(n): return n % 2
-
-
-def F(u, k):
- z = 1
- while k != 0:
- if odd(k):
- z = z * u
- k = k / 2
- u = u * u
- return z
-
-F(2, 5)
-def F(k):
- def _F(u, k=k):
- z = 1
- while k != 0:
- if odd(k):
- z = z * u
- k = k / 2
- u = u * u
- return z
- return _F
-
-F(5)(2)
-def F(k):
- def _F(u, k=k):
- if k == 0:
- z = 1
- else:
- z = F(k / 2)(u)
- z *= z
- if odd(k):
- z = z * u
- return z
- return _F
-
-F(5)(2)
-def F(k):
- if k == 0:
- z = lambda u: 1
- else:
- f = F(k / 2)
- def z(u):
- uu = f(u)
- uu *= uu
- return uu * u if odd(k) else uu
- return z
-
-F(5)(2)
-def F(k):
- if k == 0:
- z = lambda u: 1
- else:
- f = F(k / 2)
- if odd(k):
- z = lambda u: (lambda fu, u: fu * fu * u)(f(u), u)
- else:
- z = lambda u: (lambda fu, u: fu * fu)(f(u), u)
- return z
-
-F(5)(2)
-def F(k):
- if k == 0:
- z = "lambda u: 1"
- else:
- f = F(k / 2)
- if odd(k):
- z = "lambda u: (lambda fu, u: fu * fu * u)((%(f)s)(u), u)" % locals()
- else:
- z = "lambda u: (lambda fu, u: fu * fu)((%(f)s)(u), u)" % locals()
- return z
-
-source = F(5)
-print source
-eval(source)(2)
-Hmm...
- -for n in range(4):
- print F(n)
-def F(k):
- if k == 0:
- z = "lambda u: 1"
- elif k == 1:
- z = "lambda u: u"
- else:
- f = F(k / 2)
- if odd(k):
- z = "lambda u: (lambda fu, u: fu * fu * u)((%(f)s)(u), u)" % locals()
- else:
- z = "lambda u: (lambda fu, u: fu * fu)((%(f)s)(u), u)" % locals()
- return z
-
-source = F(5)
-print source
-eval(source)(2)
-for n in range(4):
- print F(n)
-def F(k):
- if k == 0:
- z = "lambda u: 1"
- elif k == 1:
- z = "lambda u: u"
- else:
- m = k / 2
- if odd(k):
- if m == 0:
- z = "lambda u: 1"
- elif m == 1:
- z = "lambda u: u * u * u"
- else:
- z = "lambda u: (lambda fu, u: fu * fu * u)((%s)(u), u)" % F(m)
- else:
- if m == 0:
- z = "lambda u: 1"
- elif m == 1:
- z = "lambda u: u * u"
- else:
- z = "lambda u: (lambda u: u * u)((%s)(u))" % F(m)
- return z
-
-source = F(5)
-print source
-eval(source)(2)
-def F(k):
- if k == 0:
- z = "lambda u: 1"
- elif k == 1:
- z = "lambda u: u"
- else:
- m = k / 2
- if m == 0:
- z = "lambda u: 1"
-
- elif odd(k):
- if m == 1:
- z = "lambda u: u * u * u"
- else:
- z = "lambda u: (lambda fu, u: fu * fu * u)((%s)(u), u)" % F(m)
- else:
- if m == 1:
- z = "lambda u: u * u"
- else:
- z = "lambda u: (lambda u: u * u)((%s)(u))" % F(m)
- return z
-
-source = F(5)
-print source
-eval(source)(2)
-for n in range(7):
- source = F(n)
- print n, '%2i' % eval(source)(2), source
-So that gets pretty good, eh?
-But looking back at the definition in Joy, it doesn't seem easy to directly apply this technique to Joy code:
- -Ft == [over] dip *
-Fb == [[sqr] dip 2 //] dip
-Fw == [pop odd] [Ft] [] ifte Fb
- F == 1 [pop] [Fw] while popopd
-
-
-
-But a direct translation of the Python code..?
- -F == [
- [[0 =] [pop 1]]
- [[1 =] []]
- [_F.0]
- ] cond
-
-_F.0 == dup 2 // [
- [[0 =] [pop 1]]
- [[pop odd] _F.1]
- [_F.2]
- ] cond
-
-_F.1 == [1 =] [pop [dup dup * *]] [popd F [dupdip over * *] cons] ifte
-_F.2 == [1 =] [pop [dup *]] [popd F [i dup *] cons] ifte
-
-
-
-Try it:
- -5 F
-5 [ [[0 =] [pop 1]] [[1 =] []] [_F.0] ] cond
-5 _F.0
-5 dup 2 // [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-5 5 2 // [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-
-5 2 [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-5 2 _F.1
-5 2 [1 =] [popop [dup dup * *]] [popd F [dupdip over * *] cons] ifte
-5 2 popd F [dupdip over * *] cons
- 2 F [dupdip over * *] cons
-
-2 F [dupdip over * *] cons
-
-2 F
-2 [ [[0 =] [pop 1]] [[1 =] []] [_F.0] ] cond
-2 _F.0
-2 dup 2 // [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-2 2 2 // [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-2 1 [ [[0 =] [pop 1]] [[pop odd] _F.1] [_F.2] ] cond
-2 1 _F.2
-2 1 [1 =] [popop [dup *]] [popd F [i dup *] cons] ifte
-2 1 popop [dup *]
-[dup *]
-
-
-2 F [dupdip over * *] cons
-[dup *] [dupdip over * *] cons
-[[dup *] dupdip over * *]
-
-
-And here it is in action:
- -2 [[dup *] dupdip over * *] i
-2 [dup *] dupdip over * *
-2 dup * 2 over * *
-2 2 * 2 over * *
-4 2 over * *
-4 2 4 * *
-4 8 *
-32
-
-
-
-
-So, it works, but in this case the results of the partial evaluation are more elegant.
- -hylomorphism():¶def hylomorphism(c, F, P, G):
- '''Return a hylomorphism function H.'''
-
- def H(a):
- if P(a):
- result = c
- else:
- b, aa = G(a)
- result = F(b, H(aa))
- return result
-
- return H
-With abuse of syntax:
- -def hylomorphism(c):
- return lambda F: lambda P: lambda G: lambda a: (
- if P(a):
- result = c
- else:
- b, aa = G(a)
- result = F(b)(H(aa))
- return result
- )
-def hylomorphism(c):
- def r(a):
- def rr(P):
- if P(a):
- return lambda F: lambda G: c
- return lambda F: lambda G: (
- b, aa = G(a)
- return F(b)(H(aa))
- )
- return rr
- return r
-def hylomorphism(c):
- def r(a):
- def rr(P):
- def rrr(G):
- if P(a):
- return lambda F: c
- b, aa = G(a)
- H = hylomorphism(c)(aa)(P)(G)
- return lambda F: F(b)(H(F))
- return rrr
- return rr
- return r
-def hylomorphism(c):
- def r(a):
- def rr(P):
- def rrr(G):
- if P(a):
- return "lambda F: %s" % (c,)
- b, aa = G(a)
- H = hylomorphism(c)(aa)(P)(G)
- return "lambda F: F(%(b)s)((%(H)s)(F))" % locals()
- return rrr
- return rr
- return r
-hylomorphism(0)(3)(lambda n: n == 0)(lambda n: (n-1, n-1))
-def F(a):
- def _F(b):
- print a, b
- return a + b
- return _F
-F(2)(3)
-eval(hylomorphism(0)(5)(lambda n: n == 0)(lambda n: (n-1, n-1)))(F)
-eval(hylomorphism([])(5)(lambda n: n == 0)(lambda n: (n-1, n-1)))(lambda a: lambda b: [a] + b)
-
-hylomorphism(0)([1, 2, 3])(lambda n: not n)(lambda n: (n[0], n[1:]))
-hylomorphism([])([1, 2, 3])(lambda n: not n)(lambda n: (n[1:], n[1:]))
-eval(hylomorphism([])([1, 2, 3])(lambda n: not n)(lambda n: (n[1:], n[1:])))(lambda a: lambda b: [a] + b)
-
-
-
-def hylomorphism(c):
- return lambda a: lambda P: (
- if P(a):
- result = lambda F: lambda G: c
- else:
- result = lambda F: lambda G: (
- b, aa = G(a)
- return F(b)(H(aa))
- )
- return result
- )
-def hylomorphism(c):
- return lambda a: (
- lambda F: lambda P: lambda G: c
- if P(a) else
- lambda F: lambda P: lambda G: (
- b, aa = G(a)
- return F(b)(H(aa))
- )
- )
-def hylomorphism(c):
- return lambda a: lambda G: (
- lambda F: lambda P: c
- if P(a) else
- b, aa = G(a)
- lambda F: lambda P: F(b)(H(aa))
- )
-