Most of the time, you want a new memory to replace a previous one, if any. For example.
Remember (you) has (6) cupcakes. (later followed by)
Remember (you) has (7) cupcakes.
Because these two statements have the same relation, the second one replaces the first, and you now have 7 cupcakes instead of 6.
Sometimes you don't want to replace everything with the same relation, but just certain ones. For example:
When /dog/ has /n/ cupcakes, /someone/ gives /dog/ another cupcake:
Remember (key dog) has (n+1) cupcakes.
End
Here you're keeping track of multiple dogs, each of which should have a single cupcake memory. By writing (key dog) instead of (dog), the memory will replace any existing memories with the same relation and which have the same dog as that argument. Any number of arguments can be marked "key". I'm not happy with this syntax, but it's the best I've got right now. (This is sort of the inverse of the "only" syntax from the previous implementation.) However, "key" is rarely needed because memories are now on objects (see below).
Sometimes you just want to add a new memory, without replacing anything. For this, use the [also] phrase.
When /someone/ wishes (you) is friends with /p/:
Remember [also] (you) has friend (p).
End
Very occasionally, you might need to be very specific about what the memory replaces. Because Remember has a higher "precedence" than Forget, you can simply Forget whatever you want to forget and then Remember whatever you want to remember, even if the new memory matches the forget pattern. I don't have a good example.
Forget something /x/.
Remember something (y).
Memory are on objects
The most significant change from the previous design is that every memory is "stuck to" some particular object. You can specify which one:
Remember [on (dog)] (dog) is a good boy.
Forget [on (dog)] (dog) wants a walk.
If you omit the [on (object)] phrase, it defaults to (you).
The remembered statements on an object are only there when the object itself is there and running. You can think of the memories as a kind of second "active text" which is more amenable to programmatic manipulation. Or as an "active database" to complement the "active text". Or even as the next generation of Josh's "storing data on pages".
Replacing, as described above, replaces only within the particular object's memories. So a more natural to way to write the dog/cupcake example would be:
When /dog/ has /n/ cupcakes, /someone/ gives /dog/ another cupcake:
Remember [on (dog)] (dog) has (n+1) cupcakes.
End
In the earlier example, every dog's information is stored on (you), so we needed to write (key dog) to replace just that one statement without affecting those about other dogs. In the example immediately above, each dog's information is stored on the dog itself.
This is usually the best way to do it. An exception might be if you need the information even when the object is not out. For example, as keyboard events come in, we remember the key state (which keys are down) on the "Keyboards" page, not on the keyboard object itself, because we need to keep track of the shift key even when the keyboard is covered up. On the other hand, the pasteboard is remembered on the keyboard itself.
Memories can be seen and edited
A page's text is visible (either it's printed, or on a blank it's illuminated), and patched text is indicated with red illumination.
Similarly, every page's memories are also visible. Like patches, you can hide them, but you should feel ashamed to do so.
Wish (you) hides memories.
Visible state takes some getting used to, but oh man, it really feels like lifting a blindfold. You can see where it is. It's really there!
The idea is that for every statement in the frame, you should be able to point to the physical location where that statement is made. Either it will be a Claim/Wish line in a page's text, or a Remembering line in a page's memories.
The memories illuminated on an page are displayed in abbreviated form. To see the full statements, you can poke an editor at them.
Memories are fully editable. You can change them, delete them, copy and paste them between pages. You can even cut and paste to move a statement from a page's memories into the page's text, changing "Remembering" to "Claim", in order "crystalize" it.
It's kind of like manually editing a database -- you shouldn't need to do it very often, but the fact that you can makes it feel very accessible and tangible.
For efficiency, memories are only converted to text when necessary. Most of the time, they're just plain old statements in the heap.
Memories are persistent
When an page goes away or the system is shut down, the page's memories are saved as text to the file "shared/memories/nnnn.txt" where nnnn is the page number. (When a non-page object's memories are saved, nnnn is based on the object id.) When the page shows up again, its memories are loaded from this file.
A memory might reference a value that isn't well-represented in editable text:
-- This is a fun program which leaves a snapshot when you stop pointing to a page.
When (you) points "left" at /p/, (you) has appearance /image/:
Remember [on (p)] (you) wishes (p) shows image (image).
End
In this case, (image) is a table containing a large userdata. If you look at the memory text, it looks like:
Remembering "<Page 30738>" wishes (you) shows image (Ref "table-9092752df80e0ec7").
The "Ref" represents a value which is serialized to a separate file when the memory is saved. These values always travel with their text. Whenever a text (either page text or memory text) is synced, the incoming and outgoing texts are scanned for Refs, and the corresponding files are downloaded or uploaded to the file server. (See below about files.)
For efficiency, again, this serialization only happens when saving. Most of the time, a remembered value is just a value in the heap.
Memories on virtual objects
The lifetime of memories is controlled by two claims:
Claim (object) can remember.
Claim (object) persists memories.
When someone starts claiming an object can remember, the object's memories are loaded and activated. When the claim goes away, the memories also go away. If someone has ever claimed an object persists memories, its memories are saved before being deactivated; otherwise they are discarded.
Both claims are always made for pages. They are useful for controlling the memories of non-page objects.
For example, you might want to spawn a turtle which remembers its own position and heading. Because a virtual object is just an id you make up, you need the first claim in order to indicate the lifetime of the object. When you stop claiming the turtle can remember, that's when its memories go away.
When /someone/ wishes (you) has a turtle:
local turtle = create_id(you, "turtle")
Claim (turtle) is a "turtle".
Claim (turtle) can remember.
End
You may want the turtle to persist memories, if it's "your page's turtle" and you want it to come back when it's wished for again. On the other hand, many virtual objects are "throwaways", like raindrops and keyboard fountains, so you don't want them to persist memories -- you want them to be totally forgotten when you're done with them.
Expiration
You can make memories that automatically go away at a particular time:
Remember [for (2) seconds] (you) is thinking very hard.
Remember [until time (t)] (you) is waiting for something.
This can be used for everything we used "hold wish" for, but in a more visible and efficient way.
Remember [for (1) seconds] (you) wishes (editor) displays banner "Printing...".
For a long time, I had been considering something like Claim [for (1) seconds], but it seemed to add a lot of complications which ruined the "purity" of Claim (e.g. it matters when the evaluation happens, what happens if it runs multiple times, where do you see the state, etc.) Turns out that these are exactly the complications which Remember deals with, so it was perfect to realize that this feature actually wanted to be a memory.
It behaves exactly like any other memory (it lives on an object, it can replace other memories, it can be forgotten with Forget, it's visible, it's persisted); it just has an expiration time when it is automatically forgotten.
A secondary goal of memories was to get rid of passing around pathnames to local files.
For example, after I take a video on my phone using the mobile camera (forward reference!), there's now a card on the table which is remembering that it represents "video" (video). That card -- the physical object -- should be our handle to the video, not a pathname in some directory. If we want to play that video, we should reference that physical object, or at least reference the (video) value that the object is remembering.
When /card/ represents "video" /video/:
Wish (card) plays video (video).
End
In a world where all Realboxes are on the LAN, that (video) value could be a pathname to an sshfs shared folder. But in our more distributed Realtalk world, we'd like a way of representing files such that they can be uploaded to a server when syncing, and downloaded only when needed, because there's no need for you to have all of my camera videos on your local machine unless I hand you an object which references one of them.
The new approach is to represent files as URLs. So, (video) would be an URL string. Whenever you need the actual contents of a file, you get it asynchronously:
When (you) reads URL (url) as string /str/:
When (you) reads URL (url) as bytes /bytes/:
When (you) reads URL (url) as local file /pathname/:
If the file has already been cached locally, these whens make their claims immediately; otherwise, they put a nice "Loading." banner on (you) while they download the file.
Web URLs ("https://...") work as you'd expect. Locally-generated files (such as my video) use the "realtalk:" URL scheme.
realtalk:46e0bcd4125a84b5fd5bc47cc92dbe05.mov
These URLs currently aren't on the web, because I don't want my camera videos on the open web. (They might be someday, if we figure out authentication or go Tailscale -- it would make it easier to e.g. stream videos.) But Realtalk knows how to download realtalk URLs from the file server, using rsync over ssh. (Our server is currently "dynamic.land", which is being hosted on the same account as "
dynamicland.org". Other Realtalk installations/universes would use their own servers.)
More importantly, Realtalk knows how to upload these files. When a memory is turned into text, any realtalk URLs are represented as Refs:
Remember "<Page 30738>" claims (you) represents "video" (Ref "46e0bcd4125a84b5fd5bc47cc92dbe05.mov").
which means, like any Ref, when syncing, the file is uploaded to the server before the memory is committed.
Here's how you create "persistent files":
local url = realtalk_URL_for_file_with_contents(contents, ".mov")
local url = realtalk_URL_for_local_file("/tmp/my_file.mov")
These are "permanent URLs" which you can remember and pass around however you like, and if they end up on another machine, that machine will be able to download the file.
I would be fine with replacing realtalk URLs with something more standard someday, if the standard thing has the properties we need.
To initiate the sync procedure:
Wish (processor) syncs.
Currently, explicit syncing is the only way to get information from one machine to another. (Currently, "areas" only work on the same machine.) Once I dive into networking, we should be able to physically carry objects from one area to another and have them just work, like the good old days, but I don't know exactly how that will work.
I changed all of the uses of "Wish (you) has persistent state" to use memories instead. It went fine. Everything with state, including text boxes, editors, and keyboards, now proudly display their memories. You can read the pasteboard on the keyboard.
A set of pages currently in Time Kit:
- Memories: adding and removing remembered statements from the frame
- Memory text: converting memory statements to/from textual representation
- Memory boxes: the visible box at the bottom of the page
- Memory macros: Remember and Forget macros
- Memory refs: making, dereferencing, and saving Refs; making and reading realtalk URLs
- Ref macro: Ref macro (separate so it can be used by itself for scanning text for Refs)
A comment on the process of design
A primary theme of Realtalk is making computational entities visible and tangible. One would hope that would serve as a guiding principle, and it must at some level, but the experience of design is usually one of struggling with a design that doesn't feel right, laboriously coming to a better solution, and recognizing only in retrospect that the essence of the solution was making some computational entity visible and tangible.
Appendix A: Keyboard fountains (when not pointing)
This shows a nice way of working with spawned virtual objects. The first rule notices keypresses, and spawns a bullet by remembering it exists with an initial state. The second rule matches each bullet, and either remembers an updated state or forgets the state (thus disposing of the bullet).
This keeps all the memories on the page (and you can see them as you hit keys!), but another approach is to keep memories on the bullets themselves.
Appendix B: Who uses state?
To convert all of the uses of "Wish (you) has persistent state" to memories, I used "print with logs" to print out a checklist, and then I checked off each page as I updated it.
Appendix C: Virtual state whiteboard from 9/11/2020
Here's the whiteboard from a year ago when we were first planning Realtalk-2020. I think memories covers most of what we were thinking about here, with the exception of subsuming printing and patched text. (That might happen eventually, but I didn't want to make such a severe change right now.)