Skip to content
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

Implementation of "lonely operator" #2177

Closed
r00takaspin opened this issue Feb 18, 2016 · 33 comments
Closed

Implementation of "lonely operator" #2177

r00takaspin opened this issue Feb 18, 2016 · 33 comments

Comments

@r00takaspin
Copy link

Hello, I found in last ruby version (2.3.0) "lonely operator" implementation. I found it very useful.

Can it be implemented in Crystal?

@stugol
Copy link

stugol commented Feb 18, 2016

+100

@asterite
Copy link
Member

You already can do what the "lonely operator" can do:

# Ruby
foo&.bar

# Crystal
foo.try &.bar

Pros of Ruby's way:

  • It's shorter

Cons of Ruby's Syntax:

  • New syntax, users have to learn something new

Advantages of Crystal's way:

  • Nothing new to learn, it's just a method call
  • The syntax allows for more complex stuff, like [10, 20, 30].map &.to_s(16).reverse)

Disadvantages of Crystal's way:

  • It's longer

And because in Crystal we try to avoid nil whenever we can, the usage of try will not be as common as in Ruby, where basically there isn't a way to prevent something becoming nil.

@stugol
Copy link

stugol commented Feb 18, 2016

You really hate implementing syntax, don't you? God forbid people might have to "learn something new". Sheesh.

a.try(&.fn).try(&.fn).try(&.fn).try(&.fn).try(&.fn)    # oh god!

@mverzilli
Copy link

Have you heard about the Law of Demeter? https://en.wikipedia.org/wiki/Law_of_Demeter. If I were to collaborate with you on a project and you wrote that expression I'd urge you to refactor that.

Would you seriously concatenate 5 nilables on a single line? Sounds to me the only goal of such a line is to ridicule @asterite's position, which happens to be contrary to you.

There are other approaches to deal with situations like that, through more cohesive design in OO and through lifting in FP for example. Sheesh.

@ysbaddaden
Copy link
Contributor

I'm not a fan of the lonely operator, and I hope it won't take off in Ruby land, so I don't have to ask myself what is that lost ampersand. I prefer the Rails way that Crystal implemented.

Also it would conflict with Crystal's &. syntax that is incredibly more useful. I wish Ruby had copied that feature instead.

@stugol
Copy link

stugol commented Feb 18, 2016

The Law of Demeter is stupid. By that law, you would never call methods of values returned by a method call:

[1,2,nil,3].compact.each { ... }     # oh noes!

Granted, I'd probably not chain five calls like that, but I might:

values.uniq.select { ... }.map { ... }.sort { ... }.first

That's the thing about fluent interfaces and functional languages: you chain method calls. That's the entire purpose of a functional language.

Law of Demeter be damned.

@asterite
Copy link
Member

@stugol I actually like adding syntax, if it's going to have many use cases. That's why we have &.. Ruby chose to add a special syntax just to deal with nil, we chose to add special syntax to deal with any block that has one argument. I think the later is more powerful and more general.

I'm closing this, we are not going to add the lonely operator.

@bcardiff
Copy link
Member

There is a beauty in a language to have the minimum set of rules. I won't go all the way to Smalltalk though.

As @asterite points, Ruby and Crystal differs on how to deal nil, hence different constructs of the language will appear from that.

I share de opinion of @ysbaddaden here also.

For a single short line #try is enough. I would like to see if Rails projects drops their #try.
For large lines with lack/urges of #try you can even do some macros, the language can be extended to some need without requiring to change it's core.

@sergeych
Copy link

sergeych commented Dec 2, 2018

So. finally, is there anythin like &. / ?. operator to let us make chains like

foo&.bar&.buzz 

or even better, as elvis op in Kotlin. The reason 'learn something new' is not a concern as there are little or no crystal coders (not language developers) that are not ready to learn something new and prefer old an ugly to new and handy...

