Photo by Florian Olivo on Unsplash
A Functional Programming Language
A fun experiment on a lazy Sunday afternoon.
For some inexplicable reason, I suddenly became interested -- and for a short time, engrossed -- in creating my own programming language. And of course, like many things, my interest waned. However! Before that happened, I was able to settle on some general syntax and write a few functions that could conceivably work.
All of this was, of course, done for no other reason than fun and games. The coolest part of all was when I showed my kids (7 and 9) on a lazy Sunday afternoon. Within five minutes, they were able to understand it and even started writing simple functions on paper!
So here it is. I'm not sure what to call it, if it even deserves a name. I probably will never write the compiler for it either. None of that really matters. It was fun.
Overview
The "big idea" is that everything is expressed as a transformation that takes a single input and generates a single output. While I like to think of this thing in the middle as a "transformation", it's more-or-less analogous to a "function".
input -> transformation -> output
Everything builds off of this idea.
Given this, I added a few additional rules (constraints) for myself:
No if/then/else
No ternaries
No "double logical operators" like
==
,!=
,<=
,>=
,&&
,||
Few datatypes (I eventually found that I need tuples, lists, and single values)
Data is immutable
Functions can be chained together with successive arrows
Starting Simple
I want to imagine that I have a simple function that doubles a number. I would call it like this.
3 -> double -> result
In this example, I put 3 into the double
function and direct its output 6
to a new variable called result
. Additionally, I imagine that if I didn't redirect its output to a variable, then it would just be printed to console.
3 -> double
That ^ would just print 6
to the console.
So, it seems like variables are instantiated and initialized directly as the output of a function... or at least maybe I could say at the end of an arrow.
Naturally, it should follow that function names are defined in the same way. Here is how I would define the double
function:
n -> n*2 -> double
Because n
is not already defined in scope, the compiler knows that it should represent the function argument. Then, given that n
, it performs a transformation n*2
, and outputs the result to the new variable double
. This is generally how functions are defined.
One other key aspect of the language is chaining. Now that I have a double function, I can call it successively.
3 -> double -> double -> result
This will store 12
in the new variable result
.
Here are some additional simple function definitions:
n -> n/2 -> halve
n -> n+1 -> increment
n -> n-1 -> decrement
Using these, I can contrive this silly example:
3 -> double -> increment -> double -> halve -> decrement -> result
This is essentially the same as doing this: (((((3*2)+1)*2)/2)-1)
and storing 6
into result
.
Slightly More Complicated
Any good language needs a way to compare things and branch. This is where I introduce a new data type: the tuple. It's essentially the same idea as a tuple in Python.
I want to write a greater-than-or-equal-to function, but I don't want to use >=
. I have =
for equality and >
for greater-than. Also, for some inexplicable reason, I don't want to have syntax for and
or or
.
Here's what I want to write:
(5, 3) -> gte -> x
(5, 5) -> gte -> y
(3, 5) -> gte -> z
When I write this, I want both x
and y
to contain 1
and z
to contain 0
. Given all of my constraints, here's how I have decided to write this function:
(a, b) -> (a > b) + (a = b) -> gte
The trick here is that booleans are simply 1
or 0
, allowing you to do arithmetic with them. C works the same way. Given this, I'm sure you could imagine how the math works out (remember, =
is not an assignment operator; it is a logical equality operator).
So, given this, I could also invent less-than-or-equal-to like this:
(a,b) -> (a<b)+(a=b) -> lte
note:*spaces don't matter*
OK cool! Now what about branching? Here's what I imagine:
(?, a, b) -> a*? + b*(1-?) -> branch
The argument is a tuple consisting of three variables: ?
, a
, and b
. The ?
is meant to be an evaluated condition. If ?
is "true", then a
is returned. Otherwise, b
is returned. This works out well for numbers, but some special rules have to be made for tuples and other data types.
For those situations, I think I could just represent tuples and other data types using two values: its actual value and it a "boolean" where 0
represents emptiness and 1
represents "fullness". Then, when you perform arithmetic operations on it, it can switch between the two values under the right circumstances.
So, using what I've come up with so far, here is a contrived example:
((2,5)->lte, 3, 4) -> branch -> double -> result
This says, "if two is less-than-or-equal-to five (which it is), then send three to the double
function and send the double
function's output to result
. In this case, I expect result
to contain 6
.
With this design, I can even make min
and max
functions:
(a, b) -> ((a, b)->lte, a, b) -> branch -> min
(a, b) -> ((a, b)->gte, a, b) -> branch -> max
That's not bad, but I kind of think the extra (a, b)
is redundant. I feel like I should be able to implicitly pass the original input to functions directly... so maybe more like this:
(a, b) -> (lte, a, b) -> branch -> min
(a, b) -> (gte, a, b) -> branch -> max
Maybe either way could be supported.
Working With Lists
Let's say I want to perform an operation on a list of numbers. For instance, let's say I want to create a sum
or product
function and I want to call them like this:
[2, 3] -> sum -> sumResult
[2, 3] -> product -> productResult
I expect sumResult
to contain 5
and productResult
to contain6
.
In order for this to work, I need a special way to work with lists. I came up with a way that is similar to Go slices.
The idea is that I can slice a list into two pieces: the head and the tail. Both the head and tail can be single numbers or a list of numbers at their respective ends. To extract that information, I imagine passing a list through a special transformer like this:
[1, 2, 3] -> [0:0] -> result
Given this, result
would be a tuple (1, 3)
. The [0:0]
transformer took the zeroth element from the head and the zeroth element from the tail and output a tuple.
Here's another example:
[1, 2, 3] -> [0:] -> result
In this example, result
contains (1, [2, 3])
. It took the zeroth element from the head and put the rest into the tail.
Here are some more examples. I added some comments after each line so you can see what result contains.
[1, 2, 3] -> [1:] -> result # ([1,2], 3)
[1, 2, 3] -> [:0] -> result # ([1,2], 3)
[1, 2, 3] -> [1:1] -> result # ([1,2], [2,3])
[1, 2 ,3] -> [:] -> result # ([1,2,3], [1,2,3])
That's generally how it is meant to work. So now, using that syntax, I can construct my sum
and product
functions like this:
[0:] -> (h, t) -> (t=[], h, h+(t->sum)) -> branch -> sum
[0:] -> (h, t) -> (t=[], h, h*(t->product)) -> branch -> product
Since the product
function is the same as sum
, I'll just walk through sum
.
Given a list, take the zeroth element from the head and the rest of the list and output a tuple
(h, t)
whereh
contains a number andt
contains a list.Send that tuple into another tuple to get it ready for the
branch
function.The
branch
function determines ift
is an empty list. If it is, then it outputsh
. Otherwise, it addsh
to(t->sum)
.
WAIT! That's right. Recursion. Did I mention I don't want explicit loops in my language?
Not just recursion, but it's recursion before sum
is even defined. I imagine that the body of the function is lazily evaluated. I don't think implementing that would be a big deal.
OK... back to it...
The successive recursive calls are evaluated and eventually
h
is returned and returned and returned and returned until everything unwraps.It stores this transformation into the
sum
variable, which becomes the function name.
It's a little crazy, I know... but it works for me.
The Factorial Function
Now with all of these things, I finally have the means by which I can write a factorial function! I will leave this up as an exercise to the reader to evaluate. That being said, I was able to work through it on pencil and paper and found that it actually works! Give it a try yourself if you're bored one lazy Sunday afternoon.
n -> ((n, 1)->lte, 1, [n, n->decrement->factorial]->product) -> branch -> factorial