Phi: A New Kind of Programming Language
Phi is a new programming language that is safe, powerful, and fun.
In essence, Phi combines the best of Python, Go, V, and Janet, and adds features seen nowhere else -- like macro methods, seamless contracts/type constraints, math that actually works correctly (thanks to Douglas Crockford's DEC64), 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.moment {:constraints [ $(v.birthdate <= time.now() - time.years(18)) ]}
fn main()
user := User(:id 1, :username "alan_kay", :birthdate time.moment(:year 1940, :month 5, :day 17)) !
print("Hello, ${user.username}")
Phi is Safe
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`, exit(1); in any other function we would `return err`
print(arr![0])
Phi does not have unsafe nil or null
In Phi, 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)}")
print("ten is at position ${find(arr, :wanted "ten")}")
# 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")
Phi is Powerful
Macros
By combining macros with flexible but "static" typing, Phi can be very flexible and very safe; see below.
Phi is Fun
Phi provides super high expressivity; it feels like a scripting language while providing type safety
fn main()
nums := [1 2 3 4 5]
# Note that `.map(...)` is a macro method
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)}")
print("10 factorial is also simply ${range(:min 2, :max 11).apply(*)}")
fn factorial(n int) int
if n <= 1
return 1
return range(:min 2, :max n+1).apply(*)
No cruft!
Every single character that Phi 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.phi
and can be used from any other .phi file in the same directory:
#
# globals.phi
#
postgres_connect := "..." {:priv true}
db := pq.Connect(postgres_connect) !
go fn()
from time.every(time.minutes(5))
db.ping() !
eprint("Error pinging Postgres: $err")
#
# main.phi
#
struct User
id int {:required true, :constraints [ $(v.id >= 0) ]}
username str {:required true, :constraints [ $(v.username.len > 0) ]}
created time.time {:default time.now}
fn main()
# `db.MyTable()` acts as if it returns a `![]MyTable`
recent_user_ids := [user.id for user in db.User!() if user.created >= time.now() - time.hours(5)]
# Alternate syntax, using `.filter(...)` method
prev_users := db.User!().filter(v.created < time.now() - time.hours(5))
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 Phi 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
How? Here's the implementation:
# Mutable `arr`; notice the `@` prefix telling us that `arr` is mutable
fn (@arr []T) reverse()
if arr.len == 0
return
@temp := T()
for i _ in range(:max arr.len/2) # Compiler optimizes to `for @i := 0; i < arr.len/2; @i += 1`
@temp = arr[i] !
@arr[i] = arr[arr.len/2 - i - 1] !
@arr[arr.len/2 - i - 1] = temp
# Immutable `arr`
fn (arr []T) reverse() []T
@new_arr := []T() {:cap arr.len}
for i in range(:max arr.len)
@new_arr += arr[arr.len - i - 1] # No `arr![...]` necessary; compiler proves OK
return new_arr
Macros
Phi macros look like normal Phi 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 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 range(:min 1, :max 9)
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
Map Macro Method
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(expr ...symbol) []T
# This is a mutable variable whose expanded 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
@new_arr_* = []T() {:cap arr.len}
for i v in arr
# Here, `new_arr_*` means "the last variable defined whose name begins with `new_arr_`"
@new_arr_* += ...expr # `...expr` unpacks to `v * 2`
emit new_arr_*
Phi 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.
Goroutines and Channels
Phi features Go-style channels, only more powerful, because Phi 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... I guess)
word_counter.resp <- outer_str.count(substr) + 1 !!
print("Our string contains approximately ${word_counter.resp.read!()} words")
Tuples and Destructuring
Phi 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 Phi
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 -- Phi requires that errors be handled immediately_args
is a tuple that is automatically created which contains the current function or method's argumentsnum
is the primary numerical type, and it is implemented as a DEC64 as smartly designed by Douglas Crockford- Phi 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
is defined usingtype 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)
, respectively
Non-features
Phi, proudly, does not have these features:
Per-source file imports
- Instead, in
globals.phi
we define which libraries should show up globally, in every .phi 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, arrays and mutable arrays are different types with different implementations; same with maps and other built-in types
Operators
- Phi has "op'able functions" like
+
,-
,*
, and/
, but no operators; put another way, all Phi 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
- Phi 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 Phi 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
Distributed Actors (thanks to the Relay protocol, from Relay Anonymous Messenger)
- Will leverage bidirectional channels that represent remote services
Svelte-style reactivity using the
$$
prefixVarious features copy/paste/modify/transplanted from a few additional sources
- Vale, which is safer than Rust yet dramatically simpler; generational references can replace Phi'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?
Create LowPhi, an implementation of Phi meant for very low-level systems, thus replacing 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 (Low)Phi:
# `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 Phi 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!