This file contains some rough sketches of what a state-machine-centric, functional language would look like. As part of the design, I aim for the syntactic simplicity of Python with the powerful type system of Rust.
Init
Comments, as is obvious by this point, are 2 forward slashes //
C-style inline/multiline comments may be used like so:
/*
This is a multi-line comment
*/
========
Primary syntax features: * New-line and space delimited, no pesky semicolons * Comments are CommonMark formatted markdown * Capital letters on symbol definitions generally mean "public" interface, use these with caution. See more info below
========
Core concepts:
The primary unit of work is a Machine. Machines are a set of functions, properties, and states.
By default, a Machine will have exactly 2 states: Uninitialized and Initialized. These states are only relevant
To Machines that define no states of their own, and in practice the only one you will ever see is Initialized.
States are a sub-construct within Machines. They define and scope the properties and functions a Machine can be in.
Properties and functions that are defined within a State are only accesible when the machine is within that state.
Names that collide with higher-order names will be shadowed when within a state. That is, if I define a Machine
with a top-level property foo
and within the state bar
I also define a foo
, functions defined within
the bar
state will read and mutate the foo
that is scoped to that state, leaving the outer-level untouched.
It's recommended to not do this.
States can be nested. Sub-states may or may not be match
ed when branching on a Machine's state, so use them
sparingly.
Protocols
Protocols are very similar to Rust Traits, Go Interfaces, and Python's typing.Protocol
. Protocols define Machine
stub that may or may not define default implementations. A Machine that implements a protocol must implement
the exact same state and function definitions as the protocol defines. It cannot overspecify states, and functions
must be defined in the same scope as the protocol defines.
For this reason, it is likely most Protocol definitions will not include states as part of their interface.
Protocols can be composed together, forming a Protocol set. Protocol members are merged together. Names
that overlap are assumed to be the same if their types match. If types do not match up, it is an error and the
program will not compile.
Machines may only implement one Protocol. If multiple protocols are desired, create a Prococol set and implement
that.
Functions
Functions are more limiting than in most other languages. They do not support bindings, meaning you cannot declare a variable within a function's body, only branch and call other functions. The return value of the last function call is the return value of the function. Get used to writing Lisp.
Branching
Most languages implement if
statements. Not this one. The only branching supported is structural pattern matching
with syntax ~~stolen~~ borrowed from Rust. The most common pattern matching structure is on a Machine's state.
All branches must always be handled.
Transmutation
Functions that take in a single argument and return a single, concrete type can be chained automatically by the compiler. This is best illustrated with an example. Say you want to open a file and read it's contents. To do so, we take a string representation of a file path, turn it into a configuration Machine, and finally into a File. One possible way to do this is like this:
func str_to_conf = (s: str) -> FileConf {
FileConf.new(path = s)
}
func file_from_config = (c: FileConf) -> File {
File.new(fd=open_fd(c))
}
A string can now be coerced into a File by implicit chaining
f: File = "./some_file.bin" // effectively calls file_from_config(str_to_conf(...))
M = state machine, contains states, properties, methods
P = protocol, an interface. A Machine can implement a Protocol implicitly or explicitly. Implicit implementations can be used within a module or package (file or exported lib), but must explicitly
// The simple machine implements the processor protocol explicitly
Machine Simple[Job]: PProcessor[J] = {
//S = state, a discrete state a Machine can be in. Contains properties, methods, and substates
State waiting = {
// P = protocol. This is a handle to something that implements the queue protocol/inteface
job_queue: Pqueue
}
// Any subset of "State" is a valid signifier
St processing = {
// States can have discrete properties that only exist when in that state
// These are freed/lost when they state changes
//
job: Pjob
}
// *any* subset of "State".
S errored = {
error: Perror
Srecoverable = {
failed_job: Pjob
// F = Function
// E = none, nil, empty
Freset: (s) -> E := Swaiting = {} // empty body
}
}
}
Machine Job = {
// Captial letter state symbol names indicate this state transition is public
// and can be transmuted by function bodies outside the scope of this Machine.
// That is, you can do something like Mmy_job.SComplete(id=123, outputs={name: "test"}) to directly mutate the
// state of the job.
S Pending {
inputs: (str, u64)
}
S Complete {
id: u64
outputs: { name: str }
}
S Errored {
message: str
error: Perror
}
}
// Describes something that has a close function that transitions into state closed
// How we get something in an open state is up to the implementor
Protocol Closeable {
State open {
Fclose = () -> Perror? := Sclosed
}
State closed {}
}
// Sub-protocols create protocol sets. The Reader protocol is whatever is defined here
// UNIONED with the body of Closeable. Collisions are merged.
Protocol Reader: P Closeable {
S open {
Fread = (count: int = 1) -> Perror[[u8; 0..$count]]
}
}
Mfile: Reader {
path: string
func New = (path: string) -> PError[self] := Sopen {
self.path = path
match Fmake_fd(self.path) {
}
Sopen(handle=Fmake_fd(self.path))
}
S open {
handle: fd
Fread = (count: usize = 1) -> [u8; $count] {
}
}
Sclosed {
Fopen
}
}
// builtins // These are stubs implemented by the runtime // T = type definition. Usually an alias, but can be a concrete definition of a tuple or a struct Type fd = ptr Fmake_fd(args) -> Tfd {}