ozra / onyx-lang

The Onyx Programming Language
Other
97 stars 5 forks source link

Nil Handling Sugar #21

Open ozra opened 8 years ago

ozra commented 8 years ago

Nil Handling Sugar

This is strongly wanted. I was inspired by LiveScript, Swift has it too, etc.

The gotcha is crystal compatibility which is desirable to maintain, because of funcs ending with ! and ?.

The sugar is when an expression is ended with ? or ! and followed by an identifier, or dot+identifier (alternatives): callable?only-if-non-nil or callable?.only-if-non-nil.

As usual, the approach is that methods are used "behind the syntax", and therefore the construct can be exploited by the programmer if wished.

x = a?b?c.d becomes x = a.try ~.b.try ~.c.d)) internally (canonical form), that is, the try-method is invoked, with the arg being a "soft-lambda". For Nil try is defined as nop, which is why it works.

The Caveats (or Features...)

Crystal stdlib has a de facto standard of suffixing method-names with ? if they return Type|Nil or Bool. This is inline with the behaviour, but clashes with the notation. This is not necessarily a problem, but can instead be utilized as a feature! For 'a?b' (or 'a?.b'), we would first look for a func named a? (unless it's a variable of course), if that is not found, we look for a. By following the pattern of primarily putting nil-check functionality in identifier? that is prioritized for this pattern.

So, to the other end: a!b!c.d or a!.b!.c.d. This of course means "a is not nil - if it is: throw exception! ...", and so on for the others. It translates to canonical form a.not_nil!.b.not_nil!.c.d.

Here, the naming scheme usage in Crystal stdlib is a bit more varied. It generally means "beware, dangerous method". For instance, in many cases it means "value or throw if nil", which is what we want, but in some cases it means "mutates stuff", and in other cases "value or throw"-methods are named without suffix !. Surprising behaviours inherited from Ruby (!)

Since this feature must rely on de facto convention of func/method naming, it needs some additional thought and examination of actually used patterns.

I would definitely favour its' implementation. The reason is that for asserting a value is not nil to the type inferer you generally assign a tested return value to a local var (which can't change from the outside), that's a design decision in the code. But if you do want to get the latest value no matter how or where it might have changed from, from a certain instance, you would want to test it at every reference. Also, when making a lookup deep in a tree of instances, it's also very clear and concise.

Syntax Alternatives

Show casing only the "try chain" side of it:

x = a?b
x = a?.b
x = a??b
x = a??.b

Personally I prefer idfr?other, with no additional dot. Since Onyx is space sensitive to some extent, this is one of the places it could be that.

stugol commented 8 years ago

It's a bugger, certainly. I'm massively in favour of this feature, but the Ruby/Crystal ? and ! make it difficult. There are plenty of methods that have both a ? and a non-? variant in Crystal, so making it "try one then the other" is a really bad idea!

Maybe ?? with spaces?

puts a ?? b? ?? c           -- b? is a method name here

And !!, of course.

Now, if only I could think of a use for a !? token... ;)

ozra commented 8 years ago

Yeah, it's not the preferred way of going, no doubt! But if gathering statistics on sources conclude that the prefer 'idfr?', else use 'idfr' could work out, and complete it with a check that return type definitely is Type|Nil (and perhaps explicit check that a raise doesn't occur in the method sugar-chained, it could still be possible to pull off. But, yes, the whole endeavour does smelllll!

As for the spaced suggestions it looks way to messy imo, gotta be a better way :-/

!? == and/or crash! ;-)

stugol commented 8 years ago

I think the spaced syntax looks quite nice, personally. In the absence of a better suggestion, I propose that we go with that. We can always change it later (or add an alternative, to avoid breaking existing code).

ozra commented 8 years ago

No, that's simply a no go for me, we must come up with something better.

ozra commented 8 years ago

In an issue raised in Crystal for a feature from Ruby 2.0 (mind you I've never coded ruby [less some config-script or so]), there they had foo&.do-stuff&.do-other, iirc, this of course is pretty much the nil-notation with an ampersand. Got me thinking to the very simple, and not at all that far fetched jump:

-- current inline-nil-check style using shorthand soft-lambda:
x = foo.try ~.do-stuff.try ~.do-other

-- derive from that soft-lambda symbol for the nil-sugar:
x = foo~.do-stuff~.do-other

-- instead of initially proposed:
-- x = foo?do-stuff?do-other

For x.not_nil! the simple alias x.some! will be fine - because its' usage is less common, so it's ok. Mirroring with none? for nil? check, just for symmetry.

stugol commented 8 years ago

Ruby 2.0 doesn't have anything of the sort, AFAIK.

For x.not_nil! the simple alias x.some! will be fine

I don't follow. Why do we need an alias here?

On that note, I've always found it strange that .any? returns false if there are only false items in the enumerable. We should have a .some? function that means .length > 0. Does none? suffer from this problem?

ozra commented 8 years ago

Ruby 2.0 doesn't have anything of the sort, AFAIK.

Lucky I made my "never rubied" disclaimer, haha, in any event it's another idea for the syntax, but I'm still inclined to think the ?-op vs fun?-idfr should be solvable.

Regarding not_nil / some. There's no "need", I just think standard lib API names should be cleaned up for the Onyx-side of the universe, there are many name choices that I find ugly and/or unintuitive. That discussion of course should be a holistic perspective issue opened later on, so we'll just leave it in this discussion for now.

For the specifics you mentioned, please open it in a new issue, since they're not about the nil-sugar.

ozra commented 8 years ago

I've implemented this for trying out (needed a break from the macro-branch).

There are plenty of methods that have both a ? and a non-? variant in Crystal, so making it "try one then the other" is a really bad idea!™

When there are two identically named functions, differing only by ending ?, it means foo? return T|Nil and foo returns T or raises. When there's only one, there can be no certainty, but that holds true sugar or not. So I chose to implement this PoC like so:

q = foo?bar?qwo  -- look first for `foo?` else `foo`, `bar?` else `bar`, and then `qwo`
--> q = foo?.try ~.bar?.try ~.qwo  -- or: foo.try ~.bar.try ~.qwo  -- etc variations

-- Works also on terse subscript sugar (sugar with sugar on top):
x = obj.1?to-s    --> obj[1]?.try ~.to-s
y = obj#key?to-s    --> obj[#key]?.try ~.to-s
z = obj:key?to-s    --> obj["key"]?.try ~.to-s

This is not locked down syntax, just what I chose for trying it in practice.

stugol commented 8 years ago

Will it work with spacing?

say foo ? bar ? qwo

What about spacing with ?-functions?

foo? ->
bar? ->
qwo? ->
say foo? ? bar? ? qwo?

Spacing shouldn't infer ?-ness:

foo? ->
say foo ? bar      -- should fail, `foo` no such method
ozra commented 8 years ago

Currently, it goes the opposite route, just like foo#bar, foo:bar. It must not be spaced. Spacing would be an option only if if x ? y : z syntax is changed.

In that case, also alternatively (just to stay with flow of other similar constructs, but it didn't turn out prettier though): say foo ?.bar ?.qwo say foo? ?.bar? ?.qwo?

So for spaced, your proposition is probably better than "staying with similar style". And, correct, in that case there is no point for ?-"inference".

Since it's a call-chain, it makes sense to connect the parts though. The essence is x = foo.bar.qwo, but with breaking if nil is encountered, so x = foo?bar?qwo conveys that imo.

stugol commented 8 years ago

I still prefer x ?? y.