Sparcl
Sparcl (a system for partially-reversible computation with linear types) is a higher-order linear-typed programming language to write invertible computation, leveraging partial invertibility.
Links
- Repository in GitHub
- Proof scripts for the core part
Programming in Sparcl
Linear Types
Sparcl adopts a linear type system to ensure reversibility.
sig id : a -o a
def id x = x
sig const : a -o b -> a
def const x _ = x
Here, a -o b
denotes a linear function type, of which inhabitants are functions that use their inputs exactly once. In contrast, a -> b
denotes a unrestricted function type.
They actually are shorthand notations for a # One -> b
and a # Omega -> b
, which allows us to define multiplicity-polymorphic functions.
sig app : p <= q => (a # p -> b) # r -> a # q -> b
def app f x = f x
sig (.) : (q <= s, q <= t, p <= t) => (b # q -> c) # r -> (a # p -> b) # s -> a # t -> c
def (.) f g x = f (g x)
Here, <=
denotes the ordering of multiplicities defined by: One <= Omega
.
Invertible Programming in Sparcl
The basic idea of invertible programming in Sparcl is that a bijection between a
and b
is represented as a linear function rev a -o rev b
.
sig notR : rev Bool -o rev Bool
def notR (rev True) = rev False with not
| (rev False) = rev True
Here, rev True : rev Bool
and rev False : rev Bool
denotes invertible constructors, which are used for constructing rev
-typed values. In general, a constructor C : a1 -o a2 -o ... -o an -o b
comes with its invertible counterpart rev C : rev a1 -o rev a2 -o ... -o rev an -o rev b
, leveraging the invertible nature of constructors.
The rev
-typed data are manipulated by the invertible pattern matching indicated by rev
in a pattern. The invertible pattern matching comes with with
conditions, which are used for determining branches in the backward execution. Here, the with
condition not
tells that the first branch must be taken for the output False
. (We assume that not
is appropriately defined.)
By unlift : (rev a -o rev b) -> (a -> b, b -> a)
, one can obtain the bijection pair as ordinary functions.
sig : (Bool -> Bool, Bool -> Bool)
def notBij = unlift notR
sig : Bool -> Bool
def notF = fst notBij
sig : Bool -> Bool
def notB = snd notBij
Here, both notF
and notB
have the same behavior as not
. (We assume that fwd
and bwd
are appropriately defined.)
The functional representation of bijections is the key to express partial invertibility in a natural way: it is done just by taking additional parameters.
data Nat = Z | S Nat
sig add : Nat -o rev Nat -o rev Nat
def add Z y = y
| (S x) y = rev S (add x y)
sig mul : Nat -> rev Nat -o rev Nat
def mul z (rev Z) = rev Z with isZ
| z (rev S y) = add z (mul z y)
The pin
operator
Having the rev
types and the functional representation is half solution to partial invertibility. We sometimes want to convert a reversible value (of type rev a
) into an ordinary value (of type a
) for natural programming. The operator pin : rev a -o (a -> rev b) -o rev (a, b)
gives a solution to the situation.
For example, the following is an invertible function that computes differences of two consecutive elements in a list.
sig s2l : rev (List Int) -o rev (List Int)
def s2l = go 0
where
def go n (rev Nil) = rev Nil with null
| n (rev Cons x xs) = -- x : rev Int
let rev (x, r) <- pin x $ \x' -> go x' xs in -- x' : Int
rev Cons (sub n x) r
end
sig sub : Int -> rev Int -o rev Int
def sub n = lift (\x -> x - n) (\x -> x + n)
Notice that go
has type Int -> rev (List Int) -o rev (List Int)
. Since x
has type rev Int
, we are not able to pass it as the first argument of go
which requires Int
. So, we use pin
to address the issue.