I have had an idea for a while now kicking around in the back of my head, and I could use some help fleshing it out into a workable solution for Swift. (Note: I am using Array below to simplify, but this would all really apply to Sequence/Collection, etc…). The syntax is mostly placeholder syntax, which is what I need help with.
Motivation
There is always a tradeoff between safety and performance when writing reusable code. Low level code either has to make assumptions about the values being passed to it (e.g. a value is non-zero, or an array is non-empty) or it has to check those assumptions. When these functions call one another, but could also be called from the outside, you often find them repeatedly performing the same check.
For example, if I have code which can only return a meaningful value if an array passed to it is non-empty (e.g. first), then it must either:
-
crash if the array is empty
-
return an optional
-
throw an error
Each of these options has an associated cost which is compounded when you call lower level functions which have to make the same choice. Each level has to deal with the optional, or the error, or has a chance of crashing.
It would be nice to be able to make that check only once, and then to tell the lower-level code that you guarantee the check has been made (and then to have the compiler enforce this guarantee). In short, it would be nice to use the type system for this, but we can’t, because the check can only be made at runtime.
Proposed solution
While the check itself can only be made at runtime, there is still a lot we can guarantee at compile time. If the check itself returns a type with the guarantee baked in, then we can just use it like we would any other type: As the type of a variable, parameter, return type, etc….
if let myNonEmptyArray = myArray as? NonEmpty Array<T> {
///We know that the array is not empty and can use it accordingly
}
Once we have that, we can make functions that only take arrays which are known not to be empty:
func foo(_ array: NonEmpty Array<T>) {
///Do something with an array guaranteed not to be empty
}
A non-empty array would still have all the methods, etc… available to any Array (i.e. it is still an Array), but we could also add conditional methods that take advantage of the additional knowledge. For example, first and last could return a non-optional result, avoiding repeated unwrapping:
extension NonEmpty Array {
var first:Element {
return self[0] ///We don't have to worry about a crash or optionals here
}
var isEmpty:Bool {
return false ///We can optimize here
}
}
I believe that these modifiers should be sub-typeable in the same way protocols are. For example, you might have a NonZero Int
, and a Positive Int
, where Positive also guarantees non-zero-ness.
A couple of other use-cases for this:
• Guaranteeing a floating point value is an actual number which isn’t NaN, inf, etc…
• Guaranteeing a non-zero or positive number
• Guaranteeing a string wasn’t created by the user (or has been appropriately escaped)
• Helping to enforce read/write access rules (especially across Actor boundaries)
Basically, anywhere where you would repeatedly need to make the same runtime check, or you are dealing with optionals which only happen in an edge case. I believe this could actually lead to both safer code (replacing areas where the check was only enforced by documentation) and faster low-level code (avoiding checks which have already been made by higher-level code).
What I Need Help With
The syntax.
How are these defined?
The main parts of the definition are:
-
The Name
-
The check (e.g.
!array.isEmpty
) -
(Optionally) A Sub-modifier (e.g. NonZero and Positive)
What does it look like?
The check would then be available by using is
and as?
if myVar is Modifier Type {
//The check succeeded
} else {
//The check failed
}
if let checkedVar = uncheckedVar as? Modifier Type {
//We can use the modified type here
}
How to spell them?
I am using ModifierName TypeName
, but if that doesn’t work for some reason we could do something else. We need to find a syntax which we know isn't ambiguous.
Any thoughts are appreciated...