This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We introduce a concept called structural programming and take the first steps toward an implementation.

00:06 Today we'll be writing a structural programming library. Structural programming refers to the idea of writing programs on top of the structures of data types. Features in Swift that use structural programming are Codable, which allows us to write serialization based on the structure of a type; Mirror, which we use to dynamically introspect values; and most recently added, macros.

00:36 Writing a macro means a lot of typing and diving deep into the syntax tree. This only allows us to write certain kinds of programs on the structures of enums and structs. The library we want to write is much simpler; we'll convert a struct into a structural representation of its properties, and then we'll write algorithms on that structural representation.

Structure of a Struct

01:18 Let's first create a sample model — a Book struct with two properties:

import Foundation

struct Book {
    var title: String
    var published: Date
}

01:30 In another file, we start writing our library with types that can represent the structure of Book. The first part of the library is a Struct type, which can hold a struct's name and some generic value for its properties:

import Foundation

struct Struct<Properties> {
    var name: String
    var properties: Properties
}

01:48 Concretely, the Properties parameter will be a list of Property values:

struct Property<Value> {
    var name: String
    var value: Value
}

02:08 The structure of Book can be represented by a Struct with the name "Book" and, for each of its properties, a Property with this name as a string. What we're missing is a way to combine the properties into one type. Modeling this as an array isn't an option, because then the Value parameter of all properties would have to be the same type.

02:40 Instead, we'll use a linked list to create a type-safe representation of differently typed properties:

struct List<Head, Tail> {
    var head: Head
    var tail: Tail
}

02:56 We're now almost ready to define the structure of Book. We add a type alias called Structure to the Book struct, and we assign a Struct with a list of properties. The list's head is a Property<String>. The tail is another list, whose head is a Property<Date>, and we don't yet know what the tail of this nested list should be, so let's just use Int as a placeholder type:

struct Book {
    var title: String
    var published: Date

    typealias Structure = Struct<List<Property<String>, List<Property<Date>, Int>>>
}

03:38 Instead of Int, we need something to indicate that there's nothing there — a sentinel value. For this, we write an Empty type, which will solely be used to represent the end of a list:

struct Empty { }
struct Book {
    var title: String
    var published: Date

    typealias Structure = Struct<List<Property<String>, List<Property<Date>, Empty>>>
}

03:58 Next, we need a way to convert a Book value into this structural representation and a way to create a Book from that representation. We add a computed property to convert a value to a Structure. Because Swift already knows the structure is a Struct, we can write .init, and it automatically completes the correct initializer for us. We pass in "Book" for the struct's name. For the properties, we write .init again to let the compiler add stubs for the list's head and tail. The list's head should hold the book's title property, which we initialize with the "title" name and the book's title as the value. The tail is another list, with the published property as its head and an Empty value as its tail:

struct Book {
    var title: String
    var published: Date

    typealias Structure = Struct<List<Property<String>, List<Property<Date>, Empty>>>

    var to: Structure {
        .init(name: "Book", properties: .init(head: .init(name: "title", value: title), tail: .init(head: .init(name: "published", value: published), tail: .init())))
    }
}

05:41 It's a bit of a hassle to type out this structure, but later on, we'll write a macro to automatically generate the Structure type alias, the to property, and the from function we need to create a Book value from a structural representation:

struct Book {
    // ...

    static func from(_ s: Structure) -> Self {

    }
}

06:05 Inside from, we want to construct a Book and pull values for its properties out of the s structure. The title value can be found in the property list's head, and for the publish date value, we go into the list nested in the tail, and we take the value of the nested list's head:

struct Book {
    var title: String
    var published: Date

    typealias Structure = Struct<List<Property<String>, List<Property<Date>, Empty>>>

    var to: Structure {
        .init(name: "Book", properties: .init(head: .init(name: "title", value: title), tail: .init(head: .init(name: "published", value: published), tail: .init())))
    }

    static func from(_ s: Structure) -> Self {
        .init(title: s.properties.head.value, published: s.properties.tail.head.value)
    }
}

06:42 If there'd be a third property, we'd read from s.properties.tail.tail.head.value — we just keep appending .tail to go deeper into the linked list.

Displaying Structures

06:56 Now we can write a generic algorithm on the structure — for example, one that converts a Struct into a SwiftUI View. This could be useful as a debugging tool that lets us display a struct value in a debug console in our app. By writing the algorithm for the structural representation, we can make this algorithm work for Book values, but also for any other structure.

07:36 In a new file, we import SwiftUI, and we can create a protocol called ToView. Types conforming to this protocol need to define a view property that returns some view:

import SwiftUI

protocol ToView {
    associatedtype V: View
    var view: V { get }
}

07:59 This protocol is literally the definition of View, but with different names, but by using a custom protocol, we get to write our own conformances. For example, Empty can conform by just returning an EmptyView:

extension Empty: ToView {
    var view: some View {
        EmptyView()
    }
}

08:29 We can conform Property as long as its Value type also conforms to ToView. The view it returns can be a LabeledContent with the property's name as the label and its value's view as the content:

extension Property: ToView where Value: ToView {
    var view: some View {
        LabeledContent(name) {
            value.view
        }
    }
}

09:11 List conditionally conforms to ToView if both its Head and Tail types conform. For its view, we could use a VStack to lay out the head and tail views, but it'll be more flexible if we just return a group of views and let the caller of the view property decide what their container should be:

extension List: ToView where Head: ToView, Tail: ToView {
    var view: some View {
        head.view
        tail.view
    }
}

To return the views separately, we need to mark the view property as being a view builder. And by doing this in the protocol, we give every conforming type this capability:

protocol ToView {
    associatedtype V: View
    @ViewBuilder var view: V { get }
}

10:04 Then there's only Struct left. To construct its view, we can return a VStack with the struct's name in bold, followed by the properties:

extension Struct: ToView where Properties: ToView {
    var view: some View {
        VStack {
            Text(name).bold()
            properties.view
        }
    }
}

Displaying Book Struct

10:30 In ContentView, we create a sample Book value, and we try to display its structure:

struct ContentView: View {
    var book = Book(title: "Thinking in SwiftUI", published: .now)

    var body: some View {
        book.to.view
            .padding()
    }
}

10:51 This doesn't yet compile and the warnings are clear: the ToView conformance of Property dictates that we conform both Date and String to ToView. So, we go back to our ToView file and add conformances for the primitive types. For String, we can return a Text view containing the string itself:

extension String: ToView {
    var view: some View {
        Text(self)
    }
}

11:38 And for Date, we can use the Text initializer that takes a Date and a format:

extension Date: ToView {
    var view: some View {
        Text(self, format: .dateTime)
    }
}

11:58 Now our code compiles, and when we run it, we see a view describing our Book value and listing the name of the struct and the name and value of each property:

12:13 We've now created a rather complicated way to display this tiny bit of information. But next time, we'll work on a macro that generates the structural representation of a Struct. That means we only have to add another property to Book, and the displayed structure will automatically change to include that new field.

Resources

  • Sample Code

    Written in Swift 5.9

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

57 Episodes · 20h06min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes