Experience the Charm of Swift: One-Click DataFrame Export

Published on

A few months ago, I was having dinner and chatting with several developers. I was the only one focused on Swift; the others had extensive experience in Python, Java, JavaScript, and other languages. To my surprise, their opinions of Swift weren’t particularly high—they thought it had too much “syntactic sugar” and that many of its features seemed more for show than for practical use. They asked me with genuine curiosity, “If you weren’t developing for Apple platforms, would you still choose Swift?”

I replied without hesitation: Absolutely—I really do love Swift!

As my understanding of Swift has deepened, its appeal has become increasingly dazzling to me. Swift allows me to express programming ideas in a way that is clear, precise, safe, modern, and elegant. Yet, whenever I try to enumerate all its shining points, I find myself at a loss for words. That changed just a few days ago when I implemented a small feature module—barely a few hundred lines—that perfectly tied together some of the traits that attract me most to Swift. Today, let me use that code to guide you through Swift’s unique charm.

You can view the full code on Gist.

Requirements Background

In my current project, I needed to convert an array of Core Data objects into a DataFrame from Apple’s TabularData framework in order to achieve two core goals:

  1. Data Export: Easily save data as JSON/CSV files.
  2. Data Analysis: Leverage TabularData’s powerful API to efficiently perform filtering, aggregation, statistics, and more.

TabularData is Apple’s official data manipulation and analysis library—think of it as Swift’s equivalent to Pandas. It’s powerful yet often overlooked.

On the surface, this is just converting from a row-oriented to a column-oriented structure; but given that the project contains dozens of different entities, hand-writing conversion code for each one would be tedious and error-prone. So, I designed a generic solution that must support:

  • Universal Dataset Export Non-invasively convert any array of objects (Core Data entities, plain structs, etc.) into a TabularData DataFrame.
  • Selective Columns & Order Control Precisely specify which properties to export, and customize the order of columns in the result.
  • Custom Column Names Assign friendly, intuitive display names to each column.
  • High-Order Mapping Provide a mapping closure per column to transform, format, or adapt raw values before exporting.
  • Conditional Mapping Built-in filtering and conditional mapping logic, dynamically switching mapping functions based on a custom filter.

Next, let’s walk through the implementation step by step, experiencing Swift’s strengths along the way.

TabularColumn — A Generic Abstraction for Columns

First, we need a generic type to accurately describe a column’s “name,” the property KeyPath, and the mapping logic:

At this initial stage, we’ll omit conditional mapping.

Swift
public struct TabularColumn<Object, Value, Output> {
    // Column name
    public let name: String
    // Corresponding property
    public let keyPath: KeyPath<Object, Value>
    // Mapping operation
    public let mapping: (Value) -> Output

    public init(
        name: String,
        keyPath: KeyPath<Object, Value>,
        mapping: @escaping (Value) -> Output)
    {
        self.name = name
        self.keyPath = keyPath
        self.mapping = mapping
    }
}

In just a few lines, this code already showcases several Swift advantages:

  • Type Safety & Strong Typing: TabularColumn has three generic parameters—Object for the entity type, Value for the raw property type, and Output for the mapped type. This type-safe conversion approach is far more reliable than implementations in dynamic languages like Python or JavaScript.
  • KeyPath: Swift’s KeyPath allows us to reference object properties in a type-safe way, which is much more elegant than using strings or reflection.

To dive deeper into advanced KeyPath usage, check out Comprehensive Guide to Mastering KeyPath in Swift.

Now let’s add a method to TabularColumn that transforms a given array of Object into a TabularData Column:

Swift
public func makeColumn(objects: [Object]) -> Column<Output> {
    let values = objects.map { object in
        let raw = object[keyPath: keyPath]
        return mapping(raw)
    }
    return Column(name: name, contents: values)
}

Using this, we can generate columns for name and age:

Swift
let students = [
  Student(name: "fat", age: 100)
  // More student data...
]
let ageColumn = TabularColumn(
    name: "student_age",
    keyPath: \Student.age,
    mapping: { Double($0) }) // Convert Int to Double
let nameColumn = TabularColumn(
    name: "student_name",
    keyPath: \Student.name,
    mapping: { $0 } // No conversion
)

let ageColumnData = ageColumn.makeColumn(objects: students)
let nameColumnData = nameColumn.makeColumn(objects: students)

where Constraint — Simplifying the Default Initializer

In the example above, even if we don’t want to transform name, we still must provide a mapping closure, or the compiler can’t infer Output. We can simplify this by adding a constrained initializer:

Swift
public init(
    name: String,
    keyPath: KeyPath<Object, Value>) where Value == Output
{
    self.init(name: name, keyPath: keyPath, mapping: { $0 })
}

With where Value == Output, the compiler knows Output, making usage more concise:

Swift
TabularColumn(name: "student_name", keyPath: \Student.name)

AnyTabularColumn — Type Erasure & Column Order Control

To control column order, we need to place different TabularColumn instances in the same array, but we’ll encounter this error:

Swift
let columns = [ageColumn, nameColumn]
// Error: Heterogeneous collection literal could only be inferred to '[Any]'

Although Object matches, the other generics differ. To retain type details, we use a static type-erasure wrapper:

Swift
public struct AnyTabularColumn<Object> {
    // AnyColumn is TabularData’s erased column type for building a DataFrame
    private let _make: ([Object]) -> AnyColumn

    public init<Value, Output>(
        _ column: TabularColumn<Object, Value, Output>)
    {
        _make = { objects in
            column.makeColumn(objects: objects).eraseToAnyColumn()
        }
    }

    public func makeColumn(objects: [Object]) -> AnyColumn {
        _make(objects)
    }
}

Now we can collect heterogeneous columns while preserving their generic logic:

Swift
let columns = [AnyTabularColumn(ageColumn), AnyTabularColumn(nameColumn)]

let ageColumnData = AnyTabularColumn(ageColumn).makeColumn(objects: students)

Protocol Extensions — Injecting Export Capability

Protocols are another Swift highlight. They can define interfaces and provide default implementations, which simplifies adding DataFrame export to any type:

Swift
public protocol DataFrameConvertible {
    static func makeDataFrame(
        objects: [Self],
        anyColumns: [AnyTabularColumn<Self>]
    ) -> DataFrame
}

extension DataFrameConvertible {
    public static func makeDataFrame(
        objects: [Self],
        anyColumns: [AnyTabularColumn<Self>]
    ) -> DataFrame {
        let columns = anyColumns.map { $0.makeColumn(objects: objects) }
        return DataFrame(columns: columns)
    }
}

Now any type can gain export capability simply by conforming:

Swift
extension Student: DataFrameConvertible {}

let columns = [AnyTabularColumn(ageColumn), AnyTabularColumn(nameColumn)]
let dataFrame = Student.makeDataFrame(objects: students, anyColumns: columns)

conditional Factory — Implementing Conditional Mapping

We also need conditional mapping so that, for example, a Student’s id is exported only when age > 50, otherwise nil. First, add two properties to TabularColumn:

Swift
public var conditionalMapping: ((Bool, Value) -> Output)?
public var filter: ((Object) -> Bool)?

Rather than a new initializer, we use a factory method for a fluent, value-type style:

Swift
public static func conditional(
    name: String,
    keyPath: KeyPath<Object, Value>,
    filter: @escaping (Object) -> Bool,
    // Branch when condition is met
    then thenMap: @escaping (Value) -> Output,
    // Branch when condition is not met
    else elseMap: @escaping (Value) -> Output
) -> Self {
    var col = Self(name: name, keyPath: keyPath, mapping: thenMap)
    col.conditionalMapping = { passed, raw in
        passed ? thenMap(raw) : elseMap(raw)
    }
    col.filter = filter
    return col
}

// Update makeColumn to handle filtering
public func makeColumn(objects: [Object]) -> Column<Output> {
    let values = objects.map { object in
        let raw = object[keyPath: keyPath]
        if let filter, let conditionalMapping {
            return conditionalMapping(filter(object), raw)
        } else {
            return mapping(raw)
        }
    }
    return Column(name: name, contents: values)
}

With this, building a conditional id column is elegant:

Swift
let idColumn = TabularColumn
                  .conditional(
                      name: "id",
                      keyPath: \Student.id,
                      filter: { $0.age > 50 },
                      then: { $0 as UUID? },
                      else: { _ in nil }
                  )

let columns = [
    AnyTabularColumn(ageColumn),
    AnyTabularColumn(nameColumn),
    AnyTabularColumn(idColumn)
]
let dataFrame = Student.makeDataFrame(objects: students, anyColumns: columns)

Result Builder — Crafting a Declarative DSL for Column Definitions

We’ve met all requirements, but building an [AnyTabularColumn] array still feels verbose. Enter Swift’s Result Builder:

Swift
@resultBuilder
public enum TabularColumnBuilder<Object> {
    public static func buildBlock(
        _ components: [AnyTabularColumn<Object>]...
    ) -> [AnyTabularColumn<Object>] {
        components.flatMap(\.self)
    }

    public static func buildExpression<Value, Output>(
        _ column: TabularColumn<Object, Value, Output>
    ) -> [AnyTabularColumn<Object>] {
        [AnyTabularColumn(column)]
    }

    public static func buildExpression(
        _ any: AnyTabularColumn<Object>
    ) -> [AnyTabularColumn<Object>] {
        [any]
    }

    public static func buildExpression(
        _ columns: [AnyTabularColumn<Object>]
    ) -> [AnyTabularColumn<Object>] {
        columns
    }
}

// Extend the protocol to use @TabularColumnBuilder
public protocol DataFrameConvertible {
    static func makeDataFrame(
        objects: [Self],
        @TabularColumnBuilder<Self> _ columns: () -> [AnyTabularColumn<Self>]
    ) -> DataFrame
}

extension DataFrameConvertible {
    public static func makeDataFrame(
        objects: [Self],
        @TabularColumnBuilder<Self> _ columns: () -> [AnyTabularColumn<Self>]
    ) -> DataFrame {
        makeDataFrame(objects: objects, anyColumns: columns())
    }
}

Here, buildExpression unifies TabularColumn, AnyTabularColumn, and [AnyTabularColumn] into the same collection, making buildBlock composition seamless.

To learn more about this powerful tool, see ViewBuilder Research: Mastering Result Builders.

Now you can define columns in a SwiftUI-like declarative syntax, achieving both clarity and elegance:

Swift
let dataFrame = Student.makeDataFrame(objects: students) {
    TabularColumn(name: "student_age", keyPath: \.age, mapping: { Double($0) })
    TabularColumn(name: "student_name", keyPath: \.name)
    TabularColumn
        .conditional(
            name: "id",
            keyPath: \.id,
            filter: { $0.age > 50 },
            then: { $0 as UUID? },
            else: { _ in nil }
        )
}

Why Swift — The Allure of Swift

For any programming language, meeting our requirements isn’t inherently difficult. But few languages manage to balance clarity, precision, safety, modernity, and elegance as comprehensively as Swift.

I believe this is exactly what draws me so deeply to Swift, and as a Swift developer, fuels my desire to see it adopted across more platforms and scenarios. I hope this small example shows you that Swift isn’t just a language with “too much syntactic sugar,” but a modern tool that truly enhances both the development experience and code quality.

Swift certainly isn’t the most perfect language, but it is indeed the one I have always cherished.

Weekly Swift & SwiftUI highlights!