declarative domain pattern discovery
I’d like to introduce you to Declarative Swift – a coding style that allows us to efficiently and effectively create domain-driven code that is easy to verify. This type of writing code has proven to be faster than traditional coding, while it provides fewer places for bugs to hide.
Let’s start with a simple example: the todo list.
The following code block contains a datatype for todo items and todo lists:
struct TodoItem:Identifiable
enum Change
case title (to:String)
case due (to:TodoDate)
case location(to:Location)
case finish
case unfinish
// members
let id : UUID
let title : String
let completed: Bool
let created : Date
let due : TodoDate
let location : Location
// initialisers
init(title:String) self.init(UUID(),title,false,.unknown, Date(),.unknown)
private
init(_ i:UUID,_ t:String,_ c:Bool,_ d:TodoDate,_ cd:Date,_ l:Location) id=i; title=t; completed=c; due=d; created=cd; location=l
func alter(_ c:Change...) -> Self c.reduce(self) $0.alter($1)
private
func alter(_ c:Change ) -> Self
switch c
case let .title(to:t): return Self(id,t ,completed,due,created,location)
case .finish : return Self(id,title,true ,due,created,location)
case .unfinish : return Self(id,title,false ,due,created,location)
case let .location(to:l): return Self(id,title,false ,due,created,l )
case let .due(to:.timeSpan(.start(.from(b),.to(e)))):
return e > b //check for timespans if dates are in correct order
? Self(id,title,completed,.timeSpan(.start(.from(b),.to(e))),created,location)
: self
case let .due(to:d): return Self(id,title,completed,d ,created,location)
struct TodoList: Identifiable
enum Change
case add (Add ); enum Add case item(TodoItem)
case remove(Remove); enum Remove case item(TodoItem)
case update(Update); enum Update case item(TodoItem)
// members
let id : UUID
let items: [TodoItem]
// initializers
init() self.init(UUID(),[])
private
init(_ i:UUID,_ its:[TodoItem]) id = i; items = its
func alter(_ c:Change...) -> Self c.reduce(self) $0.alter($1)
private
func alter(_ c:Change ) -> Self
switch c
case let .add (.item(i)): return Self(id,items + [i])
case let .remove(.item(i)): return Self(id,items - [i])
case let .update(.item(i)): return
items.contains(where: $0.id == i.id )
? self
.alter(
.remove(.item(i)),
.add (.item(i)))
: self
all member properties are defined let
, Hence, these datatypes are actually immutable. To reflect the changes, we need to regenerate a new object from the previous state and present the changes. here it is done by calling alter
Each call to the method changes results in a new object – with one or more change values.
In the following example, we create a TodoItem
And then change the title to something more pressing:
let t0 = TodoItem(title:"Get Coffee")
let t1 = t0.alter(.title("Get Coffee — ASAP"))
The next call will complete one item:
let t1 = t0.alter(.finish)
While this will make it incomplete:
let t1 = t0.alter(.unfinish)
It’s trivial to change the title and eliminate/finish an item. Let’s look at some more interesting examples.
The following example code contains the declaration for TodoDate
,
It might be unknown
One Date
or a TimeSpan
,
a TimeSpan
Can be defined as a start and end or duration.
enum TodoDate {
case unknown
case date(Date)
case timeSpan(TimeSpan)
var timeSpan:TimeSpan? { switch self { case let .timeSpan