Remove `{} of Typle` and `[] of Type`

There are too much syntax to create an Array or a Hash, it isn’t needed to have 3 ways to do the same thing. This also confuse newcomers, and experienced ones. This isn’t also obvious what is the of word (I guess it’s a keyword), a bit too magic.

Example:

This is the same array:

p [0, "a"]
p Array(String | Int32){0, "a"}
p [0, "a"] of String | Int32

This is the same hash:

p {"a" => 0}
p Hash(String, Int32){"a" => 0}
# Requires parentheses
p ({"a" => 0} of String => Int32)
2 Likes

It was discussed before in https://github.com/crystal-lang/crystal/issues/3399
Maybe it is time to rediscuss, but for me [] of String | Int32 looks slightly shorter then Array(String | Int32).new and {} of String => Int32 is definitely easier to understand than Hash(String, Int32).new.
For a nonempty arrays\hashes this syntax is pretty useless though (on the other hand, it could be useful too. You just take existing array literal declaration and add of T1 | T2... to it to refine its type).

I guess we could remove of because one can always write it with the longer form. I think of is the only infix keyword we have left.

Maybe this is only because I spend 99% of my coding in ruby:

Array(String | Int32).new

Is this syn. sugar for a method call of initialize with a Type as parameter?

I would expect something like Array.new(String, 8)

“But wait! The core developers are not open to change!” - Reddit user #284715

@girng that’s an unrelated issue. This was about missing parentheses with of, not to keep/remove them.

This isn’t just about Syntax Sugar, but also about correctness. The of syntax hide the fact we instantiate a new generic object Object(T).new. The Hash like litteral (https://crystal-lang.org/reference/syntax_and_semantics/literals/hash.html) {} uses []= to create initial entries.

This may be slightly offtopic but too much sugar really makes things hard for beginners in Crystal.

Take for instance this:

class X
  @service_files : {} of String => String

>  expecting token 'CONST', not '}'

Well, looks correct no? I define the variable as a hash, with String key and String value. Wrong … did not work.

Hmmmm, maybe you need a specific different type.

class X
   @service_files : Hash( String => String )

>  instance variable '@service_files' of Shared::Service was not initialized directly in all of the 'initialize' methods, rendering it nilable. Indirect initialization is not supported.

No … more errors. And you keep going down the list of possibilities until you get fed up and write your code to simply auto initialize like this and are left with a bad taste because it just does not feel the correct way of doing it ( especially when you come from different language ).

class X
    def initialize
        @service_files = {} of String => String    
    end

> Yay, works ... but it feels like a hack rather then the correct way. 

It was only until two days ago, when i noticed a piece of code on Gitter, where somebody finally used the ( in my eyes ) correct way of doing it.

class X

 @service_files : Hash(String, String) = {} of String => String

This is frankly way too long and is undocumented. Searched for days trying to find a answer how to properly write this and there was no documented example available.

Good luck on figuring out that this is the way to write this because when you have Hash, {} of, and other ways of writing, it becomes very fast confusing. And yes, i had the answer in my hands all the time but your trying so many different combinations to figure out what, how, why???

I expect to see something

class X

 @service_files : Hash(String, String) = {} of String => String

to work like

class X

 @service_files : {} of String => String

 @service_files : {"a" => 0} of String => String

Trust me when i say that Crystal at times really confuses me as a beginner. PHP has a bunch of gotcha’s but Crystal really makes things confusing.

Well, this also works:

class X

 @service_files = {} of String => String

Slaps hand on head.

So you try:

class X

 @service_files = Hash( String, String )

> But that does not work

They look the same in my eyes but they are not. And the documentation is very, very unclear in this regards, making it impossible to learn by example.

I have no problem figuring out { } of = Hash and [] of = Array but the initialization on a class level was annoying.

PS: Personally i noticed mostly the {} / [] of syntax as in my head {} = Hash, [] = Array. As a web developer your used to this pattern more with stuff like Jsons. The syntax is more clean to write.

{"a" => 0} of String => Int32
Hash(String, Int32){"a" => 0}

I dislike the “Hash” version because its less clean to read. Most of my code uses the “of” syntax.
The fact that {“a” => 0} of String => Int32 requires parentheses in specific situations looks more like a issue with the Crystal compiler. And yes, it confuses new users also. It needs to be a compiler check or the compiler needs to accept the syntax without the parentheses.

That is why I asked if all this “char-salad” is syn sugar for @service_files = Hash.new( String, String ) which would be absolute clear to me.
This @service_files = Hash( String, String ) let’s me ask why Hash class does not use a constructor call.
The colon : ensures I missed something…

=> In general I really like the ruby like syntax but in detail I am absolutely confused by crystal ways that are somehow unexplained.
One thing I hate in ruby as well are the 20 ways to write the same that force me to learn foreign ruby-styles more than using the time to understand foreign algorithms and ideas.

There is already Bytes[1u8, 0u8, 0u8], why won’t we have Array[0, 1, 2] and Array(Int32 | String)[0, 1, 2] too?

Because you can write [0, 1, 2] and [0, 1, 2] of Int32 | String

@bachmarc
Array(String | Int32) is a name of type. This is a syntax for a generic types - types that has compile-time parameters.
Array(String | Int32).new means we are calling new method of this type.

Now about :.
Full syntax for a field declaration is @field : type = value. You can skip : type part though, and compiler will try to deduce type from = value part. You can also skip = value part, but then you have to initialize a field in initialize method. Compiler just have to know what type @field has, so he will use any available source to get this information - either : type part, or = value part, or check of initialize methods.

Now to the example with @service_files.
Hash( String, String ) is a type name, Hash(String, String).new or {} of String => String are both an expressions that represents empty Hash.
Full definition well be
@service_files : Hash( String, String ) = Hash(String, String).new.
You can skip : Hash( String, String ) part as the type is evident (for a compiler) from the following part.
So, declaration become @service_files = Hash(String, String).new.
Now you can use a syntax suger to replace it with @service_files = {} of String => String. By the way, in this forum thread we are discussing removal of this sugar.

If you instead remove the = value part, like so:
@service_files : Hash(String, String)
The declaration will be correct, but now compiler knows nothing about initial value of field, it knows only it’s type.

Declaration @service_files = Hash(String, String) is incorrect because = means that we are defining initial value, and Hash(String, String) isn’t value, it’s a type name.
Declaration @service_files : {} of String => String is incorrect because : means that we are defining type of field, and {} of String => String is a shortcut for a Hash(String, String).new, it’s a value (empty hash), not a type.

Of course compiler errors about these declarations could be improved, right now they are pretty cryptic. But overall concept of types&values seems pretty simple and evident to me. Documentation could have some improvement too, right now it talks mostly about initialization in an initialize, mentions @field : type and @field = value once and completely skips @field : type = value syntax.

3 Likes