337 lines
8.6 KiB
ReStructuredText
337 lines
8.6 KiB
ReStructuredText
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 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``.)
|
||
|
||
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 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.
|
||
|
||
“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
|
||
|
||
(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 ...]
|
||
|
||
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 I’m not going to worry about
|
||
it. (And I think the Categories can deal with it anyhow? Incremental
|
||
evaluation, yeah?)
|