• Keine Ergebnisse gefunden

5.3 Type-Safe Object-Tree Lenses in Scala

5.3.3 Type-Parameterized Lenses

Now that we have term types which allow the type information of their domain class counterparts to be preserved, we can start to implement a reusable library of pre-defined atomic lenses and lens combinators which are defined on those term types. However, in spite of the lens library being defined independently from actual domain classes, we want to type-check whether a given lens – which may be composed out of many small sublenses – conforms to the structures it is intended to synchronize.

A Simple Type-Safe Lens

With a lens which is not parameterized – such as the hoist lens, where the structural modification is always the same – the two typesC andAbetween which a lens translates, only depend on each other.Hoist’sC is always a type of a term with one single edge at the root (this is theC-side constraint of thehoist lens) andAis always the type of the single child that this edge refers to. Thus, the type list of term typeC is a list of length 1 with type A as the only component at position 0. This is written asA::TNil, which denotes a type A being prepended to the end of a type list,TNil. Type C can be described as TupleTerm[A::TNil]– a constructor term is also a tuple term, so defining hoist on tuple terms is more flexible. Thus, the type of thehoist lens isLens[TupleTerm[A::TNil], A]

extending the generic lens typeLens[C <: Term, A <: Term]. So the only free type-variable ofhoist isA. The following listing shows the complete Scala definition of a type-safehoist lens which processes tuple terms.

Ais the only type parameter ofhoist (line 2) and thehoist class extends the basic lens type from Listing 5.1. In line 3, Cis introduced as a type alias for TupleTerm[A::TNil]

which makes the definition of the three lens functions shorter. Now, when type parameter Aofhoist is set to a specific term type (such asTupleTerm[ValueTerm[String]::TNil])

5.3. Type-Safe Object-Tree Lenses in Scala 119 Listing 5.7: A type-parameterized type-safe hoist lens class

1 // an atomic hoist lens which removes the single edge at the root of a tree

2 class Hoist[A <: Term]() extends Lens[TupleTerm[A::TNil], A] {

3 type C = TupleTerm[A::TNil] // constraints possible concrete terms to this shape

4 def get(c: C): A = c.subterms.head // returning the head of the list of subterms

5 def put(a: A, c: C): C = this.create(a) // oblivious lens: put = create; c not needed

6 def create(a: A) = TupleTerm(a::HNil) // adds the single edge ’_0 -> a’

7 }

C is determined byA. Thus, hoist’s lens functions are statically typed and their usage can be type-checked.

Type-Level Parameters and Abstract Type Members

When a lens is further parameterizable – such as the focus lens which accepts an edge label to focus on – we can type-check a parameterized lens instance only if the parameter is also specified at type level. To achieve this, some basic type-level programming is re-quired. We already showed in the previous code listing how a type alias can be introduced using the keywordtype inside a class declaration. Such a type declaration inside a class is called atype member and is considered a member of that class, like methods and fields.

A static type member – that is, a type member which is the same for all instances of a (possibly type-parameterized) class – is accessed using#, which is called atype projection.

For instance, in the previous example, type memberCof theHoist class can be accessed by Hoist#C. Like other members, type members can be declared as abstract type mem-bersin an abstract class (or an interface) and can be implemented by subclasses. As type members can have type parameters themselves, they can be used to realize type func-tions which are evaluated at compile-time. The declarationtype Fun[X <: Dom] <: CoDom defines an abstract type function that has one parameter, which has to be a subtype of a typeDom, and that evaluates to a subtype of a type CoDom.

Because the tree lenses which we implement primarily use edge labels as parameters, and because edge labels in our object tree data model are translated to indices (which represent field names or collection indices), we need to encode indices – in other words, natural numbers – as Scala types. Such type-level numbers can be implemented as recur-sively nested successors of a bottom type, which in this case obviously is the number 0.

Numbers encoded this way are calledPeano numbers2. In Scala, type-level Peano num-bers can be implemented by declaring an abstract type Nat for natural numbers, and a successor class Succ[P <: Nat](i: Int) which extends Nat. Then a type-level num-ber literal can be declared as type _1 = Succ[_0] with a corresponding instance-level number literal being defined asobject _1 = new Succ[_0](1). If such a number literal is passed to a generic instance-level function, the number type can be inferred and is therefore available at compile-time (refer to type parameter inference, Sec. 2.4.6).

