Modular Domain-Specific Language Components in Scala
Total Page:16
File Type:pdf, Size:1020Kb
Submitted to GPCE ’10 Modular Domain-Specific Language Components in Scala Christian Hofer Klaus Ostermann Aarhus University, Denmark University of Marburg, Germany [email protected] [email protected] Abstract simply compose these languages, if the concrete representations of Programs in domain-specific embedded languages (DSELs) can be their types in the host language match. Assuming Scala as our host represented in the host language in different ways, for instance im- language, we can then write a term like: plicitly as libraries, or explicitly in the form of abstract syntax trees. app(lam((x: Region) )union(vr(x),univ)), Each of these representations has its own strengths and weaknesses. scale(circle(3), The implicit approach has good composability properties, whereas add(vec(1,2),vec(3,4)))) the explicit approach allows more freedom in making syntactic pro- This term applies a function which maps a region to its union with gram transformations. the universal region to a circle that is scaled by a vector. Traditional designs for DSELs fix the form of representation, However, the main advantage of this method is also its main which means that it is not possible to choose the best representation disadvantage. It restricts the implementation to a fixed interpreta- for a particular interpretation or transformation. We propose a new tion which has to be compositional, i. e., the meaning of an expres- design for implementing DSELs in Scala which makes it easy to sion may only depend only on the meaning of its sub-expressions, use different program representations at the same time. It enables not on their syntactic form or some context. In the above example, the DSL implementor to define modular language components and we could perform two optimizations. First, the union of any region to compose transformations and interpretations for them. with the universal region is itself the universal region, allowing us Categories and Subject Descriptors D.1.5 [Programming Tech- to replace the body of the lambda abstraction with univ. And sec- niques]: Object-oriented Programming; D.2.3 [Software Engi- ond, we see that the parameter x is not used (or without the first neering]: Reusable Software—Reusable Libraries; D.3.2 [Pro- optimization: only used once) in the body, allowing us to inline gramming Languages]: Language Classifications—Extensible Lan- the application [1], reducing the whole term to univ. Optimizations guages, Specialized Application Languages like these are hard to implement using compositional interpreta- tions [2, 12]. General Terms Languages, Design In order to perform analyses on the code or write a more ef- ficient implementation (or even a compiler) of the language, the Keywords Embedded Languages, Domain-Specific Languages, DSL backend can be replaced by an explicit representation of the Term Representation, Visitor Pattern, Scala abstract syntax tree (AST) [8]. Then, arbitrary traversals over the AST can be implemented for analysis and interpretation. However, 1. Introduction this flexibility comes at a price: extending the DSL with new oper- The methodology of domain-specific embedded languages, where ations, or even composing it with other languages, requires adapta- a domain-specific language (DSL) is embedded as a library into tion of both the data structure and the tree traversals. a typed host language, instead of creating a stand-alone DSL, is Recently, variants of the DSEL approach have been proposed nowadays well-known. It goes back to Reynolds [25] and has been that introduce a separation between language interface and imple- systematically described by Hudak [13]. It is ideal for prototyping a mentation [2, 6, 12]. In this way, it is possible to define several language for two reasons. Firstly, simple interpreters can be quickly (compositional) interpretations for the same language, e. g., partial derived and implemented from the denotational semantics of the evaluators or transformations to continuation-passing style [6]. Fur- DSL. Secondly, many parts of the host language can be directly thermore, the languages can still be extended and even composed reused: not only its syntax, but also some semantic features like with other languages in the sense of extending and composing the its libraries and even its type system. Furthermore, it is easy to interpretations [12]. extend the DSL or compose and integrate several DSLs into the However, two challenges remain. Firstly, can we extend and same host language, since DSL composition is the same as library compose languages in the sense of extending and composing their composition. representations? Then we could use these representations as target Assume for example that we have three different DSLs: a lan- domains for program transformations and in this way perform one guage of regions, one of vectors, and a lambda calculus. We can or more program transformations before interpreting the program. Secondly, can we express non-compositional interpretations? We believe that a scalable approach to embedding DSLs should address these problems. We propose a design that extends tech- niques developed in recent studies of the visitor pattern [5, 20], expression problem [32], and our own work on polymorphic em- bedding [12]. More specifically, the design goals set forth in this paper are: • The design should enable the composition of independently [Copyright notice will appear here once ’preprint’ option is removed.] developed languages and their representations. MDSLCS 1 2010/7/5 • Embedded languages are statically typed (uni-typed in the sim- trait RegionLI f ple case such as the region DSL). A composed language should type Region preserve the types of the individual languages. def univ : Region def circle(radius : Double) : Region • It should be possible to apply different (kinds of) interpretations def union(reg1 : Region, reg2 : Region) : Region on the same language representation. g The target domain of the interpretation should be allowed trait Example f val regionInterpretation: RegionLI to be the term representation (program transformation). It import regionInterpretation. should be possible to compose several program transforma- tions in this way before interpreting to another domain. ... union(circle(2.0), univ) ... g It should be possible to define compositional as well as non- object ExampleInstance extends Module f compositional interpretations. val regionInterpretation = new EvalRegion fg • The language representations and their interpretations should be g independent. That is, it should be possible to add new interpre- Figure 1. The region language interface and its usage tations without having to change the language representation. We choose Scala as the implementation language, as its com- bination of language features both allows for solving the expres- sion problem and makes DSL embedding smooth [12, 32]. We will 2.1 Defining the Language Interface only show incomplete code examples for space reasons. In partic- ular, we will not discuss infix operations in the paper. The com- Each language specifies the language interface as the signature plete source code with further examples can be downloaded at: of an algebra: abstract type members declare the sorts (domains) 1 http://www.cs.au.dk/~chmh/mdslcs/. of the algebra, methods its constants and operations [12]. The The central contributions of this paper are: language interface of the regions language is shown in the trait RegionLI in Fig. 1. Region is the only sort in this algebra. univ is 1. We show how to integrate extensible term representations into the universal region, circle is a circle around the origin and union a DSEL approach in Scala. These term representations can be is a binary operation to construct the union of two regions. More used as the target domain of program transformations on DSL operations could be defined in the same way. We could also add terms. them later by defining an extended language interface that inherits 2. We show that those term representations are composable in the from RegionLI: The language interface is extensible. same way as the languages that they represent. In particular, this To create a term of a language, we need an object that imple- composition also reflects the types of the DSL expressions. ments the language interface. However, it is easy to abstract over the actual interpretation such that the same DSL program can be in- 3. Our representation accommodates for three kinds of interpre- terpreted in multiple ways. One way to do that is shown in the trait tations: compositional interpretations, interpretations based on Example. Here, we specify a dependency of the example on some explicit AST traversal, and interpretations based on AST in- interpretation of the region language. The object ExampleInstance spection. We motivate and compare these three options for writ- then fixes a specific interpretation. Note that the import construct ing interpretations. in Scala can appear anywhere in the code and can refer to arbi- 4. We discuss name-binding on DSELs by means of composing trary values. We use it here to import the operations of the inter- with a lambda calculus language. In particular, we present an pretation so we can use them without prefixing them by the name extensible term representation for the simply-typed lambda cal- regionInterpretation. culus using higher-order abstract syntax. We will present the core of the representation for a single, uni- 2.2 Defining Compositional Interpretations typed language in Sec. 2. In Section 3, we present how different lan-