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:
- Data Export: Easily save data as JSON/CSV files.
- 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.
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, andOutput
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:
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
:
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:
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:
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:
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:
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:
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:
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:
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
:
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:
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:
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
:
@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:
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.