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
Typesafe predicates #26
Comments
@Revolucent One thing to clarify about the 2nd requirement. If we're generating typesafe attribute descriptions, we wouldn't want to inadvertently fallback to a "loose" comparison because a caller used an invalid type. So, if we want to opt for type safety by default and with the same operators, I don't see a way we can allow the loose comparisons to continue working in that scenario.The only impact on existing code is that invalid comparisons currently generating runtime errors become compile time errors, which I think is a plus. |
@patgoley That's not quite what I meant by "fallback". Either you have type safety or you don't. Instead, I meant that we should allow the existing non-typesafe comparisons to work as an option. In other words, type safety should be an opt-out default, but once you've opted in (for a particular model attribute), you're in. Right now, the comparison operators are implemented in terms of As for the numeric types, I think I was overzealous. A better way to do it would just be to support the numeric types supported by Core Data ( |
@Revolucent Makes sense about the fallback option, I'll see what I can do. Just to be clear, do we want to be able to opt in per attribute? I think it might be a bit confusing to mix typesafe and non typesafe attributes on a model, possibly leading you to think you're being safe when you're actually not. I may have actually come up with a solution to handle any numeric type without unnecessary casting. By creating a single NumericAttribute class with ValueType = NSNumber, swift will intelligently coerce many primitive types (Int, UInt, CGFloat, Double) into NSNumber for comparison. For other types that won't coerce, I've made a protocol NumericValueType which allows a primitive to create an NSNumber version of itself using the proper constructor, which is then used for comparison. This allows for any numeric type to work and will correctly compare values of different types without requiring a manual cast or precision loss. |
For the fallback stuff, what I meant was that implementers could fallback manually by editing the attribute proxy, or they could specify a But perhaps I'm being overcautious. I'm trying to think of a reason we'd need to preserve loose typing. I'm not married to it, and I can tell you don't have much enthusiasm for it. Why don't we just skip loose typing and go all-in with type safety? We can release this with CDQI 4.0. |
I have some major projects for corporate clients that are using CDQI. (These are not in the App Store.) While unit tests are awesome, the real test of this change will be how well those projects compile and run after the new attribute proxies are generated. In theory, very few code changes should be needed. |
I totally understand the need for backwards compatibility, but if we can achieve full type safety without public API changes, the only code that will break on upgrade are places where invalid type comparisons were made, no refactoring would be necessary. At least, that's what I'm hoping to achieve with the updates I'm making. You're right, my enthusiasm for the unsafe way is lacking :P As you mentioned in the original issue, it doesn't live up to the value proposition for the framework. I'm happy to continue supporting it if there are breaking changes introduced, though I do think it should be deprecated at some point. |
Let's roll the dice and drop loosely typed comparisons. I'm sold. Out of curiosity, are you using CDQI in any "real" projects? |
Awesome. At this point I haven't used it on a real project, but I do have plans to. I'm happy to let you try out my branch on one of your big projects before we merge to determine the impact. I expect the PR will be ready in the next few days. |
@Revolucent Progress is coming along nicely here. If you check out the typesafe-attributes branch on my fork you'll see there is working type safety for primitive types and relationships, both to-one and to-many. However, it's now apparent that we also need to limit certain comparison functions and operators to certain types. For example, you shouldn't be able to call Basically, we need to take the extension on CustomExpressionConvertible with equalTo, greaterThan, etc and break it down into multiple protocols. Something like EquatableAttribute and ComparableAttribute (or perhaps it should be on the value types, not sure yet). Here's how I'm picturing the breakdown but I wanted your input. Equatable attributes have Comparable attributes have only String attributes should have: Collection attributes (to many relationships) should also have I hope that makes sense. I really just wanted your take on which comparison functions should be available for which types. Thanks! |
I think you're on the right track here. I'd avoid putting I'm spending some quality time with your code right now, and will be looking over it, stepping through it, examining it, and perhaps even having a glass of wine with it over the next hour or two until my wife gets home. I'll let you know my thoughts. |
@patgoley It's a shame that |
@Revolucent It is a shame. It's too bad there isn't an inverse of IntegerLiteralConvertible that we can extend it ourselves. I'm trying to figure out if there's a better way to convert the primitives to AnyObject so the already class types don't have to implement boxedValue. On some level you could check if x is AnyObject or is ExpressionValueType but you can't really use an OR on the associatedType of TypedExpressionConvertible. |
Your CodeAlright, I didn't need to spend that much time with this code. It's really very simple. A Numeric comparison rely on the (unfortunately not universal) capability of Swift to autobox some common numeric types to Operators / Comparison ProtocolsI agree with your choices. Obviously, as you suggested extension ExpressionValueType: CustomExpressionConvertible {
var expression: NSExpression {
return NSExpression(forConstantValue: boxedValue)
}
}
public struct PredicateBuilder { // or some useful name
public static func compare(lhs: CustomExpressionConvertible, rhs: CustomExpressionConvertible?, type: NSPredicateOperatorType, options: NSComparisonPredicateOptions = []) -> NSPredicate
let le = lhs.expression
let re = rhs?.expression ?? NSExpression(forConstantValue: nil)
return NSComparisonPredicate(leftExpression: le, rightExpression: re, modifier: .DirectPredicateModifier, type: type, options: options)
}
// Use compare to build the rest of the needed methods, like equalTo, greaterThan, etc.
} You would then use this public, generalized I'm happy to write this struct if you'd like. I can get it done in probably a few hours. ExpressionValueTypeNot sure about this name. Its real purpose is to "box" something. So I prefer one of the following: protocol ExpressionValueType {
var expressionValue: AnyObject { get }
} or protocol BoxedValueType {
var boxedValue: AnyObject { get }
} I think the first one fits a little better with the names used in the rest of the project, but I'm open to either one. |
So, this is what I had in mind. This code is pretty off the cuff and has never been compiled or run by anyone ever: protocol CustomExpressionConvertible {
var expression: NSExpression { get }
}
struct PredicateBuilder {
static func compare(lhs lhs: CustomExpressionConvertible, rhs: CustomExpressionConvertible?, type: NSPredicateOperatorType, options: NSComparisonPredicateOptions = []) -> NSPredicate {
let le = lhs.expression
let re = rhs?.expression ?? NSExpression(forConstantValue: nil)
return NSComparisonPredicate(leftExpression: le, rightExpression: re, modifier: .DirectPredicateModifier, type: type, options: options)
}
static func greaterThan(lhs lhs: CustomExpressionConvertible, rhs: CustomExpressionConvertible, options: NSComparisonPredicateOptions = []) -> NSPredicate {
return compare(lhs: lhs, rhs: rhs, type: .GreaterThanPredicateOperatorType, options: options)
}
}
protocol ExpressionValueType: CustomExpressionConvertible {
var expressionValue: AnyObject { get }
}
extension ExpressionValueType {
var expression: NSExpression {
return NSExpression(forConstantValue: expressionValue)
}
}
extension String: ExpressionValueType {
var expressionValue: AnyObject {
return self as NSString
}
}
protocol ComparableValueType: ExpressionValueType {}
protocol TypedExpressionConvertible: CustomExpressionConvertible {
associatedtype ValueType: ExpressionValueType
}
extension TypedExpressionConvertible where ValueType: ComparableValueType {
func greaterThan(other: ValueType) -> NSPredicate {
return PredicateBuilder.greaterThan(lhs: self, rhs: other)
}
}
func ><E: TypedExpressionConvertible, V: ComparableValueType where E.ValueType == V>(lhs: E, rhs: V) -> NSPredicate {
return lhs.greaterThan(rhs)
} |
Ah, another thing that just occurred to me, and that unfortunately is not reflected in the unit tests, but is currently supported: You must be able to compare attributes to each other, e.g., moc.from(Foo).filter { $0.bar >= $0.baz } So far, it doesn't look like your additions will support that. I admit doing this is rare, but it's a must-have. |
Never mind. It's actually pretty easy: extension TypedExpressionConvertible where ValueType: ComparableValueType {
func greaterThan(other: ValueType) -> NSPredicate {
return PredicateBuilder.greaterThan(lhs: self, rhs: other)
}
func greaterThan(other: Self) -> NSPredicate {
return PredicateBuilder.greaterThan(lhs: self, rhs: other)
}
}
func ><E: TypedExpressionConvertible, V: ComparableValueType where E.ValueType == V>(lhs: E, rhs: V) -> NSPredicate {
return lhs.greaterThan(rhs)
}
func ><E: TypedExpressionConvertible where E.ValueType: ComparableValueType>(lhs: E, rhs: E) -> NSPredicate {
return lhs.greaterThan(rhs)
} |
@Revolucent Thanks Gregory, I appreciate the feedback. I had actually begun a refactor on the comparison functions before you posted and wanted to run it by you. Instead of the PredicateBuilder, I had basically added extensions to TypedExpressionConvertible so we can leverage the ValueType (see below). I like the abstracted nature of PredicateBuilder, allowing direct access to the functionality, but with TypedExpressionConvertible extensions, we can make sure that operators are only applied to matching and appropriate values. I recognize the value of flexible helpers underneath the strictly-typed public API, but I think once we have the very general
In this fashion, we don't expose an API that allows you to make mistakes. PredicateBuilder, while flexible and a solid abstraction, could allow you to compare invalid types or use an invalid operator on certain types. If we want PredicateBuilder, I don't think it should be public on the package level. About the error you found comparing attributes, I just actually ran across that same limitation in a different form. The problem I ran into actually was not being able to compare a countable.count > 0 complaining because the result of countable.count is an NSExpression and didn't meet the type constraints of > which expected a numeric or comparable type. I think we can get around this, basically switching to TypedExpressionConvertible in a few places that CustomExpressionConvertible or NSExpression are used right now. I don't think it should be too difficult. Thanks for your help Greg! Hoping to get some more done this weekend. Let me know if you have any thoughts or ideas. |
Ah it seems we arrived at the same conclusion but I didn't see your post until now. Yeah, the extensions on TypedExpressionConvertible are nice. Still have to see how to handle the countable expression result and attribute to attribute comparisons. |
Well, my suggestion actually is that we have both. In other words, the |
I'm good with that idea. It makes the predicate building nice and testable without having to create expressions first. |
My branch is to date with everything minus PredicateBuilder and the errors I mentioned. Not sure where you have that living right now but I'm happy to merge it into my fork whenever it's ready. Or we can publish this feature branch on the main repo and you can merge it there. Whatever works for you. |
I can refactor this code to use It looks like it does not. |
Great, thanks. It doesn't support attribute to attribute comparisons at this point. I'm also a bit stuck on the countable and subquery function results. We somehow need to know those are numeric value expressions and comparable to other numbers. One of the tests isn't compiling at the moment for that reason. Anyway, thanks for the help. I'm probably done for tonight but I think if we can round out these edge cases we'll be pretty close to finishing. |
Yes, this is what I'll do. I'll add |
@patgoley I've copied your feature branch over to the main repo. I'm still doing some work in it. I want to warn you that I've made some pretty extensive changes, so you might want to wait a bit before continuing any work. I've changed the protocol TypedExpressionConvertible: CustomExpressionConvertible {
associatedtype ExpressionValueType
} Note that…
Why? Boxing is unnecessary. It comes from a misunderstanding of how CDQI and Core Data predicates work under the hood. Core Data predicates want to compare So have I thrown type safety out the window? No. My way of doing it is just as typesafe as yours. The key is the Here's some code that should make it all clear: extension String: TypedExpressionConvertible {
public typealias ExpressionValueType = Self
public var expression: NSExpression {
return NSExpression(forConstantValue: self)
}
}
extension TypedExpressionConvertible where ExpressionValueType: Equatable {
public func equalTo<R: TypedExpressionConvertible where Self.ExpressionValueType == R.ExpressionValueType>(rhs: R?, options: NSComparisonPredicateOptions = []) -> NSPredicate {
return PredicateBuilder.equalTo(lhs: self, rhs: rhs, options: options)
}
public func notEqualTo<R: TypedExpressionConvertible where Self.ExpressionValueType == R.ExpressionValueType>(rhs: R?, options: NSComparisonPredicateOptions = []) -> NSPredicate {
return PredicateBuilder.notEqualTo(lhs: self, rhs: rhs, options: options)
}
public func among<R: TypedExpressionConvertible where Self.ExpressionValueType == R.ExpressionValueType>(rhs: [R], options: NSComparisonPredicateOptions = []) -> NSPredicate {
return PredicateBuilder.among(lhs: self, rhs: rhs.map { $0 as CustomExpressionConvertible }, options: options)
}
}
public func ==<L: TypedExpressionConvertible, R: TypedExpressionConvertible where L.ExpressionValueType == R.ExpressionValueType, L.ExpressionValueType: Equatable>(lhs: L, rhs: R?) -> NSPredicate {
return lhs.equalTo(rhs)
}
let predicate: NSPredicate = department.name == "foo" Autoboxing of This code is not pushed yet. It still has a few minor issues, but I'm too tired to solve them right now. |
@Revolucent This is great! I think the use of TypedExpressionConvertible for the constant values makes total sense. The only reason I wanted the boxed value was so it could be passed to NSExpression(forConstantValue:), but this makes that requirement more formalized. I had thought about using Swift's Comparable and Equatable, but I found that there wasn't a 1 - 1 relationship between types that are Comparable by the protocol and types that can be compared with NSExpression. Namely, NSDate is not Comparable. I'm not sure that as a framework we should make NSDate conform to Comparable just to make this work for two reasons:
There's still a bit of work to be done. Need to update cdqi to generate EntityCollectionAttributes for to-many relationships and need to handle Transformable types (stubbed out in a comment in TypedExpressionConvertible). I think I can get to these items at some point today, but let me know when you've pushed your changes so I can grab them. Thanks! |
P.s. I guess to handle the myriad numeric types now, we just make them all have NSNumber for the ExpressionValueType. You probably figured that out already.
|
Yes, I've already got all the comparisons working very well. As for public struct Date: Equatable, Comparable {
}
public DateAttribute: TypedExpressionConvertible {
public typealias ExpressionValueType = Date
}
extension NSDate: TypedExpressionConvertible {
public typealias ExpressionValueType = Date
} This |
I'm watching my two-year-old until around 2PM EST. Around 5 PM, my old Gen X ass is going to see Duran Duran. But between those times, I'm confident I'll be able to push some code for you to work with. |
OK, I'm very close to pushing my changes. A few things to note: EntityCollectionAttributeUnfortunately I had to get rid of this class and go back to what I was using before. The reason is that
The reason is that At some point, we have to keep in mind that CDQI is reflecting what Apple's predicate language allows underneath the hood, and I think it should do it in as seamless a way as possible. The only "violation" of type safety here is that you could perform |
@patgoley OK, it's pushed. Have a look and let me know what you think. |
@patgoley OK, just pushed some last minute changes. Had to fix up |
I think the only thing missing is transformable attributes. |
Actually, Pat, I think I've got this sewn up. I'd really love for you to look at it and give your feedback. I admit I feel a tad guilty that I effectively rewrote what you did. (Hope you're not too disappointed. I tremendously appreciate the initiative you took on this.) On the other hand, I really think this is the right approach. I'm a little iffy on a few things, and there are a few oddities with the numeric types, although all the unit tests pass. I'm not worried about transformable attributes, though if you want to tackle them, awesome. I will probably push those in a point release. I'm going to tinker with this a bit more to tighten it up, and I encourage you to do the same. But I'm going to create a |
@Revolucent No worries, Greg. As far as rewriting it, as long as my name is in the commit history, I have no complaints. I appreciate the opportunity to contribute to a great framework and I honestly don't care who gets the last word on this feature, so to speak. I just wasn't about to let you rewrite your readme instead of improving your code :) I would love to take a thorough look over it before it gets merged in, just to make sure I don't find any gotcha cases etc. Are all of your latest commits pushed up? From what's up there now it doesn't seem quite done, and I'm not 100% sure on all of the choices yet. Here's a few things I noticed:
The other way that we had before I think made more sense and required less boilerplate. We have empty protocols such as ComparableValueType which are only used to constrain the appropriate functions to types that we've "tagged" as such. We can add these to real types (NSDate, NSNumber) without possibly interfering with anyone else's code. As far as Null goes, it's really unfortunate that we have to duplicate anything that could possibly take Null. Was there some limitation not allowing us to use an optional CustomExpressionConvertible? and then convert using NSExpression(forConstantValue: nil) at the last second inside of
|
@patgoley I don't have a lot of time to answer this, so I'll just jot a few notes. Later tonight I can answer more fully, if needed.
The problem with your approach is that it doesn't get us all the way there. For instance, take a look at how I implemented
|
@Revolucent Tell Duran Duran you have important work to do! Just kidding, have fun. Sorry I missed your post on EntityCollectionAttribute, that makes sense. Those kind of queries did work but only by building it with subquery using the Aggregable type. I've expressed the direct comparison case in a unit test to make sure it always compiles :) All great points above, I've demonstrated them to myself and they makes sense. The main thing my implementation was missing was applying TypedExpressionConvertible to the value types themselves, things follow pretty naturally from there. There's one thing I've updated from your code (on my fork) and I think it's an improvement, but I'll let you decide... I really don't think we need the intermediate types Data, Date, and Entity. I also think it's not ideal that we're conflating Equatable and Comparable to our own unrelated use case. Just because a type is Comparable does not necessarily mean NSPredicate knows how to compare it, and vice-versa. In fact, whether or not a type is Comparable is irrelevant to CDQI, because we don't ever call the related operators (only our overridden ones that create NSPredicates). Also, our implementation assumes that String is Comparable and always will be, but if this changes for whatever reason (they removed ++ for christ sake), CDQI will stop compiling (even though it would actually still work at runtime). For this reason, I suggest we return to the previous idea of using our own, empty protocols and apply them to real types (instead of applying real protocols to empty types). I've pushed a working example to my fork that doesn't include Date, Data, or Entity structs and still works just the same. See below:
So what's different?
I understand now why the Null struct is necessary. Swift won't recognize the type conformance on nil to the protocol in question because it doesn't know what T is on the Optional. We need a concrete, NilLiteralConvertible type that conforms to trampoline off of. This is a special case, other types don't require the intermediate type to work properly. I hope that makes sense. If we can get by in all scenarios with out the intermediate structs, I think we should toss them. Let me know if you find a case where this doesn't work. |
@patgoley I'm back. Your message popped up on my Apple Watch between sets, so I was pondering what you wrote while Simon Le Bon was belting out the ancient pop classics from my early teenage years. So, with the caveat that I haven't looked at your code yet—I'm old as fuck, somewhat inebriated, and pretty tired, so that will have to wait until sometime Sunday—let me sum up what I like about both of our approaches. The difference between your approach and mine can be summed up as: You don't want new concrete types and I don't want new protocols. Both solutions are mildly ugly, but c'est la vie in software. I'm much less concerned than you are that something like So, I rather like the fact that I'm using But…I think what may have sold me on your approach is that it's less code to write. Just some empty protocols and that's it. So, with the proviso that I still need to review your code, I think we'll go with your approach. And now I'm going to crash. |
Oh, there is another thing I like about your approach, which I meant to mention. I do prefer that the types are more "natural", even if they are never used as such. |
OK, I took a look at your code this morning and I'm sold. I do prefer your approach. However, in the interest of parsimony, let's get rid of the This leaves us with just the empty |
OK, I integrated your changes, removed |
I deleted the |
Awesome! No I agree, if everything is considered equatable it's just a pain to tag everything as such. Glad it's merged in. Did you get a chance to run it on of you bigger projects? Seems like once you run cdqi again it should "just work". I hope Duran was awesome and you were dancing on the sand. |
I'll try it on the most complex project today and let you know what happens. |
This is a large project that makes VERY extensive use of CDQI, so to have so few errors is excellent. I will probably edit CDQI in situ alongside this project until the errors go away. Then I will write some unit tests to cover these cases. |
Actually, most of them were fixed by making |
Duran was awesome. For guys in their late 50s, they were absolutely rock solid. They're all so rich they could have hung it up long ago, so they obviously love what they do, and it shows. I actually don't listen to Duran much, but I didn't want to pass up the chance to see them. My musical taste these days tends more towards Röyksopp and Purity Ring, musical nephews and nieces of Duran. |
I've squashed all of the compilation errors with this project, and everything works fine. However, one thing I noticed is that Swift's autoboxing of certain numeric types to Another huge win here is the ability to make any type participate in CDQI comparisons. This is especially useful with enumerations, e.g., extension Weekday: TypedExpressionConvertible {
public typealias ExpressionValueType = NSNumber
public var expression: NSExpression {
return NSExpression(forConstantValue: rawValue)
}
} |
@Revolucent That's awesome. Glad you were able to fix the edge cases and get enum support! I'm going to take a look later tonight about updating the unit tests and readme and hopefully we can get this release merged in soon. Nice work! |
@patgoley Looks like we wrapped this one up. Thanks enormously for your help. |
Here are the features I'd like to see for type-safe predicates.
The text was updated successfully, but these errors were encountered: