Functional language with template meta programming
We have all heard that C++ template metaprogramming is essentially functional programming, but how functional is it really? Let’s find out!
In this article, I will use C++ templates as a programming language to implement an interpreter for a simple functional language called skibid-lang, which includes a reasonable set of features. This implementation is similar to the meta-circular interpreter in SICP.
Ok, let’s begin!
Warm up
Let’s start with some simple utilities. Below is the definition of an identity function. Since we are operating at the type level, struct
will be used as our function to map from type to type. Here we define a struct
that takes a type T
and returns the type T
. The full evaluation is identity<T>::type
. Note that a C++ type is a value in skibid-lang, and skibid-lang itself is duck typed, just like the template.
Identity
template <typename T> struct identity { using type = T; };
To handle all variables in a uniform manner, all values in skibid-lang are struct
s and are subclassed from template <typename T> struct identity
. This ensures that we can always consult the ::type
, regardless of the value. This also means skibid-lang values are lazy and need to be explicitly evaluated.
Convention 1: Any skibid-lang value always contains a ::type
attribute
Quotes & unquotes
Quotes allow you to control when an expression is evaluated. For example, identity<int>::type
will be evaluated to int
, but quote<identity<int>::type>
is just a quote
and can be passed around. To retrieve the value, you can use unquote<quote<identity<int>>>::type
, which cancels out the quote
.
template <typename T> struct quote : identity<quote<T>> {};
template <typename T> struct unquote<quote<T>> : T {};
Eval
We write an eval
helper so we don’t need to write ::type
all the time. This technique is commonly seen in template libraries as well.
template <typename T> struct eval : T::type {};
Values
Let’s define some data types for the language. C++ supports basic types like int
and bool
directly at the type level. We will try to reuse as much of C++ as possible to avoid implementing these types ourselves.
Integer
The following is the data constructor for a type-level integer. It’s defined as a skibid-lang type with the additional ::value
attribute. ::value
is used to retrieve the constant value from a skibid-lang value.
template <int N> struct int_ : identity<int_<N>> { static const int value = N; };
Convention 2: A skibidi constant has a ::value
attribute
Here are some arithmetic operations for integers. Instead of implementing them within skibid-lang, we take the shortcut and reuse C++ facilities directly. Here we simply created some skibid-lang functions, unwrap parameters to retrieve the C++ integer from arguments, and perform C++ arithmetic operations. A skibid-lang function always comes with an ::apply
attribute that should be templated with some parameters.
struct plus : identity<plus> {
template <typename A, typename B>
using apply = int_<A::type::value + B::type::value>;
};
struct minus : identity<minus> { ... };
struct times : identity<times> { ... };
Now we can perform basic arithmetic with these operations. For instance, plus::apply<int_<1>, int_<2>>::type::value
should evaluate to int_<3>
.
Convention 3: A skibidi function has a ::apply
attribute that takes some arguments and evaluates to some other skibidi value
We can use the following helper to perform application instead of manually accessing ::apply
.
template <typename F, typename... Ts> struct apply {
using type = typename F::template apply<Ts...>::type;
};
Boolean
Boolean is defined in a similar way.
template <bool N> struct bool_ : identity<bool_<N>> { static const bool value = N; };
We define not_
using template specialization. First, we declare the function not_
, and then we add cases for it. Each specialization is like a branch in pattern matching, and the inheritance here serves as a return. Since bool_
is already a skibid-lang value, we don’t need to define ::type
for not_
again.
template <typename T> struct not_;
template <> struct not_<bool_<true>> : bool_<false> {};
template <> struct not_<bool_<false>> : bool_<true> {};
and_
and or_
are variadic templates. You can also make them binary operations, but the template syntax is already quite cluttered, so the fewer angle brackets, the better for us.
template <typename... Ts>
struct and_ : bool_<(Ts::type::value && ... && true)> {};
template <typename... Ts>
struct or_ : bool_<(Ts::type::value || ... || false)> {};
Ordering on integer
With boolean and integer defined, we can implement some ordering operations for integers as follows:
struct less_equal : identity<less_equal> {
template <typename A, typename B>
using apply = bool_<(A::type::value <= B::type::value)>;
};
struct less : identity<less> { ... };
struct greater : identity<greater> { ... };
struct greater_equal : identity<greater_equal> { ... };
This gives us the foundation to implemnet conditional later.
List
Let’s define a type level list. First we need two constructors cons
and nil
.
template <typename H, typename T> struct cons : identity<cons<H, T>> {};
struct nil : identity<nil> {};
It’s pretty straightforward to work with this type of list in templates. We can use template specialization to deconstruct the cons
constructor and work with the head and tail individually. Since specialization can be recursive, we can write recursive functions to work on the entire list.
template <typename L, typename R> struct append;
template <typename R> struct append<nil, R> : R {};
template <typename H, typename T, typename R> struct append<cons<H, T>, R> : cons<H, append<T, R>> {};
template <typename T> struct last;
template <> struct last<nil> : nil {};
template <typename N> struct last<cons<N, nil>> : N {};
template <typename T, typename N> struct last<cons<T, N>> : last<N> {};
template <typename T> struct length;
template <> struct length<nil> : int_<0> {};
template <typename N, typename T> struct length<cons<N, T>> : apply<plus, int_<1>, length<T>> {};
template <typename T> struct null : bool_<false> {};
template <> struct null<nil> : bool_<true> {};
template <typename F, typename L> struct map;
template <typename F> struct map<F, nil> : nil {};
template <typename F, typename H, typename T> struct map<F, cons<H, T>> : cons<apply<F, H>, map<F, T>>{ };
template <typename T> struct reverse;
template <typename S1, typename S2> struct __reverse;
template <typename S2> struct __reverse<nil, S2> : S2 {};
template <typename H, typename T, typename S2> struct __reverse<cons<H, T>, S2> : __reverse<T, cons<H, S2>> {};
template <typename L> struct reverse : __reverse<L, nil> {};
Conditional
The only conditional we need is if ... then ... else ...
. The helper if_impl
selects the skibidi value base on the condition.
template <typename C, typename T, typename F> struct if_ {
private:
template <bool C_, typename T_, typename F_> struct if_impl;
template <typename T_, typename F_> struct if_impl<true, T_, F_> : T {};
template <typename T_, typename F_> struct if_impl<false, T_, F_> : F {};
public:
using type = typename if_impl<C::type::value, T, F>::type;
};
Variables?
Variables are defined as arbitrary empty structs. For example, to use a variable x
, we need to first define struct x {};
, then wrap it as var<x>
.
template <typename Id> struct var : identity<var<Id>> {};
Different from most languages, in skibid-lang, before using any variable, we need to declare them first. So if you have a lambda that uses an x
, you need to have the following definition somewhere before the usage.
struct x__ {};
using x = var<x__>;
This is rather daunting, we can make it less painful with some macros.
#define declare(n) \
struct n##__tml__internal_defined_var_ {}; \
using n = var<n##__tml__internal_defined_var_>
// we need to declare variables before being able to use them.
(_v0);
declare(_v1);
declare(_v2); declare
Let
The let
binding comes in the form let <var> = <expr1> in <expr2>
. To evaluate a let
expression, we need to substitute the occurrence of <var>
in the body <expr2>
with <expr1>
. To do so, we need to quote <expr1>
and <expr2>
to move them around during the substitution, and then unquote everything to evaluate the substituted <expr3>
.
We now declare our substitution function for let
. Here we have let_subst_1
and let_subst_2
, two functions, but they’re really just one function. The order of specialization matters; when there are multiple possible specializations, we want to prioritize some over others. let_subst_1
acts like a filter; we only call let_subst_2
after all specializations in let_subst_1
have failed.
template <typename A, typename E, typename In> struct let_subst_1;
template <typename A, typename E, typename In> struct let_subst_2;
This case interfere with let_subst_2<_A, _E, quote<V>>
, so we pick V
out of the quote it here.
template <typename A, typename E, typename V>
struct let_subst_1<A, E, unquote<quote<V>>> : let_subst_1<A, E, V> {};
For expressions like let x = y in x
, y is .redundant and can be discarded.
template <typename A, typename E> struct let_subst_1<A, E, A> {
using type = E;
};
With all cases above out of the way, we move into let_subst_2
.
template <typename A, typename E, typename In>
struct let_subst_1 : let_subst_2<A, E, In> {};
let_subst_2
contains the full substitution.
If the body is quoted don’t touch it quoted.
template <typename _A, typename _E, typename V>
struct let_subst_2<_A, _E, quote<V>> : quote<V> {};
If right-hand side expression is an application, we substitute left-hand side expression to the body of every arguments.
template <typename A, typename E, template <typename...> typename F,
typename... Ts>
struct let_subst_2<A, E, F<Ts...>> {
using type = F<typename let_subst_1<A, E, Ts>::type...>;
};
Finally, we simply evaluate the let body.
template <typename _A, typename _E, typename In> struct let_subst_2 {
using type = In;
};
We define a new helper strict_let
that extracts quoted E
and In
, call let_subst_1
on it, and evaluates the resulted expression. We quote the result so during the substitution the evaluation is not performed. If we want to get the value back we can simply unquote the whole thing.
template <typename A, typename E, typename In> struct strict_let;
template <typename A, typename E, typename In>
struct strict_let<A, quote<E>, quote<In>>
: quote<typename let_subst_1<A, E, In>::type> {};
template <typename A, typename E, typename In>
struct let_quoted :
<typename A::type, typename E::type, typename In::type> {}; strict_let
Finally we tight everything together to make let
. let
only have specialization when In
is in
so this forces us to use the let ... in ...
syntax. let
specializes to unquote<typename let_quoted<A, quoted<E>, quote<In>>>
, it unquotes the quote added put by strict_let
.
template <typename In> struct in : In {};
template <typename A, typename E, typename In> struct let;
template <typename A, typename E, typename In>
struct let<A, E, in<In>>
: unquote<typename let_quoted<A, quote<E>, quote<In>>::type> {};
We use let like this:
<x, int_<10>, in <
let<y, int_<20>, in <
let>>>>; y
Lambda
lambda is represented as a pair of parameter list and quoted body expression. The implementation is very similar to let
. Actually we can always convert a let expression let x = <expr> in <expr>
into (\x -> <expr>) <expr>
. Because the implementation is so close, I will not explain as detailed as the let
section.
We have the similar mutual recursive substitution functions as let
.
template <typename Body, typename... Ts> struct lambda_subst_1;
template <typename Body, typename...> struct lambda_subst_2;
template <typename Body, typename... Ts>
struct lambda_subst_1 : lambda_subst_2<Body, Ts...> {};
template <typename Body, typename... Ts>
struct lambda_subst_1<unquote<quote<Body>>, Ts...> : lambda_subst_1<Body, Ts...> {
};
template <typename Body, typename... Ts>
struct lambda_subst_2<quote<Body>, Ts...>
: identity<lambda_subst_2<quote<Body>, Ts...>> {
using apply = quote<Body>;
};
template <typename Body> struct lambda_subst_2<Body> : Body {};
template <typename Body, typename T, typename... Ts>
struct lambda_subst_2<Body, T, Ts...> : identity<lambda_subst_2<Body, T, Ts...>> {
template <typename U>
using apply = lambda_subst_1<let<T, U, in<Body>>, Ts...>;
};
Like let
, we need to handle quotes for lambda as well.
template <typename Body, typename... Ts> struct strict_lambda;
template <typename Body, typename... Ts>
struct strict_lambda<quote<Body>, Ts...>
: quote<typename lambda_subst_1<Body, Ts...>::type> {};
template <typename Body, typename... Ts>
struct lambda_quoted : strict_lambda<typename Body::type, typename Ts::type...> {};
template <typename... Ts> struct lambda {
template <typename Body>
using begin = unquote<typename lambda_quoted<quote<Body>, Ts...>::type>;
};
We can now define lambdas like this:
(x); declare(y); declare(z);
declareusing func =
<x, y>::begin<
lambda<z, int_<2>, in <
let< plus, apply <plus, x, y>
apply , apply <times, z, z>
>>>>;
Put it together
We have implemented a lot of features so far, let’s tight everything together and see how does skibidi-lang look like in action. The following program computes 10 - (3 + factorial(10))
.
(x); declare(y);
declare
template <typename N>
struct factorial :
if_ < apply <less, N, int_<1>>
, int_<1>
, apply <times, factorial <apply <minus, N, int_<1>>>, N>
> {};
using plus3 =
<x>::begin
lambda<
<plus, x, 3>
apply >
using program =
<x, factorial<int_<10>, in <
let <y, int_<10>, in <
let <minus, y, apply <plus3, x>>
apply >>>>>;
// output: -3628793
int main () {
std::cout << program::type::value << std::endl;
}
It’s probably not the prettiest, definitely not the most practical, but I like how chaotic it is.
Conclusion
That’s it! We’ve implemented a simple functional language interpreter using only C++ templates and functional programming techniques. While it’s not practical for real-world use, it’s a meaningful exercise to explore the power and flexibility of C++ templates. And the most important part is, it was tons of fun! The full code can be found on github