Frank Marx IT-Consulting

Softwareentwicklung

  • Start
  • Biographie
  • Leistungen
  • Dies & Das
  • Stats
  • Impressum
  • Blog – Gedankensplitter
  • Off the record
  • Prototyping
Menu
  • Start
  • Biographie
  • Leistungen
  • Dies & Das
  • Stats
  • Impressum
  • Blog – Gedankensplitter
  • Off the record
  • Prototyping
Burger Construction Kit
November 15, 2021
Published by Frank Marx on November 8, 2021
Categories
  • Programming
  • Swift
  • Technical Article
Tags
  • DSL
  • ResultBuilder
  • Swift


Wozu eine Domain Specific Language?

Eine DSL ist eine anwendungsspezifische Sprache, die konkret auf ein bestimmtes Anwendungsgebiet und dessen typisches Vokabular zugeschnitten ist.

Die Gründe eine solche anwendungsspezifische Sprache zu definieren sind, unter anderen, folgende:

  • Domain-Experten können die DSL nutzen um sehr domain-spezifische Sachverhalte mit der DSL auszudrücken
  • Verständlicherer und klarere Notation in einem konkreten Anwendungsfall
  • Eliminierung von “syntaktischem Rauschen”
  • Erhöhung der semantischen Klarheit

Wie werden üblicherweise DSLs implementiert?

Der klassische Weg eine DSL zu implementieren ist der, eine Compiler für diese DLS zu implementieren der dann, entweder ein Modul generiert was in ein anderes Programm eingebunden werden kann, um dann dort, mittels der dort verwendeten Programmiersprache, genutzt zu werden.

Wenn man sich den Aufwand sparen möchte einen vollständigen Compiler zu schreiben besteht die Möglichkeit einen “Transpiler” zu schreiben. Dieser generiert auf der Basis einer DSL-Sprachbeschreibung Programmcode in der gewünschten Zielsprache, z.B Java oder Swift.

Diese transpilierte Modul kann dann direkt von der Host-Programmiersprache der Anwendung angesprochen werden.

Bei den beiden zuvor aufgeführten Lösungsansätzen ist aber ein gewisser, nicht zu unterschätzender Aufwand, einzuplanen.

DSL und ResultBuilder in Swift

Als dritte Möglichkeit, anstatt der beiden zuvor aufgeführten ( Compiler und Transpiler), bietet sich in Swift seit der Version 5.4 bzw. 5.5 die Möglichkeit das Konstrukt der sogenannten ResultBuilder ( https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html#ID630)zu verwenden.

In der dazugehörigen Dokumentation zu ResultBuildern , auf swift.org, findet man folgende Aussage:

“A result builder is a type you define that adds syntax for creating nested data, like a list or tree, in a natural, declarative way. The code that uses the result builder can include ordinary Swift syntax, like if and for, to handle conditional or repeated pieces of data.”

ResultBuilder ist also ein Typ oder Konstrukt der es ermöglicht verschachtelte, komplexe Datentypen, wie z.B. Listen oder Bäume, auf eine deklarative Art und Weise zu erstellen – und zwar unter der Verwendung der Sprache Swift.

Der Code, welcher diesen ResultBuilder benutzt, kann normale Swift-Syntax verwenden, inklusive der Konstrukte wie z.B. “if-then-else” und “for”-Schleifen.

Beispiel einer DSL

Die Anwendungsfälle für eine DSL sind sicherlich sehr vielfältig und nahezu grenzenlos. Prinzipiell machen sie dort Sinn, wo man komplexe Datenstrukturen auf eine einfache und verständliche Art und Weise erstellen will.

Eine komplexe Datenstruktur kann ein Dokument sein, in HTML oder im Word-Format. Strukturierte Datensätze, hierarchische Strukturen, wie eben z.B. HTML-Dokumente oder andere Konstrukte dieser Art.

Eine DSL sollte in solch einem Anwendungsfall dahin ausgerichtet sein folgende Ziele zu erreichen:

  1. Vermeidung von “noise”, d.h. die Syntax sollte klar und verständlich sein und frei von unnötigen Konstrukten und Ausdrücken
  2. Semantisch leicht zu verstehen, einprägsam und prägnant
  3. Verständliche, klare und genaue Fehlermeldungen
  4. Durch die Verwendung der DSL sollte eindeutig ein Mehrwert entstehen

Entwurf einer Beispiel DSL

Als praktisches Beispiel für eine DSL implementiere ich das Beispiel einer rudimentären Burger-DSL. Es ist eine Domain Specific Language um Hamburger “zusammenzubauen”.

Zuerst definiere ich die prinzipielle Struktur eines Burgers, dieser besteht generell aus folgenden Komponenten, hierbei ist auch die Reihenfolge wichtig:

BURGER {
	TOPBUN
	TOPPING(1..n)
	PADDY
	SPREAD
	BOTTOMBUN
}

Dabei hat ein Burger zwei halbe Brötchenteile, die auch aus verschiedenen Getreidearten bestehen können. Nach dem oberen Brötchen gibt es 1 bis N verschiedene Topping, wie zum Beispiel Tomaten, Gurken, Zwiebeln und Käse.

Danach kommt ein Paddy und dann ein Aufstrich, wobei hier auch EINER aus verschiedenen Sorten gewählt werden kann ( Ketchup, Senf, Mayonnaise etc.).

Anwendung der Burger-DSL

Die konkrete Anwendung der Burger-DSL um einen Burger zusammenzubauen könnten wie folgt aussehen:

let burger = makeBurger {
	addWheatBun()
	addToppings {
		addBacon()
		addPickles()
		addOnions()
	}
	addBeefPaddy()
	addKetchup()
	addRyeBun()
}

Beim genauen Hinsehen erkennt man, dass ein Burger letztendlich auch eine Hierarchie von untergeordneten Zutaten bzw. Elementen darstellt.

Die Reihenfolge der Elemente ist dabei relevant, wie man sehen kann. Eine beliebige Anzahl von Toppings, es können auch KEINE Toppings auf dem Burger sein.

Eventuell wäre es sinnvoll mehrere, auch verschieden Paddy’s zu erlauben, ebenso mit dem Aufstrich – dem Spread.

Die Struktur eines Burger-Objektes könnte wie folgt aussehen, bezogen auf das zuvor aufgeführte Beispiel:

struct Burger {
    var topBun: TopBun
    var toppings: [Topping]
    var paddy: Paddy
    var spread: Spread?
    var bottomBun: BottomBun
}

Ein Burger besteht aus zwei komplexen Strukturen. Zum einen ist das der Burger selbst und dann sind das noch die Toppings, welches ein Array ist – und damit auch eine komplexe Struktur.

Prinzipiell gibt es zwei Möglichkeiten diesen Entwurf mit ResultBuildern zu implementieren:

  1. Top-To-Down
  2. Bottom-Up

Welche Möglichkeit man letztendlich wählt hat her etwas mit persönlichem Geschmack zu tun. Ich habe mich für die Bottom-Up-Methode entschieden. D.h. ich beginne den notwendigen Code für die Toppings zu implementieren um diese durch einen ResultBuilder erstellen zu lassen.

Topping(s)

Für einen Burger können aktuell vier verschiedene Arten von Toppings verarbeitet werden:

  1. Bacon
  2. Onions
  3. Pickles
  4. Salad

Als erstes wird ein “Marker”-Protokoll definiert, dass dann alle Toppings implementieren:

protocol Topping { }

Anschließend die Implementierungen des Protokolls:

struct Salad: Topping {}

struct Onions: Topping {}

struct Pickles: Topping {}

struct Bacon: Topping {}

Die gewünschte Clojure-basierende Syntax für das hinzufügen von Toppings ist wie folgt:

addToppings {
	addBacon()
	addPickles()
	addOnions()
	addSalad()
}

Es wird also ein ResultBuilder benötigt der Topping als einen variadischen Parameter entgegen nimmt und daraus ein Array von Toppings generiert:

@resultBuilder
enum ToppingBuilder {
    static func buildBlock(_ toppings: Topping...) -> [Topping] {
		print("ToppingBuilder was called") // Debug code
        let t = toppings.map{$0}
        return t
    }
}

Der ResultBuilder ist ein Enum definiert, da man aus einem Enum, dass keine Cases enthält, keine Instanz erzeugen kann.

Abschließend noch die Implementierung einer Funktion welche diesen ToppingBuilder-ResultBuilder verwendet:

func addToppings( @ToppingBuilder _ toppings: () -> [Topping]) -> [Topping] {
    let t = toppings()
    return t
}

Durch die Annotation @ToppingBuilder weiß der Compiler, dass er beim Aufruf der Closure toppings()den Code der Funktion buildBlock(…) der Enumeration ToppingBuilder aufrufen soll. Durch diese Ausgabe des print – Statements kann man dies auch sehen, dass letztendlich im Zuge der Konstruktion des Topping-Arrays dieser Code ausgeführt wird.

        let toppings = addToppings{
            addBacon()
            addSalad()
        }

Bei der Untersuchung der Variablen toppings im Debugger, ist zu erkennen, dass diese ein Array von Toppings ist und in diesem Fall aus genau zwei Elementen besteht:

Buns

Es gibt zwei Brötchentypen. Eine Brötchenhälfte für den oberen Deckel und eine andere Brötchenhälfte für den unteren Deckel.

Da ein Burger IMMER aus genau einem oberen und einem unteren Deckel besteht definieren wird ein Protokoll was generell Bun’s repräsentiert und dann zwei weitere Protokoll welche ein Top-Bun und ein Bottom-Bun repräsentieren.

protocol Bun { }

protocol TopBun: Bun {}

protocol BottomBun: Bun {}


struct WheatTopBun: TopBun {}
struct WheatBottomBun: BottomBun {}

Paddy
Spread

DSL in Swift mit ResultBuilder

Share
0
Frank Marx
Frank Marx

Related posts

November 4, 2022

Prototype Game Scene Burger Construction Kit


Read more
November 15, 2021

UI – Prototyping


Read more
November 15, 2021

Burger Construction Kit


Read more

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *