“Groovy in builder-style DSL syntax that leverages closures” .. is where I was last month with GPT writing code in a particular style. And “Ruby in a builder-style DSL syntax that leverages blocks” is what it thought was the best way to describe the same for Ruby. Read on for the rabbit hole I went down with this.

Getting AI’s (GPT4 and Gemini) to describe this form of programming, was fun but won’t pay the bills. Here is a prexisting Ruby example using the library green_shoes for some context: https://raw.githubusercontent.com/Alexanderlol/GS-Calc/master/calc.rb. The code for that:

require 'green_shoes'

Shoes.app(title: "My calculator", width: 200, height: 240) do
	number_field = nil
	@number = 0
	@op = nil
	@previous = 0

	flow width: 200, height: 240 do
		flow width: 0.7, height: 0.2 do
			background rgb(0, 157, 228)
			number_field = para @number, margin: 10
		end

		flow width: 0.3, height: 0.2 do
			button 'Clr', width: 1.0, height: 1.0 do
				@number = 0
				number_field.replace(@number)
			end
		end

		flow width: 1.0, height: 0.8 do
			background rgb(139, 206, 236)
			%w(7 8 9 + 4 5 6 - 1 2 3 / 0 . = *).each do |btn|
				button btn, width: 50, height: 50 do
					case btn
						when /[0-9]/
							@number = @number.to_i * 10 + btn.to_i
						when '='
							@number = @previous.send(@op, @number)
						else
							@previous, @number = @number, nil
							@op = btn
					end
					number_field.replace(@number)
				end
			end
		end
	end
end

It would work with { and } instead of do and end. Credits: Calculator code by Alex Afeltra, Green_shoes gem by ashbb but unmaintained today. Shoes originally by _why. Even if it was maintained, you could not reasonably deploy to Android or iOS.

GPT thinks that is “Ruby in a builder-style DSL syntax that leverages blocks” and that would work for the people in the Ruby community.” Groovy would call those closures, while Ruby ‘blocks’. So there’s no easy canonical way of describing it without a language prefix.

I’ve always said pseudo-declarative DSL, but that would itself need qualification. HTML is declarative. Gemini agrees:

Reasons for classifying HTML as declarative:

1. Focus on describing desired state: HTML focuses on describing the content 
and structure you want on a web page, not how to achieve it step-by-step. You 
say what you want, and the browser handles the rendering details.
2. No control flow statements: Unlike languages with if/else or for loops, 
HTML avoids control flow mechanisms. It simply defines the elements and their 
relationships, allowing the browser to interpret and handle the presentation 
and interactions dynamically.
2. Data-driven and reusable: HTML elements hold data and properties that define 
their appearance and behavior. This creates reusability and modularity, allowing 
the same element types to be used in different contexts with varying attributes.

While HTML is predominantly declarative, there are nuances to consider:

* Scripting languages like JavaScript can be embedded within HTML: This adds 
  imperative elements and control flow possibilities, though the overall 
  structure remains declarative.
* CSS stylesheets apply presentation rules: CSS, while separate from HTML, 
  defines how elements are presented. This introduces non-declarative aspects 
  as you specify colors, layouts, and animations.
  
Overall, calling HTML a "declarative DSL" is accurate, while acknowledging the potential 
integration with imperative scripting for more complex interactions. Compared to the 
"pseudo-declarative" style with mixed imperative elements, HTML leans far more towards 
describing the desired state without explicitly controlling the steps to achieve it.

Gemini goes on after I asked for consideration of “HTML (and declarative languages) are parsed and loaded into navigable structures”

HTML and Declarative Languages:

* Parsing: As you mentioned, declarative languages like HTML are typically 
  parsed into a structured representation (e.g., DOM tree in HTML). This 
  structure captures the relationships and properties of elements within the 
  document.
* Data-driven: The parsed structure serves as the basis for further processing 
  and display. Styling and behavior are often applied based on this structure 
  and associated data.
