This is a design for a unnamed programming language that I don't intend currently intend to make. It's dynamically typed, heavily inspired by Clojure, and designed with little thought given to performance.
The main features are:
- Inbuilt pattern matching on functions
- Error values instead of exceptions
- Unconstrained polymorphism
- Modules that are easier to statically analyze than namespaces
The reader is identical to Clojure's, with one difference: a map {: foo, : bar}
is short for {:foo foo, :bar bar}
.
This syntax sugar is lifted from Fennel.
This language has matching and destructuring similar to core.match.
[] ;; will match an empty vector
[x y] ;; will match a vector of two elements, assigning to x and y
[x x] ;; will match a vector of two equal elements, assigning to x
[x 1] ;; will match a vector of one unknown element, followed by a 1
[x _] ;; will match a vector of two elements, assigning the 1st to x and discarding the 2nd
Maps can also be matched:
{} ;; will match an empty map
{:x x, :y y} ;; will match a map containing :x and :y keys (and nothing else)
{: x, : y} ;; the same as above
{: x, :y 0} ;; will match a map containing :x and :y, where :y is 0
The ..
operator replaces the &
operator in Clojure. This defines a splice.
[x .. xs] ;; will match a vector of 1 or more elements, putting the head in x and the tail in xs
This can also be used on maps.
{: x .. kvs} ;; will match a map containing the key :x, and will put that value in x,
;; and the rest of the map (excluding :x) in kvs
A splice may also be used to construct a literal, working in reverse to a match.
We can use this to define basic data structure functions like conj
and rest
:
(fn conj [xs x]
[x .. xs])
(fn rest
([[]] [])
([[x .. xs] xs))
Because splicing works with maps, we can also define assoc
and dissoc
in the same way.
(fn assoc [m k v]
{k v .. m})
(fn dissoc
([{k _ .. m} k] m)
([m k] m))
We can further limit matching with guards. Guards are assigned per symbol using a map that's placed before the arguments. For example:
(fn inc {x num?} [x]
(+ x 1))
(fn add {x num? y num?}
([] 0)
([x] x)
([x y] (+ x y)))
This language doesn't have exceptions. Instead it uses error values.
(fn divide {x num? y num?}
([x 0] #err :divide-by-zero)
([x y] (/ x y)))
Here Clojure's data reader syntax is used. When an error is evaluated, it is bundled with information about the local environment and the stack.
A function will also generate an error if there is no match to its arguments:
(fn divide {x num? y pos-num?} [x y]
(/ x y))
This feature allows errors to propagate up the stack. Variables without guards have an implicit guard of ok?
, which returns true iff the value is not an error.
(fn reduce
([f init []] init)
([f init [x .. xs]] (reduce f (f init x) xs)))
Note that if (f init x)
returns an error, then no argument list matches because init
is implicitly guarded as ok?
.
The same syntax as Clojure is used to add metadata to values. In the case of functions, metadata is added directly to the function itself, rather than the symbol.
^{:pure true}
(fn square {x num?} [x]
(* x x))
As this language is a Lisp, it may be unsurprising that it has macros.
(macro if [p? a b]
`((fn ([false] ~b) ([_] ~a)) ~p?))
(macro do [.. body]
`((fn [] ~..body)))
(macro when [p? .. body]
`(if ~p? (do ~..body) false))
(macro let
([[] .. body] (do ~..body))
([[k v .. kvs] .. body]
`((fn [k] (let [~..kvs] ~..body)) v)))
The function syntax is flexible enough that we can define basic control structures using only functions.
Internally, a macro is just a function with the :macro
metadata key.
There is no nil
.
All lists are eager.
A module is analogous to a Clojure namespace and is defined in a file. The first top-level form must be a map, and every subsequent form must be a value with :name
metadata.
{:module math}
(fn inc {x num?} [x]
(+ x 1))
This results in a symbol map with :module
metadata:
^{:module math}
{inc (fn inc {x num?} [x]
(+ x 1))}
Modules can require other modules.
{:module example
:requires [math]}
(fn test []
(math/inc 2))
Note that math/inc
could also be written (math inc)
.
A definition is a wrapper that directly associates a symbol with a var. This is often used to embed constants in modules.
{:module math}
(def pi 3.141592653589793)
An existing function can be extended to achieve polymorphism.
The extensions are tried first, and if none match it falls back to the original function. If more than one extension matches an error is reported.
(extend str {x num? y num?} [{: x, : y}]
(str "<" x ", " y ">"))
Functions may be extended from any module.