Thun/docs/notebooks/The_Four_Operations.rst

337 lines
8.6 KiB
ReStructuredText
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

The Four Fundamental Operations of Definite Action
==================================================
All definite actions (computer program) can be defined by four
fundamental patterns of combination:
1. Sequence
2. Branch
3. Loop
4. Parallel
Sequence
--------
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``.
Branch
------
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 doesnt *have*
to be Boolean because Pythons 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``.)
Loop
----
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
Parallel
--------
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 couldnt 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
Elliotts “Compiling to Categories” paper, et. al.)
Just a thought, if you ``cleave`` two jobs and one requires more time to
finish than the other youd like to be able to assign resources
accordingly so that they both finish at the same time.
“Apply” Functions
~~~~~~~~~~~~~~~~~
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
(Im 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`` couldnt 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 ...]
Handling Other Kinds of Join
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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.
first-to-finish
^^^^^^^^^^^^^^^
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.
“Fulminators”
^^^^^^^^^^^^^
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 Im not going to worry about
it. (And I think the Categories can deal with it anyhow? Incremental
evaluation, yeah?)