Keelung Basics
This article is a brief introduction to the fundamental features and capabilities of Keelung.
We've linked the headers of each feature to dedicated articles with more in-depth information for those who want to learn more.
Input / Output
In the previous section, we looked at the echo
example, which had 1 private input and 1 output.
However, Keelung programs can have any number of inputs and outputs.
Private & Public Inputs
In zero-knowledge proofs, it's vital to differentiate between two input types: public and private. Here's how they differ:
A private input's value is exclusively known to the prover, whereas a public input's value is shared with both the prover and the verifier.
Additionally, output values are always public, regardless of their input source.
Keelung uses modifiers like Public
or Private
to indicate this distinction.
The next example combines values from both public and private inputs and adds them together:
Arithmetics & Literals
Now that you've learned how to add two numbers together, why not take it a step further? Here's how you can perform various arithmetic operations and represent literal numbers in Keelung:
Here, we've introduced a temporary variable named temp
to avoid writing x * (3 - y)
twice.
Keep in mind that this variable exists solely within the language itself (both Keelung and Haskell); whether or not it exists in the resulting constraint system depends on optimization considerations.
Due to the limitation of constraint systems, an expression in Keelung may also be compiled into multiple small constraints, with intermediate variables in between.
Types
Let's now explore types in Keelung.
The first line is the type signature. It declares us that arithmetics
should have a type of Comp Field
, while the following lines define what the arithmetics
actually is.
You can ask the REPL about the type of a definition like this:
Field
is the type of the number we've been dealing with, while Comp
represents the context needed for constructing a Keelung program. Comp Field
means that arithmetics
is a Keelung program that computes a value of Field
.
Booleans
Field
isn’t the only type in Keelung. There’s also Boolean
, a datatype that has just two inhabitants: true
and false
.
Logical operaters
There is a whole range of logical operators for manipulating Boolean values.
For example, here’s a program that returns the conjunction of a private and a public input:
Type of .&.
.&.
.&.
is an infix operator. If you want to inquire about its type in the REPL, enclose the operator within parentheses like this to appease the REPL (or otherwise you’ll get parsing errors):
This type signature may come as a surprise. Instead of the expected type Boolean -> Boolean -> Boolean
, we get something a bit more general.
This is because there are other datatypes in Keelung that can also undergo conjunction.
Rather than having separate conjunction operators for each datatype, we have consolidated them all under .&.
.
Interpreting Boolean values
Keelung’s interpreter only accepts and returns lists of integers as inputs and outputs.
The Boolean value true
is translated as 1
while false
is translated as 0
.
Field equality
We can see if two Field
values are equal or not with functions like eq
:
As eq
is not an infix operator like .&.
, we enclose it in backquotes, allowing us to use it in a similar manner. inputField
is input
specialized on Field
so that we don’t have to annotate the type ourselves.
When we interpret equalField
with these inputs, it should give us the expected outputs:
Assertions
With Booleans at our disposal, we can now formulate assertions.
Assertions are arguably the most important construct in Keelung, because they allow us to articulate the relationship (and thus the constraint) between variables, which is what ZKPs are all about.
Assertions are made using the following function:
It takes something of the Boolean
type and "remembers" it in the Comp
context, so that the compiler can generate constraints for the Boolean expression later on.
Here’s a modified version of equalField
, which asserts that 2 inputs to be equal instead of returning the result of comparison.
Assertion v.s. Computation
If we compile both equalField
and assertEqualField
and compare the resulting constraints side by side, we’ll notice that assertEqualField
generates fewer constraints, because it doesn’t need to compute the value of the comparison, it just assert it as-is.
You should use assertions rather than computations whenever possible, unless you need the result for subsequent calculations.
Conditionals
Another thing we can achieve with Booleans is the ability to modify the flow of control within a Keelung program.
Let's explore an example that dynamically returns either of the two Field inputs based on another Boolean input:
The cond
function takes a Boolean
predicate, and if that predicate evaluates to true
, it returns the first branch of type t
; otherwise, it returns the second branch of type t
.
It's important to note that cond
is an expression, not a statement that causes side effects as seen in some languages like C. In Keelung, you must provide both "then" and "else" branches for the conditional; partial branching is not allowed.
Loops
Unlike conditionals, Keelung does not have constructs like cond
for expressing loops, because they are simply not allowed in zero-knowledge proofs.
However, this doesn't mean we cannot use loops to generate Keelung code in Haskell. We'll explore this concept further in the following examples.
List of Inputs
Suppose we need a list of 4 inputs. This can be done as shown below:
However, this code doesn’t scale well as the required number of inputs increases.
Fortunately, Haskell provides combinators like replicateM :: Int -> Comp a -> Comp [a]
, which allows you to execute a statement repeatedly a certain number of times and collect the results as a list:
Int
is a Haskell datatype for representing signed integers, since there are no way of converting a Keelung number to Int
, the number of repetitions cannot be dynamically determined from inputs.
Keelung also comes a similar helper function called inputList
:
But there’s really nothing special about replicateM
or inputList
; you can always roll your own in Haskell.
Iterating a list
Now that we have a list of Field values [Field]
, let's iterate through the list and make assertions about each of its elements:
The function forM_ :: [a] -> (a -> Comp b) -> Comp ()
accepts a list and a function that operates on each element of the list, discarding the result of that operation. On the other hand, (!!) :: [a] -> Int -> a
allows for list indexing.
Instead of iterating over a list of indices and then accessing list elements using indices, we can also iterate directly over the list elements themselves:
Programmers coming from imperative languages like C might find the former style more familiar, whereas the latter style is more common in functional languages like Haskell. Feel free to use any programming style you prefer in Keelung, as they should all generate the same zero-knowledge proof.
Aggregating a list
Here we calculate the sum of them with built-in Haskell function sum :: [Field] -> Field
:
Functions
Let’s generalize sumOf4Fields
by abstracting it as some function so that we don’t need to come up with something like sumOf5Fields
.
Like loops, functions are not first-class citizens in Keelung, but that’s okay, because Haskell as the metalanguage has it all!
Function Definition
Here’s how functions are defined in Haskell:
Arguments of a function are placed on the left-hand side of the definition. In the case of sumOfFields
, it has an argument called n
which has the type of Int
. Note that the types of arguments are also reflected in the function’s type signature.
Function Instantiation
On call sites, apply functions with arguments by juxtapositioning them like this:
You can think of Haskell functions as Keelung macros. Functions are inlined and expanded at the call sites, with arguments substituted as if you had manually written the code:
Conclusion
Congratulations on completing this tutorial! We've touched on key concepts of Keelung, including input/output, datatypes, assertions, control flows, and techniques of abstraction.
Check out other guides and examples if you want to venture further into the world of Keelung and zero-knowledge proofs. Our comprehensive language reference also provides detailed information into Keelung's syntax and semantics.
Happy coding!
Last updated