-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Skip evaluation of a Modifying block iff it doesn't do anything #9
Comments
Current StateSo the Modifying construct is of this simplified form: struct Modifying<Content: Modification> {
typealias ModificationBody = (SelectedRange) -> Content
let range: SelectedRange
let modification: ModificationBody
init(
_ range: SelectedRange,
@ModificationBuilder body: @escaping ModificationBody
) { ... }
}
protocol Modification: Expression
where Evaluation == ChangeInLength, Failure == BufferAccessFailure {
// boils down to exposing:
// func evaluate(in buffer: Buffer) -> Result<ChangeInLength, BufferAccessFailure>
} The result builder doesn't permit mixing, so we have builder code paths for deletion and insertion. Take insertion for example. The relevant part of the builder is this (ignoring all the rest): @resultBuilder
struct ModificationBuilder {
static func buildArray(_ components: [Insert]) -> Insert {
return Insert(SortedArray(
unsorted: components.map(\.insertions).joined(),
areInIncreasingOrder: TextInsertion.arePositionedInIncreasingOrder
))
}
} No matter if 0, 1, 2, 100 insertions -- they are all concatenated into 1 |
Approach 1: Awareness of Emptiness
protocol Modification: Expression
where Evaluation == ChangeInLength, Failure == BufferAccessFailure {
+ var isEmpty: Bool { get }
} That makes sense from a collection perspective, which But "Is this modification emtpy?" is an odd question. |
Approach 2: Either something or nothingMake the array case a special case and not return Return either an insert or nothing. For "nothing", we have @resultBuilder
struct ModificationBuilder {
- static func buildArray(_ components: [Insert]) -> Insert {
- return Insert(SortedArray(
+ static func buildArray(_ components: [Insert]) -> Either<Insert, Identity> {
+ let insertions = SortedArray(
unsorted: components.map(\.insertions).joined(),
areInIncreasingOrder: TextInsertion.arePositionedInIncreasingOrder
- ))
+ )
+ guard !insertions.isEmpty else { return .right(Identity()) }
+ return .left(Insert(insertions)
}
} This could express a no-op with the It's less awkward to ask "do we have a modification or do we do nothing?" than "is this modification empty?" Consequently, this should be added as a test and also not trigger the text view checks: Modifying(selectedRange) { Identity() } The if modification is Identity { return } But actually, we would have // return a ChangeInLength.empty from the Modifying.evaluate function as a successful no-op
switch theModification {
case _ as Identity: return .success(.empty)
case .right(_) as Either<Insert, Identity>: return .success(.empty)
case .left(_) as Either<Identity, Insert>: return .success(.empty)
case .right(_) as Either<Delete, Identity>: return .success(.empty)
case .left(_) as Either<Identity, Delete>: return .success(.empty)
default: break
} Variation with protocolsWe can use an existential type, extension Either where Left: Modification, Right: Modification {
var modification: any Modification {
return switch self {
case .left(let l): l
case .right(let r): r
}
}
} Then we could ask But how do we get from an opaque type (the case let either as Either<Insert, Identity> where either.modification is Identity: return .success(.empty)
case let either as Either<Identity, Insert> where either.modification is Identity: return .success(.empty)
case let either as Either<Delete, Identity> where either.modification is Identity: return .success(.empty)
case let either as Either<Identity, Delete> where either.modification is Identity: return .success(.empty) Meh. That only moved the "do I have to write .left or .right" decision into the computed property. We would have to use a protocol abstraction: protocol ModificationContainer {
var modification: any Modification { get }
} Then using this base type would do the trick to simplify cases: switch theModification {
case _ as Identity: return .success(.empty)
case let container as ModificationContainer where container.modification is Identity: return .success(.empty)
default: break
} Down to two branches here, and two branches in the |
WIP Approach 3: Protocols, but earlierIf we bite the bullet and use a protocol to abstract things away, we might as well use protocols directly: protocol Noop { }
extension Identity: Noop { } Then we could check whether We would still need an We can't have So I guess it'll end up with |
WIP Approach 4: Side effects are the problemThe original question assumes that the approach is sound, but that we need to handle this one edge case of Maybe the problem is not "how do I inspect the result builder's result for non-emptiness", but rather "can I not perform side effects?" Because the existence of side effects in the current implementation is the driving force to skip the procedure completely if the modification isn't actually modifying anything. |
The
Modifying(<Range>) { <Block> }
construct always evaluates even if the block is empty.With TextKit integration, this means
NSTextView.shouldChangeText(in:replacementString:)
anddidChangeText()
are being run to guard against unwanted changesWith syntax highlighting in the text storage, you may end up processing the text for what's essentially a no-op.
How to test
To get an empty block, use a for-loop to trigger the
buildArray
path of the result builder, but without any actual iterations:I'm not sure whether we can figure out at all whether a result builder produces nothing (i.e. empty array).
Complete test case
This test fails with a thrown error at
try buffer.evaluate
because the text view doesn't permit changes in the range.This should not be a problem, because the range is not actually changed.
The text was updated successfully, but these errors were encountered: