- get functions in general not injective: many possible corresponding put functions exist to form a well-behaved lens
-
PUTINJ:
put s
is injective for any sources
, i.e.,$s' \in put s v \wedge s' \in put s v' \Rightarrow v = v'$ -
PUTTWICE:
$s' \in put s v \Rightarrow s' = put s' v$
- domain-specific programming language on tree-structures to define bidriectional transformations
- transformations map a concrete tree into a simplified abstract view, and map a modified abstract view, together with the original concrete tree, to a correspondingly modified concrete tree
- the design emphasizes robustness and ease of use
- guarantees well-behavedness and totality properties
- they state connection with update translation under a constanc complement from databases concerning definedness and continuity
- definition of a handful of lens combinators, which can be put together to describe transformations on tree
- combinators include familiar construct from functional programming
- background: Harmony, generic framework for synchronising tree-structured data, e.g. synchronising boockmark files of different web browsers
- language provides type declarations that are designed to verify well-behavedness and totality of composite lens expressions
- first step: indentifyin mathematical space of well-behaved lenses over arbitrary data structures
- second: define notion of well-behavedness that captures intuition about behavour of get and put
- third: establish foundation to define lenses by recursion; use standard tools from domain theory to define monotonicty and continuity; this is needed, because the used trees may have arbitrarily deep nested structures
- fourth: allow lenses to be used to create new conctrete structures
- well-behaved and very very well-behaved lenses correspond to classes of update translators from database
- put is injective for ${(a,c) | (a,c) \in A \times C \vee
get (put (a,c))
\downarrow}$; for total lenses, put is injective on$a \times C$ - a lens is oblivious: if
put (a,c) = put (a,c')
for all$a,c,c' \in V$ , where$V$ is a fixed set of views - defined collection of lens combinators: identity, composition, constant
- collection of lens combinators for trees: hoisting, plunging, forking, pruning, focussing (on a child), renaming, filtering, mapping, copying and merging, conditionals
- derived lenses for lists: lists are represented as trees (with two edges named head and tail or the empty tree) and a set of combinators is defined (head, tail, map, reverse, grouping, concatenation, filter)
- extended example concerning bookmark synchronisation designed with XML structures
- first approach to take totality as primary goal and emphasize types as an organizing design principle
- problem multiple reasonable translations for a given update: allow programmer to described update policy at the same time as the view definition, by enriching the relational primitives with enough annotations to select among a variety of reasonably update policies
- requires putback functions to be affine and in treeless form, that is, each view variable is used at most once and no intermediate data structures are used in definitions
- this class of functions has similarities to tree transducers
- assumes only total functions
- hybrid compositional approach, but focus on designing language to specify various primitive putback functions over algebraic data structures
- validity of putback transformations - A put is valid, if there exists a get such that GETPUT and PUTGET are satisfied
- Uniqueness of get - Given a put function, there exists at most one get function that forms a well-behaved BX
-
PUTDETERMINATION:
$\forall s,s',v,v' . put s v = put s' v' \rightarrow v = v'$ -
PUTSTABILITY:
$\forall s . \exists v . put s v = s$ - validity - A put function is valid if and only if it satisfies the PUTDETERMINATION and PUTSTABILITY properties
- problem: previous papers only consider positional lens updates - reordering is not considered
- heuristics for calculating alignments
- sources
$S$ , views$V$ , complements$C$ with following functions for a lens$l$ -
l.get
$\in S \rightarrow V$ -
l.res
$\in S \rightarrow C$ -
l.put
$\in V \rightarrow C \rightarrow S$ -
l.create
$\in V \rightarrow S$
-
- set of all basic lenses between
$S$ and$V$ with respect to$C$ :$S \LeftRightarrow^C V$ - laws
-
l.get (l.put v c) = v
(PutGet) -
l.put (l.get s) (l.res s) = s
(GetPut)
-
- laws for correctness and preserveration are based on lens laws
- presents algorithm that reconstructs source code after refactoring transformations: "CONSTRUCTTEXT(node) takes an abstract syntax term as input and constructs a string representation for this term. Three cases are distinguished; re- construction for nodes (l. 1-5), reconstruction for lists (l. 6-11), and pretty printing in case the origin term is missing, i.e. when a term is newly created in the transformation (l. 12-14). We discuss those cases."
Complement Functions
- general first-order functional language: VDL
- affine (each variable is used at most once)
- treeless (no intermediate data structure is used in the definition)
- view complement functions can be automatically derived from view functions
- inference system for validation of changes in the view
- bidirectional transformation between arbitrary algebraic data structures (like lists and trees)
- three steps: derivation of complement function, tupling and inverse transformation
- get function and complement form the tupling, that mus be injective
- minimal complement
- injectivity is decidable in VDL
- algorithm to check for injectivity: sound and complete
- backward transformation can be derived if the tupled function and its inverse can be derived effectively
- the inverse transformation is not guaranteed to be deterministic; non-failing equations can overlap on the left-hand sides and, thus, lead to nondeterministic programs, in which case a backtracking search becomes necessary
- semantic approach inspired by relational parametricity, uses free theorems for proving the consistency conditions
- higher-order function
bff :: forall a. [a] -> [a]) -> (forall a. [a] -> [a] -> [a]
implemented in Haskell that takes a polymorphic get function as argument and yields the appropriate put function, generic on input and output - the resulting put function is not a syntatical defintion, but a functional value semantically equivalent to such a function
- no restrictions on a sublanguage, any Haskell function of appropriate type can be used as argument
- restriction: any update on the shape of the view (length of the list) leads to failure
- distiguishes between
bff
,bff_EQ
,bff_ORD
- considered data structures: shape plus content (Categories of container by Th. Altenkirch et al)
- GetPut, PutGet laws are proven by free theorems
- main idea: use assumption that get is polymorphic over its element argument of type |a|, then, its behaviours does not depend on any concrete list elements, but only on positional information
- every position in the template list
[0..n]
has a mapping in g with its associated values in the original source; apply get to the template source to get an additional mapping h between the template view and the original updated view; combine both mappings to a mapping h' with precedence to h, when an integer template index is found in both mappings; in the end, fill the positions in the template mappin with the associated values according to the combined mapping h' - simple approach fails for duplicated elements; solutions: elements of the list must be mapped in a more sophistaced way, equal elements in the origial list map to the same elements in the template
- generalises Voigtlaender's approach for higher order functions that are not expressed by type classes
- defines one function bff that is parametrized over a so-called observer function, and extends the associations maps to observer tables
- introduces a type class
PackM delta alpha mu
to turn monomorphic into polymorphic transformations; a concrete datatypedelta
is abstracted to a typealpha
and the observerations made by transformations by values of typedelta
are recorded by a monadmu
liftIO
lifts any observer function[delta] -> beta
on a concrete datatypedelta
to a monadic function[alpha] -> mu beta
on an abstract datatypealpha
wherebeta
is an instance ofEq
Eq b
is needed to check validity of updates by comparing the observation results- polymorphism of
a
permits semantic bidirectionalization and polymorphicmu
guarantees integrity of the observation results recorded in the writer monad - consistency check in observation table: the observation results are not allowed to be different to possible update results, if they are different, the update is rejected; key for application of free theorems
- optional locations for newly created elements that do not have a corresponding location in the original list
- approach in this paper: same values are not mapped to the same label, that is, same values are not considered to be duplicates
- says its harder to find get functions that are suitable for syntactic bidirectionalization than semantic
- takes combination one step further: any syntactic bidirectionalization approach can be plugged in to obtain transformations on shapes
- generalises approach from lists to arbitrary data types
- enables bootstrapping in which pluggable bidirectionalisation is itself used as a plug-in
- combines syntactic and semantic bdirectionalization
- divides usage of both approaches by their specialties: semantic for content, syntactic for shape
- inherits limitations in program coverage from both techniques: only functions written in first-order language, linear, treeless and moreover polymorphic are suitable for this approach
-
in a meaningful way/suitable: GetPut, PutGet, restricted to a
defined
put s v'
for PutGet, and thatput s v
should be preferably defined for alls
andv'
of appropriate type - improved updatability: shape-changing update that are not applicable for semantic bidirectionalization; superior to syntactic bidirectionalization on its own in many cases
- syntactic bidirectonalization as black box
- semnatic bidirectionalization as glass box: look into it and refactor it to enable a plugging in of the syntactic technique
- syntactic technique on its own is never worse than the semantic technique own its own
- assumes semantic linearity: for every
$n$ get [0..n]
does not contain any duplicates, which clearly is fulfilled, ifget
's syntactic definition is lineary: linearity rules out one important cause for a potential failure, namely potential equality mismatcg inv'
- key idea: abstracting from lists to length of lists, or more generally, from data structures to shapes
- uses
Nat
instead ofInt
, move from[a]
toNat
:get
-function gets simpler, no data values have to be kept; can lead to injectivity and, hence, to simpler complement functions - explicit bias: bias to apply when reflecting specific updated views back to the source level
- this bias could be determined on a case-by-case basis, e.g. depending on a diff between updated view and original view
- approach does not hold_PutPut_ and undoability law, albeit, the two approach on their owd do satisfy these laws
- bidirectional transformations over strings, first to tackle the problem for ordered data types: misaligned data in input and output
- propose collection of lens combinators for strings (based on regular transducers, i.e., union, concatenation, Kleene-star)
- type system based on regular expressions
- design of dictionary lenses, which is an enrichement of lenses of Foster et al.
- design of Boomerang as bidirectional programming language with dictionary lenses at primitive operations
- defines property of resourcefulness and quasi-oblivious lenses
- definition of set of string lenses combinators based on typing rules that ensure lens laws
- tackle problem of previous approaches: mapping by key and not by position
- quasi-oblivious lenses: let
$~$ be an equivalence relation on C, and$l$ a lens from$C$ to$A$ ;$l$ is ~ if itsput
functions ignores differences between equivalent concrete arguments - domain and codomain types for dictionary lenses are regular languages, so that it allows precise checking
- state-based vs. operation-based
- symmetric vs. asymmetric
- lens laws and "softer" lens laws
- non-injective get
- some variables on the left-hand side of a rule disappear in the corresponding right-hand side (|fst (x, y) = x|.
- The ranges of two right-hand sides of a view function overlap.(|f ( A ) = A; f ( B ) =ˆ A|)
-
generating corresponding
get
for a definedput
-function -
functions for
put
direction are kind of dullputAppend
,putZip
-
tedious to define
put
directionputLength
(put
for length istake
)
-
corresponding
get
function is dullputDrop
just yields0
forget
direction
-
non-terminating examples (see Problems with internal structures)
putHalve
- combination oflength
and listsputSumTree
-quotRemNat
is too strict (fixed for free variables in first component)
- test laws: PutGet, GetPut, PutPut
- adjust PutGet and GetPut: laws has to hold for valid domains/defined values only (that cannot be guarenteed by type)
- test put laws: PutDet PutStab (TODO)
- reanimate EasyCheck
- inefficient for lists
- labeling bug: if a corresponding label is missing, a non-determinism error occurs
- high-potential library to include lenses for getter, setter and projection
- reads entities from database on page load - why?
readQTerm (showQTerm ())
fails with parse error- difference between
wJoinTuple
andwPair
?
lmap :: Lens a b -> f b -> f a
, wheref
is aFormLens
- use
WUILenses
instead ofWUI
in blog example of Spicey
We have the following interface for put-based lenses that generate a
corresponding get
-function for a defined lens.
data Lens a b = a -> b -> a
put :: Lens a b -> a -> b -> a
put lens s v = lens s v
get :: Lens a b -> a -> b
get lens s | put s v == s = v
where v free
Our aim is to generate a get
-Function for putHalve
:
putHalve :: [a] -> [a] -> [a]
putHalve xs xs' | length xs' == n = xs' ++ drop n xs
where
n = length xs `div` 2
putHalve
takes to lists, a source list and a view list, and
concatenates the second list with the second half of the first
list. Valid view lists have half the length of the source list, if
otherwise, the function yields failed
, i.e., no result is produced.
(BeginHint: the wished get
function is equivalent to
halve :: [a] -> [a]
halve xs = take (length xs `div` s) xs
EndHint)
With help of our interface, we can derive a correspondingget
function by simply
calling get
with putHalve
and our source value.
getHalve = get putHalve
Unfortunately, a function call like getHalve [(),()]
does not terminate.
The problem is the combination of the internal list and
and numbers data structures, they do not harmonize well. This effect
is triggered by the usage of length
. The free variable v
corresponds to xs'
in the definition of putHalve
,i.e., the system
guesses values for xs'
. To be more precise, the system needs to
guess lists of type ()
for xs'
and check, if their length is the
same as div length [(),()] 2
. For the specific example above, we
can simplify our definition of putHalve
in order to focus on the
problematic part.
putHalveSimple :: [a] -> [a]
putHalveSimple xs' | length xs' == 1 = xs' ++ drop 1 [(),()]
If we now consider the internal structure for Int
values, we end up
with the following function
putHalveSimple :: [a] -> [a]
putHalveSimple xs' | length xs' == Pos IHi = xs' ++ drop 1 [(),()]
with respect to the internal data structure BinInt
.
data BinInt = Neg Nat | Zero | Pos Nat
data Nat = IHi | O Nat | I Nat
So, how does the evaluation steps look like, when we want to compute the length of a list?
length :: [a] -> Bin Int
length [] = Zero
length x:xs = inc (length xs)
inc :: BinInt -> BinInt
inc Zero = Pos IHi
inc (Pos n) = Pos (succ n)
inc (Neg IHi) = Zero
inc (Neg (O n)) = Neg (pred (O n))
inc (Neg (I n)) = Neg (O n)
That is, for an empty list, we can directly compute the result, but
for a non-empty list we build a sequence of inc
-operations,
e.g. inc (inc Zero)
for a two-valued list, inc (inc (inc Zero))
for a three-valued list etc. In order to take this investigation
one step ahead, we need to look at the definition of inc
, where only
lines 6 and 7 are of further interest.
(1) Zero == Pos IHi
False
(2) inc (Zero) == Pos IHi
Pos IHi == Pos IHi
True
(3) inc (inc Zero)) == Pos IHi
inc (Pos IHi) == Pos IHi
Pos (succ IHi) == Pos IHi
Pos (O IHi) == Pos IHi
False
(4) inc (inc (inc Zero))) == Pos IHi
inc (inc (Pos IHi)) == Pos IHi
inc (Pos (succ IHi)) == Pos IHi
Pos (succ (succ IHi)) == Pos IHi
Pos (succ (O IHi)) == Pos IHi
Pos (I IHi) == Pos IHi
False
The attentive reader may have already noticed that the definition of
inc
is strict in its first argument and does not propagate its constructor, the successor
function on binary numbers is also strict. Unfortunately, the length
function cannot be implemented in a way that is sufficient to
propagate a constructor, because, as we will see later, it is
problematic to map the empty list to a value of type Nat
.
Therefore, the whole list has to be evaluated in order to determine its length, which leads to
non-evaluation when guessing a list with a specific length. The
built-in search for free variables in KICS2 can be translated in
nested injections of ?
-operations, where every constructor of the
given type is a possible guess and arguments of constructors are also
free variables. For our example, we can illustrate the
built-in search as follows.
length v == Pos IHi where v free
length ([] ? _x2:xs) == Pos IHi where _x2,xs free
(length [] ? length _x2:xs) == Pos IHi where _x2, xs free
length [] == Pos IHi ? length _x2:xs == Pos IHi where _x2,xs free
Zero == Pos IHi ? length _x2:xs == Pos IHi where _x2,xs free
False ? length _x2:xs == Pos IHi where _x2,xs free
False ? inc (length xs) == PosIHi where xs free
False ? inc (length [] ? length _x4:ys) == PosIHi where _x4,ys free
False ? inc length [] == Pos IHi ? inc (length _x4:ys) == PosIHi where _x4,ys free
{v = []} False
{v = [_x2]} True
{v = [_x2,_x4]} False
...
The given type is [a]
with two constructors []
for an empty list
and (:) x xs
for non-empty lists with an element x
and the
remaining list xs
. In our example, for every free variable of type
[a]
both constructors are possible values, therefore, both
expressions are introduced with the ?
-operator. The ?
-operator
explicitly inserts non-determinism and is defined as follows.
(?) :: a -> a -> a
x ? y = x
x ? y = y
In the end, the built-in search collects all possible values and works henceforth with a set of values, i.e., every list of the resulting set is used for further function calls.
As we said in the beginning, the internal structure for lists and
numbers do not harmonise well - how can we solve the problem that
putHalve
does not terminate with the current implementation? We will
present two different approaches. Beforehand, we exchange the usage of
BinInt
by Nat
to see, if this little change will do the
difference. Up to now, it seems the built-in search cannot termine,
because the Pos
constructor takes to long to be propagated in
front of the expression.
lengthNat :: [a] -> Nat
lengthNat [_] = IHi
lengthNat (_:y:ys) = succ (lengthNat (y:ys))
lengthNat v == IHI where v free
lengthNat ([] ? _x2:xs) == IHi where _x2,xs free
(lengthNat [] ? lengthNat (_x2:xs)) == IHi where _x2,xs free
lengthNat [] == IHi ? lengthNat (_x2:xs) == IHi where _x2,xs free
failed ? lengthNat (_x2:xs) == IHi where _x2,xs free
failed ? lengthNat (_x2:([] ? (_x4:ys))) == IHI where _x2,_x4,ys free
failed ? lengthNat [_x2] == IHI ? lengthNat (_x2:_x4:ys) == IHi where _x2 _x4,ys
failed ? IHi == IHi ? succ (lengthNat (_x4:ys)) == IHi where _x4,ys free
failed ? True ? succ (lengthNat (_x4:([] ? (_x6:zs)))) == IHi where _x4,_x6,zs
...
At first, we use another data structure for numbers that has an unary representation, i.e. we will use peano numbers.
data Peano = Zero | S Peano
lengthPeano :: [a] -> Peano
-- lengthPeano = foldr (const S) Z
lengthPeano [] = Z
lengthPeano (x:xs) = S (lengthPeano xs)
Peano numbers are represented with a constructor for Zero
and a
successor constructor S Peano
. The corresponding length
function
introduces an S
-constructor for every element of the list and yields
Zero
for an empty list.
Let us now take a look at the simplified implementation
of putHalvePeano
that uses peano numbers instead of Int
values and works on a list with two elements.
putHalvePeano :: [a] -> [a]
putHalvePeano xs' | lengthPeano xs' == S Z = xs' ++ [()]
lengthPeano v == S Z where v free
lengthPeano ([] ? _x3:xs) == S Z where _x3,xs free
(lengthPeano [] ? lengthPeano (_x3:xs)) == S Z where _x3,xs free
lengthPeano [] == S Z ? lengthPeano (_x3:xs) == S Z where _x3,xs free
Z == S Z ? S (lengthPeano xs) == S Z where xs free
Z == S Z ? lengthPeano xs == Z where xs free
False ? lengthPeano [] ? lengthPeano (_x4:_x5) == Z where _x4,_x5 free
False ? lengthPeano [] == Z ? lengthPeano (_x4:_x5) == Z where _x4,_x5 free
False ? Z == Z ? S (lengthPeano _x5) == S where _x5 free
False ? True ? False
In the end, the expression yields the following result in KICS2:
{v = []} False
{v = [_x3]} True
{v = (_x3:_x4:_x5)} False
That is, the expression putHalve [(),()] v == [(),()] where v free
evaluates to {v = [()]} True
.
The main difference to the first implementation is that length can
propagate the constructor at the front of the remaining
evaluation. That is, the nested ?
-operators only occur as the argument of a sequence of
S
-constructors, which leads to a terminating search. The last line
of the example shows that no further guesses for free variables are
necessary, because the partial evaluation of S n
can never be
evaluated to Z
, hence, the expression fails and the evaluation terminates.
The second approach is to choose another list representation, more
precisely, a representation that behaves well with the internal
BinInt
data structure. We define binary lists as follows.
data L a = LIHi a | LO (L (a,a)) | LI (L (a,a)) a
data BinaryList a = Empty | NonEmpty (L a)
At first, we define the data structure for non-empty lists that
corresponds to binary numbers, where LIHi a
is a list with one
element, LO (L (a,a))
represents a list with at least two elements,
and LI (L (a,a)) a
is the constructor for an at least three-valued
list. Since this data structure has no representation for an empty
list, we introduce an additional data type BinaryList
that wraps a
constructor NonEmpty
around the list representation L a
and
consists of a constructor Empty
for an empty list, respectively.
lengthBList :: BinaryList a -> BinInt
lengthBList Empty = Zero
lengthBList (NonEmpty list) = Pos (lengthL list)
where
lengthL :: L a -> Nat
lengthL (LIHi _) = IHi
lengthL (LO l) = O (lengthL l)
lengthL (LI l _) = I (lengthL l)
With the given type BinaryList
we can utilize that we have a special
constructor for non-empty lists with an inner
representation. Therefore, we can propagate Pos
for an non-empty
list without evaluating the actual inner list that NonEmpty
is
holding. Furthermore, the list structure reflects which
Nat
-constructor to use, so that, the constructor is again propagated
to the front of the expression. Again, let us see the evaluation in
action for the putHalve
example.
putHalveBinaryList :: BinaryList a -> BinaryList a -> BinaryList a
putHalveBinaryList xs' | lengthBList xs' == Pos IHi = xs' ++ [()]
The following evaluation steps through the expression lengthBList v == Pos IHi where v free
that is essential for the evaluation of get putHalveBinaryList
applied to an arbitrary list.
lengthBList v == Pos IHi where v free
lengthBList (Empty ? NonEmpty xs) == Pos IHi where xs free
(lengthBList Empty ? lengthBList (NonEmpty xs)) == Pos IHi where xs free
lengthBList Empty == Pos IHi ? lengthBList (NonEmpty xs) == Pos IHi where xs free
Zero == Pos IHi ? Pos (lengthL xs) == Pos IHi where xs free
False ? Pos (lengthL (LIHI _x2 ? LO _x2 ? LI _x2 _x3) ) == Pos IHi where _x2,_x3 free
False ? (lengthL (LIHI _x2 ? LO _x2 ? LI _x2 _x3) == IHi where _x2,_x3 free
False ? (lengthL (LIHi _x2) ? lengthL (LO _x2) ? lengthL (LI _x2 y)) == IHi where _x2,_x3 free
False ? lengthL (LIHi _x2) == IHi ? (lengthL (LO _x2) ? lengthL (LI _x2 _x3)) == IHi where _x2,_x3 free
False ? IHi == IHi ? (O (lengthL _x2) ? I (lengthL _x2 _x3)) == IHi where _x2,_x3 free
False ? True ? O (lengthL _x2) == IHi ? I (lengthL _x2 _x3) == Pos IHi where _x2,_x3 free
False ? True ? False ? False
In the end, this expression yields the following result in KICS2:
{v = Empty} False
{v = (NonEmpty (LIHi _x2))} True
{v = (NonEmpty (LO _x2))} False
{v = (NonEmpty (LI _x2 _x3))} False
That is, the expression putHalveBinaryList v == [(),()] where v free
yields {v = NonEmpty (LIHi ())} True
and get putHalveBinaryList [(),()]
yields
NonEmpty (LIHi ())
, respectively.