The kcats Programming Language
Table of Contents
- 1. Summary
- 2. Examples
- 3. Benefits
- 4. Getting Started
- 5. Fundamentals
- 5.1. Key terminology
- 5.2. The stack
- 5.3. Exploring
- 5.4. Data types
- 5.5. Stack motion
- 5.6. Cloning and dropping
- 5.7. Programs that write programs
- 5.8. Looping and branching
- 5.9. Argument order
- 5.10. Item hiding
- 5.11. Stack effect control
- 5.12. 'down' and 'deep' variants
- 5.13. Programmable runtime
- 5.14. Error handling
- 5.15. Your own words
- 5.16. Generators
- 5.17. Coordination and Input/Output
- 6. More Examples
- 7. Contributing
- 8. FAQ
- 9. Issues
- 9.1. DONE Build without using emacs interactively
- 9.2. DONE Remove platform interop from lexicon
- 9.3. DONE 'unassign' doesn't take a keylist, only a single key
- 9.4. DONE More support for nested/related envs
- 9.5. TODO Graphical environment browser/editor
- 9.6. TODO Code distribution method
- 9.7. DONE Clean up all the vector conversion
- 9.8. DONE org-babel-execute for kcats
- 9.9. INPROGRESS At least one example for each word in lexicon
- 9.10. INPROGRESS Prime number sieve example
- 9.11. TODO FAQ entry about composability
- 10. Acknowledgements
- 11. Roadmap Notes
1. Summary
A simple, powerful, beginner-friendly programming language.
2. Examples
2.1. Produce fibonacci sequence
Here we show a local word definition fibonacci
which accepts a
number n
(how many items in the sequence to produce) and produces a
list (the sequence).
Then we call the word with n = 20
.
[[fibonacci [[1 0 [[+] shielddown swap clone] [float] dive taker collect] shielddown]]] [20 fibonacci] let execute
[1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765]
2.2. Sequence operations
Find highest priced of all products rated 4.5 or higher, and in stock
[[[[name "Mouse"] [price 50] [rating 4.4] [inStock yes]] [[name "Laptop"] [price 1000] [rating 4.7] [inStock yes]] [[name "Desktop"] [price 1500] [rating 4.9] [inStock []]] [[name "Tungsten Cube"] [price 2500] [rating 3.4] [inStock yes]] [[name "Keyboard"] [price 70] [rating 4.8] [inStock []]] [[name "Monitor"] [price 200] [rating 4.5] [inStock yes]] [[name "Camera"] [price 600] [rating 4.3] [inStock yes]]] ;; make a generator [take] ;; keep only rating >= 4.5 AND inStock [[[[rating] lookup 4.5 >=] [[inStock] lookup]] [execute] every?] keep ;; find the remaining product with highest price [[pair [[price] lookup] map unwrap >] [] [swap] if drop] fold] shield
[[name "Laptop"] [price 1000] [rating 4.7] [inStock yes]]
3. Benefits
3.1. Easy for beginners
Kcats is designed to require learning as few concepts as possible. It's made with:
- Stacks
- Data values (numbers, strings, bytes)
- Lists
- Functions
- Pipes
All of these are combined in various ways to achieve any type of expression, the same way a few types of lego bricks can be combined to make any object.1
Kcats code is just english words, quotation marks, [
and ]
. No other
symbols. 2
Format code however you want, kcats doesn't care.
Interactive development is encouraged. The documentation for the standard library can be queried with the language itself. See 5.3.
3.2. Powerful
Kcats' metaprogramming facilities (programs that write programs) allow you to express yourself succinctly.
3.3. Excels in the small
There is zero ceremony. A program can easily be a single line consisting of a few words:
"contacts.txt" slurp read [[age] lookup 55 >=] filter
That reads a database file of contacts, and prints only those contacts at least 55 years old. The program is 8 words.
You don't need a "main" method, or class declaration or any other boilerplate. You only need to specify what the program is actually supposed to do.
3.4. Features
[X]
Automatic memory management[X]
Multithreading[X]
Error handling[X]
Metaprogramming[X]
Channels/CSPs[X]
Introspection[X]
Filesystem I/O[X]
TCP/IP Socket I/O[X]
Generators[X]
Sequence library[X]
Cryptographic primitives[X]
Builtin debugging tools[X]
Serialization
3.5. Tradeoffs
Kcats is meant to handle personal automation tasks, where expressivity and simplicity matter far more than raw performance. It's not currently suitable for processing huge datasets or high performance number crunching, and likely never will be.
4. Getting Started
4.1. Builds
4.1.1. Download pre-built binaries
See Releases. Binaries currently do not have installers, you will need
to place the executable in a convenient place and chmod
it (on mac/linux).
4.1.2. Build from source
You can skip this section if you're using pre-built binaries described above.
4.1.2.1. Dependencies
- emacs
- rustc
- cargo
4.1.2.2. Creating the source
The source is contained within these org files, along with annotations and other non-code information. To generate the source code (that the runtime or compiler needs as separate files) before running or building, run the following command:
emacs --batch --load org --load tangle.el
4.1.2.3. Building
See Building
4.2. Using
See Using
4.3. Developing
4.3.1. Browsing the source
It can be checked out from git or viewed here: Production Source
4.3.2. Emacs IDE
See emacs-ide.org
in the source tree for more info.
5. Fundamentals
5.1. Key terminology
- item
- A unit of information (aka value) of various types: numbers,
strings, byte arrays, words, characters, and lists. (examples:
5
,"Bob"
,swap
,[1 2 3]
) - list
- An item that contains other items, in a particular order
(delimited by square braces). Example:
[a "foo" 2]
. - program
- a list of instructions intended to be carried out by a
machine. (example:
[[odd?] filter]
). - stack
- A list with a first-in, first-out interface. This is where the program stores all the data it needs to manipulate.
- word
- causes the program to do something, usually taking some
items from the top of the stack, and using them to create new
stack items. (examples:
swap
,+
,dip
) - axiom word
- A word not defined in terms of other words.
- definition
- what a word is supposed to do, represented either in the base language for axiom words, or as a program.
- dictionary
- a set of available words and their definitions.
- environment
- the entire state of an executing program, which includes a stack, program, and dictionary.
- pipe
- A conduit to communicate between environments, and to the outside world. Items are put into pipes and emerge somewhere else (another environment, a file on disk, a remote machine, etc).
5.2. The stack
Kcats uses a stack to keep track of all the values it needs to work with, instead of variables and function parameters. To manipulate data, you put it onto a stack, and then words operate on the items at the top of the stack (which might remove, shuffle, replace, or add new items). If you're familiar with functions in other languages, that's all words are - they're a function of the current stack, and they return a new stack.
Here's a simple example. If we mentally execute the program below, we
first put 1
onto the stack. Then we put 2
onto the stack. 2
is now on
top of 1
. Then we put the word +
onto the stack, where it will consume
the 2
and the 1
, and leave their sum, 3
.
1 2 +
3
Multiple steps are accomplished just by adding more words and
data. For example, in the program below we can add 1
and 2
(leaving 3
on the stack), and then multiply by 5
, leaving 15
.
1 2 + 5 *
15
Here's how it would look step by step (where the |
separates the
program that hasn't run yet - on the right, from the stack on the
left). The stack's top item is just to the left of the |
.
;; stack | remaining program ;; ------|-------------------- | 1 2 + 5 * 1 | 2 + 5 * 1 2 | + 5 * 3 | 5 * 3 5 | * 15 |
When there is nothing remaining to the right of the |
, the program
is finished. The result is what is left on the stack (in this case
15
).
Note the stack can end up with multiple items. When it's printed, it will always start with the top of the stack - the last thing in is the first thing out.
1 2 3
3 2 1
Lists are denoted with square brackets, like [1 2 3]
. When
encountered, they just go onto the stack as a single unit. Words can
operate on lists once the list is on the stack. You can see below the
word join
combines two lists.
[1 2 3] [4 5] join
[1 2 3 4 5]
5.3. Exploring
5.3.1. Overview
Now that we know the very basics, we can explore and learn as we go. Kcats lets you treat the standard library (the dictionary) as data, and you can process it with… itself. Documentation is in there. You just need to know how to ask for it. So here's how you do it, and you'll understand how exactly it works later.
In all the examples in this document, you can run them on the command
line, by running kcats -p
, followed by the program in single quotes,
like this:
kats -p 'words [first] map [] sort'
In case you want to view it in its entirety, the standard library is part of the source, it lives in the lexicon file.
5.3.2. What words or functions are available?
This program retrieves the dictionary of the current environment, and prints just the name of each, sorted in alphabetical order.
words [first] map [] sort
[* + - / < <= = > >= abs addmethod advance and animate any? assemble assert assign association association? attend autoformat bail bits both both? branch break breakpoint butlast bytes? cache catcher ceiling clone clonedeep clonedown close collect combinations compare confine contains? count cram cut database days dec decache decide decodejson decorate decorated definition dictionary dictmerge dip dipdeep dipdown dive divedeep divedown draft drop dropdeep dropdown dropper dump each emit empty empty? encode encodejson encodenumber encodestring ends? entry environment environment? error? eval-step evaluate even? evert every? execute exp fail file-in file-out filter finished? first flatten flip float floor fold format frequencies future generate generator get group handle handoff hashbytes heatmap hours if inc indexed indexer indexof inject inscribe inspect integers interpose intersection into join joiner keep label last let liberate list? log lookup loop map max method? milliseconds min minutes mod module negative? not number number? odd? or over pack pad pair pair? pairwise partition persist pipe-in pipe-out pipe? pop positive? prepend prime primrec print put quot radix range read receiver recover recur rem repeat resolve rest restore retry reverse round second seconds select sender serversocket set set? shield shielddeep shielddown sink siphon skipper sleep slice slurp snapshot socket something? sort sort-indexed spawn spit split sprint sqrt stage standard starts? step stepper string string? swap swapdown take taker timer times timestamps toe tos tracer triplet tunnel type unassign under until unwrap update updates use using value walk when while within? word word? words wrap xor yes zero? zip]
Even though the rest of this document will explain a lot of these words and how they work, the above program does the following:
words
: retrieves the dictionary words, and puts a copy of them on the stack[first] map
: for each item in the dictionary words (which is a key/value pair, where the key is the word and the value is the definition) take thefirst
, which is the word.[] sort
: sort takes a program to transform each item in the list it's sorting, to use for comparison. We want to use the word itself for comparison, so we don't need to transform it at all, hence the empty program.
5.3.3. What inputs/outputs does a particular word have?
The specification of a word's input and output types is stored in the
dictionary too. It's in the property called spec
. Let's say you're
interested in the word swap
.
words [swap spec] lookup
[[[item a] [item b]] [[item b] [item a]]]
What this program does is fetches the dictionary words, then looks up
the swap
definition, then within that definition, looks up the spec
property.
In the result, what we have here is two lists - the spec of the input, and the spec of the output.
The input spec is [[item a] [item b]]
. The output spec is [[item b]
[item a]]
. What it's telling you is that it requires two items on the
stack, any two, we'll call them a
(on top) and b
beneath. There may be
more items below that but they won't be touched. When swap is
finished, a
and b
will have their places swapped so that b
is on
top. And in fact that's what we get:
"b" "a" swap
"b" "a"
Remember the top of the stack is printed first, and so b
is now on top.
5.3.4. What are some example usages of a word?
words [swap examples] lookup
[[[1 2 3 swap] [1 3 2] "Swap top two items"]]
This is a list of examples, and each example is a triple:
- A program that calls the given word
- A program that doesn't call the word that gives the same result 3
- A description of what the example is demonstrating
5.3.5. Continuing exploration
Use the same technique to explore other words. You can simply replace
the word in the code snippets above with some other word. Here's how
you find the examples for =
, which tests for equality of two items -
just replaced swap
with =
.
words [= examples] lookup
[[[1 2 =] [[]] "Different Numbers are not equal"] [[1 1 =] [yes] "Same numbers are equal"] [[1 1 =] [yes] "Same value integer and float are equal"] [[[1] [] =] [[]] "Number and Nothing are unequal"] [[[1 [[]]] [1 [[]]] =] [yes] "Same nested list with numbers are equal"] [[[1 ["foo"]] [1 ["foo"]] =] [yes] "Same nested list with string are equal"] [["hi" "hi" =] [yes] "Same strings are equal"] [["hi" "there" =] [[]] "Different strings are unequal"] [[\h \h =] [yes] "Same characters are equal"] [[\h \i =] [[]] "Different characters are unequal"] [["hi" encode "hi" encode =] [yes] "Same bytes are equal"] [["hi" encode "there" encode =] [[]] "Different bytes are unequal"] [[[] yes =] [[]] "Different booleans unequal"] [[[1 ["foo"]] [1 ["bar"]] =] [[]] "Nested lists with different strings are unequal"] [[[] [] =] [yes] "'Nothing' is equal to itself"] [[[] [] association =] [yes] "List/Association empty container types are equal"] [[[] [] set =] [yes] "List/Set empty container types are equal"] [[[[a b]] [[a b]] association =] [[]] "Nonempty List/Association types are unequal"] [[[1 2 3] set [3 1 2] set =] [yes] "Sets constructed from different lists are equal"]]
5.4. Data types
5.4.1. Types
5.4.1.1. Words
In kcats, words can be in the dictionary or not.
The first type, verbs, are used directly in the execution of programs,
like clone
and swap
:
1 clone 2 swap
1 2 1
The second type are used inside lists, often as field names. These words can't be executed because they're not in the dictionary and not intended to be. They're used more like you'd use strings or keywords in other programming languages.
[foo bar baz] [quux] unwrap put
[foo bar baz quux]
Note the use of unwrap
here. What's wrong with just trying to put
quux
directly into the list?
[foo bar baz] quux put
[[type error] [asked [quux]] [reason "word is not defined"] [unwound [quux put]]] [foo bar baz]
The problem is when kcats encounters a word during execution, it checks the dictionary to see what to do. If the word isn't isn't in the dictionary, that's an error. When a word is inside a list, kcats treats the whole list as an item and doesn't look inside the list.
What we want is to get quux
onto the stack by itself without actually
executing it. We can do that with [quux] unwrap
. The word unwrap
does
just what it says, removes the list wrapper and leaves a bare word on
the stack. No error occurs here because the bare word is already on
the stack, it's not part of a program. Another way to go about this is
to use join
so we don't need unwrap
:
[foo bar baz] [quux] join
[foo bar baz quux]
5.4.1.2. Booleans
Most programming languages have special values true
and false
. Kcats
does not. In kcats decision making, an empty container is negative
(acts like false
), and anything else is affirmative (acts like true)
.
[] ["yes"] ["no"] branch
"no"
versus
"anything" ["yes"] ["no"] branch
"yes"
But wait, what about this?
3 odd?
yes
Some words will return the word yes
, but it's not really a boolean,
it's just the word yes
which has no special meaning to kcats other
than that it's an arbitrary affirmative value. For convenience, yes
is
in the dictionary, so you do not have to quote it. It puts itself on
the stack, the same way a number or string or list would. The word no
is not in the dictionary, since it would need special handling to be
negative when every other word is affirmative. Use empty list []
to
mean no
.
5.4.1.3. Numbers
Integers and floats are supported (64 bit).
Supported math operations include +
, -
, *
, /
, mod
, rem
, min
, max
, abs
,
inc
, dec
, <
, >
, <=
, >=
, ceil
, sqrt
, odd?
, even?
.
5.4.1.4. Containers
- Overview
Containers are types that contain other items. Some containers are homogenous. For example, strings are a container for characters and if you try to put anything else in it, that's an error. Other containers can hold any kind of item, for example lists and sets.
- Lists
Lists are multiple items bound up into a single unit, where their order is maintained. Lists are heterogenous and can accept any type, including other lists.
- Comprehension
See the word
step
, which runs the same program on each item in a list.0 [12 6 13 7 5] [+] step
43
Similar to
step
, but more strict, ismap
, which only allows the program to work on a given item and can't permanently alter the rest of the stack. Use that to transform each item in a list, in the same way (in this case showing the remainder when dividing by 5).[12 6 13 7 5] [5 mod] map
[2 1 3 2 0]
- Comprehension
- Strings
Strings are a container for character types, and work much like in other programming languages. Most of the functions that work on other containers will work on strings.
"Hello World!" count
12
"Hello World!" first
\H
"Hello World" 0 5 slice
"Hello"
- Bytes (byte array)
Byte arrays are a sort of "lowest common denominator" data format. It's what you use to interact with files or sockets. Byte arrays are printed in base64 encoding and denoted as such with the
#b64
tag. You can encode any item into bytes."Hello World!" encode
#b64 "SGVsbG8gV29ybGQh"
[1 2 3] encode
#b64 "MSAyIDM"
and you can treat those byte arrays as lists of integers:
#b64 "SGVsbG8gV29ybGQh" take
72 #b64 "ZWxsbyBXb3JsZCE"
72 is the ASCII encoding for
H
. - Associations
An association is made from a list of pairs, like this:
[[name "Alice"] [age 24] [favorite-color "brown"]]
However there are some words you can use that make this list behave a bit differently than a normal list. For example:
[[name "Alice"] [age 24] [favorite-color "brown"]] [age] 25 assign
[[age 25] [favorite-color "brown"] [name "Alice"]]
Here we use
assign
to reset Alice's age - it does not simply add a new item to the list. It will find the existing key and replace it. It will create a new item only if the key didn't already exist:[[name "Alice"] [age 24] [favorite-color "brown"]] [department] "Sales" assign
[[age 24] [department "Sales"] [favorite-color "brown"] [name "Alice"]]
Note that the order of the items is not preserved. Here we take a key/value pair out of the association but which one we get is arbitrary:
[[name "Alice"] [age 24] [favorite-color "brown"]] [department] "Sales" assign take
[name "Alice"] [[age 24] [department "Sales"] [favorite-color "brown"]]
Once you treat a list as an association, it "sticks" (see 5.4.3 for details). It acts like an association from then on, and order is no longer maintained.
We can improve upon our example that incremented Alice's age (presumably after her birthday) with the word
update
. That will run a program on the item of whatever key (or keys) you specify.[[name "Alice"] [age 24] [favorite-color "brown"]] [age] [inc] update
[[age 25] [name "Alice"] [favorite-color "brown"]]
Note that associations and lists look the same when printed, but testing them for equality will reveal they are not the same:
[[name "Alice"] [age 24] [favorite-color "brown"]] [age] [inc] update [[name "Alice"] [age 25] [favorite-color "brown"]] =
[]
Here we are comparing an association with a list. The
=
operator has no way of knowing whether you want the list semantics (which does care about order), or the association semantics (which doesn't care about order). It defaults to the more strict rules, so they are not equal.The act of using a list as an association (by applying words to it like
assign
orupdate
) will convert it to an association, but what if you just want to convert a list to an association, without doing anything else?You can use the word
association
to convert the list to an association:[[name "Alice"] [age 24] [favorite-color "brown"]] [age] [inc] update [[name "Alice"] [age 25] [favorite-color "brown"]] association =
yes
- Sets
Sets are made to test for membership, and do not care about order.
["Larry" "Curly" "Moe"] set "Moe" contains?
yes
If you add an item to a set, but it's already there, nothing changes.
["Larry" "Curly" "Moe"] set "Curly" put
["Larry" "Moe" "Curly"]
You can
take
from a set but since order doesn't matter, you get an arbitrary item.1 20 1 range set take
15 [8 3 18 16 6 13 11 12 4 7 2 10 14 17 19 5 9 1]
5.4.1.5. Errors
See 5.14
5.4.1.6. Pipes
See 5.17
5.4.2. Traits
There are words that operate on multiple types, and it's helpful to talk about what those types have in common. Specs use these traits to describe groups of types that a word will accept or produce.
5.4.2.1. Dispenser
Containers from which you can take out items, one by one. Includes:
- Strings - dispenses Characters
- Bytes - dispenses Integers
- Lists - dispenses Items
- Associations - dispenses key/value pairs
- Sets - dispenses Items
- Out Pipes - dispenses Bytes
Tunnels - dispenses Bytes
We can query the dictionary to see what words take a
dispenser
:
words [second [spec] lookup first set [dispenser] unwrap contains?] filter
[[assemble [[definition [[*2 [take] **1 collect] pack shield]] [examples [[[[1 2 3 4 5] [[odd?] keep [inc] each] assemble] [[2 4 6]] "Assemble a sequence from a dispenser and a stack of generators"]]] [spec [[[program generators] dispenser] [sized]]]]] [take [[definition builtin-function] [examples [[[["a" "b" "c"] take] [["b" "c"] "a"] "Take a string from a list"] [[[1 2 3] take] [[2 3] 1] "Take a number from a list"] [[[[a "foo"] [b "foo"] [c "foo"]] take dropdown second] ["foo"] "Take an entry from association is nondeterministic"] [[[1 3 5 7 9] set take dropdown odd?] [yes] "Take item from set is nondeterministic"]]] [spec [[dispenser] [item dispenser]]]]] [step [[definition builtin-function] [examples [[[1 [2 3 4] [*] step] [24] "Step through numbers doing arithmetic"] [[1 [] [*] step] [1] "Stepping through empty list is no-op"]]] [spec [[program dispenser] [*]]]]]]
5.4.2.2. Receptacle
Containers into which you can put items, one by one. Includes:
- Strings - accepts Character
- Bytes - accepts Integer
- Lists - accepts Item
- Associations - accepts key/value pairs
- Sets - accepts Item
- In Pipes - accepts Bytes
- Tunnels - accepts Bytes
words [second [spec] lookup first set [receptacle] unwrap contains?] filter
[[put [[definition builtin-function] [examples [[[[] 1 put] [[1]] "Put integer into empty list"] [[[1 2 3] 4 put] [[1 2 3 4]] "Put integer into list"] [["foo" \d put] ["food"] "Put character into string"] [["foo" encode 32 put string] ["foo "] "Put byte into byte array"]]] [spec [[item receptacle] [receptacle]]]]]]
Supported words:
put
5.4.2.3. Sized
Containers whose items can be counted. Includes:
- Strings
- Bytes
- Lists
- Associations
- Sets
Just list the names of the words that use sized
, since there's a lot:
words [second [spec] lookup first set [sized] unwrap contains?] filter [first] map [] sort
[assign butlast count cut empty environment every? fail filter get intersection into join lookup map pad persist prepend rest sort sort-indexed split]
5.4.2.4. Ordered
Containers whose items are kept in a specific order. Includes
- Strings
- Bytes
- Lists
words [second [spec] lookup first set [ordered] unwrap contains?] filter
[[last [[definition builtin-function] [examples [[[[3 4 5 6] last] [6] "Get last item of list"] [["foo" last [\o]] "Get last item of string"] [[[] last] [[]] "Get last item of empty list -> Nothing"]]] [spec [[ordered] [item]]]]] [slice [[definition builtin-function] [examples [[["foobar" 0 3 slice] ["foo"] "Slice a string with valid indices"] [["foobar" 0 7 slice] [[]] "Slice a string with index past end -> Nothing"] [["foobar" encode 0 3 slice] ["foo" encode] "Slice a byte array with valid indices"] [[[a b c d e] 0 3 slice] [[a b c]] "Slice a list with valid indices"]]] [spec [[integer integer ordered] [ordered]]]]] [ends? [[definition [[reverse] both starts?]] [examples [[["abcd" "cd" ends?] [yes] "String ends with matching string"] [["abcd" "" ends?] [yes] "String ends with empty string"] [["abcd" "bb" ends?] [[]] "String doesn't end with non-matching string"] [[[1 2 3 4] [3 4] ends?] [yes] "List ends with matching list"]]] [spec [[ordered ordered] [boolean]]]]] [reverse [[definition builtin-function] [examples [[[[1 2 3] reverse] [[3 2 1]] "Reverse a list"] [["123" reverse] ["321"] "Reverse a string"]]] [spec [[ordered] [ordered]]]]] [interpose [[definition [[] flip [swap pair join [pop] shield] step drop pop drop]] [examples [[[[foo bar baz] "hi" interpose] [[foo "hi" bar "hi" baz]] "Interpose string between words"] [[[] "hi" interpose] [[]] "Empty list is a no-op"] [[[foo] "hi" interpose] [[foo]] "Single item list is a no-op"]]] [spec [[item ordered] [ordered]]]]] [second [[definition builtin-function] [examples [[[[4 5 6] second] [5] "Get second item of list"] [["foo" second [\o]] "Get second item of string"] [[[] second] [[]] "Get second item of empty list -> Nothing"]]] [spec [[ordered] [item]]]]] [first [[definition [take dropdown]] [examples [[[[4 5 6] first] [4] "Get the first item of a list"] [["foo" first] [\f] "The first item of a string is the first character"] [[[] first] [[]] "The first item of an empty list is Nothing"]]] [spec [[ordered] [item]]]]] [pop [[definition builtin-function] [examples [[[["a" "b" "c"] pop] [["a" "b"] "c"] "Pop last string from list"] [[[1 2 3] pop] [[1 2] 3] "Pop last number from list"]]] [spec [[ordered] [item ordered]]]]]]
5.4.3. Promotion
Data types are automatically converted when needed.
For example, if you have a list of pairs and you use the word lookup
,
it assumes your intention is to use the list as an associative data
type, so it will be automatically converted, and remain converted
after lookup
completes.
You can often tell by the spec when the return type is a promoted type:
words [assign spec] lookup
[[[item value] [list keys] sized] [association]]
Here you can see that the spec for assign
takes a sized
and returns an
association
. This allows you to do things like this:
[[name "Susie"] [age 25]] [sport] "bowling" assign
[[age 25] [name "Susie"] [sport "bowling"]]
The initial value of [[name "Susie"] [age 25]]
is not an associative
,
it's just a list
. You could explicitly convert it using the word
association
but assign
will do it for you, because it is a function
that operates on an associative type.
Note that the conversion can fail, because converting to associative
requires that you have a list of pairs. If you don't, that's an error:
["foo" "bar"] [age] 25 assign
[[actual "foo"] [asked [pair]] [handled []] [reason "type mismatch"] [type error] [unwound [assign]]] 25 [age] ["foo" "bar"]
The most common promotion is from list
to associative
but there are
others.
5.5. Stack motion
Often you have all the data a word needs on the stack, but it's in the wrong order. There's lots of handy words to help there.
- swap
- swap the top two items
- float
- float the 3rd item up to the top
- sink
- sink the top item down to 3rd
- flip
reverse the top 3 items
These words can also be combined with
dip
and its variants to reach deeper into the stack.
5.6. Cloning and dropping
When you're done with an item, you can drop
it, which eliminates it
from the top of the stack. If you know a word will consume an item you
need afterward, you can clone
it.
5.7. Programs that write programs
The most important expressive feature of kcats is that you can manipulate programs exactly the same way as you can any other data.
One thing you can do with a list, is treat it like a program and
execute
it. Notice that on the 5th and 6th line of the execution trace
below, the word execute
takes the list from the top of the stack on
the left, and puts its contents back on the right, making it part of
the program remaining to be run!
;; stack | remaining program ;; ---------|-------------------- | 4 5 6 [* +] execute inc 4 | 5 6 [* +] execute inc 4 5 | 6 [* +] execute inc 4 5 6 | [* +] execute inc 4 5 6 [* +] | execute inc 4 5 6 | * + inc 4 30 | + inc 34 | inc 35 |
Note that, when * +
gets moved back to the program, it went in
front of inc
. The program acts just like a stack - the last thing in
is the first thing out.
The same way we used join
to combine two lists, we can combine two
small programs into one, and then execute
it:
4 5 6 [+] [*] join execute
44
Note that words inside lists don't perform any action when the list is put on the stack. You can think of it as a quotation - a message being being passed along, not acted upon.
5.8. Looping and branching
5.8.1. if
if
takes 3 programs from the stack:
condition
a program whose result decides which branch to take- the
yes
branch - the
no
branch
An important detail: after condition
runs, its stack effects are
erased. The yes
or no
branch runs on whatever was underneath the 3
programs at the start.
1 2 3 [odd?] ["it's odd"] ["it's even"] if
"it's odd" 3 2 1
Notice how the 3
is still there. The word odd?
normally consumes its
argument.
3 odd?
yes
Here's a more extreme example:
1 2 3 [drop odd?] ["it's odd"] ["it's even"] if
"it's even" 3 2 1
See how we drop
the 3
and test odd?
against 2
instead? Normally we'd
have consumed both the 3
and the 2
but the conditional is not allowed
to have any stack effect. See 5.11.
5.8.2. loop
Loops take a program to run, and a boolean (See 5.4.1.2) condition
whether to run the program. If the condition is false, the program
never runs. If it's true, the program runs, and loop
expects another
boolean condition to be on top to see whether to run the program
again.
Note that the item on top only determines whether the program runs
again, it's dropped and not accessible to the program. If the program
needs it, be sure to clone
it. Usually it doesn't need that item for
anything except deciding whether to continue the loop, which is why
it's dropped automatically.
Here's an example:
1 yes [2 * clone 100 <] loop
128
Notice that loop
receives the program and yes
the first time. The
program never sees yes
, only the 1
underneath it - it multiplies it by
2, then clones it and checks if it's less than 100. If so, it drops
that boolean value and continues and multiplies the number beneath by
2 again, and so on, until the number is greater than or equal
to 100. Finally that false
value is dropped and the loop
is done,
leaving just the final number 128
.
5.8.3. while
Kcats also has while
, which is a bit higher level than loop
. Instead
of expecting a boolean value on top each time through, you provide a
condition program similar to what if
requires. while
runs the
condition program, if it leaves a affirmative value, the loop
continues. Like loop
, while
's body program does not have access to the
affirmative value.
1 [100 <] [2 *] while
128
Like if
, the condition program cannot permanently affect the stack. So
we don't need clone
like we did with loop
. After we compare the number
to 100, it's restored so the body program can see it on top.
5.8.4. until
It's just like while
but with the condition's logic reversed, so that
it stops when the condition is true.
1 [100 >=] [2 *] until
128
Unlike while
(which runs the body 0 or more times), until
will always
run it at least once.
1 [yes] [2 *] until
2
5.9. Argument order
Kcats stack-based nature can take a little getting used to, and the reversing of the order you wrote something is perhaps the biggest stumbling block.
Notice how if
is designed to have the conditional/true/false branch in
the order you expect when you write code. However remember if you
print the stack the order will be reversed - the false
program will be
on top, followed by the true
program, followed by the conditional
:
1 2 3 [drop odd?] ["it's odd"] ["it's even"] ;; if
["it's even"] ["it's odd"] [drop odd?] 3 2 1
This is a common theme in kcats, where argument order is designed to make the code readable - if a word takes multiple arguments, and the order matters, the "first" logical argument is not the top of the stack. Here's an example:
1 2 <
yes
When we write 1 2 <
, we mean "1 is less than 2". Even though the top
of the stack is 2, we don't consider 2 the "first" argument.
5.10. Item hiding
Sometimes you have a program that you don't trust with a certain stack item. Perhaps there's a password on the stack, and you're running an untrusted program given to you by someone else.
What if there was a way to hide that password behind your back such that the program never even knew it was there, and then restore it after the untrusted program was finished?
dip
takes an item on the top of the stack, and a program. It
temporarily hides the item, and runs the program. After the program is
done, it puts the item back on the stack.
1 2 "mypassword" [+] dip
"mypassword" 3
Notice the addition program could not access the password even if it tried. It isn't on the stack while it's executing, it's hidden away elsewhere in the runtime, temporarily.
dip
is very common in kcats, and it's used mostly in cases where you
don't actually care if a program reads a item, you just want the
item out of the way temporarily, and it's easier than finicky
swapping. However in cases where there is a trust issue, no amount of
swapping can fix the problem and you definitely should reach for dip
.
5.11. Stack effect control
Kcats provides some facilities to let you avoid tedious cloning of items to keep from losing them. Most words consume items from the stack to produce new items. Sometimes you'll still need those old items again later.
We saw earlier how if
runs a condition program, and no matter how
badly that program messes with the stack, that effect is wiped clean
and only the top result of that program remains.
That magic is not locked away inside if
- you can use it in your own
programs.
Earlier we showed how to examine the dictionary. Here's how you see
the definition of if
:
words [if definition] lookup
[[shield] dipdown branch]
if
runs the condition program with shield
. shield
runs the given
program, takes the top item and places it on top of the original stack
(before the program ran). Let's look at the first example of shield
-
remember each example shows two programs that produce the same result.
Here's how we view the first example of shield
.
words [shield examples] lookup first
[[1 2 3 [=] shield] [1 2 3 []]]
Here we're checking whether 2
and 3
are equal without consuming
anything.
Here's what it would look like without shield
:
1 2 3 =
[] 1
The 2
and 3
are consumed, leaving only the empty list to signal the
negative condition.
5.12. 'down' and 'deep' variants
There are words like dipdown
, shielddown
, swapdown
, dropdown
,
divedown
. What are those?
It's a modification of the original where the effect is one stack element further down from the original. What exactly is further down, depends on the word.
- swapdown
- swap not the top two items, but the 2nd and 3rd items
- dipdown
- hide not the top stack item, but the top two items
- shielddown
- instead of allowing no stack items to be consumed, allow one to be consumed.
- dropdown
- drops the 2nd item instead of the top item
- divedown
- hides the top two items but then floats the result back to the top above the previously hidden items
Similarly the deep
variants are one level even deeper than that:
- swapdeep
- swap the 3rd and 4th items
- dipdeep
- hide the top 3 items
- shielddeep
- protect all but the top two items
- dropdeep
- drops the 3rd item
5.13. Programmable runtime
5.13.1. Overview
One unusual and important feature of kcats is that you can program the runtime. What does that mean? It means you can control exactly how a program is executed.
Why would you want to do that? Well, one common use case is that you want to debug the program: you'd like to manually control execution while you examine the state of the program, to figure out where it's going wrong.
Another use case is security: you want to execute a program but not allow it to access things that programs normally can access (like the filesystem or network). You also don't want the program to be able to permanently redefine what words mean.
So how exactly do we alter how programs are executed? A kcats program keep track of 3 things as it is running, its state: the stack (the data the program is working with), the program (the remaining instructions left to be executed), and the dictionary (the words that have meaning in a program). We call the whole state an environment, and we can create and work with environments in kcats just like any other data, including executing the progams within them.
5.13.2. Environments
First let's look at how to create an environment:
[[program [1 2 +]]] environment
[[dictionary [[modules []] [words 249_entries]]] [program [1 2 +]] [stack []]]
Notice how `environment` takes an association and fills out the `dictionary` and `stack`. Why does it say `249entries`? Normally dictionaries would be printed out in their entirety, just like any other data, but because they're large (there are hundreds of words) by default the runtime prints it as the count of the words.
So this is an environment. What words are useful here? Well, first of all we can treat it as an association, which it is. Here we replace the last item in the program so we're subtracting instead of adding.
[[program [1 2 +]]] environment [program] [pop drop [-] join] update
[[dictionary [[modules []] [words 249_entries]]] [program [1 2 -]] [stack []]]
Ok, well, this is not that exciting, if environment
is just another
kind of association right? Point taken, but now let's use the word
`eval-step`:
[[program [1 2 +]]] environment eval-step
[[dictionary [[modules []] [words 249_entries]]] [program [2 +]] [stack [1]]]
That evaluates the environment one step! We can do two more steps to finish the program:
[[program [1 2 +]]] environment [eval-step] 3 times
[[dictionary [[modules []] [words 249_entries]]] [program []] [stack [3]]]
5.13.3. Debugging
5.13.3.1. The debugger
In kcats, we don't need an external debugger. We can debug our programs right in the kcats interpeter. We can specify the program to run and step through it.
Let's say this is the program we want to step through. This is how we'd normally run it:
0 1 3 inc 1 range [+] step
6
To debug, we put it into an environment item which we can then use
debugging words like advance
:
[[program [0 1 3 inc 1 range [+] step]]] environment [advance] 7 times eval-step
[[dictionary [[modules []] [words 249_entries]]] [program [+ [2 3] [+] step]] [stack [1 0]]]
Note that advance
is like step-over
in a traditional debugger, and
eval-step
is like step-into
. So above we advance until we reach the
word step
in the program, and then we step into it. We end up showing
the environment in the middle of execution. The stack has a program
[+]
on top, and the next word is execute
which will run that program.
You can also use a until
loop to run the program until an arbitrary
condition is hit. Here's one that runs the program until the number 4
is on the top of stack (note the handy word tos
shortcut)
[[program [0 1 3 inc 1 range [+] step]]] environment [tos 2 =] [eval-step] until
[[dictionary [[modules []] [words 249_entries]]] [program [+ [3] [+] step]] [stack [2 1]]]
You can do whatever you want with the environment data - you can retain the environment at every step, filter the steps, change them, and continue the execution from any arbitrary place.
It's particularly handy to save an environment at the "last known good" state and continue from there, instead of having to re-execute from the beginning each time.
Just as an example of what's possible, here we show only the states
where +
is about to be executed. stepper
is a generator that takes an
environment and generates all the steps of execution. Note top
means
"top of program", so it's keeping the states where +
is the next
item in the program.
[[[program [0 1 3 inc 1 range [+] step]]] environment stepper [top wrap [+] =] keep collect] shield
[[[dictionary [[modules []] [words 249_entries]]] [program [+ [2 3] [+] step]] [stack [1 0]]] [[dictionary [[modules []] [words 249_entries]]] [program [+ [3] [+] step]] [stack [2 1]]] [[dictionary [[modules []] [words 249_entries]]] [program [+ [] [+] step]] [stack [3 3]]]]
Then just to show that all these environments work on their own, we'll add some code to select the first one and step it forward. So we've essentially gone back in time and rolled forward again.
[[[program [0 1 3 inc 1 range [+] step]]] environment stepper [top wrap [+] =] keep collect] shield ;; add this first ;; to select the first env from above eval-step ;; advance one step
[[dictionary [[modules []] [words 249_entries]]] [program [[2 3] [+] step]] [stack [1]]]
We can even mess with the stack and the program:
[[[program [0 1 3 inc 1 range [+] step]]] environment stepper [top wrap [+] =] keep collect] shield ;; add this first ;; to select the first env from above [program 0] [-] unwrap assign ;; change + to - right before it is run ;; now step forward again eval-step
[[dictionary [[modules []] [words 249_entries]]] [program [[2 3] [+] step]] [stack [-1]]]
5.13.3.2. Other tools
The "good old fashioned" method of debugging a program is to put print
statements throughout the program to get insight into what's
happening.
You can do that in kcats too!
3 [odd?] ["it was odd" print inc] ["it was even" print 2 *] if
print
takes a string and will print it to standard out.
it was odd 4
There's also a way to print the current stack, it's called dump
, and
it has has no stack effect, so it's safe to put anywhere.
1 2 3 [odd?] [dump inc] [dump 2 *] if
[3 2 1] 4 2 1
5.14. Error handling
In kcats, when a program encounters an error, an error item is placed on the stack instead of the usual result.
2 3 "four" * +
[[actual "four"] [asked [number]] [handled []] [reason "type mismatch"] [type error] [unwound [* +]]] "four" 3 2
Notice the unwound
field contains the rest of the program that
remained when the error occurred.
We can fix the problem and continue, but only if we can stop the
unwinding before our entire program is unwound. We can do that using
the word recover
, which takes two programs: p
and r
, p
is run and if
it results in an error, the unwinding is limited to p
and then r
is
run. When r
runs, the error item is on the top of stack. If there is no
error, r
does not run.
In the program below, we recover by discarding the error and the
string "four", and replacing it with the number 4
. Then trying the
operations * +
again.
2 3 "four" [* +] [drop drop 4 * +] recover
14
The problem with the usage of recover
above is that we had to specify
the arithmetic words * +
twice - once in p
and again in r
in case they
failed the first time. Remember those operations are saved in the
unwound
field of the error, and we can access them and even execute
them. There is a word that does this for you: retry
: it takes an error
on the top of stack, and executes its unwound
program.
2 3 "four" [* +] [[drop 4] dip retry] recover
14
In the above program, after the error occurs, we discard the string
underneath the error and replace it with the integer 4
.
Sometimes you need to raise your own errors, you can do that with the
word fail
.
2 [odd?] ["ok"] [[[type error] [asked odd?] [reason "expected odd number"]] association fail] if 3 4 +
[[asked odd?] [handled []] [reason "expected odd number"] [type error] [unwound [3 4 +]]] 2
Sometimes you want to handle some errors but not others. There's no
error type matching like you'd find with java's catch
. You have to
recover, examine the error, and if it's one you don't want to handle,
re-activate it with fail
.
5.15. Your own words
You're not stuck with just the vocabulary in the starting environment. You can add your own vocabulary!
5.15.1. Concepts
One hard rule of kcats: "an environment cannot alter its own dictionary". That means, a program that's running can't do anything to change the meanings of words that come later in the program. This is in order for kcats to be able to provide a relatively trustworthy execution environment - you can load a library and be sure there's nothing it can do to alter the meaning of your program. Monkeypatching is not allowed.
So the question then is, if we can't alter the current dictionary, what can we do? You can run your program inside another environment with a different dictionary.
The word dictionary
retrieves the current dictionary and places it on
the stack. Then we can treat it like any other data and alter it. Then
we can create a new environment and evaluate
that environment, and
retrieve its stack.
Doing this with low level constructs looks like this:
;; Fetch the current dictionary dictionary ;; Create a new set of words [[square [clone *]]] draft ;; apply the change to the dictionary shielddown ;; The program to run with the new dictionary [9 square] ;; Create an environment and evaluate it [program dictionary] label environment evaluate ;; make the inner stack the new stack [stack] lookup restore
81
Obviously this is quite cumbersome, so there are higher level words to do all of this for you, like `let`.
5.15.2. New Words
The most straightforward and common change you can make to the dictionary, is to add a word that wasn't in there before, and use it in some limited scope after which it is no longer accessible.
The word let
is handy for small bits of code where you don't want to
repeat yourself:
[[square [clone *]]] [9 square 8 square +] let execute
145
Let's break this down. The word let
takes two arguments, a list of new
words paired with their definitions, and a program to run that uses
those words. We define a new word square
to mean [clone *]
, and then
we create a program that runs [9 square 8 square +]
inside an
environment with the new word square
defined. Then finally execute
that program to get 145
.
The list of new words can even refer to another word from the same list:
[[square [clone *]] [fourth [square square]]] [3 fourth] let execute
81
Words you're defining can refer to themselves - recursive functions are great!
[[factorial [[swap positive?] [[*] shielddown [dec] dip factorial] when]]] [9 1 factorial dropdown] let execute
362880
5.15.3. Altering meanings of existing words
Creating new words is relatively safe - presumably no one is using those words, so giving them meaning doesn't cause any confusion. It's when you start changing an existing meaning that things get hairy.
As we have seen, words perform actions, and those actions are
specified by other words. So a word foo
can use the word bar
as part
of its execution. So let's say I change the meaning of the word
bar
. Does that mean I changed the meaning of foo
as well (because bar
is part of the meaning of foo
)?
The answer is it depends.
In kcats, by default, the answer is no. When you are changing the meaning, it's for you own immediate use of the word. Let's go over some examples.
Let's say we want to alter the meaning of a rather important word
that's used all over the place in the standard library: swap
. "When I
say swap
I want to just insert the number 5."
[[swap [5]]] ["a" "b" "c" swap] let execute
5 "c" "b" "a"
Ok, straightforward enough, right? But what happens if we call another
word that uses swap
, like times
? First let's look at the definition of
times
, to see that it really does call swap
:
[times] definition
[swap [dec] swap put [dip] join [0 >] swap while drop]
It calls swap
quite a bit! Now let's alter the meaning of swap
and call times
.
[[swap [5]]] [["hi"] 3 times swap] let execute
5 "hi" "hi" "hi"
Notice that times
still works as expected! Even though it calls swap
internally, it didn't insert any 5
's. Only our own swap
did that.
You can even refer to the old behavior of a word when defining new behavior:
[[swap [5 swap]]] [["hi"] 3 times swap] let
"hi" 5 "hi" "hi"
Here we redefine swap
to mean "insert 5, and then do whatever swap did
before". This works even though swap
is an axiom word:
[swap] definition
builtin
There are two other words related to let
that are more flexible for
times when you need to do a bit more complex alterations of existing
meaning. One of the primary use cases is adding a method. You've got a
word that behaves differently depending on its argument and you want
to add a new behavior.
[[hash [[type [foo] unwrap =] [drop "foo" hash] addmethod]]] [[[foo myfoo]] association hash] revise "foo" hash =
yes
What have we done here? We're taking the word hash, which is just a decide
:
[hash] definition
[[[[bytes?] [hashbytes]] [[yes] [encode hash]]] decide]
See how it behaves differently for a byte array by calling hashbytes
and by default it calls encode
and then tries hash
again? So now we've
added a new logic branch there:
[[hash [[type [foo] unwrap =] [drop "foo" hash] addmethod]]] [[hash] definition] revise
[[[[type [foo] unwrap =] [drop "foo" hash]] [[bytes?] [hashbytes]] [[yes] [encode hash]]] decide]
When the type is 'foo', we use the hash of the string "foo".
Now, there are times when you do want to alter the meaning of a word globally. It's called Monkey Patching, and it's not something you should do unless you have no other choice. It can make a program's behavior very hard to analyze or debug, and is very easy to misuse, and is referred to as a "foot gun", as in, a gun with which it is very easy to shoot yourself in the foot.
Kcats does support both temporarily and permanently monkey
patching. See the words define
for permanent and lingo
for
temporary.
One use case for permanent changes might be to add methods to a multimethod. If done carefully, it can add new behavior without interfering with existing behavior.
dictionary [encode definition] [[type [foo] unwrap =] [drop "foo" encode] addmethod] update define [[foo myfoo]] association hash "foo" hash =
yes
Here we change how a foo
is encoded, and specify it's always encoded
as the word "foo" converted to bytes. Then we test that equality holds.
5.15.4. Sharing code
5.15.4.1. Overview
Sometimes you want to add some vocabulary to your environment that
someone else wrote. We call those modules
. A module makes some changes
to the standard dictionary, usually by adding new words (as you saw
already with let
). The difference with a module is that a set of
changes are bound up together and given an alias
. You can load a
module and then use it by using some words in it, we'll see how in a
bit.
5.15.4.2. Contents
The contents of a module is a program whose input will be the current environment's dictionary, and it should output a new, modified dictionary. Typically, the module will add some new words to the dictionary, but it can also alter or delete them. Here's some example modules:
This one adds two words, square
and cube
- it gives two definitions in
a list and join
s them with the existing dictionary, merging them
together.
[[square [[definition [clone *]]]] [cube [[definition [clone square *]]]]] join
This one changes the definition of an existing word: it adds the
action dropdown
to the end of the definition of the word foo
.
[foo definition] [[dropdown] join] update
5.15.4.3. Inscribing a module vs using a module
When you inscribe
a module, you make it available to be used, but the
module's changes remain invisible until called upon with using
.
;; inscribe a new module that we explicitly pass in [mymodule] unwrap [[[plus2 [[definition [2 +]]]]] join] inscribe ;; leaves the hash on the stack but let's drop it and pretend we want ;; to use the module later drop ;; If we try to use plus2 it won't work yet: ;; ;; 3 plus2 ;; ;; we have to declare we're using mymodule: [mymodule] [3 plus2] using
5
using
takes a list of modules so you can use more than one at a
time. It only applies to the given program.
5.15.4.4. Quick and dirty words with let
Sometimes you want to define some words and use them right away and
not care about making a module for use elsewhere. There's a word that
combines inscribe
and using
for you, it's called let
:
[[plus2 [2 +]]] [3 plus2] let
5
However there are some downsides to using let
. You should not call let
in a tight loop, because each time it's called it's going to re-define
the module, which is a relatively expensive operation. Instead you
should call inscribe
outside the loop, and use resolve
. The difference
between using
and resolve
becomes clear when we look at how using
is
defined:
dictionary [using definition] lookup
[resolve execute]
using
modifies a program by resolving all words to their modules, and
then executes the program. If you intend to use a module's words in a
tight loop, you can resolve
the program and then later pass that
program to the looping construct:
[mymodule] unwrap [[plus2 [2 +]]] draft inscribe drop 1 10 1 range
[[actual [[definition [2 +]]]] [asked [program]] [handled yes] [reason "type mismatch"] [type error] [unwound [update [[dictionary_redacted #b64 "M4G/6zwsAHWojAeJtU/Zrg/qYfE2QrnF3l5Bh9pqja4="]] unwrap evert first dropdown dictmerge drop 1 10 1 range]]] [[definition [2 +]]] [plus2] dictionary_redacted #b64 "M4G/6zwsAHWojAeJtU/Zrg/qYfE2QrnF3l5Bh9pqja4="
5.15.4.5. Aliases
Modules are identified by their cryptographic hash 4. That lets
you be sure that the code you meant to run, is actually what you are
running. However since hashes look like this
/1vRbfFezlcTCUfQCjC1FKukWLoOAeBuvxNXUDbFKSk=
, kcats can keep track of
human-readable module nicknames for you, and they're called
aliases
. For example you might call
/1vRbfFezlcTCUfQCjC1FKukWLoOAeBuvxNXUDbFKSk=
by the name customers
.
When you refer to a module, you can refer to it either by its hash or its alias. Aliases are not universal names, they're local to your program. So you can use whatever aliases you want, without worrying about them colliding with other people's aliases.
5.15.4.6. Security rules
There are some safeguards in place to ensure that words mean what you expect them to mean, and that modules you load can't change the meaning of words outside of where you intended.
- Aliases are immutable for the lifetime of your program - once you alias a name to hash, you cannot change it. You can only create new aliases. Trying to modify an alias to a new hash, is an error.
- You cannot alter words in the core dictionary (the words of the default environment before any modules are loaded).
- You cannot truly delete words either, you can only shadow them. If you delete a word from the dictionary in a module definition, what happens is the runtime makes a copy of that word, whose definition is to return the same error as if the word isn't defined. This allows you to block a program from accessing some words, but it is not a global or permanent change.
5.16. Generators
5.16.1. Overview
Sometimes in programming, having the concept of an indefinite sequence is handy. You have part of your program producing data, and another consuming it, but the producer doesn't know how much the consumer will actually need. A producer might calculate a huge number of items at great expense, only for the consumer to only need a tiny fraction of them. Generators allow the consumer to tell the producer when to produce, but the producer still retains all the logic of how that's done.
In kcats there's no special sauce for generators, we can implement them as a pattern with just the standard words we've already seen.
5.16.2. Example
Let's say you want to create the fibonacci sequence. Let's see how we can code that without worrying about how many items in the sequence we'll eventually need.
A generator consists of two things: state, and a program. Each time we want to generate an item, we run the program. The program should produce a new item and update the state. We just put however many state items we need on the stack, and then a program that can work with those items.
1 0 [[+] shielddown swap clone]
So here we start with 1 0
. That's the starting state. Normally we'd
start fibonacci with 1 1
but this isn't the actual first two numbers
in the sequence, it's starting values we use to calculate them. Then
we have a program that takes two numbers as input and leaves one new
number. Let's just execute
that program and see the result:
1 0 [[+] shielddown swap clone] execute
1 1 1
We can see the 0
is now 1
and there's an extra 1
on the
stack. Remember the generator must do two things, produce a new item
and update the state. It updated the state from 0 1
to 1 1
, and
produced the first item, 1
.
This gets us one number, but not the whole fibonacci sequence. Let's
look at the word generate
. All it does is run the program, pulls the
generated item to the top of the stack, and puts a new copy of the
program in place so that when we want the next item, we can call
generate
again:
1 0 [[+] shielddown swap clone] generate
1 [[+] shielddown swap clone] 1 1
Notice here that the only difference from before is that the program is sandwiched between the fibonacci number we produced, and the state.
Let's keep going and call generate again! But wait, before we do that
we need to do something with item we just produced, to get it out of
the way. For now we'll just drop
it. We've seen it and we want to
see what's next.
1 0 [[+] shielddown swap clone] generate ;; what we had before drop ;; throw away the first item generate ;; the 2nd item
1 [[+] shielddown swap clone] 1 2
Ok, so the 2nd item is 1
and we can see the state is updated -
instead of 1 1
we have 1 2
.
One more time:
1 0 [[+] shielddown swap clone] [generate drop] 2 times ;; generate and drop the first two items generate ;; the 3rd item
2 [[+] shielddown swap clone] 2 3
Ok we can see that we can get items one at a time by calling
generate
, but this is not very useful. What we really want is to get
the first 20
numbers in the fibonacci sequence, and collect them into a
list. We can do exactly that:
1 0 [[+] shielddown swap clone] ;; our original generator 20 taker ;; another generator that stops generating after 20 items collect ;; collects all the generated items into a list
[1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765] [[positive?] [dec [generate] dive] [[]] if] 0 [[+] shielddown swap clone] 6765 10946
There's the fibonacci sequence! Hey wait, what's all that stuff after
it? We just want fibonacci! That's there in case you wanted to keep
generating more items. If you want to just get the result and throw
away the generators, you can do that with shield
, which erases all
stack effects except whatever was on top. So we'll just shield
the
entire thing:
[1 0 [[+] shielddown swap clone] 20 taker collect] shield
[1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765]
So what is happening here? We're stacking up generators. Starting with
the last, we have collect
which will repeatedly call generate
on the
generator below it. It keeps going and collecting the generated items
in a list, until the generator below returns nothing
. Then it stops
and returns what it collected.
Then below collect
we have a generator 20 taker
- what that does is
keeps its own state of how many items we want it to take. It counts
down as it generates items below it, passing them up to collect
and
when it hits zero, it returns nothing
(even if the generator below it
would have produced something, taker
won't even ask). That will signal
collect
to stop.
We have other handy generators we can stack up. Let's say for whatever
reason we want to know what are the first 20 odd fibonacci numbers?
Well, we have keep
:
[1 0 [[+] shielddown swap clone] ;; our original generator [odd?] keep ;; a generator that keeps calling the one ;; below it until it gets something that ;; passes the predicate we specified 20 taker ;; another generator that calls generate 20 times collect] ;; collects all the generated items into a list shield
[1 1 3 5 13 21 55 89 233 377 987 1597 4181 6765 17711 28657 75025 121393 317811 514229]
There it is, the first 20 odd fibonacci numbers!
Let's say instead we wanted to know the prime factors that make up
each of the first 20 fibonacci numbers. We can do that with each
:
[1 0 [[+] shielddown swap clone] ;; our original generator ;; a program to give the prime factors of a given number [[] swap 2 ;; current-divisor input result [[sqrt] dip >=] [[mod zero?] [clone ;; c-d c-d i r sink ;; c-d i c-d r [put] dipdown ;; c-d i new-r / 2] ;; dividend new-r [inc] ;; c-d++ i r if] while drop put] each 20 taker ;; another generator that calls generate 20 times collect] ;; collects all the generated items into a list shield
[[1] [1] [2] [3] [5] [2 2 2] [13] [3 7] [2 17] [5 11] [89] [2 2 2 2 3 3] [233] [13 29] [2 5 61] [3 7 47] [1597] [2 2 2 17 19] [37 113] [3 5 11 41]]
There we have it. We can see that [2 2 2]
is what makes up 8
, etc.
Other included generators are:
- dropper
- Inverse of
taker
- drops the first n items of the sequence and returns the rest. - joiner
- Joins items together
- integers
- all the numbers starting with 0
reduce
will consume what a generator produces. You provide a program
that takes 2 arguments, and reduce
will generate all the items, and
pass to your program: the result so far and the next item generated,
and repeat that until there are no items left:
[integers 1 dropper ;; drop 0 so we start with 1 10 taker [3 *] each [+] reduce] shield
135
5.16.3. Compatibility
Let's say you go to the trouble of making a beautiful stack of
transformations and you want to re-use it, but you don't have a
generator, you have a list! Our transformation stack needs a
generator! How are we supposed to use it? Never fear, there is a
simple way to adapt transformations to work on anything that works
with the word take
. You can use the word liberate
to convert a list to
a generator. (It's just an alias for [take]
which is even shorter than
liberate
so feel free to just use [take]
).
Do you see why [take]
converts a list to a generator? Remember,
generators are a state and a program. If we already have a list or
pipe, we can just treat that as the state. And [take]
as the program
does exactly what we want, removes an item from the list and returns
it, leaving the state with one fewer item.
5.16.4. Capturing items from the stack
One pitfall with generators is that sometimes you want to transform generated items and use some item from the stack to help do it. The problem with this is that generators can be arbitrarily deep and you won't know exactly how deep that item is.
The solution is to capture
the items you want.
Let's look at a simple example. Let's say we want to generate every
multiple of n (where n is some number on the stack). We can already
generate every integer, we just need to multiply each one by n. A naive
solution would be to just use each
, but it doesn't work:
3 integers [*] each 1 dropper ;; drop 0 10 taker collect
[[reason "type mismatch"] [unwound [* [[0 [inc clone] 0 3]] unwrap evert first dropdown [[generate [[*] bail] shielddown]] unwrap swap drop [1] unwrap dec [positive?] shield [[generate drop] dip dec [positive?] shield] loop [generate swap] dip float [[[[positive?] [[generate drop] dip dec] while [generate swap] dip float] bail]] unwrap swap [9] unwrap swap [[[positive?] [dec [generate] dive] [[]] if]] unwrap swap [] swap clone [put [generate] dip swap clone] loop drop]] [actual [inc clone]] [asked [number]] [type error] [handled yes]] 0 [inc clone] 0 3
This doesn't work because n and the last integer we generated aren't
next to each other on the stack, there's a bunch of generator
machinery in between. We could try to guess exactly how deep the
machinery is, but then our generators aren't composable anymore - we
couldn't move that call to each
somewhere else in the generator stack,
without having to change the program. What we really should do is
create our program for each
first, before we start stacking up
generators, and bind
n:
3 [*] bind [integers] dip ;; insert the integers generator below the each program each 1 dropper 10 taker collect
[3 6 9 12 15 18 21 24 27 30] [[positive?] [dec [generate] dive] [[]] if] 0 [[[positive?] [[generate drop] dip dec] while [generate swap] dip float] bail] 0 [generate [[3 *] bail] shielddown] [inc clone] 10
What exactly is this doing? We're taking values from the current stack, and prepending them to a program, so that later when the program executes, it'll find that value on the top of the stack. Put another way, we're binding the value of the first argument to the program now, rather than letting it take a value from the top of the stack later.
5.17. Coordination and Input/Output
5.17.1. Basics
In kcats, both coordination and input/output are done with pipes
. See
the definition for pipe.
Let's take a common example of coordination. Your program has to do
several very long and intensive calculations but doesn't want to make
the user wait to do other things. The way that's done in kcats is by
creating multiple environments, and have them communicate with each
other using pipes. You can send any item through a pipe that you
could put onto the stack, including other pipes. You can clone
a pipe
to give access to it to more than one environment.
There are two main operations a pipe supports: put
and take
. You
either put an item in, or take an item out. Either one of those
operations may block, if the pipe is either full (when putting) or
empty (when taking). Your environment would have to wait for some
other environment to take something out so there's space to put, or
put something in so that there's something to take out.
All pipes share the put
and take
operations but they can differ in
other ways.
Note that put
and take
can also be used on plain lists. put
adds to
the end, and take
removes the first item. Neither will ever block when
used on a list. Another slight difference is what happens when you've
reached the end of the content (either the list is empty or the pipe
has, for example, hit the end of file condition): a take
from an empty
list will just return nothing
, but a take
from a pipe that is at EOF
will result in an error.
5.17.2. Input/output
Let's look at how we do I/O using files as an example - let's say we
want to write the word foo
to a file called bar
:
[[file "bar"]] pipe-in ;; create the pipe to the given file "foo" "foo" encode ;; we have to convert string to bytes first, using the word ;; =encode=. put ;; finally, put the bytes into the pipe, and they are written to ;; the file
[[to [[file "bar"]]] [type tunnel] [values [[type bytes]]]]
Note the representation of the pipe shows where it leads (the to
field), and what types of items it can carry (the values
field).
Neither put
nor take
consume the pipe from the stack,
for convenience, as most of the time you'll want to use it again.
Let's look at reading from a file:
[[file "bar"]] pipe-out take string
"Hello World!" [[type tunnel] [values [[type bytes]]] [to [[file "bar"]]]]
Note that the amount of bytes you'll get from a file on each take, is
limited. You will only get the entire contents if the file is
small. We'll want to repeatedly take
until there's nothing left, and
put all the taken parts together.
Here's how we do it:
- turn the pipe that provides chunks of a file into a generator, with
[take]
. - Assemble the chunks with
reduce
. It requires a program to say how to combine the chunks. We want tojoin
them, so the program is[join]
.
We can also use the word file-out
as a shortcut to get a pipe given a
file's name.
"bar" file-out [take] join reduce string
"Hello World!" [take] [[type tunnel] [values [[type bytes]]] [to [[file "bar"]]]]
Finally there's a convenient alias for [take] [join] reduce string
, it's called slurp
:
dictionary [slurp] lookup
[[definition [[take] [join] reduce string [drop drop] dip]] [spec [[pipe] [item]]]]
It actually drops the generator for you as well, since we know it's already been fully read from. So you can do this:
"bar" file-out slurp
"Hello World!"
5.17.3. Coordination of simultaneous programs
5.17.3.1. Basics
The way kcats handles parallel processing (aka multithreading) is by allowing you to create multiple environments, each with their own programs, that run simultaneously.
Often you need the environments to communicate with each other, and not just with the outside world. That's done with channels. Channels let you send items from one environment to another.
5.17.3.2. Channels
Channels are a type of pipe. They are different from other pipes, like files or network sockets, in that while files and sockets only deal with bytes, channels can pass any kind of item (numbers, strings, words, lists, and even other pipes). If you can put an item on the stack, you can also pass it through a channel.
- Handoff
This is the most common type of channel. Think of it as a pipe with no length or capacity. It's more like a hole in a wall, than an actual pipe. You can pass items through the hole, but only if someone is already on the other side waiting to take it. If your program tries to pass an item through the handoff, but no other program running simultaneously is already waiting to take it, your program will block. That means it stops and waits. The same goes for receiving items - if your program tries to receive but no one is sending yet, your program will wait until someone sends. Handoffs support multiple senders and receivers through the same "hole" - imagine multiple people standing on either side of the wall, holding items they need to pass through, and others empty handed waiting to receive. Only one person can receive per send - it's a direct handoff and not a broadcast.
You pass items from one environment to another by giving both environments a copy of the handoff pipe. One environment calls
put
(with an item) and the other callstake
. If they're doing that at the same time, the item moves from one environment to the other and both theput
andtake
complete at the same time.You can pass as many items through the handoff as you want.
- Buffered
This channel works similarly to a Handoff, but instead of having no capacity, it has a fixed capacity that you specify when you create it. Let's say you give it a capacity of 10. That means the sender can put 10 items in, even when no one is receiving yet. On the 11th item, sending will block just like a handoff would.
If a receiver takes an item out, that frees up space for one more item that the sender can put in without blocking.
- Signaling when you're done
It's common for a program to keep taking from a pipe until there's nothing left to take, and then stop. How does it know there's nothing left (in other words, nothing more will ever arrive, no matter how long it waits)? With channels there's 2 ways to know:
- One of the senders sends the item
nothing
. All the standard library words that pull from a channel repeatedly, will stop when they receivenothing
. - All the senders copies of the channel are dropped.
How is a sender dropped? That can happen several ways:
- The channel item is dropped from the stack with the word
drop
or its variants, or otherwise consumed. - The entire environment is dropped (as often happens when its program is finished)
- A program receives on this copy of the channel. By default, channels are one direction only. As soon as you send, the receiver is dropped, and vice versa. Bidirectional channels (where the same program can send and receive on the same channel) are possible but currently not supported because they take a bit more care to use properly.
When writing a program that needs to know about the end of a stream, you must take care to not leave extra copies of channels lying around. Any extra channel copy that you don't
drop
when finished, will keep the stream open forever and will result in a deadlock. A deadlock is when some program is waiting for a condition that can never occur. In this case, a receiver is waiting for the end of the stream, and it will never get it. The primary symptom of a deadlock is a program that appears to be stuck or doing nothing, doesn't produce the intended result, doesn't produce an error, and doesn't exit.Note that it's also possible for receivers to signal they have everything they want, by dropping their copy of the channel. After all receivers are dropped, all senders will get errors when they try to send.
- One of the senders sends the item
5.17.3.3. Multiple environments
Parallel processing is done with multiple environments running simultaneously, but how exactly do we set that up? And how do we enable them to communicate with each other? How do we create two environments with a copy of the same channel, so one can send and the other receive?
One handy way is to use the word spawn
. It takes a program and creates
a new environment, such that its program is the given program and the
stack is a copy of the current environment's stack. That way, you end
up with two environments, with different programs but the same stack.
Once the new environment is created, it doesn't actually do anything,
it's just another item inside the current environment. To make it
actually run the program, we need to animate
it. At that point it
disappears from this environment and goes off on its own.
handoff [5 put] spawn animate take
5 [[type out] [handoff 0]]
Here we create a handoff, and then create a new environment that
inherits that handoff in its stack. That new environment will put
the
number 5
into the channel. Then we call take
on the other copy of the
channel to receive the number 5
.
Note that when you animate
a new environment, you can't debug it using
the same debugging tools we saw earlier. You actually lose all contact
with it, except for whatever pipe communication you program it to
do. Debugging simultaneous programs is a bit more involved and this
will be covered later.
5.17.3.4. Example
We'll show how parallel programming (multithreading) works in kcats,
with an example. Let's say we have a database of books, and we have 3
librarians checking in returned books. Let's say for simplicity that
all the books in the database were checked out and have now been
returned and are ready for checkin. We want to update the database so
that each book has a last-checkin
property with the timestamp the
librarian processed it, and checked-in-by
with the librarian's
name. We want all the books to be processed exactly once, with 3
librarians working in parallel.
We'll create chunks of our program and put it all together later. Here's one chunk. Let's read in our database and parse it into a list:
"examples/books.kcats" file-out slurp read
So that's our database of unprocessed books. Now let's feed our database into a pipe, in a separate environment. We could also have used a buffered channel here.
[[put] step] spawn animate drop ;; this copy of the library as the task to feed it is already running
Now we can create two channels - one to carry unstamped books (the input for the librarian), and one to carry stamped books (the librarian's output). Then we create another pipe that gives the current time.
handoff ;; a pipe for unprocessed books handoff ;; a pipe to collect completed work timestamps ;; pipe for the librarians to stamp their books with
Now we specify what each librarian should do. This is the meat of the
program. Note the use of the word siphon
here. It takes a generator
and a pipe, and keeps generating items and putting them into the
pipe, until the end of the stream is reached (generator produces
nothing
).
;; the spawn prog should see: n ts po pi (librarian-name, timestamps, output-pipe, input-pipe) ;; the book each prog should see: book n ts po pi [[[[take dropdown ;; get a timestamp and then drop the ts pipe pair [checked-in-by last-checkin] swap zip] dip join] binddown binddown ;; bind n, ts. [[take]] dip ;; make a generator out of pi each ;; book, do the stamping float siphon] ;; from the each generator to the output pipe spawn animate] ;; launch a separate environment [binddown binddown binddown] shielddown ;; bind the 2-4th args ;; now that we've already captured the pipes to the workers we ;; can drop pi and ts dropdown dropdeep
Now that we've specified what each librarian should do, we specify the librarians. In this case, all we need is their name.
["Darlene" "Thomas" "Ethel"] ;; the names of the librarians swap map ;; for each librarian do the above program drop ;; the resulting list, we only care about launching envs
Finally we can just collect all the results from the output channel, and drop the other items we don't need anymore, leaving just the results.
[take] collect ;; the results ;; drop generator, leaving only results sink drop drop
Here's the whole program:
handoff ;; a pipe for unprocessed books "examples/books.kcats" file-out slurp read ;; read the library ;; feed all the unprocessed books into a pipe, in a separate task [[put] step] spawn animate drop ;; this copy of the library as the task to feed it is already running handoff ;; a pipe to collect completed work timestamps ;; pipe for the librarians to stamp their books with ;; the spawn prog should see: n ts po pi (librarian-name, timestamps, output-pipe, input-pipe) ;; the book each prog should see: book n ts po pi [[[[swap take dropdown ;; get a timestamp and then drop the ts pipe pair [checked-in-by last-checkin] swap zip] dip join] binddown binddown ;; bind n, ts. [[take]] dip ;; make a generator out of pi each ;; book, do the stamping float siphon] ;; from the each generator to the output pipe spawn animate] ;; launch a separate environment [binddown binddown binddown] shielddown ;; bind the 2-4th args ;; now that we've already captured the pipes to the workers we ;; can drop pi and ts dropdown dropdeep ["Darlene" "Thomas" "Ethel"] ;; the names of the librarians swap map ;; for each librarian do the above program drop ;; the resulting list, we only care about launching envs [take] collect ;; the results ;; drop generator, leaving only results sink drop drop
[[[checked-in-by "Darlene"] [last-checkin 1698764958940] [author-first "George"] [author-last "Orwell"] [title "1984"] [year 1949] [subjects [government dystopia surveillance totalitarianism freedom]]] [[checked-in-by "Thomas"] [last-checkin 1698764958945] [author-first "Aldous"] [author-last "Huxley"] [title "Brave New World"] [year 1932] [subjects [society technology dystopia happiness drugs]]] [[checked-in-by "Ethel"] [last-checkin 1698764958950] [author-first "F. Scott"] [author-last "Fitzgerald"] [title "The Great Gatsby"] [year 1925] [subjects [wealth love obsession american-dream tragedy]]] [[checked-in-by "Ethel"] [last-checkin 1698764958950] [author-first "J.D."] [author-last "Salinger"] [title "The Catcher in the Rye"] [year 1951] [subjects [adolescence alienation innocence society adulthood]]] [[checked-in-by "Thomas"] [last-checkin 1698764958950] [author-first "Jane"] [author-last "Austen"] [title "Pride and Prejudice"] [year 1813] [subjects [love marriage society class reputation]]] [[checked-in-by "Ethel"] [last-checkin 1698764958950] [author-first "Mary"] [author-last "Shelley"] [title "Frankenstein"] [year 1818] [subjects [creation science responsibility monster humanity]]] [[checked-in-by "Thomas"] [last-checkin 1698764958950] [author-first "John"] [author-last "Steinbeck"] [title "Of Mice and Men"] [year 1937] [subjects [friendship dream loneliness society tragedy]]] [[checked-in-by "Thomas"] [last-checkin 1698764958950] [author-first "Ernest"] [author-last "Hemingway"] [title "The Old Man and the Sea"] [year 1952] [subjects [endurance nature old-age fisherman sea]]] [[checked-in-by "Ethel"] [last-checkin 1698764958950] [author-first "Harper"] [author-last "Lee"] [title "To Kill a Mockingbird"] [year 1960] [subjects [racism innocence morality law childhood]]] [[checked-in-by "Thomas"] [last-checkin 1698764958950] [author-first "J.R.R."] [author-last "Tolkien"] [title "The Lord of the Rings"] [year 1954] [subjects [adventure elf dwarf hobbit ring journey magic evil]]] [[checked-in-by "Ethel"] [last-checkin 1698764958951] [author-first "Joseph"] [author-last "Conrad"] [title "Heart of Darkness"] [year 1899] [subjects [colonization africa journey morality darkness europeans]]] [[checked-in-by "Thomas"] [last-checkin 1698764958951] [author-first "Leo"] [author-last "Tolstoy"] [title "War and Peace"] [year 1869] [subjects [war peace society history love aristocracy]]] [[checked-in-by "Ethel"] [last-checkin 1698764958951] [author-first "Homer"] [title "The Odyssey"] [year -800] [subjects [journey odyssey homecoming gods heroism adventure]]] [[checked-in-by "Ethel"] [last-checkin 1698764958951] [author-first "Charlotte"] [author-last "Bronte"] [title "Jane Eyre"] [year 1847] [subjects [love morality society class womanhood independence]]] [[checked-in-by "Thomas"] [last-checkin 1698764958951] [author-first "Mark"] [author-last "Twain"] [title "Adventures of Huckleberry Finn"] [year 1884] [subjects [adventure racism slavery morality friendship river]]] [[checked-in-by "Darlene"] [last-checkin 1698764958951] [author-first "Ray"] [author-last "Bradbury"] [title "Fahrenheit 451"] [year 1953] [subjects [censorship knowledge books society dystopia future]]] [[checked-in-by "Ethel"] [last-checkin 1698764958951] [author-first "Charles"] [author-last "Dickens"] [title "A Tale of Two Cities"] [year 1859] [subjects [revolution love sacrifice resurrection society history]]] [[checked-in-by "Thomas"] [last-checkin 1698764958951] [author-first "William"] [author-last "Golding"] [title "Lord of the Flies"] [year 1954] [subjects [society civilization savagery childhood morality island]]] [[checked-in-by "Thomas"] [last-checkin 1698764958951] [author-first "Miguel de"] [author-last "Cervantes"] [title "Don Quixote"] [year 1605] [subjects [adventure idealism reality knight insanity literature]]] [[checked-in-by "Ethel"] [last-checkin 1698764958951] [author-first "H.G."] [author-last "Wells"] [title "The War of the Worlds"] [year 1898] [subjects [invasion aliens society technology war humanity]]]]
6. More Examples
6.1. Query a flat file database
["examples/books.kcats" file-out slurp read [[subjects] lookup set [dystopia] unwrap contains?] filter] shield
[[[author-first "George"] [author-last "Orwell"] [title "1984"] [year 1949] [subjects [government dystopia surveillance totalitarianism freedom]]] [[author-first "Aldous"] [author-last "Huxley"] [title "Brave New World"] [year 1932] [subjects [society technology dystopia happiness drugs]]] [[author-first "Ray"] [author-last "Bradbury"] [title "Fahrenheit 451"] [year 1953] [subjects [censorship knowledge books society dystopia future]]]]
6.2. Factorial
6.2.1. Recursive with recur
10 [1 <=] [] [clone dec] [execute *] recur
3628800
6.2.2. Using range
10 inc [1 1] dip 1 range [*] step
3628800
6.2.3. Plain loop
10 clone yes [dec clone [*] dip clone 1 >] loop drop
3628800
6.3. Jensen's Device
https://rosettacode.org/wiki/Jensen%27s_Device
100 [0] [[1.0 swap /] dip +] primrec
5.187377517639621
6.4. Fibonacci
[1 0 [[+] shielddown swap clone] ;; fibonacci generator 20 taker ;; another generator that calls generate 20 times collect] ;; collects all the generated items into a list shield
[1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765]
6.5. Prime factors
36023425442111112 [] swap 2 ;; current-divisor input result [[sqrt] dip >=] [[mod zero?] [clone ;; c-d c-d i r sink ;; c-d i c-d r [put] dipdown ;; c-d i new-r / 2] ;; dividend new-r [inc] ;; c-d++ i r if] while drop put
[2 2 2 3 17 29 643781 4729211]
Prime factor generator
1231231231231231 2 [[[[[mod zero? not] [[sqrt] dip >=]] [execute] every?] [inc] while [mod zero?] [[/] shield dropdeep swap clone] [drop [] swap] if] bail] collect dropdown
[89 3271 5683 744203]
6.6. bidirectional comms from a socket
["" [string join] ;; each group of bytes that come out of the tunnel, ;; convert to string and join to whatever we already ;; collectd [[type ip-host] [address "localhost"] [port 9988]] association ;; description of where to connect to (an ip port) tunnel ;; make a bidirectional tunnel "foo! bar!" put ;; send this string collect] ;; receive
6.7. Write string to a file
[[[file "/tmp/foo"]] pipe-in "blah" put close]
6.8. Search the dictionary
dictionary ;; put the dictionary of all words onto the stack as key value pairs [second ;; the value of one of those pairs [spec] lookup ;; look up the spec field first ;; the input part of the spec [number number] =] ;; is it taking two number inputs? filter ;; filter the dictionary using the above criteria [first] map ;; of what remains, just keep the key (which is the word itself)
[quot rem > + / * max - >= min < <= mod within?]
6.9. Copy data from one file to another
[[file "/tmp/buffer-content-bkWB9Z"]] pipe-out [[file "/tmp/mytest"]] pipe-in [[take]] dip siphon drop
"/tmp/foo" "/tmp/bar" pair [[] [file] float assign] map ;; make file descriptors for both take pipe-out swap unwrap pipe-in
[[unwound [[type] unwrap = [[[[file "/tmp/foo"]] [[[file "/tmp/bar"]]]]] unwrap evert first [[first second] [first first]] unwrap branch [[[[count 1 =] [[first [type] unwrap =] [first second] [first first] if] [[]] if] [[file "/tmp/foo"]] [[[file "/tmp/bar"]]]]] unwrap evert first swap drop [[[[association] unwrap]]] unwrap swap [[]] unwrap or [[[[nothing?] shield] dip swap [or] shielddown] [] [[take swap [[execute] shielddown] dip swap] dip or] [execute] recur] execute swap drop swap drop [file] unwrap = [[[[file "/tmp/foo"]] [[[file "/tmp/bar"]]]]] unwrap evert first [[[[file "/tmp/foo"]] [[[file "/tmp/bar"]]]]] unwrap evert first [[value file-out] [[[[type [ip-host] unwrap =] [clone [port] lookup [[address] lookup] dip serversocket]] [[list?] [+kcats.pipe/->filled]]] decide]] unwrap branch swap unwrap pipe-in]] [type error] [reason "type mismatch"] [actual [[file "/tmp/foo"]]] [asked [list]] [handled yes]] [[[file "/tmp/bar"]]]
6.10. List the steps of program execution
[0 [1 2 3] [+] step] ;; the program to trace [program] swap put wrap environment ;; create a starting env ;; now create a generator of environment states for each step of execution [[[program] lookup] ;; if the program is not empty [eval-step clone] ;; step [[]] ;; otherwise emit nothing to stop the consumption if] ;; consume the generator collect
[[[stack [0]] [program [[1 2 3] [+] step]]] [[stack [[1 2 3] 0]] [program [[+] step]]] [[stack [[+] [1 2 3] 0]] [program [step]]] [[stack [[+] 1 0]] [program [execute [2 3] [+] step]]] [[stack [1 0]] [program [+ [2 3] [+] step]]] [[stack [1]] [program [[2 3] [+] step]]] [[stack [[2 3] 1]] [program [[+] step]]] [[stack [[+] [2 3] 1]] [program [step]]] [[stack [[+] 2 1]] [program [execute [3] [+] step]]] [[stack [2 1]] [program [+ [3] [+] step]]] [[stack [3]] [program [[3] [+] step]]] [[stack [[3] 3]] [program [[+] step]]] [[stack [[+] [3] 3]] [program [step]]] [[stack [[+] 3 3]] [program [execute [] [+] step]]] [[stack [3 3]] [program [+ [] [+] step]]] [[stack [6]] [program [[] [+] step]]] [[stack [[] 6]] [program [[+] step]]] [[stack [[+] [] 6]] [program [step]]] [[stack [6]] [program []]]] [[[program] lookup] [eval-step clone] [[]] if] [[program []] [stack [6]]]
We could ensure the stack/program are printed in the same order each time
[0 [1 2 3] [+] step] ;; the program to debug [program] swap put wrap environment ;; create a starting env ;; now create a generator of environment states for each step of execution [[[program] lookup] [eval-step clone] [[]] if] ;; print with the fields sorted the same way for each step [ [[stack [[+] 3 3]] [program [execute [] [+] step]]] [[stack [3 3]] [program [+ [] [+] step]]] [[stack [6]] [program [[] [+] step]]] [[stack [[] 6]] [program [[+] step]]] [[stack [[+] [] 6]] [program [step]]] [[stack [6]] [program []]]] [[[program] lookup] [eval-step clone] [[]] if] [[program []] [stack [6]]]
7. Contributing
7.1. Issue reporting
Instead of opening a github issue, add a TODO
subheading to the
Issues heading. Commit the change and submit it as a pull request. In
the branch where that issue is being fixed, it will be changed to
INPROGRESS
. When the issue is fixed, the heading will be
removed. (If you disagree that it's been fixed, submit a PR that
reverts the commit to remove it).
You can edit this file right on github, in your own fork of the project, if you prefer.
Why do things this weird way? I don't want to rely on github, nice as it is.
Please do report design improvements you'd like to see - for example, inconsistencies in how words expect stack arguments to be, ways to make the standard library easier to work with, etc.
8. FAQ
8.1. How do I keep track of what should be on the stack, as I write programs?
There's tooling planned that will help show what should be on the stack at any point in a given program. However until that exists, you can use comments to annotate your program line by line, and show what is on the stack at each step.
See 6.5 example.
This is admittedly low tech, but it isn't as tedious as you might first expect. You only have to pay attention to the stack items actually touched by the code you're writing, which usually is rather small - if you need to annotate a line with more than 5 or 6 items you are probably doing something wrong. The solution could be to use an association or list to hold multiple properties of the same conceptual object, in one stack item.
8.2. Why is there no word 'no' in kcats, but there is 'yes'?
The logic rules are that empty containers are 'negative', and every
other value is 'affirmative'. So the word no
, by those rules would be
affirmative, which would be very surprising! So the word no
is not
used in the language. To convey a logical negative, use empty list []
.
8.3. I got an error, how do I diagnose the problem?
The first thing to do is check the contents of the error. The unwound
field will show you the instruction that failed along with the
remaining program.
If that doesn't tell you enough about what's wrong (and it often doesn't), there are several more tools at your disposal:
dump
- this word prints the stack to stdout, if you add it to your program at strategic places, you can see if the stack looks the way you would expect at that point in the execution.- built-in debugger - this allows you to step through program execution.
- tracing - see 6.10
8.4. kcats panics and exits, why?
This is due to a known unimplemented feature in the interpreter, or an unknown bug. Please see 9, and if you don't see it there, please add a new one and submit a pull request. Even for unimplemented features it's good to let us know you need that feature so we can prioritize it.
The goal is for kcats to never panic.
9. Issues
9.1. DONE Build without using emacs interactively
Users should not be required to know emacs to build the project, only have it installed. The build should be accessible from bash without having to use emacs interactively.
9.2. DONE Remove platform interop from lexicon
That was only there as a cheat when there was only the prototype implementation. The platforms are different and their function names don't belong in the lexicon.
I'm not even sure there should be platform interop at all - it doesn't appear to be possible in the rust impl anyway.
So far what I've done is have some lower level words actually in the dictionary but marked them like `++lookup`. I haven't decided what to do about this yet. Lower level words probably should just be first class citizens and I just need to think of better names. Right now the low level (single-depth) lookup is `++lookup` and the user-facing `lookup` does the arbitrary depth. In this case, the user-facing name probably needs to change to reflect what it does (something like `drill` or `extract`), and then the low level can just be `lookup`.
That means for all the i/o and crypto interactions, there needs to be low-level words. I'm not sure yet how to prevent namespace pollution, as one of the design choices is
9.3. DONE 'unassign' doesn't take a keylist, only a single key
Should change to match assign
and lookup
, accept a list instead of
a single bare word.
9.4. DONE More support for nested/related envs
Debuggers, spawning, ingesting etc
9.5. TODO Graphical environment browser/editor
It would be nice to have a graphical display of all the environments in an application, and be able to
- Drill into the environment and read the stack/program/dictionary
- Pause/resume execution
- Apply debugging (breakpoint, step etc)
- View pipes and what/where they connect to (draw lines if they connect somewhere else in the app)
- Manually put things into pipes or take them out
- Create new envs
- Persist changes
- Revert changes
9.6. TODO Code distribution method
Let's say we write an app or library, how do we distribute it?
This ties in with durability - where do we store things in general, and not just libraries? kcats does support the filesystem but I would like that to be for compatibility only. The "native" kcats way of storing and retrieving things should be via hash keys. There may also be a fact database, probably with sparse tables (aka eavt format).
It brings up the question of what should "come with" the language. I am thinking maybe there's a "barebones" version of the language with no library management or anything. Then on top of that, build some durability and networking to distribute code and other data. Then the question is, what do we need to support in the base language? Seems like there needs to be database/network functionality there, but unused? Maybe make it a feature flag?
Let's explore the various options
9.6.1. Durability
It's tempting to want the flexibility of EAV (where there's basically just one big db table with 3 columns and every attribute is a row).
However this may be a little hasty. Perhaps what we're really after here is custom tables - the idea being that each user's db schema might be different depending on what data is important to them.
We've basically got a database schema consensus problem. Maybe Alice has a table CATS with columns SIZE COLOR AGE and Bob has a table CATS with columns HEIGHT COAT-COLOR AGE. How do they share data? The two tables are not really compatible without a specialized conversion tool and even then some data would be missing. So Alice and Bob ideally should agree on what a CATS schema is, otherwise they can't really share CAT facts. The advantage of EAV might be that even if they had different schemas they could stlil perhaps meaningfully talk about AGE and possibly even COLOR (with a bit of intervention, or even another fact that equates COLOR and COAT-COLOR in CATS).
The drawback of EAV is of course that it would perform rather terribly as the database grows. I can't say for sure how many facts could potentially be stored here, but here are some constraints:
- Assume individual data only (no facebooks that store millions of people's data)
- Assume popularity of the app (users may try to cram every fact they "know" into this db)
Assume there's some kind of garbage collection - Alice may collect weather observations or predictions constantly but doesn't need to keep old data. Maybe facts have a TTL? Not sure how that could be determined automatically.
It's hard to estimate how large the db might get, but I suspect a lower bound of supporting 1M words is safe. As for upper bound, it's more difficult to say, but I would think the hardware limits of mobile devices would come into play. As of 2023 I think a db size on the order of 10gb would be approaching the device's capability limits, so maybe 100M words or so. I think it would be difficult to get an EAV database to perform well at that size, especially on mobile. Note datomic can handle that size so it's theoretically within reach.
It may be possible to pick a standard db now (sqlite maybe) and not worry too much about performance. As long as the facts are portable to another db (which shouldn't be that hard), the issue can be revisited when it becomes an issue.
Even using sqlite though, just building proper queries may be difficult. It may be possible to skirt that problem too and just do a minimal query to get a dataset that fits easily in memory and then post-process the rest. Let's say the query is "List all predictors (people who made predictions) and their accuracy", you could get all the unique predictor ids in a query, then one by one get all their predictions, then get all the relevant observations and compare them. Slow but not the type of query that will be done often, and possibly indexable.
9.6.1.1. Possible dynamic sql db
One possible design is to just use plain old sql (sqlite?) and create normal tables. However the table names would be namespaced, possibly with some sort of hash. That way, one person's "Customer" schema could be in the same database as another person's without interfering.
So for example, the kcats language might need to keep track of library
dependencies. There could be a table dependencies-01234abcd
with
columns name
, version
, hash
, blob
etc. Anything else wanting to use the
same schema could refer to it by hash. It would be possible to have
foriegn keys too.
One thing we want to avoid is having kcats users writing sql query
strings, that is not the idiomatic way of dealing with i/o. What
should happen is there's a query
word that takes a program and db
descriptor of where the db is, and returns a pipe (where results come
out). The program is a "query equivalent" and would need to be
translated to sql and post-processed. This is very much nontrivial and
a naive implementation probably wouldn't perform well but we will try
it anyway. For example instead of writing
insert into Customers (name, age) values ("Bob", 25); select * from Customers where name="Bob";
you'd write something like
customers [[name "Bob"] [age 25]] put customers [[name] lookup "Bob" =] filter
and
And then the translation would see we're selecting from customers,
then there's a filter. The filter might not translate to sql so it
will either just select all, or if it sees a certain format for the
predicate it can translate to a where
clause. This is going to be
complex and bug prone but hopefully can be done in a way that the
worst case is poor performance and then iterate to get better
speed.
I suppose content distribution might need to be done alongside this.
9.7. DONE Clean up all the vector conversion
I've been calling vec
a lot, sometimes just so the list will print
out with square braces. I now have a repr
function that could do
this, so using vec
for that purpose is no longer needed.
However, I can't get rid of all of them- for example, calling conj
on a vector vs list adds at different ends of the list so they are not
interchangeable in that respect. It may be dangerous to leave any
lists lying around if they might get conjed onto expecting it to go on
the end.
9.8. DONE org-babel-execute for kcats
9.9. INPROGRESS At least one example for each word in lexicon
10 0.5 *
5
"foo" encode
#b64 "Zm9v"
[[a b] [c [[d e]]]] [c d] 5 assign
[[c [[d 5]]] [a b]]
[[a b] [c []]] [c] [[d 5]] association assign
[[c [[d 5]]] [a b]]
[[a b] [c [[d e]]]] [1 1 0 1] 5 assign
[[a b] [c [[d 5]]]]
[[a b] [c [[d e]]]] [1 0] 5 assign
4 3 [>] shield [wrap [wrap] dip] dip sink branch
4
yes 4 2 branch
[[asked [program]] [reason "type mismatch"] [type error] [unwound [branch]]] 2 4 yes
5 [1 2 "oh fudge"] [[+] [] recover] map
[[[type error] [reason "word is not defined"] [asked [handle]] [unwound []]] [[unwound []] [asked [handle]] [reason "word is not defined"] [type error]] [[asked [number]] [type error] [reason "type mismatch"] [unwound [+]]]] 5
5 1 [+] [] recover
[[unwound []] [asked [handle]] [reason "word is not defined"] [type error]] 1 5
1 type
number
5.01 5 0.1 swap [- abs] dip <
yes
9.10. INPROGRESS Prime number sieve example
2000 clone 2 swap range ;; all the numbers up to n [sqrt 2] dip ;; start counter at 2, stop at sqrt of n [sink =] ;; stop loop when the counter hits sqrt n [[drop drop] dip] ;; drop the original args, just leaving the primes [[[[=] [swap mod positive?]] [execute] any?] filter ;; keep the counter but no multiples of it [inc] dip] ;; increment counter [execute] recur
[[asked [consume]] [handled yes] [reason "not enough items on stack"] [type error] [unwound [sqrt 2 [[]] unwrap [sink =] [[drop drop] dip] [[[[=] [swap mod positive?]] [execute] any?] filter [inc] dip] [execute] recur]]]
Here's a mimic of the python version, WIP:
;; num 10 [[[] [yes put]] dip times] shield ; a n 2 ;; p a n [swapdown clone * > ] ;; while test [[wrap lookup] ; if test - fetch by index [ swapdown ;; p n a clone ; p clone * ; p^2 p n a ;; range wants p, n+1, p^2 sink ;; p n p^2 [inc] dip ;; p n+1 p^2 [range] shield ;; r p n+1 p^2 a [dec sink drop] dipdown ;; r p a n swapdown ;; r a p [ ;; i r a p wrap ;;swapdown ;; [i] a r p [[]] update ;; set to false: a r p swap ;; r a p ] step ;; a p swap ] ; do the for loop [] ; else do nothing if inc ;; p++ ] while
[[actual 2] [asked [sized]] [handled yes] [reason "type mismatch"] [type error] [unwound [update swap [8 10] [wrap [[]] update swap] step swap inc [swapdown clone * >] shield [[wrap lookup] [swapdown clone clone * sink [inc] dip [range] shield [dec sink drop] dipdown swapdown [wrap [[]] update swap] step swap] [] if inc [swapdown clone * >] shield] loop]]] [[]] [6] 2 [yes yes yes yes [] yes yes yes yes yes] 10
[yes yes yes yes yes yes yes yes yes yes]
[] [[yes] 15 times] inject 2 swap ;; p a [clone clone *] dip swap ;; p^2 a p [[[count] shield] dip swap [<] shielddown] ;; b p^2 a p [[wrap [drop []] update] shield ;; do the update float drop sink [+] shielddown swapdown] ;; ;while
[[wrap [drop []] update] shield float drop sink [+] shielddown swapdown] [[[count] shield] dip swap [<] shielddown] 4 [yes yes yes yes yes yes yes yes yes yes yes yes yes yes yes] 2
How do we write this code? Generally, how do we decide what order things go on the stack?
It looks like the array of bools is the main piece of data here, that is used throughout the algorithm. The other commonly used variable is p, the one that's incremented. I think probably p should remain on top. The outermost loop needs to know when to stop, and that needs to compare to num. That can go on the bottom.
The inner loop uses i. That should probably replace p on top when in use.
So it should be [p a]
and later [i a p]
.
Now that lingo
exists, maybe should also write let
for variables
(where the values are evaluated before updating the dictionary)? Also
these aren't actually "variables" because you can't change the value,
without an inner let
.
Actually this is probably best implemented in two parts:
- a word that takes a set of bindings and evaluates the values, leaving a map of word to value
- a word that takes the map above and inserts it into the
dictionary. I think
lingo
does this already.
let's try to write the former here. I think we need map-values
type of
thing here, which requires treating a map as a list.
[[[a [+ 5 6]] [b [- 100 8]]] [a b +] let]
ok round 2 here, let's just do the updating loop first: expect p, a,
n, and modify a in place such that all multples of p are flipped to []
.
10 clone yes swap repeat 2
first though let's just produce the array of indices to mark from p and n.
10 2 clone sink range
[2 4 6 8]
now given that and a, mark all those indices
1000 clone yes swap repeat 2 swapdown [clone * >] [[dropdown wrap lookup] [[clone sink range rest] shield sink [[wrap [] assign] step] dipdown] when inc] while drop drop indexed [second] filter [first] map rest rest
[2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997]
Now take an array of bools and filter by index
[yes [] yes] [count] shield [0] dip 1 range swap zip [second] filter [first] map
[0 2]
this is too inefficient (copying the whole array due to the shield
[ wrap [] assign] shield
Need to avoid using shield, as it causes copying of the large array.
[1 2 3] [0] 100 [pair] shield [assign] dip unwrap
100 [0] [100 2 3]
9.11. TODO FAQ entry about composability
Kcats is difficult to use in the same mindset as other languages. The amount of complexity you can fit into a single function or subroutine, before it gets too difficult to reason about, is quite a bit smaller. However kcats is designed to be used this way. The kcats way is building small combinators, and then using those combinators to build what you need.
Kcats has some distinct advantages here:
- The overhead of putting pieces together is basically zero, since function composition is the default.
- Combinators are very easy to test independently.
Kcats makes it easy to test combinators independent of the context where you will use it. In purely functional code, this is straightforward: you just place some sample data on the stack and then the combinator you want to test, then run it.
With i/o it's not quite as straightforward. However kcats pipes are designed to be easily swappable for regular values. Let's say you have some code that expects to read from a file but you want to test it without having to maintain state in your local filesystem:
"bar" file-out take string
"Hello World!" [[type tunnel] [values [[type bytes]]] [to [[file "bar"]]]]
Swap the file-out
pipe for a plain list of byte arrays, they
have the same interface, and gives the same result:
[#b64 "SGVsbG8gV29ybGQh"] take string
"Hello World!" []
and of course you can build that byte array out of strings, if you prefer:
["Hello World!"] [encode] map take string
"Hello World!" []
This technique works for any use case where you're using a pipe for its usual take/put/step interface, and timing is not critical.
10. Acknowledgements
11. Roadmap Notes
11.1. Higher level persistence abstraction
I wonder whether kcats should have any notion of files and sockets at all. Sort of like java doesn't have any notion of memory addresses or malloc/free - it operates at a higher level and handles mem management for you. Maybe kcats handles persistence for you. This may be a sort of chicken/egg problem where I need a network protocol to help w persistence and I want that protocol to include kcats as a language. Can they be bootstrapped as a single unit? Seems possible but not easy. Persistence might involve having another party store data for you, which might involve identity (to limit access) and money (to incentivize someone to keep your data for later). That might be a bit of a reach for a programming standard lib to handle.
And then there's the question of interop with other programs, how would they communicate if kcats doesn't know what a file or socket is? Maybe it can know what a file/socket is but you don't need to use it except as interop (like clojure's java interop or java's jni).
So what would this look like?
Instead of telling the program where to persist, you just want it persisted and you get a sort of claim check (maybe the hash of the data?). Then to get it back later, you present the claim check. Persistence is a best-effort deal (you can't be 100% sure no disaster could wipe it out). So maybe also include some optional params to indicate:
- how long until you might need this again
- how long you can wait between requesting it and getting it
- how disaster-proof it needs to be
- how much you're willing to pay to store it
Maybe we can even put messaging under this model - after all, sending someone a message is in fact making a copy of data you have. You don't necessarily want to retrieve it later though.
Computing might be better thought of as a worldwide resource - you might not be able to trust someone else to do a computation for you (yet, unless it's a specific type where you can verify without doing the full computation yourself) but you can trust them with storage (given enough redundancy - they can't steal your data because it's encrypted).
11.2. Object construction, caching
Often we create objects similar to java construction, where the input and output are informationally equivalent (you can reconstruct the output from the input anytime you want, and sometimes vice versa).
It might be nice if kcats didn't force you as a user to do this type of operation and just let you use the original data.
For example, lets say you have [[file "/tmp/foo"]]
. That's an
association of file
(a type) to a string. Really what that means is
we're referring to a file on disk. In java we'd construct a File
object with new File("/tmp/foo")
. It'd be nice if everywhere in
kcats you never needed a File
object and could use the original
descriptor instead (or a pipe you've already created, if state
matters). On the jvm platform obviously somewhere a File
object
would get created but that should be hidden from view. How would that
work?
I thought of a word like derive
that caches these things? Maybe it
would keep a cache of previously derived things and just return the
answer if asked again (like memoized function in clojure and could
even be implemented that way). It would also have a mapping of how
to derive one thing from another. eg [[file "foo"]]
and create a
pipe-in to write to it. You'd first need an inputstream to the file
(as inputstream is what the pipe protocol is actually using).
The thing is, inputstreams are not values. They're stateful, pointers to places on disk. So we probably can't cache them nor need to.
derive
would be more for things like crypto keys created from a
seed.
For pipes, we need to go from a descriptor, to some platform specific
object, to a pipe. How do we keep platform specific code isolated? I'm
hesitant to make public abstractions for anything but pipes. I don't
want a file
word that creates file objects from descriptors, kcats
users should never see that. The only solution I can think of is to
just leave the platform-specific code where it is, and have some kind
of switching mechanism like clj/cljs has.
Footnotes:
Inspired by Alan Kay's quote "Lisp isn't a language, it's a building material".
Function names can and do have symbols, eg +
for addtion, but
it's just a name and it's not part of the language syntax. You can
change the name to something else very easily. There are some symbols
currently in the language that are implementation detail leaks that
will be removed as soon as a suitable replacement is found (for
example, byte array literals are base64 encoded strings with a prefix
of #b64
, which comes from EDN syntax, which is what kcats is currently
using for serialization). Filling in code templates currently uses
symbols like *3
to refer to the 3rd stack item.
Why is it done this way instead of just giving a program and its expected result? Some results don't have literal representations, so there's no way to express the expected result as a literal.
A cryptographic hash is a function that converts an arbitrary block of data into a fixed-size string of bytes. The function is designed to make it computationally infeasible to find an input that gives a particular output. This property, along with the fact that even a small change in the original data produces a significantly different hash, makes cryptographic hashes essential for content integrity checks. By comparing the computed hash of the content at two points in time, one can verify that the content has not been altered, providing a simple yet powerful tool for ensuring data integrity and security.