* No execution: Declarative languages themselves don't execute code or define 
  algorithms. They provide a way to describe what elements and their properties 
  should be, leaving the interpretation and processing to other systems (like 
  browsers for HTML).
    

I’d wanted Gemini to say “the DOM” and it did. Gemini on the pseudo declarative DSL way I’m fascinated by:

Green Shoes and Similar Styles:

* Interpretation: Instead of parsing, this style relies on interpretation. 
  The code is executed line by line, and elements are dynamically created 
  and placed as the code runs.
* No explicit structure: While structures can emerge from the code 
  execution, there's no upfront parsing and creation of a separate data 
  structure representing the UI elements.
* Executable logic: The code includes both declarations of elements and 
  their properties, as well as logic for handling events and user 
  interactions. 

So longer description of the scripting possible with this language feature could be: Pseudo-declarative DSL that leverage builder-style blocks or closures, and are interpreted (or compiled) as all 3GLs. And thus, are not merely parsed into DOM-alike for a secondary actioning (or rendering if the use is for UIs).

For UI usages, Kivy’s kv templates were nice, but not it. QML looks fairly close by the blocks/closures are not open to arbitrary bits of code. I’m told Nix’s expression language is attractive, but I can’t quite coerce GPT4 into showing me what Calculator would look like in a contrived nixGuiLib:

{ lib, nixGuiLib }:

let
  # Function to create number buttons
  createNumberButton = number: nixGuiLib.button {
    label = toString number;
    width = 50;
    height = 50;
    onClick = ''self: { self.number = self.number * 10 + ${toString number}; }'';
  };

  # Function to create operator buttons
  createOperatorButton = op: nixGuiLib.button {
    label = op;
    width = 50;
    height = 50;
    onClick = ''self: { ... }''; # Placeholder for operator functionality
  };

  # Generate number buttons
  numberButtons = map createNumberButton [7 8 9 4 5 6 1 2 3 0];

  # Generate operator buttons
  operatorButtons = map createOperatorButton ["+", "-", "*", "/", "=", "."];

  # Combine number and operator buttons
  combinedButtons = lib.concatLists [numberButtons operatorButtons];

in
nixGuiLib.app {
  title = "My calculator";
  width = 200;
  height = 240;
  content = nixGuiLib.flow {
    width = 200;
    height = 240;
    children = [
      nixGuiLib.flow {
        width = 0.7;
        height = 0.2;
        children = [
          nixGuiLib.background {
            color = "rgb(0, 157, 228)";
            child = nixGuiLib.para {
              text = "0"; # Initial number display
              margin = 10;
            };
          }
        ];
      }
      nixGuiLib.flow {
        width = 0.3;
        height = 0.2;
        children = [
          nixGuiLib.button {
            label = "Clr";
            width = 1.0;
            height = 1.0;
            onClick = ''self: { self.number = 0; }''; # Reset the display
          }
        ];
      }
      nixGuiLib.flow {
        width = 1.0;
        height = 0.8;
        children = [
          nixGuiLib.background {
            color = "rgb(139, 206, 236)";
            child = nixGuiLib.flow {
              children = combinedButtons;
              # Additional properties for layout if necessary
            };
          }
        ];
      }
    ];
  };
}

In that last, concatLists of two sets of buttons would give buttons incorrectly placed in a flowing four by four grid. The next generation was worse, and it was clear that the for-loop possible in the Ruby and Groovy scripts wasn’t possible. Even if that was possible, there is too much prefixing of nixGuiLib. on the functions that could infer context of what instance to operate on as Ruby/Groovy do.

Groovy + SwingBuilder port of the calculator (no good to iOS or Android deployments):

import groovy.swing.SwingBuilder
import javax.swing.JFrame
import javax.swing.WindowConstants