I was about to make my company to switch to crystal and maybe invest a little in this language because I really like it and we have to get rid of Oracle JVM anyway, but some ugly things in this great language makes me rethink. When I was, to get proper impression, porting one if our small libraries from ruby to crystal I got stuck into this and into impossibility to hold references as hash values.

So if the maintainer position is "simple to implement syntax" - okay, we'll reconsider scala native, despite of it immaturity. Still, if it is just matter of time and effort to add the neat syntax - we could event try to help :)

@ysbaddaden
Copy link
Contributor

This is all explained above:

  1. use .try with the crystal way to pass chainable calls as blocks: &.foo(arg) which is lovely and so pretty outside of nilables (map, each, ...).

  2. use a simple if check:

if value = foo(arg)
  # value can't be nil
end

All in all, don't bother much, in practice there aren't much nilable situations in crystal, and even less chained nilables (unlike other languages such as JS).

@straight-shoota
Copy link
Member

I got stuck into this and into impossibility to hold references as hash values.

Looks like you're trying to simply translate Ruby syntax to Crystal syntax. This could be more or less productive, but it's generally not a good idea. If you're just trying to write Ruby in Crystal syntax, you're wasting a lot of efficiency. Ruby and Crystal are different languages and thus porting code means to als transform concepts. Features like Crystal's type system allow using different solutions, which are most likely easier and more performant than an equivalent Ruby implementation.

@RX14
Copy link
Contributor

RX14 commented Dec 2, 2018

impossibility to hold references as hash values

I'm not sure sure what you mean by this? reference types can be used as hash values quite easily - in fact String is a reference type. Could you give a code example of what you're trying to do?

@sergeych
Copy link

sergeych commented Dec 3, 2018

Thank you for you answer. I like crystal and would love to battle test it in our family of procudts. In fact yes, I'm not yet get the crystal way, and the languages we are using last years are ruby/coffe/java/scala/c++... So maybe I am wrong. Here is the very typical situation in our code: there are nested classes, contract.transactional.export_address. The transactional could be nil, and event if it is not, export_address could be, too. This is very often and real life sample from our huge codebase we are to rewrite. We could do it, for example in ruby:

unless (address = contract&.transaction&.export_address).nil?
   # perform export operation

In crysral it is different and yes, I could accept something like:

if address = contract.transactional.export
   # do the export
  • it will be enough clear and readable. But it does not work.

What I can think of is ugly superpositinos like

if (transactional=contract.transactional) && (address=transactinoal.export_address)
  # do something

This is, actually, too bad, as it is not only too long and hard to read, it looks like an error, I'd
think first the coder has omitted one = (transactional == contract.transactional) &&...

As all we know the code we didn't write is a correct code, so we all love ruby and crystal for its
expressive minimalism. That's why I would be very pleased to see in crystal something like &., ?. or elvis from kotlin.

About the references in Hash. We too often deal with Hash as a container for arbitrary data - for example, to pack/unpack to various formats we use Java Map<String,Object> as intermediate container between the object morel and actual packing code. For example our contrat could exist as tightly packed binary, JSON, YAML or XML - whatever our client would like to use in their system. The trick is, we unpack it into Map<String,Object> and construct everything else from such nested maps. I understand that in Crystal it is not practical to hold value types in the hash, so I would love to be able to have such a hash of references, e.g {} of String => Reference or anything like. But it seems impossible at the moment, so we have to introduce bug and ugly union type to hold anything. And, as our pams are self-containing (map could be inside another map at very big depth) such a recursive declaration could be a problem itself.

Thank you very much for your help.

@asterite
Copy link
Member

asterite commented Dec 3, 2018

@sergeych you can do:

unless (address = contract.try(&.transaction.try(&.export_address)).nil?
end

@asterite
Copy link
Member

asterite commented Dec 3, 2018

Or probably just:

if contract.try &.transaction.try &.export_address
end

@straight-shoota
Copy link
Member

That's another one:

contract.try(&.transaction).try(&.export_address)

@sergeych
Copy link

sergeych commented Dec 4, 2018

Well, this is somewhat better, bu why don't we instead of

if adddress = (contract.try &.transactional.try &.export address)
  puts "Exporting to #{address}"

make it as clean and intuitive as

if address = contract?.transactional?.export
  puts "Exporting to #{address}"

From my experience of 30+ years of coding, the readability and cleanness of the compiled code worth an order of magnitude more than compiler itself source/architecture considerations. This case, actually, it does not look a big deal to add new operator. Wish you integrated the scala parser idea that let code to introduce new operators.

@refi64
Copy link
Contributor

refi64 commented Dec 4, 2018

The problem is largely just that the operator really wouldn't be used that often in idiomatic Crystal code.

Wish you integrated the scala parser idea that let code to introduce new operators.

This is a great way to make your parser incredibly complex...

@sergeych
Copy link

sergeych commented Dec 4, 2018

This is a great way to make your parser incredibly complex...

true. well, forget it ;)

I have tried many approaches with Crystal, and found how to rewrite our code in an elegant and effective way, and is pleased to admit - the language is an outstanding and promising work.

Still, you should agree that each programming language (not counting brainfuck and some student homework) is intended to make writing real-world code easy and pleasant. What means, when the language provides no way to code some typical real-world pattern easily, no idiomatic approach could help. I have shown you the real sample, which often occurs not only in our code, but also in many others - that's why all these ?. &. and ?: are so popular and requested. Even ObjC in its stone age already had a solution for chained nullable calls. Moreover, I and my company write a lot in Scala, where idiomatic way pushes us too to get rid of chained nullable calls, or put them into a for {}, and it was, is, and always be a pain and no idiomatic usage can help to get rid of chained calls.

Crystal actually admits it - thats why there is Hash#dig?. Now why don't we just extend this to all the objects, not just hashes?

@sergeych
Copy link

sergeych commented Dec 4, 2018

I'm not sure sure what you mean by this? reference types can be used as hash values quite easily - in fact String is a reference type. Could you give a code example of what you're trying to do?

Well I mean having Hash(T,Reference) could be handy. Now we have to use at least some module for it which is too restrictive.

@asterite
Copy link
Member

asterite commented Dec 4, 2018

@sergeych not being able to do that is a current limitation. It's not that we don't want you to do it, it's just that there wasn't enough time to do it.

@RX14
Copy link
Contributor

RX14 commented Dec 4, 2018

The fact that .try(&.foo) is ugly is a feature not a bug :)

It pushes code to get rid of nil where possible - and Crystal provides a lot of powerful tools to make that possible in a very clean way. It's just a matter of experience learning how to architect code in Crystal, and some design patterns used in the language.

nil itself doesn't hold any information about the error. When I see contract.try(&.transaction).try(&.export_address) I think why is contract nil? Shouldn't I be returning this error to the user in a clean way instead of ignoring it with try? Does this never happen in practice? Then raise as close as possible to the source of the nil so you can catch the error better if it occurs in production. Can this nil be caused by user input? Then you should detect and report that error nicely to the user.

Crystal makes me think more about what happens when things fail, and I enjoy that.

Hint: the guard pattern is very common in crystal, i.e.

raise "foo" unless bar

which raises an error if bar is nil. bar will then be non-nillable for the rest of the function since raise is NoReturn.

@sergeych
Copy link

sergeych commented Dec 4, 2018

It pushes code to get rid of nil where possible - and Crystal provides a lot of powerful tools to make that possible in a very clean way. It's just a matter of experience learning how to architect code in Crystal, and some design patterns used in the language.

nil itself doesn't hold any information about the error. When I see contract.try(&.transaction).try(&.export_address) I think why is contract nil? Shouldn't I be returning this error to the user in a clean way instead of ignoring it with try? Does this never happen in practice? Then raise as close as possible to the source of the nil so you can catch the error better if it occurs in production. Can this nil be caused by user input? Then you should detect and report that error nicely to the user.

