tciuro / NanoStore

NanoStore is an open source, lightweight schema-less local key-value document store written in Objective-C for Mac OS X and iOS.
Other
404 stars 39 forks source link

Issue finding columns with NULL values in RubyMotion #70

Closed DougPuchalski closed 11 years ago

DougPuchalski commented 11 years ago

This is a follow-up to Issue #64.

Background: RubyMotion allows the use of compiled ruby and maps datatypes to Objective-C, and allows the use of Objective-C libraries to build iOS apps.

RubyMotion collapses many datatypes, including the value of nil and NSNull.null. This breaks the assertions in initWithColumn:(NSFTableColumnType)type matching:(NSFMatchType)matching value:(id)aValue and the first if in + (NSString *)_querySegmentForAttributeColumnWithValue:(id)anAttributeValue matching:(NSFMatchType)match valueColumnWithValue:(id)aValue.

The referenced code appears to be trying to catch an illegal use of nil as a value for a column. It is not clear to me, looking at this, why NSNull.null is preferable.

Can you explain the reason for distinguishing the values nil and NSNull.null in this context?

tciuro commented 11 years ago

Doug, can you explain what you mean with "RubyMotion collapses many datatypes, including the value of nil and NSNull.null"? Do you mean that RubyMotion treats nil and NSNull.null the same way?

I treat them differently because nil != NSNull.null (the former meaning "nothing" and the latter is an object that represents "nothing"). I'm not very familiar with Ruby/Rubymotion. The fact that RubyMotion "collapses" both types, is is considered standard practice or is this a RubyMotion-only decision?

tciuro commented 11 years ago

I've found this: https://github.com/rubymotion/sugarcube

"When storing nil into NSUserDefaults, it is converted into false, because Cocoa complains if you give it nil, and the RubyMotion runtime refuses to allow the NSNull.null object. Without relying on an external project (like [nsnulldammit]https://github.com/colinta/nsnulldammit) I don't know of a sensible workaround..."

So if I understand correctly, RubyMotion doesn't allow to pass NSNull.null, right? If so, is there a reason? In Cocoa it's often the case where nil arguments are not allowed. In NanoStore, we use [NSNull null] because they can be stored in collections and it's a datatype that represents nil uniformly.

DougPuchalski commented 11 years ago

Full disclosure, I'm far from an Objective C expert. My iOS work consists mostly of RubyMotion and the bit of Objective C as required.

RubyMotion (and I assume MacRuby as well) merges the functionality of corresponding datatypes, i.e. NSMutableString and ruby String.

My understanding was that NSNull is designed to represent null in places where Objective C wants to have an object, but not that it is intended to have a different meaning--this because of limitations of static typing of C. In RubyMotion (and maybe MacRuby?), the dynamic typing of ruby comes into play. If I declare NSNull.null, I just get nil.

So this is why I was asking why you would want to check for nil in these assertions, where you would otherwise accept NSNull.null. Is there a different meaning there, or are you just trying to be helpful and catch mistakes?

As a head's up, I'm using several different Objective C libraries, cocoapods, etc. and I haven't run into this elsewhere.

Commentary: I'm hoping we can find a way to make this work. I built a light ORM using CoreData and it was a huge ordeal. When I ported to NanoStore things became very easy. So thanks for that. I'm using NanoStoreInMotion but there are some shortcomings that I'm working on, including this -- no way to find objects that have nil in columns, i.e. for unassociated objects. Kind of important! :P It seems that I may be one of the few people pushing RubyMotion's boundaries, but I think you'll find that it's going to gain popularity. It makes writing iOS apps really high level and fun.

tciuro commented 11 years ago

Hi Doug,

Quick example:

NSString *someValue;
NSPredicate *predicate = [NSFNanoPredicate predicateWithColumn:NSFValueColumn matching:NSFEqualTo value:someValue];

Do you really intended to look for values matching nil? Or did you perhaps forget to specify the value? If we rewrite it like this, there is no ambiguity and the intent is clear:

NSNull *nullValue = [NSNull null];
NSPredicate *predicate = [NSFNanoPredicate predicateWithColumn:NSFValueColumn matching:NSFEqualTo value:nullValue];

The assertion is there to protect the developer from passing the "wrong" value by accident. As you point out, some libraries do not check for potential nil values. This doesn't help the developer, where one could end up with badly-initialized objects that don't do their job, as advertised. This is why we also see lots of client code around the framework performing sanity checks, hence making the code less readable and more redundant.

My question is why RubyMotion flattens NSNull.null into nil before calling Objective-C? What if the Obj-C code requires a valid object? In this case, we're hosed. While this might be fine between Ruby layers, it might not be (is definitely not!) right when you cross language barriers. When this happens, you start imposing foreign rules and constraints that might not follow standard practices, as is the case with NanoStoreInMotion and NanoStore.

DougPuchalski commented 11 years ago

Ruby doesn't (in general) use pointers, nil really is always an object, a singleton I think. RubyMotion adds pointers, but I think a pointer with a null reference wouldn't help here.

I get what you're trying to do from a C perspective. In Rails ActiveRecord, #where(value: nil) is mapped to "VALUE IS NULL" in SQL, rather than using a special object, or a string or whatever. To me that makes sense, if I accidentally pass in a nil then I have a bug to fix there. I would tend to add application level checks for this rather than rely on the library. Another thing you'll sometimes see in ruby is the use of symbols (effectively immutable strings which map to integers) as parameters that have special values. So you might define :nil or :null and pass that around if wanted to differentiate between them and nil. But ActiveRecord is pretty war-hardened and they don't bother.

RubyMotion makes NSNull.null == nil just like it does with many other types, to make seamless the use of calls between ruby and Objective C. It's actually quite impressive and makes things very easy. If you think about it, since ruby's nil is an object, it really /is/ the same thing as NSNull.null. What's not true, is that it's a literal zero like nil is in C (is that right?). There's no such thing. So, to answer your question, we are indeed passing in an object. We can't pass in what C would call a null pointer.

tciuro commented 11 years ago

In C:

define NULL ((char *)0)

I'm confused with your comment: "we are indeed passing in an object. We can't pass in what C would call a null pointer." So if you're passing an object and you can't pass a NULL pointer, where is the problem then? You should be able to pass an NSNull object and the init's assertion wouldn't be triggered. What are we talking about then? :-/

DougPuchalski commented 11 years ago

Since NSNull.null == nil, nil != aValue will always be false if I pass in either.

tciuro commented 11 years ago

Check this issue:

I'm trying to set a property to nil for the Parse framework. In Obj-C this requires using [NSNull null], but when I try
the equivalent in RubyMotion:

  parse_object['name'] = NSNull.null

I get:

  <RuntimeError: NSInvalidArgumentException: Can't use nil for keys or values on PFObject. Use NSNull for values.>

It seems the NSNull is being converted to nil before the Parse framework can handle it.

Any way around this?

Andy

Here we go: RubyMotion clashes with Cocoa's best practices. [NSNull null] != nil and RubyMotion is enforcing this behavior, which is simply wrong in Cocoa land.

Reference: https://groups.google.com/forum/?fromgroups=#!topic/rubymotion/WwRejMdC_Mc

DougPuchalski commented 11 years ago

Thanks for the reference. Maybe that will do the trick well enough. I am surprised I haven't needed this before if it's indeed not isolated.

tciuro commented 11 years ago

No problem!

Another reference: https://github.com/rubymotion/sugarcube

When storing nil into NSUserDefaults, it is converted into false, because Cocoa complains
if you give it nil, and the RubyMotion runtime refuses to allow the NSNull.null object. Without
relying on an external project (like [nsnulldammit]https://github.com/colinta/nsnulldammit) I don't
know of a sensible workaround...

I personally consider this a bug in RubyMotion. The NSNull.null object should make it all the way to framework. Manipulating it along the way changes the behavior underneath (as you've seen.) That is a bad thing™. Looks like this issue is far from isolated.

DougPuchalski commented 11 years ago

Hard to say, I'm sure it's there for a reason. In any event, not sure this workaround is what I thought. Mind leaving this issue open for a bit while I investigate more?

defvol commented 11 years ago

@aceofspades

why not continue the discussion in the RubyMotion repo? https://github.com/HipByte/RubyMotion/issues

DougPuchalski commented 11 years ago

I've talked to Laurent (the author of RubyMotion) a bit. This is currently not possible, so I may be forced to use a fork with the assertions disabled.

tciuro commented 11 years ago

That's unfortunate. What's the reasoning behind this NSNull behavior in RubyMotion?

DougPuchalski commented 11 years ago

It's more general than that -- type conversions need to be done by default to make calling between ruby and Objective-C seamless. What is needed is a way to disable the conversion for specific cases. So then I would have a special way to pass in the literal NSNull.null to NanaStore.

Hopefully this is a temporary workaround until he can implement a fix. Just not sure when that will be.

No biggie, since it's just an assertion check.

tciuro commented 11 years ago

Interesting. By manipulating an NSNull object into nil, it's actually eliminating the solution it's trying to solve. I mean, why is this manipulation going to make any communication between RubyMotion and Objective-C seamless? I just don't get it. I wrote him an email, but he hasn't responded yet. I'll shed some light to my comments here if he does reply some day.

DougPuchalski commented 11 years ago

Yes he and I chatted about your email. He's pondering a fix.

tciuro commented 11 years ago

Sounds good. Can we close this issue then?

DougPuchalski commented 11 years ago

I guess--maybe I'll just maintain my own fork. Gotta keep moving forward.

tciuro commented 11 years ago

I understand. I hope we see a RubyMotion fix soon.