Voltair: A New Kind of Programming Language
Voltair is a new programming language that is powerful, safe, and fun.
In essence, Voltair seeks to combine the best of Go, V, Python, and Janet (a Lisp), and adds features seen nowhere else -- like macro methods, seamless contracts/type constraints, math that actually works correctly (thanks to Douglas Crockford's DEC64; no more 1.1 + 2.2 != 3.3
), and a distributed, end-to-end encrypted actor model.
Here is a quick example:
struct User
id int {:min 1}
username str {:min_len 2}
birthdate ?time.datetime {:constraints [ $(v == nil or v <= time.now() - time.years(18)) ]}
fn main()
user := User(:id 1, :username "alan_kay", :birthdate time.datetime(:year 1940, :month 5, :day 17)) !
print("Hello, ${user.username}")
The features utilized here that are explained in more detail below:
Struct types
Struct field metadata that includes constraints (e.g.,
{:min_len 2}
)Sumtypes!
?time.datetime
is short forsumtype<nil time.datetime>
, representing a value that is eithernil
or of typetime.datetime
- In general the special syntax seen above, namely
?T
whereT
a type, is short forsumtype<nil T>
- In general the special syntax seen above, namely
More generally: variable constraints (see below)
Lambdas with type inference (i.e.,
$(...)
)- For example, in a case like
{:constraints [ $(v == nil or v <= time.now() - time.years(18)) ]}
above (wherev
stands for "value"), the type system knows that the constraint functions are of typefn<T>(v T) bool
, which in the above case concretizes tofn(v ?time.datetime) bool
- Thus,
$(...)
is shorthand for(fn(v ?time.datetime) bool return ...)(user.birthdate)
inside of a for-loop that iterates over each constraint and passes inuser.birthdate
- That is,
$(v == nil or v <= time.now() - time.years(18))
is shorthand for(fn(v ?time.datetime) bool return v == nil or v <= time.now() - time.years(18))(user.birthdate)
. Much cleaner!
- For example, in a case like
Type instantiation via type functions (e.g.,
User(...)
)Named function parameters (e.g.,
User(:id ..., :username ..., :birthdate ...)
)Automatic error handling via trailing
!
after a function call that returns an error (e.g.,user := User(...) !
); see below for moreString interpolation (e.g.,
"Hello, ${user.username}"
)
Voltair is Powerful
Macros
By combining macros with flexible but "static" typing, Voltair can be very flexible and very safe; see below.
Voltair macros look like normal Voltair code:
# `$#`, below, purposely looks kind of like a comment because it doesn't run at
# runtime, just like comments don't run at runtime (or at all)
macro def_strs(vars ...symbol)
$# for var in vars
$var := "$var"
def_strs(a b c)
# The above macro-expands at compile-time to:
a := "a"
b := "b"
c := "c"
# Thefore, at the end of the call to the `def_strs(...)` macro:
print(a == "a") # true
print(b == "b") # true
print(c == "c") # true
Compile-time type generation using macros:
$# for n in 1 thru 8
type int${n*8} [$n]byte
This macro-expands to:
type int8 [1]byte
type int16 [2]byte
type int24 [3]byte
type int32 [4]byte
type int40 [5]byte
type int48 [6]byte
type int56 [7]byte
type int64 [8]byte
Macros can also be used to ensure that certain computations are done at compile-time, not at runtime, although the compiler will do its best to compute as much up front so as to prevent runtime slow-downs:
macro calc_powers(base int, max_power int) []int
@pow_arr_ := []int() {:cap $max_power + 1}
@current_ := 1
$# for _ in 0 thru $max_power
@pow_arr_ += current_
@current_ *= $base
emit pow_arr_
fn main()
powers_of_two := calc_powers(:base 2, :max_power 10)
print("2 to the 10th power is ${powers_of_two[10]})
This macro-expands to something like:
fn main()
powers_of_two := do
@pow_arr_a := []int() {:cap 10 + 1}
@current_b := 1
$# for _ in 0 thru 10
@pow_arr_a += current_b
@current_b *= 2
emit pow_arr_a
print("2 to the 10th power is ${powers_of_two[10]})
The $#
before for
ensures that the loop is executed at compile-time.
Here is the fully expanded code that gets directly compiled after all macro expansions and calculations have occurred:
fn main()
powers_of_two := do
emit []int(1 2 4 8 16 32 64 128 256 512 1024)
print("2 to the 10th power is ${powers_of_two[10]})
Macro methods
This code:
doubled := [1 2 3].map(v * 2)
...macro-expands to:
doubled := [v * 2 from [1 2 3]]
...which finally macro-expands to:
# `i` stands for "index", `v` stands for "value"
doubled := [v * 2 for i v in [1 2 3]]
Under the hood, this code is as efficient as the array.map(...)
macro method:
macro (arr []T) map(use_v_or_i expr) []T
# This is a mutable variable whose ultimate name begins with `new_arr_` and
# ends with a randomly-generated suffix guaranteed to prevent this variable
# name from colliding with an existing variable in the context where
# this macro method is called; this is true for any variable defined
# in a macro whose name ends with `_`
@new_arr_ := []T() {:cap arr.len}
for i v in arr
# `$use_v_or_i` expands to `v * 2` in the example above
@new_arr_ += $use_v_or_i
emit new_arr_
Voltair macros are purposely unhygienic so as to make the above possible, where v
from the calling context can be referenced inside the macro.
Unwanted conflicts between the macro's "local" variables with the calling context's variables is done via the _
suffix trick described above.
Another macro method, arr.partition()
:
db := pq.connect("...") !
struct User {:db db}
id int {:min 1}
username str {:min_len 2}
birthdate ?time.datetime {:constraints [ $(v == nil or v <= time.now() - time.years(18)) ]}
fn main()
all_users := db.User!()
with_birthdates nil_birthdates := all_users.partition(v.birthdate != nil)
Another macro method, arr.count(...)
, as a more efficient way to do arr.filter(...).len
:
how_many_evens_under_100 := to(1 100).count(v mod 2 == 0)
Voltair is Safe
Type constraints and other metadata
# The following creates mutable variables `x`, `y`, and `z`, makes
# each of them of type `num` with a default value of `-1`, and uses a
# constraint to ensure they cannot ever be set equal to 0:
def @x @y @z num {:default -1, :constraints [ $(v != 0) ]}
x = 2 !
x = 1 !
x = 8 - 5 - 3 ! # Will throw error
Voltair has dependent types
The number of outputs depends on the number of inputs. Here we also see the use of a macro method and generics:
macro (arr []T) group(exprs ...expr) [exprs.len][]T
@groups_ := [exprs.len][]T()
for v in arr
for i exp_ in exprs
if $exp_
@groups_[i] += v
emit groups_
# Note that this requires being able to destructure a fixed-size
# array, which is a good idea anyway:
small big := to(1 1000).group(v < 100, v >= 100)
a2m n2z vowels := to("a" "z").group(v <= "m", v > "m", v in "aeiou")
Accessing array elements is always safe
fn main()
arr := []str()
# Trying to get the first element of an empty array will not blow up.
#
# This will not panic or crash, but gracefully handle the error and, since
# we are in `main`, `eprint(err)` then `exit(1)`; in any other
# function, an attempted out-of-bounds array access would result in
# `return errors.out_of_bounds`
result := arr[0] !
print(result)
Note that arr![0]
is shorthand for arr[0] !
, which again is shorthand for arr[0] ! return errors.out_of_bounds
, which in turn is shorthand for something like:
n := int(0)
result := if n < 0 or n > arr.len - 1
return errors.out_of_bounds
else
arr[n]
print(result)
Negative indices are OK as long as they are hardcoded:
last := arr[-1] !
Voltair does not have unsafe nil or null
In Voltair, nil
is its own type; there is no such thing as a nil
map, array, channel, pointer, or any other type. If we want to indicate the lack of a value, we can create a ?T
rather than a T
, where T
is a type.
For example:
fn find(arr []T, wanted T) ?int
for i v in arr
if v == wanted
return i
return nil
fn main()
arr := ["zero" "one" "two" "three" "four"]
wanted := "four"
print("four is at position ${find(arr wanted)}") # "4"
print("ten is at position ${find(arr, :wanted "ten")}") # "nil"
# Another example:
hello := ?str("I am essentially a nilable string that can be safely used as a string only after ensuring I'm not nil")
if hello == nil
print("nil")
else
print("`hello` as a str: $hello")
Voltair is Fun
Voltair provides super high expressivity; it feels like a scripting language while providing type safety
The arr.apply(some_func)
method "applies" in the Lisp sense; that is, arr.apply(some_func)
means some_func(arr)
. This is syntactic sugar to enable convenient method chaining as well as to improve the readability of using functions that look like operators:
fn main()
nums := [1 2 3 4 5]
# Note that `.map(...)` is a macro method; see below for details
nums_str := nums.map(v.str()).join(", ")
print("The sum of $nums_str is ${nums.apply(+)}")
print("The product of $nums_str is ${nums.apply(*)}")
print("10 factorial is ${factorial(10)}")
total := thru(2 10).apply(*)
print("10 factorial, calculated more simply, is (thankfully) still $total")
fn factorial(n int) int
if n <= 1
return 1
return thru(2 n).apply(*)
No cruft!
Every single character that Voltair makes you type is necessary while maintaining readability and clarity regarding what exactly is happening (regarding mutations, allocations, etc):
fn main()
# The `:` in `:=` means we are allocating, and the `=` means we are assigning.
#
# No commas are required between values in an array:
nums := [1 2 3]
print(nums)
Seamless database integration
All imports and global constants are defined in globals.volt
and can be used from any other .volt file in the same directory:
#
# globals.volt
#
postgres_connect := "..." {:priv true}
db := pq.connect(postgres_connect) !
go fn()
# `10m` is a duration literal representing 10 minutes
for _ in time.every(10m)
db.ping() !
# Manual error handling (in the case where `db.ping` returns an error)
eprint("Error pinging Postgres: $err")
continue
log.debug("DB pinged successfully")
#
# main.volt
#
struct User {:db db} # Enables database integration
id int {:required true, :constraints [ $(v > 0) ]}
username str {:required true, :constraints [ $(v.len > 0) ]}
created time.datetime {:default time.now}
# Struct default values can either be a `T` or a `fn() T`. Use the
# latter to delay when the default value is set until object
# instantiation (rather than, if possible (e.g., `{:default 5}`),
# compile-time)
fn main()
# `db.MyType()` acts as if it returns a `![]MyType` -- that is, an
# `error` or a `[]MyType` for any struct type that opts into database
# integration by by doing `{:db db}` above
recent_user_ids := [user.id for user in db.User!() if user.created >= time.now() - 5h]
# Note that `f!()` is shorthand for `f() !`, but is much more
# easily chainable.
#
# Alternate syntax, using `.filter(...)` method:
prev_users := db.User!().filter(v.created < time.now() - 5h)
Mutable and Immutable Versions of Methods
In Python we have to do this:
nums = [5, 4, 3, 2, 1]
ordered = sorted(nums) # Immutable sort
nums.sort() # Mutable sort
But in Voltair we don't need 2 names for the same concept. Instead we can create 2 different methods with the same name -- one for sorting immutable arrays (and thus returning a value), and one for sorting mutable ones (and thus not).
@nums := [5 4 3 2 1] # `@` makes `nums` mutable
ordered := nums.sort() # Immutable sort; no `@` needed here because `nums` is not mutated on this line
@nums.sort() # Mutable sort; `@` is needed here because `nums` _is_ mutated
We can of course do the same when it comes to reversing an array:
@nums := [5 4 3 2 1]
ordered := nums.reverse()
@nums.reverse()
How? Here's the implementation:
# Mutable `arr`; notice the `@` prefix telling us that `arr` is mutable
fn (@arr []T) reverse()
for i in 0 to arr.len/2
swap(@arr[i], @arr[arr.len - i - 1])
macro swap(@a @b expr)
@temp_ := $a
@$a = $b
@$b = temp_
# Immutable `arr`
fn (arr []T) reverse() []T
@new_arr := []T() {:cap arr.len}
for i _ in arr
@new_arr += arr[arr.len - i - 1]
return new_arr
Goroutines and Channels
Voltair features Go-style channels, only more powerful, because Voltair also gives us bidirectional typed channels. That is, you can define not just a chan<T>
but a chan<T U>
to support request/response patterns, where you want to send a value of one type over a channel, and receive a response that is a value of another type:
# Define a channel where we want to send a `tup<str str>` and receive back a `num`
word_counter := chan<tup<str str>, num>() {:cap tup(1 0)}
# Remember, `tup("..." "...")` is shorthand for `tup<str str>("..." "...")`
word_counter.req <- tup("Hello there! Count the words." " ") !
go fn()
# Note that `<-ch` is the same as `ch.read()`
# Tuple destructuring:
outer_str substr := <-word_counter.req !
eprint("Error reading request from channel `word_counter`: $err")
return
# This trailing `!` is also for handling write errs (e.g., chan could be closed)
word_counter.resp <- outer_str.count(substr) + 1 !
print("Our string contains approximately ${word_counter.resp.read!()} words")
Tuples and Destructuring
Voltair has generic tuples with type inference:
# Tuples
fn get_tup() tup<num bool str>
return tup(1 true "three")
# Note that
#
# tup(1 true "three")
#
# is shorthand for
#
# tup<num bool str>(1 true "three")
# Valid
t := get_tup()
# Destructuring a tuple; the variable names can be anything
a b c := get_tup()
# Destructuring a struct; the variable names must match the `user` struct's field names
id username email := user
Facts About Voltair
All mutations are explicit; variables that are about to be modified must be prefixed by
@
Key axioms guiding its design:
- Asking, "What should I be able to do?"
Optionals via sumtypes:
?T
is short forsumtype<nil T>
whereT
is a type (e.g.,num
,str
,bool
, etc)Results via sumtypes:
!T
is short forsumtype<error T>
whereT
is a type, but with one very important difference -- Voltair by default requires that errors be handled immediately, not passed around- If you want the
!T
instead of returning the error immediately, you can doerr_or_return_value := f() ! err
- If you want the
_args
is a tuple that is automatically created which contains the current function or method's arguments- This is very similar to JavaScript's
arguments
variable
- This is very similar to JavaScript's
_fn_name
is a string that is automatically created which equals the current function or method's namenum
is the primary numerical type, and it is implemented as a DEC64 as smartly designed by Douglas Crockford- Voltair originally used Dec128 (IEEE 754-2008) but this was later rejected for being too complex, needlessly slow, needlessly large, and for treating positive and negative infinity as valid numbers, which one almost never wants
int
should be treated as if it is defined as such, thus making it compatible withnum
:type int num {:constraints [ $(v mod 1 == 0) ]}
from arr
is exactly equivalent tofor i v in arr
The methods
arr.keys()
andarr.values()
are not needed because we can simply doarr.map(k)
andarr.map(v)
, respectivelyIn Lisp-speak, macro bodies are quoted by default, and unquoted using the
$
prefix. The analogy here is that a macro body is almost like a (type-checked) multi-line string, and so we use string interpolation-like syntax to unquote the symbols or expression passed in.struct MyType ...
is currently shorthand fortype MyType struct<...>
, but may be removed in the futureVoltair has generic, anonymous struct literals, e.g.,
{:name "Alice", :age 35}
, which is shorthand forstruct<name str, age num>(:name "Alice", :age 35)
- Create an empty struct like this:
struct()
.{}
may also be supported in the future
- Create an empty struct like this:
All variables are public by default. To make them private, use
defpriv
to define them or, more generally, set{:priv true}
Named enums
- E.g.,
type FD int {:min 0, :named {:stdin 0, :stdout 1, :stderr 2}}
- This enables us to do
write_bytes_to_fd(.stdin "Hello, world.bytes())
; see below for full code example
- E.g.,
emit ...
causes a macro or block to evaluate to the given expression, kind of like a return value
Examples:
big := if n > 100
emit "Yes"
else
emit "No"
But that example is so simple that we might as well just do
big := if n > 100
"Yes"
else
"No"
More complex example that puts emit
to better use:
body status_code := http.get_all(url) !
if err.str() == "Some error we don't care about"
emit tup([]byte() 200)
return err
See the macro methods section above for an example of using emit
inside a macro.
Voltair also supports one-line
if
expressions:big := if n > 100 then "Yes" else "No"
- Leaving out the
else
clause will result in a?T
rather than aT
, sobig := if n > 100 then "Yes"
would result intype(big) == ?str
, also known assumtype<nil str>
- Leaving out the
The compiler will optimize
for n in min to max
into a traditional for-loop (i.e.,for @n := min; n < max; @n += 1
)
Non-features
Voltair, proudly, does not have these features:
Per-source file imports
- Instead, in
globals.volt
we define which libraries should show up globally, in every .volt source file in the same folder
- Instead, in
nil
values that can be passed around and blow shit up when used- Unlike in Go, the default value of arrays (slices), channels, and pointers is not
nil
but rather a useful, allocated, empty value of the desired type
- Unlike in Go, the default value of arrays (slices), channels, and pointers is not
Variable shadowing
Boilerplate everywhere
Parentheses everywhere, as in all other Lisp-like languages
Commas everywhere, as in non-Lisps (Go, C, C++, Python, JavaScript, Java, etc)
Print functions other than
print
,prin
(to exclude trailing newline),eprint
(for printing tostderr
), andeprin
- No
println
,printf
,sprintf
, etc
- No
Ways of defining a variable other than
type
for types,fn
for functions, anddef
for everything else- No
var
,let
,const
, etc - Putting an
@
in front of the var makes it mutable (e.g.,def @nums [1 2 3]
or@nums := [1 2 3]
)
- No
Static-feeling code
Pretending types don't exist
Unhelpful helper functions that make the language gigantic and harder to learn
- See: much of Clojure
Unnecessary types
- In Janet, I believe that mutable arrays and immutable arrays are different types with different implementations; same with maps and other built-in types. In Voltair's case, mutability is attached to the variable, not the type.
Operators
- Voltair has "op'able functions" like
+
,-
,*
, and/
, but no operators; put another way, all Voltair operators are functions, which eliminates the need for having 2 names for the same concept and functionality;+
is a real function that can be passed around, for example, like in Lisps, but not in the weaker scripting languages we are used to
- Voltair has "op'able functions" like
Explicit pointers
- If for performance reasons it is important for certain types or values to not be pointer types under the hood, you can do
{:no_ptr true}
- If for performance reasons it is important for certain types or values to not be pointer types under the hood, you can do
Future Features
SkyCastles, which is what Voltair calls a not-common-enough pattern in Go code.
- In essence, a SkyCastle provides a service to the other parts of your program
- It's not just a passive, inert data structure, but a service that receives values and even functions for mutating the data structures that are private to said service/SkyCastle
- This is usually implemented as an infinite loop in a function or method that's been launched in a goroutine listens on 1 or more channels for values that are then processed. By making SkyCastle a built-in data type, the boilerplate code can be eliminated and this pattern made more popular, often supplanting the passing around of a mutable data structure
- With SkyCastles in place, combined with secure distributed actors (see below), scaling systems should become much simpler, since the major parts of the system will be able to refer to each other via local objects, with local "router" goroutines directing requests to SkyCastles that happen to be running in the same process or machine, or routing them to other servers as need be (via Relay)
- Using this machinery, I imagine a world in which "microservices" self-monitor and, when scaling or reconfiguration is necessary, can move themselves between physical servers on the fly, or "call for back up" and spin up new instances of themselves, all without any downtime, and without the code talking to these microservices changing a single line
Secure distributed actors (thanks to the Relay protocol, which comes from the Relay Anonymous Messenger I've been creating since mid-2023)
- Will leverage bidirectional channels that represent remote services, just like bidirectional channels represent local services
Svelte-style reactivity using the
$$
prefix (e.g.,$$followers := db.User!().filter(me.id in v.follows)
)- The frontend should be able to look like it is directly representing the backend database and subscribe to updates coming from the DB, receiving them via WebSocket, just like Hyperfiddle brilliantly does (see below)
Various features copy/paste/modify/transplanted from a few additional sources
- Vale, which is safer than Rust yet dramatically simpler; generational references can replace Voltair's locks on global mutable variables, for example
- Hyperfiddle's Electric Clojure, which abstracts away the network calls between the frontend and backend! It also lets programmers write simple functions that get compiled to some frontend code and some backend code
Generative testing, a la clojure/test.generative
Possible Future Features
Create convenient syntax for creating lazy sequences?
- Perhaps by allowing array metadata to include
{:lazy true}
- Perhaps by allowing array metadata to include
Create an implementation of Voltair meant for very low-level systems in an attempt to replace/displace C.
C code example from a book on Xv6 Unix:
char buf[512];
int n;
for(;;){
n = read(0, buf, sizeof buf);
if(n == 0)
break;
if(n < 0){
fprintf(2, "read error\n");
exit(1);
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit(1);
}
}
The equivalent solution in Voltair:
# `FD` == File Descriptor
type FD int {:min 0, :named {:stdin 0, :stdout 1, :stderr 2}}
fn main()
cat() !
fn cat() !
@buf := []byte() {:len 512, :fixed_len true}
for
n := read(.stdin @buf buf.len) !
if n == 0
break
# `.stdout` is short for `FD.stdout` and `FD[:stdout]`. This works because
# I'm imagining that `write()`'s first argument is of type `FD`
write(.stdout buf n) !
Implementation Status
The Voltair compiler is currently being prototyped and is in the very early stages. Please steal these language features to help ensure that the future of software is much more reliable than it is today!
In particular, I consider very few programming languages to be designed well. The following languages largely are, and thus they are worth salvaging, and thus I hope they steal as many good ideas from Voltair as possible:
- V
- Go
- Janet
- Vale
- Electric Clojure
- Elixir
- Amber
- Nim
- Kotlin
- TypeScript