new SwingBuilder().edt {
    JFrame frame = frame(title: 'My calculator', size: [200, 240], defaultCloseOperation: WindowConstants.EXIT_ON_CLOSE) {
        def numberField
        def number = 0
        def op = null
        def previous = 0

        // Top number display
        panel(layout: new BorderLayout(), preferredSize: [200, 48]) {
            background = new Color(0, 157, 228)
            numberField = label(text: "${number}", horizontalAlignment: SwingConstants.CENTER)
        }

        // Clear button
        panel(layout: new FlowLayout(), preferredSize: [200, 48]) {
            button('Clr', size: [94, 48]) {
                action = { evt ->
                    number = 0
                    numberField.text = "${number}"
                }
            }
        }

        // Number and operation buttons
        panel(layout: new GridLayout(4, 4, 5, 5), preferredSize: [200, 144]) {
            background = new Color(139, 206, 236)
            ['7', '8', '9', '+', '4', '5', '6', '-', '1', '2', '3', '/', '0', '.', '=', '*'].each { btn ->
                button(btn, size: [50, 50]) {
                    action = { evt ->
                        switch(btn) {
                            case ~/[0-9]/:
                                number = number.toInteger() * 10 + btn.toInteger()
                                break
                            case '=':
                                if (op != null && previous != null) {
                                    number = previous."$op"(number)
                                    previous = null
                                    op = null
                                }
                                break
                            default:
                                previous = number
                                number = 0
                                op = btn
                                break
                        }
                        numberField.text = "${number}"
                    }
                }
            }
        }
    }
    frame.pack()
    frame.show()
}

Here’s the QML port by GPT4:

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
title: "My Calculator"
width: 200
height: 240
visible: true

    property int number: 0
    property string op: ""
    property int previous: 0

    ColumnLayout {
        anchors.fill: parent

        Rectangle {
            Layout.preferredWidth: parent.width
            Layout.preferredHeight: parent.height * 0.2
            color: "#009de4"

            Text {
                id: numberField
                text: number.toString()
                anchors.centerIn: parent
                font.pixelSize: 24
            }
        }

        Rectangle {
            Layout.preferredWidth: parent.width * 0.3
            Layout.preferredHeight: parent.height * 0.2
            Button {
                text: "Clr"
                anchors.fill: parent
                onClicked: {
                    number = 0;
                    numberField.text = number.toString();
                }
            }
        }

        GridLayout {
            Layout.preferredHeight: parent.height * 0.8
            columns: 4
            rowSpacing: 10
            columnSpacing: 10

            property var buttons: ["7", "8", "9", "+", "4", "5", "6", "-", "1", "2", "3", "/", "0", ".", "=", "*"]

            Repeater {
                model: buttons
                Button {
                    text: modelData
                    width: 50
                    height: 50
                    onClicked: {
                        var btn = text;
                        if (/[0-9]/.test(btn)) {
                            number = number * 10 + parseInt(btn);
                        } else if (btn === '=') {
                            if (op) {
                                number = eval(previous + op + number);
                            }
                        } else {
                            previous = number;
                            number = 0;
                            op = btn;
                        }
                        numberField.text = number.toString();
                    }
                }
            }
        }
    }
}

Compatibility: iOS & Android (via felgo.com tech), and macOS, Linux and Windows for QML

Note that I’ve previously been critical of QML. There is a theoretical deployment possibility to iOS & Android (via felgo.com tech), and macOS, Linux and Windows for QML.

Here is the GPT4 port to Flutter with inline Dart:

import 'package:flutter/material.dart';

void main() => runApp(MyCalculator());

class MyCalculator extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My calculator',
      home: Calculator(),
    );
  }
}

class Calculator extends StatefulWidget {
  @override
  _CalculatorState createState() => _CalculatorState();
}

class _CalculatorState extends State<Calculator> {
  String _display = '0';
  double _number = 0;
  String _op = '';
  double _previous = 0;

