Extended functions in Wipple
Wipple
I can’t believe it’s been over a year since I started working on the Wipple interpreter! I’m still going after my original goal of learning how programming languages work, and as my understanding has increased, so have my ambitions. I want to build the “perfect” programming language that’s both natural to learn and extremely powerful and flexible.
Over the past few months, I’ve been thinking about building a compiler for Wipple — this has a lot of advantages, including the ability to run Wipple code on embedded devices like robots (I plan to use Wipple on my robotics team this year), and full static analysis and typechecking. The Wipple interpreter manages a full graph between traits and relations at runtime, and traverses it on every evaluation. By doing this analysis before the program runs, Wipple code will run faster and be easier to reason about.
I’ve also been thinking about how to implement a better type system in a way that feels natural to Wipple, and after much experimentation, I’ve come up with a solution that utilizes function syntax in a streamlined way for many aspects of the language. Let’s take a look!
Functions and values
Let’s build a simple Wipple program to start:
a : 1
b : 2
sum : a + b
This code should be pretty easy to understand;Â we define a
and b
as numbers and take their
sum
. How does this happen? At runtime, a + b
is computed (ie. an ADD
instruction is sent to
the CPU) and the result is stored. This kind of evaluation occurs implicitly — by simply writing the
expression in code, addition is performed.
Now what about functions? Let’s refactor the above example to use one:
add : a -> b -> a + b
sum : add 1 2
Now, the addition is parameterized: it takes place on two unknown inputs. This makes our program
more general and enables it to work for any a
and b
. We can call add 1 2
like in the
example, or add 10 20
, or add
any other numbers.
Let’s extend this concept to another part of the language!
Functions and types
Wipple has always been a strongly-typed programming language — with a compiler, the only difference
is that typechecking occurs before the program runs. Every value has a type, which we can express
using ::
:
42 :: Number
"hello" :: Text
2 + 2 = 4 :: Boolean
show "hello" :: .
Just like the value of an expression is determined implicitly, so is the type of an expression. Now let’s write a more general example:
two : x -> '(x x)
This function duplicates its input, returning a pair. If we call two 42
, we get (42 42)
; if we
call two "hi"
, we get ("hi" "hi")
, and so on. It’s easy to determine the type of the result, but
what’s the type of the input? Just like a function’s value depends on its input, so can we make a
function’s type. This idea is called “parametric polymorphism” (aka. generics), and we can use it
to define type functions!
Two : for A -> '(A A)
This definition looks very similar to our regular function definition, but to indicate that the
input is a type and not a value, we add the for
prefix. By convention, types are capitalized.
Let’s try calling our type function:
Two Number
This is equivalent to writing Number -> '(Number Number)
, just like add 1 2
from the previous
section is equivalent to writing 1 + 2
. Now let’s try calling two
with different inputs and see
how its type changes:
two :: Two
two 42 -- two :: Two Number
two "hi" -- two :: Two Text
Great! The compiler takes care of calling our type function based on the type of the input value. Of course, you could still add manual type annotations if you wish:
(two :: Two Number) 42
(two :: Two Text) 42 -- error: expected Text, found Number
Let’s extend this concept one more time.
Functions and expressions
Wipple is inspired by Lisp, and while its syntax looks much different, fundamentally everything is still an expression. Just like Lisp, you can quote expressions to use them as values:
(1 2 3) -- error: 1 is not a function
'(1 2 3) :: List Number
But unlike Lisp, the new compiled version of Wipple will use quasiquoting by default, and evaluate each item in a quoted list. That is, the following code is valid:
a : 1
b : 2
c : 3
'(a b c) = '(1 2 3) -- True
This “filling-in” of inner expressions is shallow, so you can also evaluate complex expressions inside quotes:
'((1 + 2) (10 + 20) (100 + 200)) = '(3 30 300) -- True
Notice a pattern? Just like evaluation and type inference, the filling-in of quoted expressions is
implicit — it occurs automatically whenever you use a '
. In the Wipple interpreter, parameterized
filling is possible with a special template syntax. In the new, compiled Wipple, we can use a fill
function!
rev : fill a b -> '(b a)
Calling a fill function doesn’t immediately result in evaluation; instead, the unevaluated expressions are passed in as input, and then the filled-in expression is evaluated in place of the call. For example, the following code is valid:
assign : fill name value -> '(name : value)
assign num 42
num = 42 -- True
Conclusion
I’m really proud of this extended function syntax. In the future, I hope to extend it even more to support things like context and other cool features! I’ve been working less on programming lately and more on adapting Wipple’s design to better support static analysis, but I hope to start making some real progress again very soon. Be sure to check out the Wipple Playground in the meantime — eventually I plan to migrate it over to Replit and take advantage of their recently-added custom language support!
Thanks for reading, and I look forward to sharing more updates soon.