Thank you. Bad sample, actually, neede one more level of indirection to illustrate. We never use nil as an error result since C++ exceptions appearance, but we still use it as an absence flag. For example, when we get result from the remote method call, it is handy to check like this:

  if code = response&.error&.code
     raise Umi::Error.new(code)

in our case, error block contains more information, but we need only the code. this was handly, though for sure I can live without it some time ;)

Thank you very much, all of you who answered.

BTW - if it is a limitation, why to close? leave it open and mark help wanted. Who wants it that bad could try to implement ;)

@asterite
Copy link
Member

asterite commented Dec 4, 2018

@sergeych the limitation is Reference in Hash. The lonely operator is not a limitation, it's a choice. Sorry I wasn't clear, I was replying to the last thing you said.

@RX14
Copy link
Contributor

RX14 commented Dec 4, 2018

@sergeych your code still implies response and error can be nil. It seems like in real code response being nil should be an exception instead - probably some IO error.

Then it's just

if error = response.error
  raise Umi::Error.new(error.code)
end

@straight-shoota
Copy link
Member

@RX14

Shouldn't I be returning this error to the user in a clean way instead of ignoring it with try?

A value being nil doesn't need to be an error. It can just be an optional value. That's what try or lonely iterator is meant for: Use the value if given, otherwise don't complain. Obviously, this happens much less frequent in Crystal than in other languages because of its type system.

@sergeych
Copy link

sergeych commented Dec 4, 2018

AFAIK it is the same as Option in scala so it should be basically the same. And my opinion - and shared by manu of my colleagues - that it is a bad approach. Scala is a great language, maybe the best, but many have left it for Kotlin because its nil processing is much more convenient.

If my opinion does not seem valuable to you, ask yourself why in most popular languages this feature exist? See https://en.wikipedia.org/wiki/Safe_navigation_operator - 10 top languages do implement it.

Pity you insist crystal should never be as good as other languages are. Well, as you wish. Personally, after recent scala collapse I feel bad about languages where idiomatic concerns prevail the convenience and beauty.

@asterite
Copy link
Member

asterite commented Dec 4, 2018

@sergeych That wikipedia page shows an example for Crystal. There's a way to do it, just not with a syntax you like.

@j8r
Copy link
Contributor

j8r commented Dec 4, 2018

I was thinking, if we were able to declare a ? method:

if address = contract.? &.transactional.? &.export
  puts "Exporting to #{address}"

This is renaming try to ?. The syntax is closer of other languages.

@straight-shoota
Copy link
Member

That saves exactly two characters but results in a pretty cryptic syntax. ? is an operator, so it's not possible anyway to use it as a method.

@j8r
Copy link
Contributor

j8r commented Dec 4, 2018

Yeah indeed it's a bad idea @straight-shoota , just wondering. Even & could be used, but that's an even worse idea for readability.

@RX14
Copy link
Contributor

RX14 commented Dec 4, 2018

@sergeych The semantics already exists in .try, so the question is whether this semantics should have the syntax of foo.try &.bar or foo&.bar. The difference between those two expressions is 5 characters. Clearly we all agree it doesn't make much sense to assign less-common operations shorter forms. .try is already pretty short, so the disagreement clearly lies in exactly how often the safe navigation operator is used in idiomatic code.

The crystal core team's argument is that the safe navigation operator is uncommon, therefore doesn't deserve such short syntax. This is different to other languages such as scala and kotlin which have less-powerful type systems and less-powerful type inference, where there is no flow typing, no union types, and variables certainly cannot change type in the middle of a function (for example, to discard a Nil from the union by using raise "foo" unless bar).

Try is only used 502 times in the crystal repository, that is one usage for every three crystal files, or once usage every 400 lines of code. By data, and by my experience with larger crystal codebases, .try is not something common enough to be worth optimizing the syntax for. This is different to other languages, every language is different, and just because safe-navigation is common in other languages does not mean it is a good fit for Crystal. Crystal solves the same problem, but with other tools.

@zw963
Copy link
Contributor

zw963 commented Nov 3, 2022

Have to praise, &. is definitely an VERY excellent design in Crystal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests