• Keine Ergebnisse gefunden

5.3 Type-Safe Object-Tree Lenses in Scala

5.3.4 Fully Generic Lenses

For some lenses – such as the focus lens – the approach of type-parameterization pre-sented so far works well. However, for each class whose objects are to be transformed, a parameterized lens has to be instantiated. For instance, in the previous section we param-eterizedfocus to extract the first child of aContactInfoobject. This type-parameterized lens instance can only be used withContactInfo objects – it cannot be used to extract the first child of objects of other classes because the abstract typeA(in the phone exam-ple: Int) is determined at the time the lens is parameterized and instantiated. We call this kind of lens instance pre-typed because its types are fixed after instantiation, and the lens has to be typed before using it.

This pre-typing approach becomes an issue if, for instance, one lens is to be applied to one subterm and another lens is to be applied to all other subterms. In this case, differently typed instances of the latter lens have to be provided for each type of subterm.

This applies already to very simple examples. InFocal, the basicfilterlens is implemented by parameterizing thefork lens: The deletion lensconst({}) is applied to one subset of a tree’s subtrees to filter them out, and the non-modifying id lens is applied to the other subtrees to keep them. However, with a heterogeneously typed term such as a constructor or tuple term, applying the pre-typed approach would require to provide a parameterized instance of const orid for each type which occurs in the list of direct subterms. Using a common supertype to pre-type a lens (e.g.,Id[Term]) does not help either, because this way the specific type information of the different subterms is not preserved and therefore useful type-checking is not possible.

Inferring the Concrete Type and Computing the Abstract Type

To solve this issue, we developed lenses which are even more generic than pre-typed type-parameterized lenses. We call them fully generic lenses and describe them with a new abstract lens type called GenericLens. The main difference to the lens type presented in Listing 5.1 is that now the lens functions have a type parameter themselves which is automatically inferred from the passed function arguments. This way, type C of the concrete term is not determined until – at compile-time – a lens’ function is called.

The type A of the abstract term is then determined by a type function to express Ain relation to C. This type function A[C <: Term] <: Term, which computes one term type from another, is an abstract type member of the abstract GenericLens type, and must be implemented by concrete lens types such asFilterN etc. The following listing shows a snippet of the GenericLens interface with the type function for A, and the modified signature of lens functionget which uses this type function.

1 type A[C <: Term] <: Term // type function to derive A from C

2 def get[C](c: C): A[C] // C is inferred and determines A

124 Chapter 5. A Compositional Language for Bidirectional Model Transformation Now, when get is used, type C is inferred from the passed instance-level argument c (line 2) and it is type-checked whether the result type of get matches with A[C]. With this fully generic approach, a concrete lens type does not specify the relation between its typesC and A by type parameters which have common (sub-)type parameters like, for instance, in the pre-typed FilterN(also shown in Listing 5.9):

FilterN ... extends Lens[C, TupleTerm[C#Nth[N]::TNil]] { ... }

Instead, the relation betweenC andA is described as a type function which maps from one term type to another; in the case of FilterN by:

FilterN ... { type A[C <: Term] = TupleTerm[C#Nth[N]::TNil] ... }

Encoding Concrete-Side Constraints at Type-Level

An issue with describing type A as a type function from C to A is that it is difficult to define constraints regarding the shape of type C. We already saw an example of such a C-side constraint in the hoist lens, where the concrete term must have exactly one child. In the pre-typed version of hoist, we encoded this constraint by ...extends Lens[TupleTerm[A::TNil], A]. With a fully generic lens, we first infer typeC (instead of specifying it explicitly) and compute A from it. We could define two sorts of generic lenses: those that expressA in relation toC and those that express C in relation toA.

However, to be able to type-check composition of generic lenses, we need one common supertype and therefore have to decide for one direction of type inference and computa-tion. Because our lenses are informationally asymmetric with a discrete incremental put function, type C occurs in the parameter list of both get and put. Therefore it makes more sense to use type inference and computation fromC to A.

We then implement C-side constraints by a boolean type function. For this, we need (1) a supertypeBoolfor type-level booleans, (2) two boolean type-level literalsTrueand False, and (3) type functions which evaluate to boolean types such as, for instance, a comparison type function defined on type-level numbers:Nat#Equals[N <: Nat] <: Bool.

Furthermore, we need the ability to test for type equality to ensure boolean C-side constraints at compile-time. For this, Scala provides a type operator=:=which is a type alias for a type Equal[A,B]. A value-level function which has an implicit parameter of such a parameterized equality type, is automatically provided with an argument of that type if equality of the two compared types can be proved by the Scala compiler. If however type equality cannot be proved, type-checking fails because of a missing implicit argument, and the Scala compiler will report that it was not possible to prove the specified type equality. We add such type equality expression as an implicit parameter to the lens functions, and define C-side constraints as a type member of a lens. Using this type member in the type equality expression, we can then check C-side constraint on the basis of the type C which is inferred when (at compile-time) a concrete argument is passed to a lens function.

The following listing shows the abstract generic lens typeGenericLens. We define type bounds (individual for type C and type A) as type parameters CBound and ABound of

5.3. Type-Safe Object-Tree Lenses in Scala 125 Listing 5.11: An abstract type for fully generic lenses

1 abstract class GenericLens[CBound <: Term, ABound <: Term] {

2 type Constraint[C <: CBound] <: Bool // type function for defining constraints on C

3 type A[C <: CBound] <: ABound // type function to derive A from C

4 def get[C <: CBound](c: C)(implicit check: Constraint[C]=:=True): A[C]

5 def put[C <: CBound](a: A[C], c: C)(implicit check: Constraint[C]=:=True): C

6 def create[C <: CBound](a: A[C])(implicit check: Constraint[C]=:=True): C

7 }

classGenericLens(line 1). These type bounds are then used by the boolean type function forC-side constraints (Constraint[C]in line 2), and in the type function for computing type A from typeC (A[C] in line 3) for restricting the input and output types of these type functions. The three lens functions then use type functionA[C]to determine typeA from the inferred function type parameterC, which is also restricted by theC-side type bound. Note that the backward function create (lacking a concrete term parameter c) infers type C bycreate’s result type, which is also supported in Scala. Furthermore, all three lens functions have an additional implicit parametercheck (in a separate implicit parameter list), which ensures that the constraint type function evaluates to true when parameterized with the inferred type C (expressed by Constraint[C]=:=True).

Based on the abstract generic lens type, one can use both type bounds and boolean constraints to describe the specifics of a fully generic lens. The following listing shows the definition of a generichoist, which implements the abstract generic lens type and – in contrast to the pre-typed version of hoist – has no free type parameter.

Listing 5.12: A fully generic version of thehoist lens

1 class GenericHoist extends GenericLens[CompositeTerm, Term] {

2 type CBound = CompositeTerm // just a type alias to make the definitions below shorter

3 type Constraint[C <: CBound] = C#Length#Equals[_1] // using Nat’s Equals type function

4 type A[C <: CBound] = C#Nth[_0]

5 def get[C <: CBound](c: C)(implicit check: Constraint[C]=:=True): A[C] = c.nth(_0)

6 def put[C <: CBound](a: A[C], c: C)(implicit check: Constraint[C]=:=True) = create(a)

7 def create[C <: CBound](a: A[C])(implicit check: Constraint[C]=:=True) = Term(a::HNil)

8 }

As can be seen in line 1,GenericHoistsynchronizes between composite terms – which is in our term type hierarchy a generalization of any non-value term with a list of subterms – and terms of any term type. In line 3hoist’sC-side constraint is encoded by the boolean expressionC#Length#Equals[_1], which says that the length of the subterms list of type C must be 1, in other words, that the concrete term has exactly one child. Length is a type function which provides the length of a term’s subterms list as a Nat type and is therefore only available with composite terms. Value terms have no subterms and consequently cannot be hoisted – theC-side type bound determines which helper type-functions can be used in a lens’ type type-functions. As CompositeTerm#Lengthevaluates to Nat, we can useNat’s type functionEquals, which compares two type-level numbers and evaluates to a type-level boolean. This boolean type can then be checked for being true using the implicit type equality check in every lens functions.

126 Chapter 5. A Compositional Language for Bidirectional Model Transformation Composition and Usage of Fully Generic Lenses

To sequentially compose fully generic lenses, we must compose the type bounds, the boolean constraints, and the type function which expresses type A in relation to C.

The following listing shows parts of the definition of the sequential lens combinator GenericCompwhich puts two generic sublenses l and k (of types L and K) in sequence.

Listing 5.13: The sequential composition lens combinator for fully generic lenses

1 class GenericComp[L <: GenericLens[LC,LA], K <: GenericLens[KC,KA], LA <: KC,

2 KC <: Term, LC <: Term, T <: Term, KA <: Term](l: L, k: K) extends GenericLens[LC,KA]{

3 type Constraint[C <: LC] = L#Constraint[C] && K#Constraint[L#A[C]]

4 type A[C <: LC] = K#A[L#A[C]]

5 def get[C <: LC](c: C)(implicit check: Constraint[C]=:=True): A[C] = k.get(l.get(c))

6 ...

As can be seen in line 1, the type parameters of GenericCompensure that the A-side type bound of L, type LA, is a subtype of the C-side type bound of type K, type KC.

TheC-side constraint of typeL, typeLC, is also theC-side type bound of the composed lens. To compose the C-side constraints of the two sublenses, in line 3, their boolean type functions are composed with a logical type-level AND-operator written &&. Note that the constraint ofK cannot be applied directly on typeC but has to be applied to the result of computing typeA of lens type L, which is type C of lens type K (written K#Constraint[L#A[C]], also in line 3). Finally in line 4, the type function A[C]of the composed lens is implemented by applying K’s type function to the result of L’s type function, similar to how the instance-level lens functions are composed (e.g., end of line 5).

The implementation of fully generic lenses and the implementation of their composition is more complex than with pre-typed lenses. The following listing shows that the increased complexity in the implementation enables easier composition and more flexibility .

Listing 5.14: Fully generic lens composition

1 val hoist2x = GenericComp(GenericHoist(), GenericHoist())//a generic lens that hoists 2x

2 // testing the same lens instance with different terms:

3 val str1 = hoist2x get Term(Term("str"::HNil)::HNil) //will compile, inferred to String

4 val int1 = hoist2x get Term(Term(12345::HNil)::HNil) //will compile, inferred to Int

5 val str2 = hoist2x get Term("str"::HNil) // won’t compile: cannot be hoisted two times

6 val int2 = hoist2x get Term(Term(1234::"str"::HNil)::HNil) //won’t compile: not 1 child

In line 1, we use the generic hoist lens and the generic sequential composition to compose a generic lens which hoists two times. We neither specify what the input nor what the output type should be. Still, the lens is completely type-checked. The test in line 3 compiles without errors because the passed term consists of two nested composite terms with exactly one child each, so that both the type bounds and the constraints hold;

the output is inferred to be a string. The test in line 4 also compiles but its output is inferred to be an integer. This demonstrates how the type of a fully generic lens is not fixed but is inferred anew every time one of its functions is used. Consequently, the test in line 5 fails to compile because the composed type bound does not hold: the output of hoisting the first time is a value term, which cannot be hoisted anymore, and thus is no correct input for the second hoist lens, which expects a composite term. The test in

5.3. Type-Safe Object-Tree Lenses in Scala 127 line 6 fails to compile because the composed constraint does not hold: the output term of hoisting the first time has more than one child, so that the secondhoist’s constraint is violated. This shows how type bounds together with boolean type functions can be used effectively to describe and enforce the constraints of individual lenses.

5.3.5 Composing Pre-Typed Lenses With Type-Inferring Operators