This raises a question I've been mulling for a year now: is a global variable just an implicit parameter passed into all functions? What does 'functional' really mean? I've been exploring new interfaces at the lowest levels of the OS that make all operations referentially transparent without any constraints on mutability. For example, the 'print' syscall takes a screen object as an ingredient. (The real hardware screen is represented as nil/0.) Is that as 'functional' as Haskell?
Store-passing style! I've been using it in my programs too. It even came up again on David Barbour's blog yesterday.[1] It's kinda funny how everything keeps coming back to the same continuation-passing styles and store-passing styles.
I think I've followed all these to a nice, general conclusion in Staccato. :)
Staccato's going to have at least two completely different (and not necessarily compatible) families of side effects: At compile time, macros will install definitions as a form of side effect. At run time, microservices will have continuous reactive side effects for communicating with each other. Nevertheless, I'm taking one consistent approach to side effects that should work in both cases. (It might be pretty inefficient when used for continuous reactive effects, but I'm optimistic.)
Staccato has no static type system (yet?), but even if it did, I would expect to have to think about run time errors anyway: Usually, a program can be written that bides its time until the Earth gets consumed by the sun, and then there's no way it'll successfully proceed to return a value. So, I accept dynamic errors, but I'll be mindful of where and when any given error could gum up the works, e.g. whether it happens on the browser side or the server side, and whether it happens before or after some other side effects occur.
So the kind of purity I'm going for involves quasi-determinism in the sense I've seen Lindsey Kuper use it when talking about LVars[2]: As long as a program isn't swallowed by the sun or otherwise interrupted, it will always return the same value. I'll still need to be mindful of where and when errors may occur in the language (e.g. server-side or client-side), so that I know which of the program's side effects should be aborted or reverted.
If a "side effect" only communicates with the language implementation itself (e.g. for debugging or profiling), that's fine. We already trust the language implementation to implement the language semantics in a single deterministic way, so we can trust it to respond to these communications in a single deterministic way as well!
If a "side effect" is tame enough that it can be removed by dead code elimination if the result value is never used, that's fine too. Arguably it has no side effects at all; the effects are all represented in its result. This means there can be some minimal support for operations that read some value from an opaque external reference (e.g. a file handle, a socket). In order to preserve quasi-determinism, the output may only vary if the input does, so these operations will tend to take an explicit parameter designating the time/world at which to do the reading. If a program makes pervasive use of these operations, it will take the shape of a sort of store-passing style, though it doesn't ever need to return a new version of the store. (For context: Whereas Haskell's State monad is used for store-passing style, its Reader monad is simplified for this special case.)
I'll do all other side effects using a commutative monad. By commutativity, any two commands in this monad can be reordered, which should guide me toward easy refactoring, extensibility, and concurrency. If I need to write any code that depends on the result of an effect, this can't usually be done in a commutative way, but a commutative effect could still set up an asynchronous callback, which can run a separate set of commutative effects in a future tick. If a computation spans more than a few of these ticks, it'll start to look like continuation-passing style. Staccato's syntax is actually pretty nice for continuation-passing style code, so this isn't a problem. CPS necessarily sequentializes the code, but when I need concurrency, I can synchronize between concurrent code the way JavaScript programmers often do these days, using promises.[3]
[3] To preserve quasi-determinism and to ensure that no two promise allocations give the same promise as their result, the allocation of a promise will itself be an asynchronous operation. (This is sort of a note to myself, because I haven't written up designs for the promise primitives yet.)
Sounds like it. The idea is that you have some irreducible amount of imperative state out in the "real world" (files in the OS, your hardware screen, etc), and the goal is to keep that as small a part of your language as possible -- so that the rest of your language can be as functional as possible.