-
Star
(112)
You must be signed in to star a gist -
Fork
(8)
You must be signed in to fork a gist
-
-
Save nicklockwood/9b4aac87e7f88c80e932ba3c843252df to your computer and use it in GitHub Desktop.
| /// Withable is a simple protocol to make constructing | |
| /// and modifying objects with multiple properties | |
| /// more pleasant (functional, chainable, point-free) | |
| public protocol Withable { | |
| init() | |
| } | |
| public extension Withable { | |
| /// Construct a new instance, setting an arbitrary subset of properties | |
| init(with config: (inout Self) -> Void) { | |
| self.init() | |
| config(&self) | |
| } | |
| /// Create a copy, overriding an arbitrary subset of properties | |
| func with(_ config: (inout Self) -> Void) -> Self { | |
| var copy = self | |
| config(©) | |
| return copy | |
| } | |
| } | |
| //--------------------------------------------------- | |
| // Example struct | |
| struct Foo: Withable { | |
| var bar: Int = 0 | |
| var baz: Bool = false | |
| } | |
| // Construct a foo, setting an arbitrary subset of properties | |
| let foo = Foo { $0.bar = 5 } | |
| // Make a copy of foo, overriding an arbitrary subset of properties | |
| let foo2 = foo.with { $0.bar = 7; $0.baz = true } | |
| // Test | |
| print("\(foo.bar), \(foo2.bar)") // 5, 7 |
Why the protocol? Since the struct uses vars you can simply write a couple of global functions that mutate using WritableKeyPaths.
This is one of the things Lenses can do.
I find a nice addition to this to be
public extension Optional where Wrapped: Withable {
func with(_ config: (inout Wrapped) -> Void) -> Wrapped {
switch self {
case .none: return Wrapped.init(with: config)
case .some(let object): return object.with(config)
}
}
}
with this, I can modify or initialize an optional value in the same code path; good for an interface that updates the value potentially many times, but may start with it empty.
I'm using this little global bad boy:
/// Returns mutated copy of value
public func modify<A>(_ value: A, _ f: (inout A) throws -> Void) rethrows -> A {
var copy = value
try f(©)
return copy
}Alongside with curried version:
public enum Fn {
/// Curried version of modify function
static func modify<A>(_ f: @escaping (inout A) -> Void) -> (A) -> A {
return { Fx.modify($0, f) }
}
}So instead of [].map { modify($0) { ... } }
You can write [].map(Fn.modify { ... })
Basically it converts (inout A) -> Void to (A) -> A function
Nice Nick, but now you've got me wondering what interesting project you're using this in... :)
it seems like builder pattern?
@dcutting as-yet unannounced. I did try using it in SwiftFormat but it didn't play nice with Xcode 9.2 for some reason (I don't want to drop support for Sierra just yet).
@wsof401 not really. The builder pattern usually applies to class-based languages like Objective-C or Java, where you use a mutable object as a factory for an immutable one. All of that is redundant in Swift where you have compound value types.
What this allows is so-called "point-free" coding style, where you can set or change properties of structs without creating intermediate variables.
Awesome, but I can't quite get why there is an empty
init()inWithableprotocol?Ah, nvm, it's used
self.init()here