With these number types and number literals we can define type-safe methods ofHList, such as a type-safe indexed accessor method called nth as presented in Listing 5.8. In line 2,Nth[N]is declared as a type function of TList. In line 6, this type function is used

2http://www.haskell.org/haskellwiki/Peano_numbers

120 Chapter 5. A Compositional Language for Bidirectional Model Transformation Listing 5.8: Defining a type-safe indexed accessor method for HLists and tuple terms

1 abstract class TList {

2 type Nth[N <: Nat] // TList’s abstract type member / type function yielding nth’s type

3 ...

4 }

5 abstract class HList[TL <: TList] {

6 def nth[N <: Nat](n: N): TL#Nth[N] // type-safe indexed accessor on HList instances

7 ...

8 }

9 class TupleTerm[TL <: TList] {

10 val subterms: HList[TL]

11 type Nth[N <: Nat] = TL#Nth[N]

12 def nth[N <: Nat](n: Nat): Nth[N] = subterms.nth(n)

13 }

by HList’snth-method to determine the result type of accessing thenth element of the list. This nth-method expects an instance-level number literal n of type N and is used like this: val x = myhlist.nth(_2). Type N is inferred from the type of the passed instance-level number literal, so that the type parameter does not need to be specified explicitly ,although one could specify it explicitly by writingmyhlist.nth[_2](_2). This has the advantage, that the instance-level literaln can be passed tonthfrom somewhere else, which is what our tuple term implementation does. It exposes a type member Nth which is forwarded to its type list, and an nth method which is forwarded to its inner heterogeneous list of subterms (lines 11 & 12).

Lens Parameterization and Composition

With this framework of implicit conversions, term types, number types, and type-safe operations on heterogeneous lists, we can define some more interesting, parameterized lenses. As an example, we define an atomic filter lens which is parameterized with a single index – in Focal, filter takes a set of labels instead, and is implemented as a parameterized fork lens. To distinguish them, we call our variation FilterN as it takes a single index n. However, the semantics is similar to the original filter lens: In the get direction, all direct children except the specified one are filtered away, so FilterN.get always returns a tuple term with a single child. There is a subtle difference in semantics as the single remaining child is the only component of the abstract tree’s child list and therefore always gets index 0. Technically, the label is changed from n to 0 in the get direction because our data model translates label sets to indexed lists. As the lens knows about the specified index, it can reintegrate changes correctly in the put direction, so that this subtle semantics change has no practical consequences. The following listing shows the definition of FilterN and shows how the focus lens which we envisioned in Listing 5.4 can be defined by sequentially composingFilterN with theHoistlens which we defined in Listing 5.7.

FilterNhas two type parameters: the number typeN for the specified index and type C of the concrete object tree. TypeA does not need to be specified because in this lens, A is completely determined by C. A is a tuple term with C’s nth subterm type as the type of the only child. This type is expressed using the type function Nth of tuple term

5.3. Type-Safe Object-Tree Lenses in Scala 121 Listing 5.9: Composing thefocus lens from thefilter lens and thehoist lens

1 // an atomic filter lens which discards all but one (index-specified) child:

