TL;DR: HyLiMo is a Tool for hybrid (both textual and graphical) modeling of UML class diagrams. During the meeting, we discuss what the DSL (textual concrete syntax) for class diagrams could look like, and which features can be supported in the graphical view.
Creating UML class diagrams is a common task for software engineers. Therefore, several tools already exist for this purpose, e.g., MermaidJS or diagrams.net. Most of these tools can be put into two categories:
- Descriptive DSL, Autolayout: An External DSL is used to describe the structure of the Diagram, the graphical representation is then generated based on the given DSL text. Typically, manual layouting is not at all or is very limited supported.
- Graphical editor: The diagram is created using a graphical editor, typically with completely manual layouting (or limited autolayout support)
As both approaches have their benefits, recently, tools using hybrid/blended modeling appeared, e.g., D2 and Structurizr. These tools typically use a descriptive external DSL, and allow to add/remove and update the contents of diagram elements (e.g., classes) in the graphical view.
However, manual & precise layouting is not supported in the DSL, forcing users to use less efficient approaches when precise or custom layouting is required.
HyLiMo is a tool for creating diagrams using a hybrid editor consisting of a
- embedded/internal DSL to describe the diagram
- graphical view to manipulate parts of the diagram, most important layouting
Technically, it works like this:
- The diagram is described using an embedded/internal DSL in the general-purpose scripting language SyncScript.
- The DSL text is executed to generate the diagram model.
- The model is displayed in the graphical view.
- When the user manipulates the graphical view, e.g., by moving a class, the DSL code is updated to correspond to the graphical changes.
- Then, the User can do more textual or graphical changes.
To see what this flow looks like, have a look at the following video:
concept.mp4
This approach results in some unique features
- The DSL text acts as a single source of truth: diagrams can be shared as version-controlled documents
- Manual layouting is both supported and easy thanks to the graphical view
- Precise layouting can be done by manipulating layout-related information in the DSL code
- HyLiMo uses an embedded DSL: therefore, functions and control flow statements can be used to simplify creating the diagram
- The diagram is fully stylable, using a simple CSS-like syntax
- HyLiMo is based only on web technologies and can run in both browser and NodeJS environments making integration into IDEs and command line tools easy
By now, the underlaying technology stack is implemented. I will use the RE interviews to collect features and (design) ideas regarding
- the UML class diagram DSL
- the graphical editor
In a one-on-one format, I will explain the general concept again, and give a demonstration of the already working parts. Then, we will discuss how the DSL (textual concrete syntax) could look like, based on some existing ideas (see below), which graphical editing features should be supported, and how those interactive features can be implemented.
Based on the protocols of those meetings and my own ideas, I will then create a questionnaire to
- evaluate the importance of specific features
- find the most popular approach where multiple exist
classdiagram {
House = class("House") {
"roomCount" : Int
"wallColor" : "Color"
"createRoom" : fun("floor" : Int) => "Room"
} at pos(0, 100)
Room = class("Room") {
+("size") : Double
}
House --> Room {
start = 0.5
over = list(pos(200, 300), pos(400, 500))
end = 0.5
}
styles {
class("class") {
minWidth = 100
maxWidth = 600
}
}
}
Following are some of the already existing questions I try to answer during RE. Feel free to already think about those before the meeting. Of course, you can also bring and/or come up with your ideas for these or related questions.
- Rendering
- how to render visibility modifiers of fields and methods
- which types of lines should be supported for relations (bezier, direct, ...)
- DSL
- how to define class fields (following some possible examples)
+("size") : Int
+("size" : Int)
field(+, "size", Int)
- how to define class methods (following some possible examples)
+("createRoom") : fun("floor" : Int) => "Room"
+("createRoom") : fun("floor" : Int) : "Room"
+("createRoom" : fun("floor" : Int) => "Room")
fun(+, "createRoom", list("floor" : Int), "Room"
-
+("createRoom") { "floor" : Int } : "Room"
- ...and many more combinations of these ideas
- support multiple separated (by line) areas (of fields and methods) in a class?
- which visibility modifiers should be supported
- how to declare a relation between classes (or in general line segments)
- segments can be of different types, e.g., bezier, direct line, ...
- some helpers could make defining those simpler, e.g., something like
polyline(p1, p2, p3)
- how to support stereotypes for classes (and which?)
- how to define class fields (following some possible examples)
- Graphical view
- how & when to display the points which can be manipulated by moving around
- which features, apart from moving (classes and other points) and resizing (classes) should be supported (e.g., changing text values)
- should control features (e.g., resize helpers, movable points) scale with the zoom level of the diagram or not?
- SyncScript
- (context: in SyncScript, everything is an expression) should look control structures evaluate to
- null
- the last inner value
- a list of all inner values
- (context: in SyncScript, everything is an expression) should look control structures evaluate to
You can find the hosted version of HyLiMo here: https://hylimo.github.io
To get started, as an example, you can insert the content of the following file on the left side:
generateClass = {
fields = list("+x: Int", "~y: String")
methods = list("+test(x: Int): String")
classContents = list(
text(contents = list(span(text = "MyClass" + it)), class = list("title")),
rect(class = list("separator"))
)
fields.forEach {
classContents.add(text(contents = list(span(text = it))))
}
classContents.add(rect(class = list("separator")))
methods.forEach {
classContents.add(text(contents = list(span(text = it))))
}
rect(class = list("class"), content = vbox(contents = classContents))
}
tmp = 300
p1 = absolutePoint(x = 20, y = 20)
p2 = relativePoint(target=p1, offsetX = 200, offsetY = tmp + 100)
classElement = canvasElement(pos = p1, content = generateClass(0), scopes = object())
classElement2 = canvasElement(pos = p2, content = generateClass(1), scopes = object())
parentCanvas = canvas(contents = list(p1, p2, classElement, classElement2))
primary = "white"
lineWidth = 2
diagram(
parentCanvas,
styles {
type("span") {
fill = primary
}
class("class") {
stroke = primary
strokeWidth = lineWidth
width = 300
type("vbox") {
margin = 5
}
}
class("title") {
hAlign = "center"
type("span") {
fontWeight = "bold"
fontStyle = "italic"
}
}
class("separator") {
marginTop = 5
marginBottom = 5
marginLeft = -5
marginRight = -5
height = lineWidth
fill = primary
}
},
list(defaultFonts.roboto)
)
SyncScript is the general-purpose programming language in which the class diagram DSL is embedded. It is a rather simple, interpreted, dynamically typed scripting language which focuses on
- syntactic flexibility for creating internal/embedded DSLs: Scala-like higher-order functions, custom operators
- tracing: to update the DSL code based on graphical updates, one needs to know which Expression in the code caused which value
- web support: everything, from lexer to interpreter is implemented using web technologies only
Following is a very short introduction to SyncScript, in case you are interested.
println("Hello world")
- strings
- numbers
- objects
- an object can have fields indexed by both integers and strings
- objects can have prototypes. If a field is not found on the object itself, the interpreter looks at the prototype and tries to find it there. Fields can be accessed using the common
.
syntax.
- functions (see below for more details)
- null
- boolean
- technically, booleans are implemented as objects
Variables are assigned using the =
operator.
Example:
i = 1
test = "Hello world"
Functions are the most important concept in SyncScript, as all control-flow structures are implemented using functions.
To declare a function, use curly brackets:
printHelloWorld = {
println("Hello world")
}
Functions can be invoked using round brackets:
printHelloWorld()
Arguments are provided using the args
identifier, SyncScript supports both index-based and named arguments:
printSth = {
println(args.text)
}
printSth(text = "Some text")
To simplify accessing indexed-based arguments, destructuring can be used:
createPoint = {
(x, y) = args
"TODO"
}
createPoint(10, 20)
Additionally, the first positional argument can be accessed under the name it
:
myPrintln = {
println(it)
}
myPrintln("test")
As a return value, the value of the last expression in the function body is used.
SyncScript uses static scoping, each function creates a new scope. You can use this
to access the current scope directly
To allow the creation of the DSL constructs shown above, higher-order functions can be used. Similar to Scala and Kotlin,
point {
x = 1
y = 2
}
is identical to
point({
x = 1
y = 2
})
It is even possible to use multiple of these blocks, as shown in the implementation of the builtin-function if
:
if (aVariable) {
println("the variable is true")
} {
println("the variable is not true")
}
Operators are functions, which get two positional arguments: the left and right side:
div = {
(left, right) = args
left / right
}
10 div 20
- if
if(condition) { println("true") } { println("false") }
- while
while { a < b} { println("body") }
- object
aPoint = object(x = 10, y = 20)
- list
someNumbers = list(1, 2, 3, 4, 5) someNumbers.forEach { println(it) } someNumbers.add(100) lastElement = someNumbers.remove()
- error
error("Something went wrong")