  void _onPressed(String value) {
    setState(() {
      switch (value) {
        case 'Clr':
          _display = '0';
          _number = 0;
          _op = '';
          _previous = 0;
          break;
        case '=':
          if (_op != null && _op.isNotEmpty) {
            switch (_op) {
              case '+':
                _number = _previous + _number;
                break;
              case '-':
                _number = _previous - _number;
                break;
              case '*':
                _number = _previous * _number;
                break;
              case '/':
                _number = _previous / _number;
                break;
            }
            _previous = 0;
            _op = '';
            _display = _number.toString();
            _number = 0;
          }
          break;
        case '+':
        case '-':
        case '*':
        case '/':
          _previous = _number;
          _number = 0;
          _op = value;
          break;
        default: // Number buttons
          _number = _number * 10 + double.parse(value);
          _display = _number.toString();
          break;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My calculator')),
      body: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              color: Color.fromRGBO(0, 157, 228, 1),
              child: Center(
                child: Text(_display, style: TextStyle(fontSize: 24, color: Colors.white)),
              ),
            ),
          ),
          Expanded(
            child: Container(
              color: Colors.blueGrey,
              child: FlatButton(
                child: Text('Clr', style: TextStyle(fontSize: 20)),
                onPressed: () => _onPressed('Clr'),
              ),
            ),
          ),
          Expanded(
            flex: 4,
            child: Container(
              color: Color.fromRGBO(139, 206, 236, 1),
              child: GridView.count(
                crossAxisCount: 4,
                children: <String>['7', '8', '9', '+', '4', '5', '6', '-', '1', '2', '3', '/', '0', '.', '=', '*']
                    .map((String value) {
                  return GridTile(
                    child: FlatButton(
                      child: Text(value, style: TextStyle(fontSize: 24)),
                      onPressed: () => _onPressed(value),
                    ),
                  );
                }).toList(),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Flutter has a iOS, Android, macOS, Linux and Windows deployment possibilities.

GPT4-made Kotlin with TornadoFX (JVM language family, but now abandoned):

import tornadofx.*
import javafx.geometry.Pos
import javafx.scene.paint.Color

class MyCalculatorApp : App(MyCalculatorView::class)

class MyCalculatorView : View("My calculator") {
    var numberField: Text by singleAssign()
    var number: Int = 0
    var op: String? = null
    var previous: Int = 0

    override val root = vbox {
        minWidth = 200.0
        minHeight = 240.0
        alignment = Pos.TOP_CENTER

        hbox {
            minWidth = 200.0
            minHeight = 48.0 // 20% of 240
            rectangle {
                fill = Color.rgb(0, 157, 228)
                width = 140.0 // 70% of 200
                height = 48.0 // 100% height of this hbox
            }
            numberField = text(number.toString()) {
                style {
                    fontSize = 20.px
                    fill = Color.WHITE
                }
                paddingAll = 10.0
            }
            button("Clr") {
                minWidth = 60.0 // 30% of 200
                minHeight = 48.0 // 100% height of this hbox
                action {
                    number = 0
                    numberField.text = number.toString()
                }
            }
        }

        gridpane {
            minWidth = 200.0
            minHeight = 192.0 // 80% of 240
            hgap = 5.0
            vgap = 5.0
            alignment = Pos.CENTER
            rectangle {
                fill = Color.rgb(139, 206, 236)
                width = 200.0
                height = 192.0
            }
            listOf("7", "8", "9", "+", "4", "5", "6", "-", "1", "2", "3", "/", "0", ".", "=", "*").forEachIndexed { index, btn ->
                val (x, y) = index % 4 to index / 4
                button(btn) {
                    minWidth = 45.0
                    minHeight = 45.0
                    gridpaneConstraints {
                        columnIndex = x
                        rowIndex = y
                    }
                    action {
                        when {
                            btn.matches(Regex("[0-9]")) -> {
                                number = number * 10 + btn.toInt()
                            }
                            btn == "=" -> {
                                number = when (op) {
                                    "+" -> previous + number
                                    "-" -> previous - number
                                    "*" -> previous * number
                                    "/" -> previous / number
                                    else -> number
                                }
                            }
                            else -> {
                                previous = number
                                number = 0
                                op = btn
                            }
                        }
                        numberField.text = number.toString()
                    }
                }
            }
        }
    }
}

fun main() {
    launch<MyCalculatorApp>()
} 

GPT4 notes that the JavaFX without Kotlin is more verbose. I’m not doing to show it here because it does not have that language feature we’re looking for it is just a bunch of instance.method(arg) statements that superficially look nested.

Apple’s SwiftUI made by GPT4 (macOS and iOS only for now)

import SwiftUI

struct ContentView: View {
    @State private var number: Double = 0
    @State private var previous: Double = 0
    @State private var op: String? = nil
    
    let buttons: [[String]] = [
        ["7", "8", "9", "+"],
        ["4", "5", "6", "-"],
        ["1", "2", "3", "/"],
        ["0", ".", "=", "*"]
    ]
    
    var body: some View {
        VStack(spacing: 0) {
            Text("\(number)")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.blue)
                .foregroundColor(.white)
                .font(.largeTitle)
            
            Button("Clr") {
                self.number = 0
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.gray)
            .foregroundColor(.white)
            
            ForEach(buttons, id: \.self) { row in
                HStack(spacing: 0) {
                    ForEach(row, id: \.self) { button in
                        Button(button) {
                            self.buttonPressed(button)
                        }
                        .frame(width: 50, height: 50)
                        .background(Color.cyan)
                        .foregroundColor(.black)
                    }
                }
            }
        }
        .frame(width: 200, height: 240)
    }
    
    private func buttonPressed(_ button: String) {
        switch button {
        case "0"..."9":
            number = number * 10 + Double(button)!
        case "=":
            if let op = op {
                switch op {
                case "+": number = previous + number
                case "-": number = previous - number
                case "*": number = previous * number
                case "/": number = previous / number
                default: break
                }
            }
        default:
            previous = number
            number = 0
            self.op = button
        }
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

And GPT4 Common Lisp with CLIM (for Mac, Windows and Linux deployments), even though that’s a different language family than the one I’m talking about in this blog entry - it’s the epitome of terse and elegant.

(defpackage :my-calculator
  (:use :common-lisp :clim-lisp :clim))

(in-package :my-calculator)

(defun start-calculator ()
  (let ((number 0)
        (op nil)
        (previous 0))

    (define-application-frame my-calculator ()
      ()
      (:panes
       (display :application
                :display-function (lambda (frame pane)
                                    (declare (ignore frame))
                                    (format pane "~A" number))
                :scroll-bars nil)
       (buttons :push-button-panel
                :items '(("Clr" (setf number 0))
                         "7" "8" "9" "+" "4" "5" "6" "-" "1" "2" "3" "/" "0" "." "=" "*")
                :activate-callbacks '((:button-press (button)
                                       (case (string button)
                                         (("Clr") (setf number 0))
                                         ((digit when (digit-char-p (aref digit 0)))
                                          (setf number (+ (* number 10) (parse-integer digit))))
                                         (("=") (setf number (funcall (case op
                                                                        ("+" #'+)
                                                                        ("-" #'-)
                                                                        ("/" #'/)
                                                                        ("*" #'*)
                                                                        (otherwise #'identity))
                                                                      previous number)))
                                         (otherwise
                                          (setf previous number)
                                          (setf number nil)
                                          (setf op button)))
                                       (redisplay-frame-pane *application-frame* 'display)))))
      (:layouts
       (default (vertically ()
                  (scrolling (:height 120) display)
                  (fixed-size (:height 120) buttons)))))

    (run-frame-top-level (make-application-frame 'my-calculator))))

(start-calculator)

Back to Groovy - there’s an exciting JVM build tech called “Bld”that uses Java and fluent-interfaces as its way of configuring and describing targets: I made a Groovy skin over that. In execution it’s about one second worse than the pure Java one for me. Someone with Groovy skills could reduce that, I guess (bld.groovy):

@Grapes(
        @Grab(group='com.uwyn.rife2', module='bld', version='1.9.0')
)

import static rife.bld.dependencies.Repository.*
import rife.bld.dependencies.Scope
import static rife.bld.operations.TemplateType.HTML

def proj = BuildConfig.webProject() {
    pkg = "com.example"
    name = 'Mywebapp'
    mainClass = "com.example.MywebappSite"
    uberJarMainClass = "com.example.MywebappSiteUber"
    version = version(0,1,0)
    downloadSources = true
    repositories = List.of(MAVEN_CENTRAL, RIFE2_RELEASES)

    compile() {
        include() {
            dependency("com.uwyn.rife2", "rife2", version(1,7,3))
        }
    }
    test() {
        include() {
            dependency("org.jsoup", "jsoup", version(1,17,2))
            dependency("org.junit.jupiter", "junit-jupiter", version(5,10,2))
            dependency("org.junit.platform", "junit-platform-console-standalone", version(1,10,2))
        }
    }
    standalone() {
        include() {
            dependency("org.eclipse.jetty.ee10", "jetty-ee10", version(12,0,6))
            dependency("org.eclipse.jetty.ee10", "jetty-ee10-servlet", version(12,0,6))
            dependency("org.slf4j", "slf4j-simple", version(2,0,11))
        }
    }
    precompileOperation() {
        templateTypes(HTML)
    }
}

proj.start(args)

// -------------------------------------------------------------------------------------------
// All below here would be in a library no the bld.groovy file for ultimate end-user realities

class BuildConfig {
    def static webProject(Closure config) {
        def proj = new GldWebProject()
        config.delegate = proj
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()
        return proj
    }
}
class GldWebProject extends rife.bld.WebProject {
    def compile(Closure config) {
        def gldScope = new GldScope(this, scope(Scope.compile))
        config.delegate = gldScope
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()
    }
    def test(Closure config) {
        def gldScope = new GldScope(this, scope(Scope.test))
        config.delegate = gldScope
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()
    }
    def standalone(Closure config) {
        def gldScope = new GldScope(this, scope(Scope.standalone))
        config.delegate = gldScope
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()

    }
    def precompileOperation(Closure config) {
        def gldpo = new GldPrecompileOperation(this)
        config.delegate = gldpo
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()
    }
}
class GldScope {
    def proj
    def sc
    def GldScope(rife.bld.WebProject proj, rife.bld.dependencies.DependencySet scope) {
        this.sc = scope
        this.proj = proj
    }
    def include(Closure config) {
        def gldinclude = new GldInclude(proj, sc)
        config.delegate = gldinclude
        config.resolveStrategy = Closure.DELEGATE_FIRST
        config()
    }

}
class GldInclude {
    def proj
    def ds
    def GldInclude(rife.bld.WebProject proj, rife.bld.dependencies.DependencySet scope) {
        this.proj = proj
        this.ds = scope
    }
    def dependency(g, a, v) {
        ds.include(proj.dependency(g, a, v))
    }
}
class GldPrecompileOperation {
    def proj
    def ttype
    def GldPrecompileOperation(rife.bld.WebProject proj) {
        this.proj = proj
    }
    def templateTypes(rife.bld.operations.TemplateType ttype) {
        this.ttype = ttype
        proj.precompileOperation().templateTypes(ttype)
    }
}

That’s not just a speculative grammar, you can try it here: https://raw.githubusercontent.com/paul-hammant/groovy-bld-prototype. I do have a prior article on Groovy infra-as-code here

Back to the quest implicit in this post

Anyway, this is a needs a short name, in my opinion. Like a “Monads” class of short name. I asked Ruby’s creator Yukihiro “Matz” Matsumoto and he said “DSL with scope” is what he’s always called it.

Updates:

  • Feb 24th, 2024: add GPT4 ported calculator code: Nix example with a contrived UI lib, Swing example with its real SwingBuilder, QML example, Lisp example
  • Feb 25th, 2024: add Kotlin with TornadoFX and SwiftUI port of that Ruby Calculator.
  • Mar 1st, 2024: Add Groovy grammar for ‘Bld’ build language
  • Mar 7th, 2024: Add Matz feedback


Published

February 14th, 2024
Reads: