Did I Discover A New Programming Paradigm? [closed]
In the last 5 years I have explored what I think is a new programming paradigm that I call "Declarative Domain Programming". It uses type declarations to model logic, behaviour and relationships. The language I am using is Swift, as it offers nested types, enums with associated values and type inference. Let us look at an example, a simple todo item. struct TodoItem { let title : String let state : State let dueDate : DueDate let location : Location let collaborators: [Collaborator] } extension TodoItem { enum State { case unfinished case inProgress case finished } } extension TodoItem { enum DueDate { case none case date(Date) } } enum Location { case unknown case coordinate(Coordinate) case address(Address) } struct Address { let street : String let city : String let country: String let zipCode: String } struct Coordinate { let latitude : Double let longitude: Double } struct Collaborator { init(name: String) { self.init(UUID(), name) } private init (_ i: UUID, _ n: String) { id = i name = n } let id : UUID let name: String } TodoItem has a title, a state (which can be unfinished, in progress or finished), a due date (which can be none or a given date), a location (which can be unknown, a coordinate of latitude and longitude or an address) and a list of collaborators. Now let us define TodoItem's Change DSL to change any of the attributes: struct TodoItem { enum Change { case setting(Setting); enum Setting { case title (to: String) case state (to: State) case dueDate (to: DueDate) case location(to: Location) } case adding(Adding); enum Adding { case collaborator(Collaborator) } case removing(Removing); enum Removing { case collaborator(Collaborator) } } // ... } Following commands can be encoded by this DSL: .setting(.title(to: )) .setting(.state(to: )) .setting(.dueDate(to: )) .setting(.location(to: )) .adding(.collaborator()) .removing(.collaborator()) Now we add a method that pattern-matches over these values and creates a new object with the reflected changes: struct TodoItem { //... init(_ t: String, _ s:State, _ d:DueDate, _ l:Location, _ c:[Collaborator]) { title = t state = s dueDate = d location = l collaborators = c } func alter(by c: Change) -> Self { switch c { case let .setting(.title(to: t)) : Self(t , state, dueDate, location, collaborators ) case let .setting(.state(to: s)) : Self(title, s , dueDate, location, collaborators ) case let .setting(.dueDate(to: d)) : Self(title, state, d , location, collaborators ) case let .setting(.location(to: l)) : Self(title, state, dueDate, l , collaborators ) case let .adding (.collaborator(c)): Self(title, state, dueDate, location, collaborators + [c] ) case let .removing(.collaborator(c)): Self(title, state, dueDate, location, collaborators.filter{$0.id != c.id}) } } } By switching over the Change value we decode the command and write associated values to local constants. We use these constants to overwrite an existing value when creating a new object. We can now use it like: var buyBeer = TodoItem(title: "Buy beer") let cal = Calendar.current let tomorrowNoon = cal.date(byAdding: .hour, value: 12, to: cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: .now))!)! let alice = Collaborator(name: "Alice") let beerShop = Location.address(Address(street: "Kreuzbergstrasse 78", city: "Berlin", country: "Germany", zipCode: "10965")) buyBeer = buyBeer .alter(by: .setting(.title(to: "Buy beer — ASAP!"))) .alter(by: .setting(.state(to: .inProgress))) .alter(by: .setting(.dueDate(to: .date(tomorrowNoon)))) .alter(by: .setting(.location(to: beerShop))) .alter(by: .adding(.collaborator(alice))) It becomes apparent, that TodoItem's Change DSL follows spoken English quite closely: buyBeer: alter by setting title to "Buy beer — ASAP!" alter by setting state to "in progress" alter by adding collaborator "Alice" ... We can overload the alter method to accept a list of changes: struct TodoItem { //... func alter(by c:[Change]) -> Self { c.reduce(self) { $0.alter(by:$1) } } func alter(by c:Change...) -> Self { alter(by: c) } private func alter(by c: Change) -> Self { switch c { case let .setting(.title(to: t)) : Self(t , state, dueDate, location, collaborators ) case let .setting(.state(to: s)) : Self(title, s , dueDate, location
![Did I Discover A New Programming Paradigm? [closed]](https://miro.medium.com/v2/resize:fit:1200/format:webp/1*nKR2930riHA4VC7dLwIuxA.gif)
In the last 5 years I have explored what I think is a new programming paradigm that I call "Declarative Domain Programming". It uses type declarations to model logic, behaviour and relationships.
The language I am using is Swift, as it offers nested types, enums with associated values and type inference.
Let us look at an example, a simple todo item.
struct TodoItem {
let title : String
let state : State
let dueDate : DueDate
let location : Location
let collaborators: [Collaborator]
}
extension TodoItem {
enum State {
case unfinished
case inProgress
case finished
}
}
extension TodoItem {
enum DueDate {
case none
case date(Date)
}
}
enum Location {
case unknown
case coordinate(Coordinate)
case address(Address)
}
struct Address {
let street : String
let city : String
let country: String
let zipCode: String
}
struct Coordinate {
let latitude : Double
let longitude: Double
}
struct Collaborator {
init(name: String) {
self.init(UUID(), name)
}
private init (_ i: UUID, _ n: String) {
id = i
name = n
}
let id : UUID
let name: String
}
TodoItem
has a title, a state (which can be unfinished, in progress or finished), a due date (which can be none or a given date), a location (which can be unknown, a coordinate of latitude and longitude or an address) and a list of collaborators.
Now let us define TodoItem
's Change
DSL to change any of the attributes:
struct TodoItem {
enum Change {
case setting(Setting); enum Setting {
case title (to: String)
case state (to: State)
case dueDate (to: DueDate)
case location(to: Location)
}
case adding(Adding); enum Adding {
case collaborator(Collaborator)
}
case removing(Removing); enum Removing {
case collaborator(Collaborator)
}
}
// ...
}
Following commands can be encoded by this DSL:
.setting(.title(to:
)) .setting(.state(to:
)) .setting(.dueDate(to:
)) .setting(.location(to:
)) .adding(.collaborator(
)) .removing(.collaborator(
))
Now we add a method that pattern-matches over these values and creates a new object with the reflected changes:
struct TodoItem {
//...
init(_ t: String, _ s:State, _ d:DueDate, _ l:Location, _ c:[Collaborator]) {
title = t
state = s
dueDate = d
location = l
collaborators = c
}
func alter(by c: Change) -> Self {
switch c {
case let .setting(.title(to: t)) : Self(t , state, dueDate, location, collaborators )
case let .setting(.state(to: s)) : Self(title, s , dueDate, location, collaborators )
case let .setting(.dueDate(to: d)) : Self(title, state, d , location, collaborators )
case let .setting(.location(to: l)) : Self(title, state, dueDate, l , collaborators )
case let .adding (.collaborator(c)): Self(title, state, dueDate, location, collaborators + [c] )
case let .removing(.collaborator(c)): Self(title, state, dueDate, location, collaborators.filter{$0.id != c.id})
}
}
}
By switching over the Change
value we decode the command and write associated values to local constants. We use these constants to overwrite an existing value when creating a new object.
We can now use it like:
var buyBeer = TodoItem(title: "Buy beer")
let cal = Calendar.current
let tomorrowNoon = cal.date(byAdding: .hour, value: 12, to: cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: .now))!)!
let alice = Collaborator(name: "Alice")
let beerShop = Location.address(Address(street: "Kreuzbergstrasse 78", city: "Berlin", country: "Germany", zipCode: "10965"))
buyBeer =
buyBeer
.alter(by: .setting(.title(to: "Buy beer — ASAP!")))
.alter(by: .setting(.state(to: .inProgress)))
.alter(by: .setting(.dueDate(to: .date(tomorrowNoon))))
.alter(by: .setting(.location(to: beerShop)))
.alter(by: .adding(.collaborator(alice)))
It becomes apparent, that TodoItem's Change DSL follows spoken English quite closely:
buyBeer
:
- alter by setting title to "Buy beer — ASAP!"
- alter by setting state to "in progress"
- alter by adding collaborator "Alice"
- ...
We can overload the alter method to accept a list of changes:
struct TodoItem {
//...
func alter(by c:[Change]) -> Self { c.reduce(self) { $0.alter(by:$1) } }
func alter(by c:Change...) -> Self { alter(by: c) }
private func alter(by c: Change) -> Self {
switch c {
case let .setting(.title(to: t)) : Self(t , state, dueDate, location, collaborators )
case let .setting(.state(to: s)) : Self(title, s , dueDate, location, collaborators )
case let .setting(.dueDate(to: d)) : Self(title, state, d , location, collaborators )
case let .setting(.location(to: l)) : Self(title, state, dueDate, l , collaborators )
case let .adding (.collaborator(c)): Self(title, state, dueDate, location, collaborators + [c] )
case let .removing(.collaborator(c)): Self(title, state, dueDate, location, collaborators.filter{$0.id != c.id})
}
}
}
Now this is possible:
let alice = Collaborator(name: "Alice")
let bob = Collaborator(name: "Bob")
let berlin = Location.coordinate(Coordinate(latitude: 52.520008, longitude: 13.404954))
let buyChips = TodoItem(title: "Buy chips")
.alter(by:
.setting(.state(to: .inProgress)),
.setting(.dueDate(to: .date(tomorrowNoon))),
.adding (.collaborator(alice)),
.adding (.collaborator(bob)),
.setting(.location(to: berlin))
You will find a Xcode playground on GitLab. If you don't have access to Xcode you'll find an online playground on SwiftFiddle.
The following example is an implementation of Conway's Game of Life. This has significance as it proves the Turing completeness of this coding paradigm.
struct Life:Codable {
enum Change {
case process
case pause, unpause
case set (_Set); enum _Set {
case cells([Cell])
}
}
let paused : Bool
let step : Int
let size : Int
let stateForCoordiantes: [Life.Cell.Coordinate : Life.Cell.State]
init(coordinates: [Cell.Coordinate]) {
self.init(cells:coordinates.map { Cell(coordinate:$0) })
}
init(cells: [Cell]) {
self.init(0,cells,false, 0)
}
func alter(_ cs: [Change] ) -> Self { cs.reduce(self) { $0.alter($1) } }
func alter(_ cs: Change...) -> Self { cs.reduce(self) { $0.alter($1) } }
func alter(_ c : Change ) -> Self {
if paused && !unpausing(c) { return self }
switch c {
// |step|,|<----------------------- cells --------------------->|,paused,|<------- size -------->|
case let .set(.cells(cells)): return .init(step+1,cells ,paused,stateForCoordiantes.count)
case .pause : return .init(step+1,stateForCoordiantes.map{ Cell(coordinate:$0,state:$1) }, true,stateForCoordiantes.count)
case .unpause : return .init(step+1,stateForCoordiantes.map{ Cell(coordinate:$0,state:$1) }, false,stateForCoordiantes.count)
case .process : return alter(.set(.cells(applyRules())))
}
}
}
extension Life {
struct Cell: Hashable, Equatable, Codable {
init(coordinate: Coordinate, state:State = .alive) {
self.coordinate = coordinate
self.state = state
}
struct Coordinate: Equatable, Codable {
let x, y: Int
}
enum State: Hashable, Codable {
case alive, dead
}
let coordinate: Coordinate
let state : State
}
}
private extension Life {
init(_ step: Int,_ cells: [Cell] = [],_ paused:Bool,_ size:Int) {
self.step = step
self.paused = paused
self.stateForCoordiantes = cells.reduce([:]) { var a = $0; a[$1.coordinate] = $1.state; return a }
self.size = size
}
func applyRules() -> [Life.Cell] {
Array(Set(self.stateForCoordiantes.map { k,v in Cell(coordinate: k, state: v)}.flatMap { neighbors(for:$0) }))
.compactMap {
let neigbours = aliveNeighbors(for: $0)
let isAlive:Bool
switch (neigbours.count, $0.state) {
case (0...1, .alive): isAlive = false
case (2...3, .alive): isAlive = true
case (4...8, _ ): isAlive = false
case (3, .dead): isAlive = true
case (_, _ ): isAlive = false
}
return isAlive ? .init(coordinate: .init(x: $0.coordinate.x, y: $0.coordinate.y), state:.alive) : nil
}
}
func neighbors(for cell: Life.Cell) -> [Life.Cell] {
aliveNeighbors(for: cell) + deadNeighbors(for: cell)
}
func aliveNeighbors(for cell: Life.Cell) -> [Life.Cell] {
allNeigbourCoordinates(cell).map {
Cell(coordinate: $0, state: stateForCoordiantes[$0] ?? .dead)
}.filter { $0.state == .alive }
}
func allNeigbourCoordinates(_ cell: Life.Cell) -> [Life.Cell.Coordinate] {
[
.init(x:cell.coordinate.x-1, y:cell.coordinate.y-1),
.init(x:cell.coordinate.x-1, y:cell.coordinate.y ),
.init(x:cell.coordinate.x-1, y:cell.coordinate.y+1),
.init(x:cell.coordinate.x , y:cell.coordinate.y-1),
.init(x:cell.coordinate.x , y:cell.coordinate.y+1),
.init(x:cell.coordinate.x+1, y:cell.coordinate.y-1),
.init(x:cell.coordinate.x+1, y:cell.coordinate.y ),
.init(x:cell.coordinate.x+1, y:cell.coordinate.y+1),
]
}
func deadNeighbors(for cell: Life.Cell) -> [Life.Cell] {
Set(allNeigbourCoordinates(cell))
.subtracting(aliveNeighbors(for:cell).map{ $0.coordinate })
.map { Cell(coordinate:$0,state:.dead) }
}
}
private func unpausing(_ c:Life.Change) -> Bool {
switch c {
case .unpause: return true
default : return false
}
}
extension Life.Cell.Coordinate: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
You will find the project including a macOS UI on GitLab.
The next example is "Snake", which the older among us will most likely know from old Nokia phones.
struct Snake {
enum Change {
case move(Move); enum Move {
case forward
case right
case left
}
case grow
}
enum Facing {
case north
case east
case south
case west
}
let head : Coordinate
let tail : [Coordinate]
let facing: Facing
var body : [Coordinate] { [head] + tail }
init(head:Coordinate) {
self.init(head, [], .north)
}
func alter(_ c:Change) -> Self {
switch c {
case let .move(d): move(d)
case .grow : grow()
}
}
}
private extension Snake {
init(_ h:Coordinate,_ t:[Coordinate],_ f:Facing) { head = h; tail = t; facing = f }
func move(_ move:Change.Move) -> Self {
var newTail: [Coordinate] { Array(body.prefix(tail.count)) }
switch (facing, move) { // |<-------------- head -------------->| tail |facing|
case (.north,.forward): return Self(Coordinate(x:head.x ,y:head.y - 1),newTail,.north)
case (.east ,.forward): return Self(Coordinate(x:head.x + 1,y:head.y ),newTail,.east )
case (.south,.forward): return Self(Coordinate(x:head.x ,y:head.y + 1),newTail,.south)
case (.west ,.forward): return Self(Coordinate(x:head.x - 1,y:head.y ),newTail,.west )
case (.north, .left): return Self(Coordinate(x:head.x - 1,y:head.y ),newTail,.west )
case (.east , .left): return Self(Coordinate(x:head.x ,y:head.y - 1),newTail,.north)
case (.south, .left): return Self(Coordinate(x:head.x + 1,y:head.y ),newTail,.east )
case (.west , .left): return Self(Coordinate(x:head.x ,y:head.y + 1),newTail,.south)
case (.north, .right): return Self(Coordinate(x:head.x + 1,y:head.y ),newTail,.east )
case (.east , .right): return Self(Coordinate(x:head.x ,y:head.y + 1),newTail,.south)
case (.south, .right): return Self(Coordinate(x:head.x - 1,y:head.y ),newTail,.west )
case (.west , .right): return Self(Coordinate(x:head.x ,y:head.y - 1),newTail,.north)
}
}
func grow() -> Self {
// |head|<----------------- tail ----------------->|facing|
Self(head,!tail.isEmpty ? tail+[tail.last!] : [head],facing)
}
}
Snake
's Change
DSL encodes the commands
.move(.forward)
.move(.right)
.move(.left)
.grow
In the move
method all combinations of a direction command and the currently facing compass directions are mapped to new Snake instances with new head, new tail and the new compass direction.
grow
method grows the snake by adding the last tail element.
You will find the code on GitLab.
So far I have shown you some model types that are coded in Declarative Domain Coding. But I want to point out, that whole apps can be coded this way. A general purpose architecture I have implemented several times is centred around UseCases, an idea taken from Robert C. Martin's "Clean Architecture" I describe it in detail in this post, including BBD-style testing.
Now the question why I walked you through all of this: Is this indeed a new programming paradigm or did I reinvent the wheel? If reinvented: what elements in what languages would be used to achieve something similar?
Thanks for your patience.