crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.5k stars 1.62k forks source link

Usefulness of `Number.[]` #13021

Open straight-shoota opened 1 year ago

straight-shoota commented 1 year ago

Number.[] is a convenience macro that allows to define an Array of a specific numeric item type.

Similar macros exist in the form of Number.slice and Number.static_array for Slice and StaticArray collections, respectively.

But other than those I doubt this Array variant is very useful. It's semantically identical to an array literal with item type.

Float64[1, 2, 3, 4]
[1, 2, 3, 4] of Float64

The only difference is the token of. And the type is declared as a suffix instead of a prefix. Thus it's marginally shorter, but that's the only marginal benefit. The array literal syntax on the other hand is universal: it can be used with any type. Having a special alternative just for numeric types is unnecessary. It requires learning another concept for something that can be expressed with basic language syntax. And actually, there is a good chance for misinterpretation of what this macro means. In the type grammar, Float64[4] is a short representation of StaticArray(Float64, 4). And StaticArray also has a convenience macro of the same name, which could easily cause confusion. In the context of StaticArray[1, 2, 3, 4] creating a static array and Float[4] defining a static array type, one can reasonably doubt what Float64[1, 2, 3, 4] actually means.

I'm wondering if it makes sense to keep this convenience macro when it doesn't really provide much convenience and could instead be confusing, or if we should deprecate it.

straight-shoota commented 1 year ago

Alternatively, we can at least simplify the implementation to use an array literal (which then gets expanded into a very similar code by the compiler, but the point is to avoid unnecessary repetition):

diff --git i/src/number.cr w/src/number.cr
index e7b21225e..8734eabd8 100644
--- i/src/number.cr
+++ w/src/number.cr
@@ -79,12 +79,7 @@ struct Number
   # [1, 2, 3, 4] of Int64 # : Array(Int64)
   # ```
   macro [](*nums)
-    Array({{@type}}).build({{nums.size}}) do |%buffer|
-      {% for num, i in nums %}
-        %buffer[{{i}}] = {{@type}}.new({{num}})
-      {% end %}
-      {{nums.size}}
-    end
+    [{{ *nums }}] of {{ @type }}
   end

   # Creates a `Slice` of `self` with the given values, which will be casted
asterite commented 1 year ago

This macro became obsolete once we introduced autocasting. Without autocasting it makes sense because things like [1, 2] of Float64 didn't use to compile.

HertzDevil commented 1 year ago

Because of the use of Number.new, you can do this:

Int32[1.2, 3.4_f32, "56"] # => [1, 3, 56]

but not this:

[1.2, 3.4_f32, "56"] of Int32 # Error: expected argument #2 to 'Pointer(Int32)#[]=' to be Int32, not Float64

Though I wouldn't miss being able to do the former as those conversions seem like something the caller should do themself right at the call site.

(Also, oddly enough, Number.static_array and Number.slice use .new! instead of .new.)

HertzDevil commented 1 year ago

A less intrusive solution is to move this macro to Number.array instead, keeping the .new semantics. I don't think there are any drawbacks in permitting e.g. BigInt.array(1, 2, 3).

straight-shoota commented 1 year ago

Ah right, Big numbers don't support autocasting. So that's probably the only left practical use case / benefit over an array literal. [1, 2, 3] of BigInt doesn't compile.

HertzDevil commented 11 months ago

Speaking of the type grammer, today I was surprised that x : Int32[] parses to (x : Int32).[](), which calls Nil#[] because the receiver is a TypeDeclaration that has no initializer.