2 class FilterN[N <: Nat, C <: Term](n:N, d:C) extends Lens[C, TupleTerm[C#Nth[N]::TNil]]{

3 type A = TupleTerm[C#Nth[N]::TNil]

4 def get(c: C): A = TupleTerm(c.nth(n) :: HNil) // using the typesafe nth()-accessor

5 def put(a: A, c: C): C = c.replace(n, a.nth(_0))

6 def create(a: A) = d.replace(n, a.nth(_0)) // using the default term d instead of c

7 }

8 def Focus[N <: Nat, C <: Term](n: N, d: C) = Comp( FilterN(n, d), Hoist[C#Nth[N]]() )

9 // inferred type of Focus is Lens[C, C#Nth[N]], i.e. syncing between C and its nth child

which we defined in Listing 5.8. Thus, in Listing 5.7, the type of FilterN is Lens[C, TupleTerm[C#Nth[N]::TNil]](line 2). In thegetfunction (line 4), the type-safe accessor methodnth is used – the result type of c.nth(n)isC#Nth[N]. In theput function (line 5) another type-safe method replace is used for reintegration. If there is no original concrete structure, the create function (line 6) uses a default C-side term d instead of c. Therefore, FilterNexpectsd as a second instance-level parameter, in addition to the index-parameter n.

Now, when composing thefocuslens (line 8), the structure of typeAofFilterN(line 3) matches the structure of typeC ofHoist(see Listing 5.7). This satisfies the type-encoded constraint of the Comp lens and allows us to type-safely compose those two lenses. The composed Focus has the same type-level and instance-level parameters as FilterN to which they are passed – the type parameters are implicitly inferred. However, the type parameter A of Hoist still has to be specified explicitly; we will show later how lenses can be composed without any explicit type parameterization. Figure 5.10 visualizes how the composed Focus lens works in the get direction when, for example, parameterized with index 1 and applied to an exemplary tree with index-labeled edges.

c1

Figure 5.10:Focus as a composition offilter and hoist

Now, we want to use the composed focus lens to extract the phone number of a ContactInfo object, as we have envisioned it in Listing 5.4. However, for being able to directly use the lens with domain objects (and not term objects), we need to change

122 Chapter 5. A Compositional Language for Bidirectional Model Transformation the definition of Focusslightly. For the conversion from a domain object to a correspond-ing term object to be triggered implicitly, the lens functions have to expect objects of a constructor term type and not of the general abstract term type. This is because our gen-erated implicit conversions always convert from or to constructor terms. In the following listing, we first show a slightly altered definition of Focus which expects a constructor term as the default term, and infers the type of the type list of term type C instead of inferring type C itself. Afterwards, we parameterize Focus with the _0 number literal and use it to extract the phone number – the 0th member – of aContactInfo object.

Listing 5.10: Using the focus lens to extract the phone number of a contact info object

1 //defining focus so that it specifically expects CtorTerms

2 def Focus[N <: Nat, TL <: TList](n: N, d: CtorTerm[_,TL]) = Comp(...)

3

4 // a Focus instance to extract the phone number at index 0:

5 val focusCI_0 = Focus(_0, d=ContactInfo(42,"http://"))

6 // the type of this (instance-level) parameterized lens instance is inferred to:

7 // Lens[ CtorTerm[ContactInfo,ValueTerm[String]::ValueTerm[Int]::TNil], ValueTerm[Int] ]

8

9 // using the pre-typed lens instance

10 val cinfo = ContactInfo(3334444, "http://pat.com") // here, the concrete domain object

11 val phone: Int = focusCI_0.get(cinfo) // retrieving the abstract structure, type-checked

12 val ciNew: ContactInfo = focusCI_0.put(3334321,cinfo) // put changes back

As can be seen when we instantiate a parameterized Focus instance in line 5, the type parameters of Focus do not need to be specified explicitly. They are automatically inferred by the compiler: type N is inferred from the type of the instance-level number literaln (here _0) and type C (more specifically the type list typeTL of C) is inferred from the type of the passed default object d of type ContactInfo by looking up the corresponding domain-object-to-term implicit conversion. In line 7, the automatically inferred type of the parameterized Focus instance focusCI_0 is shown (read: focus on ContactInfo field number 0). Because the inferred type C of this lens instance is a parameterized constructor term type which corresponds to the ContactInfo domain class, we can now use the lens directly on domain objects: In lines 10–13, we first create a ContactInfoobjectcinfo, then use theget function to extract the phone number from it, and finally reintegrate a changed phone number back into aContactInfoobject (which is a new one because the lens functions are side-effect free). The ContactInfo object is automatically converted into a corresponding constructor term object and vice versa.

Note that the conversion between value terms and their values is also done implicitly and type-safe, here betweenValueTerm[Int]and Int. Internally,Focusfirst filters away the URL member of ContactInfo (more specifically the 1st component of the corresponding constructor term’s child list) using theFilterN lens and then usesHoist to extract the only remaining child, the phone number value term, which is then converted to anInt.

All of this is completely type-checked and any Scala IDE would report errors if any of the (optional) type annotations do not match with the lens function’s input/output types.

To conclude: because of implicit conversions to and from domain objects, the lens type (not the parameterized instance) could be specified independently from domain types but the type-parameterized lens instance can be used directly on domain objects. This

5.3. Type-Safe Object-Tree Lenses in Scala 123 was one goal for the design of a tree lens library in Scala. However, in contrast to the desired syntax as shown in Listing 5.4, the focus lens is parameterized with an index instead of a field name.