Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Created January 28, 2019 12:06
Show Gist options
  • Star 113 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.
Save nicklockwood/9b4aac87e7f88c80e932ba3c843252df to your computer and use it in GitHub Desktop.
Withable.swift
/// 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(&copy)
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
@elfenlaid
Copy link

elfenlaid commented Jan 29, 2019

Awesome, but I can't quite get why there is an empty init() in Withable protocol?
Ah, nvm, it's used self.init() here

@alessandro-martin
Copy link

Why the protocol? Since the struct uses vars you can simply write a couple of global functions that mutate using WritableKeyPaths.

@tncowart
Copy link

tncowart commented Feb 8, 2019

This is one of the things Lenses can do.

@joelk
Copy link

joelk commented Feb 8, 2019

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.

@P0ed
Copy link

P0ed commented Feb 11, 2019

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(&copy)
	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 { ... })

@P0ed
Copy link

P0ed commented Feb 11, 2019

Basically it converts (inout A) -> Void to (A) -> A function

@dcutting
Copy link

Nice Nick, but now you've got me wondering what interesting project you're using this in... :)

@wsof401
Copy link

wsof401 commented Feb 13, 2019

it seems like builder pattern?

@nicklockwood
Copy link
Author

@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).

@nicklockwood
Copy link
Author

@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.

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