As of 0.17.x Crystal's syntax for Tuple and NamedTuples is as follows:
# A Tuple
tuple = {1, "hello", 'x'}
# A NamedTuple
named_tuple = {name: "Crystal", year: 2011}
I have a proposal to change this syntax to improve the semantics and readability of the language. First Ill like to point out my motivations for this change. Tuples in mathematics normally are written by surrounding a list of elements in parentheses separated by commas. Many programing languages follow this convention, for example:
python
tuple = ("Nim", "Racket", 1984, 2021);
rust
let tuple = ("Elixir", "F#", true, 'c');
On the hand Crystal uses curly braces for Tuples witch in mathematics defines a set:
Set:
{a1, a2, a3,...an}
Tuple:
(a1, a2, a3,...an)
Now, crystal is a general purpose languages so following the semantics of mathematical notation isn't any high priority, but this leads to my next point. Crystal takes much of its syntax from Ruby including its symbol JSON style syntax notation as a shorthand for hashrockets in a hash:
# Hashrocket syntax
hash = {:foo => "hello", :bar => 2}
# JSON style syntax
hash = {foo: "hello", bar: 2}
But in crystal the latter denotes a NamedTuple. I know compatibility with Ruby isn't the goal in Crystal but personally I find this disturbing as a person who has a Ruby background. Is syntax choice can not only confuse rubyists and other programmers alike but also defeats the purpose of having a shorthand notation, instead changing it to a different data structure all together. Ruby was made with developer happiness in mind, this includes a syntax that is both attractive and elegant. I think that's one of the reasons Crystal as adopted its syntax and has improved upon it, but I find Tuples and NamedTuples to be a little hiccup in this area. To improve on this one can replace the curly braces with parentheses, but I found they would serve another awesome function (#2603, #132). Going back to math for a sec, parentheses isn't the only way to define a Tuple, one can also use square brackets or angle brackets. Well, square brackets are for Arrays, so how about angle brackets:
tuple = <1, "hello", 'x'>
This could work but its aesthetically unpleasing and confusing. Then I thought to myself, well if a Tuple is a immutable compile-time equivalent of an Array and a NamedTuple is a immutable compile-time equivalent of a Hash, how would I make the syntax reflect that. Then it hit me, let's just directly embed that thought into the meaning of the grammatical structure of the language:
# A tuple
tuple = <[1, "hello", 'x']>
# A NamedTuple
named_tuple = <{name: "Crystal", year: 2011}>
I remembered that I saw a language with a similar notation but I haven't been able to remember which one. This syntax also allows us to use the symbol JSON style shorthand for both Hashes and NamedTuples!
# A Hash
hash = {:name => "Crystal", :year => 2011}
still_a_hash = {name: "Crystal", year: 2011}
# A NamedTuple
named_tuple = <{name: "Crystal", year: 2011}>
still_a_named_tuple = <{:name => "Crystal", :year => 2011}>
This also improves clarity on which data structure is which and eliminated the need to use curly braces for Tuples. This would be a breaking change but that hasn't stopped Crystal in the past (this could be implemented in a similar way as the as
operator #2482 #2496). I really believe this relatively small change would improve the consistency of the language as well as greatly enhance syntax notation.
Lastly, Ill to thank the Crystal team and community for such an amazing language!
I don't think the language is inconsistent by using the current notation, it's just different from Ruby. We also considered using parentheses for tuples but it's ambiguous for proc types:
# A proc with two arguments, or a proc with a tuple argument?
(Foo, Bar) -> Baz
The <{...}>
syntax isn't bad, but I feel it adds some noise. There's also the thing that right now you can do:
v = {1, 2, 3}<{1, 2, 2}
p v # => false
so <{
can be kind of ambiguous if one doesn't use spaces around it.
Also, although named tuples are different than in Ruby, this snippet works and runs fine in both Ruby and Crystal:
hash = {x: 1, y: 2}
p hash[:x] # => 1
p hash[:y] # => 2
In Ruby, this kind of hashes are mainly used as options to pass to a method, or as anonymous objects if you don't want to define a type with a name. In Crystal it's exactly the same. So both the concept and the syntax translate well from Ruby to Crystal, only that in Crystal this type is immutable.
I still believe Crystal should be its own language. If it's possible to keep things from Ruby, we keep them. But designing things so things aren't (a bit) confusing if you come from Ruby shouldn't be a goal. For example this shows that it's possible to port some parts of Ruby code, though of course some changes will have to be made. But once the port is over you'd better end with a nice looking syntax.
I can chip in with some "practical experience" here, the similarities are eerie:
(a, tuple)
notation in Onyx, since some weeks back, and it looks _very_ unclear when reading the code. That's bad.<a, tuple>
notation, which required some "special rules" to not become a snaaail of parsing alternative branches ad infinitum in the clash with the binary-usage of lt/gt (it became rather unusable). With the (rather simple) rules in place (regarding demand for spacing/non spacing) it worked out with just a little branch-trying, but it still looked quite confusing (albeit in rather contrived examples mixing angular-tuples and compares heavily for testing), but still; That's bad.<[a, tuple]>
notation a few weeks back, thanks to @stugol 's imput. This coupled with unicode symbol as alternative for cleaner look will probably stay, even though, as @asterite mentions, they look a bit noisier in the ascii-version. Also, here it should be mentioned though, that spacing-rules are required, but it is not that much of a practical problem other than in macros and interpolations where you _might_ have to think about an additional space for the named version.As a parentheses: {this, is, a, set}
in Onyx.
Now that @asterite has implemented named tuples, I'll add <{a: named, tuple: here}>
too.
Likely unicode "white" versions of curly and square-brack will be used as the unicode alternatives, must test font-support etc. first, but that's out of the scope of "practical experiences" for this particular issue.
Hope this practical trying-out experience can be of some intell value.
I believe @asterite sets a good course with "Crystal is it's own language"; if something from Ruby is good, that's good, but otherwise: improve.
I'll close this. Thanks for the suggestion anyway, we do consider alternatives, but the syntax for tuples and named tuples feels lightweight and works fine like now.
Thank you @asterite and @ozra! I think we are all on the same page about this syntax adding a bit more line noise but I think that cost comes with a gr8 benefit of adding a clarity and meaning. @asterite I completely uphold your statement about Crystal being its own language, what I mean when I refer to Ruby about the JSON style and HashRocket syntax is that I think they happen to implement it first in a way that its developer friendly by being less verbose and Crystal should keep it because of that, not as a compatibility towards Ruby but because I believe it makes sense in the first place.
This syntax can also positively impact #2631 by eliminating its ambiguity. One can have a string key with JSON style on both a Hash and a NamedTuple, the internal style of the data structure will not matter because that's what there are, styles, not semantic notation. By simply adding a immutable notation (in this case <
and >
) one changes both the data structure and the meaning without sacrificing style flexibility.
if something from Ruby is good, that's good, but otherwise: improve.
This statement is how I view Crystal's relation to Ruby. In this case I think Ruby's optional symbol to object syntax is awesome and changing it to denote a different data structure misses its point of being a different style.
Ha, when I just posted the last comment the issue was closed! Well anyway thank you @asterite for your time and consideration about this. @ozra the similarities are indeed eerie, thanks for sharing your experience and hopefully we can revisit this syntax in the near future.
It might be a little noisier but it does look pretty good and makes sense. I have to agree about the moving away from JSON style, which by the way is also a subset of YAML. I understand why, but it is still rather "disturbing".
If I might make a suggestion, we do have Unicode now. If noise is the real problem, something like <{
and }>
could just serve as a stand in for a set of brackets that we can't type on our ANSI boards. And a simple format tool could be used to search and replace them. There are plenty to pick from. Math angle brackets and Asian turtle brackets look especially good. Using such a tool isn't a big deal. In fact, Go has a format tool that is standard usage, and has turned out to be really beneficial and strongly supported by the community.
@trans - the problem with unicode symbols and programming is: most of us use monospaced fonts. Go to google fonts and list the most popular monospace fonts, type in your intended brackets in the boxes. I bet 9/10 of the codepoints won't be available in the fonts. (I've looked around for such alternatives extensively myself). It's too bad the fonts omit so many useful symbols :-(
@ozra That's a really good although sad point. :disappointed:
But maybe not all is lost. I think just about every font supports extended ASCII. And as it happens it has two additional bracket pairs ‹ ›
and « »
.
If we start using non-ascii characters as part of the language syntax, the language becomes a de-facto esoteric language.
Or one that is falling behind the times.
Seriously, we are already seeing cases of other languages allowing some Unicode characters, e.g. →
in place of ->
. And, if necessary one can still fall back to the ASCII form.
In any case, I think we hardly have to worry about being esoteric for using the ISO 8859-1 character set, which has been around since 1987, has had wide support and now makes up the same initial code points in UTF-8.
@trans, haha, I do use exactly those characters as unicode _alternatives_ to ascii-chars in my pet-project. Thing is, on swedish keyboard those and a lot of other useful codepoints are readily available (and I think on most other european layouts), but the standard us-layout (I'm not too certain here - must differ in linux, mac, win, etc...) don't seem to have many syms at all except the "good ole ones".
Anyway, sorry for beating the dead horse of this issue - I know the crystal view on it - and I think it's the right decision under those premises - crystal is crystal - clear.
Most helpful comment
I don't think the language is inconsistent by using the current notation, it's just different from Ruby. We also considered using parentheses for tuples but it's ambiguous for proc types:
The
<{...}>
syntax isn't bad, but I feel it adds some noise. There's also the thing that right now you can do:so
<{
can be kind of ambiguous if one doesn't use spaces around it.Also, although named tuples are different than in Ruby, this snippet works and runs fine in both Ruby and Crystal:
In Ruby, this kind of hashes are mainly used as options to pass to a method, or as anonymous objects if you don't want to define a type with a name. In Crystal it's exactly the same. So both the concept and the syntax translate well from Ruby to Crystal, only that in Crystal this type is immutable.
I still believe Crystal should be its own language. If it's possible to keep things from Ruby, we keep them. But designing things so things aren't (a bit) confusing if you come from Ruby shouldn't be a goal. For example this shows that it's possible to port some parts of Ruby code, though of course some changes will have to be made. But once the port is over you'd better end with a nice looking syntax.