Skip to content

Instantly share code, notes, and snippets.

@weavejester
Last active February 1, 2025 19:38
Show Gist options
  • Save weavejester/b65d67e950ead19969ebfb21957df1b8 to your computer and use it in GitHub Desktop.
Save weavejester/b65d67e950ead19969ebfb21957df1b8 to your computer and use it in GitHub Desktop.
Programming Language Sketch

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

Reader

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.

Pattern Matching and Destructuring

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

Reconstructing

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))

Guarding

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)))

Errors

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?.

Metadata

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))

Macros

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.

Nil

There is no nil.

Lazy Lists

All lists are eager.

Modules

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).

Definitions

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)

Extensions

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment