Skip to content

Instantly share code, notes, and snippets.

@haikusw
Forked from noahsark769/KeyPathUpdatable.swift
Last active February 7, 2020 05:45
Show Gist options
  • Save haikusw/934e52e5d009207d43d37b5bd9039658 to your computer and use it in GitHub Desktop.
Save haikusw/934e52e5d009207d43d37b5bd9039658 to your computer and use it in GitHub Desktop.
protocol KeyPathUpdatable {}
extension KeyPathUpdatable {
func updating<LeafType>(_ keyPath: WritableKeyPath<Self, LeafType>, to value: LeafType) -> Self {
var copy = self
copy[keyPath: keyPath] = value
return copy
}
}
struct Book: KeyPathUpdatable {
var title: String
var author: String
var isbn: Int
var publishedYear: Int
}
let book = Book(title: "Something", author: "Noah", isbn: 1, publishedYear: 2020)
let updated = book.updating(\.title, to: "Else")
print(book)
print(updated)
// playground with some alternatives and showing why the above is preferable
// and also showing and addition from the swift forum that is cool.
protocol KeyPathUpdatable {}
extension KeyPathUpdatable {
func updating<LeafType>(_ keyPath: WritableKeyPath<Self, LeafType>, to value: LeafType) -> Self {
var copy = self
copy[keyPath: keyPath] = value
return copy
}
// "one more thing"
// from swift forum on the topic:
// https://forums.swift.org/t/nice-way-of-copying-an-immutable-value-while-changing-only-a-few-of-its-many-properties
static func .= <V>(lhs: Self, rhs: (WritableKeyPath<Self, V>, V)) -> Self {
return lhs.updating(rhs.0, to: rhs.1)
}
}
infix operator .=: AdditionPrecedence
struct Book: KeyPathUpdatable {
var title: String
var author: String
var isbn: Int
var publishedYear: Int
}
// Without this you have to use some less pretty ways.
// One an alternative approach that is not a protocol and only works for this struct:
// "LastWay" flavors require a custom init constructor that takes an instance and optional parameters
// so you can use an optional parameter value if present or use the value in the passed in instance.
extension Book {
init(_ book: Book, title: String? = nil, author: String? = nil, isbn: Int? = nil, publishedYear: Int? = nil) {
self.init(title: title ?? book.title,
author: author ?? book.author,
isbn: isbn ?? book.isbn,
publishedYear: publishedYear ?? book.publishedYear)
}
func updating2(title: String? = nil, author: String? = nil, isbn: Int? = nil, publishedYear: Int? = nil) -> Book {
Book(self, title: title, author: author, isbn: isbn, publishedYear: publishedYear)
}
}
let book = Book(title: "Something", author: "Noah", isbn: 1, publishedYear: 2020)
let updated = book.updating(\.title, to: "Else")
// How you'd do this without theKeyPathUpdateble:
let updatedOldWay = Book(title: "Else", author: book.author, isbn: book.isbn, publishedYear: book.publishedYear)
let updatedAlternateWay = { () -> Book in var temp = book; temp.title = "Else"; return temp }()
// the "LastWay" extension makes it possible for this particular struct where you added this code,
// but not generically like keypath:
let updatedLastWay = Book(book, title: "Else")
let updatedLastLastWay = book.updating2(title: "Else")
let updatedOneMoreThing = book .= (\.title, "Else!")
.= (\.publishedYear, 2021)
print(book)
print(updated)
print(updatedOldWay)
print(updatedAlternateWay)
print(updatedLastWay)
print(updatedLastLastWay)
print(updatedOneMoreThing)
/*
Book(title: "Something", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else", author: "Noah", isbn: 1, publishedYear: 2020)
Book(title: "Else!", author: "Noah", isbn: 1, publishedYear: 2021)
*/
// nice way to update one field in a let struct:
//
// This version provides two approaches.
// One with explicit keypaths and one using direct dot notation:
// for the direct . notation version
@dynamicMemberLookup
struct Updater<Root> {
let root: Root
subscript<Value>(dynamicMember keyPath: WritableKeyPath<Root, Value>) -> (Value) -> Root {
{ [root] value in
var copy = root
copy[keyPath: keyPath] = value
return copy
}
}
}
protocol KeyPathUpdatable {
var update: Updater<Self> { get }
}
extension KeyPathUpdatable {
var update: Updater<Self> { .init(root: self) }
}
extension KeyPathUpdatable {
func updating<LeafType>(_ keyPath: WritableKeyPath<Self, LeafType>, to value: LeafType) -> Self {
var copy = self
copy[keyPath: keyPath] = value
return copy
}
}
//// adds operator, but I don't personally like adding operators so I'm leaving this commented out.
//extension KeyPathUpdatable {
// // "one more thing"
// // from swift forum on the topic:
// // https://forums.swift.org/t/nice-way-of-copying-an-immutable-value-while-changing-only-a-few-of-its-many-properties
// static func .= <V>(lhs: Self, rhs: (WritableKeyPath<Self, V>, V)) -> Self {
// return lhs.updating(rhs.0, to: rhs.1)
// }
//}
//infix operator .=: AdditionPrecedence
struct Book: KeyPathUpdatable {
var title: String
var author: String
var isbn: Int
var publishedYear: Int
}
// example uses showing different syntax
let book = Book(title: "Something", author: "Noah", isbn: 1, publishedYear: 2020)
// make a copy with an modified title
let updated = book.updating(\.title, to: "Else")
print(book)
print(updated)
// the operator example
//let updatedOneMoreThing = book .= (\.title, "Else!")
// .= (\.publishedYear, 2021)
// print(updatedOneMoreThing)
// try out the alternative using dynamic lookup instead of keypath
let bbook = book.update.title( "Alternative Book - radical!")
print(bbook)
@haikusw
Copy link
Author

haikusw commented Feb 7, 2020

testKeyPathUpdateableVariants2.swift is probably the form I'll end up putting together as a utility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment