I just finished reading the article linked in this Figure.ink post. It's about Haskell, but the principles apply to Swift as well.
The article's called "Parse, don't validate" and in summary, the author makes a distinction between "parsing" which preserves the "knowledge gained by a function about its input data" versus "validation" which performs the same checks, e.g. !array.isEmpty
, but doesn't "preserve the knowledge" in any way.
Your pitch is exactly in line with the "parse, don't validate" idea, and I'm a big fan of making this kind of thing easier in Swift.
I'd like to suggest that adding syntactic sugar in the form of an annotation as you suggest may not be the right approach here. Consider that we can already get kinda close to the desired behavior today by using @dynamicMemberLookup
. Let me explain with a code sample.
let nea = NonEmptyArray(validating: [1, 2, 3])!
nea.first // uses efficient NonEmpty implenentation
nea.last // uses Array implementation via dynamicMember subscript
nea.append(4) // unsupported today, we to implement this manually
// Implementation:
@dynamicMemberLookup
struct NonEmptyArray<Element> {
private(set) var array: [Element]
init?(validating array: [Element]) {
if array.isEmpty {
return nil
}
self.array = array
}
// Allows us to forward any undefined properties to the underlying Array.
// Only supports properties, not functions :(
subscript<T>(dynamicMember keyPath: KeyPath<[Element], T>) -> T {
get {
array[keyPath: keyPath]
}
}
var first: Element {
array[0]
}
}
You could implement this generically over Collection, but I stuck with Array for simplicity.
The really cool thing is that we can use keyPaths with dynamicMemberLookup. This makes our NonEmptyArray act just like an Array from that perspective.
Now consider if we could do the same thing with functions, e.g. implement @dynamicCallable
by using keyPaths somehow! We could allow most function calls like append
to pass-through to the Array implementation while shadowing functions like remove
with safer, semantics-preserving implementations. Then our NonEmptyArray would behave just like an Array, but with stronger requirements, practically for free.
Side note: it's possible that "dynamicCallable with KeyPaths" isn't really the right way to implement this. Maybe giving dynamicMemberLookup the ability to refer to methods would be better. Or maybe a new annotation will be easier to implement in the compiler, e.g. @forwardCalls(to: self.array). I use dynamicCallable here just to provide a point of reference.
If this were possible, we should even be able to declare NonEmptyArray: Collection, Sequence, etc.
and gain all the nice language sugar that comes with that without having to implement every single requirement — we'd get them for free with @dynamicCallable
!
The only thing missing here compared to your pitch is that there wouldn't be any special syntactic sugar like NonEmpty [Int]
— It would just be NonEmpty<[Int]>
. Still, if sugar like this were appropriate, @propertyWrapper
might be a good fit to give us that.
Also, considering where Compile-Time Constant Expressions might take us in the future, we may even be able to promote some of those run-time guarantees into compile-time guarantees.
TL;DR — If we can make @dynamicCallable
support static function passthrough the way @dynamicMemberLookup
does, when combined with existing compiler sugar, I think we'd be able to implement 90% to what's being pitched here without adding another new compiler sugar . Either way, I would love to see these kind of wrapper types become way easier to write and hopefully way more common.