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

Replace Indexable#at with #fetch #6296

Merged

Conversation

AlexWayfer
Copy link
Contributor

Add more implementations similar to Hash#fetch

Inspired by #6293 (comment)

@AlexWayfer AlexWayfer mentioned this pull request Jun 29, 2018
@j8r
Copy link
Contributor

j8r commented Jun 29, 2018

Please open an issue to talk about this, what the community thinks about at, fetch and [].
When we will found a consensus solution on this topic then the changes could be made :)

@AlexWayfer AlexWayfer force-pushed the stdlib/replace/Indexable#at/#fetch branch from ac293f6 to e7e4f5f Compare June 29, 2018 16:15
@AlexWayfer
Copy link
Contributor Author

AlexWayfer commented Jun 29, 2018

Please open an issue to talk about this, what the community thinks about at, fetch and [].
When we will found a consensus solution on this topic then the changes could be made :)

Why do you need for an issue for this if there is already PR?

It's even available by /issues link instead of /pull: https://github.com/crystal-lang/crystal/issues/6296 (GitHub automatically convert regular link to <a ...>#6296</a>)

(UPD: /issue fixed with /issues)

And PRs in search results along with issues: https://github.com/crystal-lang/crystal/search?q=indexable+at&type=Issues

Anybody can discuss anything here about code, approach, needing.

@asterite
Copy link
Member

By the way, I like this PR. It makes the API a bit more consistent with Hash, which also has fetch, despite the fact that it's a breaking change.

@ysbaddaden
Copy link
Contributor

Huum, fetch for arrays disturbs me, because fetch is associated to hashmaps in my head. I've been wondering, why do we have at or fetch without a block or default value? Do they have any value compared to []? I even wonder whether unsafe_at(i) is really useful. Isn't it just aliasing to_unsafe[i] usually?

I'd be fine with .fetch(index) { default } only.

@asterite
Copy link
Member

Well, an array could be viewed as a mapping from integers to some values, with missing keys being those outside the array's range. For example in PHP arrays are both arrays and hashes (not that it means something, but it's a nice mathematical view).

@jkthorne
Copy link
Contributor

Can at and fetch be aliases?

@asterite
Copy link
Member

They can, but we try to avoid aliases in Crystal.

@hugoabonizio
Copy link
Contributor

Is there a reason to this syntax be invalid?

array[100] { nil }

@asterite
Copy link
Member

It looks a bit weird. And it's not valid Ruby syntax either (maybe for a reason).

@straight-shoota
Copy link
Member

I like the idea. Both Indexable and Hash describe collections that allow directly accessing it's members by a key. It makes sense to use the same names for this concept.

For reference: in Java both Map and List have a get method. I'd like #get for Array and Indexable in Crystal, too. But I don't think it would necessarily be better than #fetch and certainly not worth breaking both APIs.

#at always sounded a bit strange to me and I find it hard to make sense of it.

@straight-shoota
Copy link
Member

We could think about renaming values_at as well. It doesn't necessarily need to change because the name makes sense even with out #at. But maybe #fetch_all would be a more intuitive idea.

@AlexWayfer
Copy link
Contributor Author

We could think about renaming values_at as well

For me, at is for specific position (like []), and fetch is more softer, like "get an element or return/do something (default)".

It doesn't necessarily need to change because the name makes sense even with out #at.

Are you about renaming #values_at to #values? I disagree:

{ a: 1, b: 2, c: 3 }.values_at(:c, :a) #=> [3, 1]
{ a: 1, b: 2, c: 3 }.values #=> [1, 2, 3]

And #values_at sounds more correct than #values(:c, :a).

@asterite
Copy link
Member

Note that we could add a[1] { ... } as a valid syntax, though. It would at least solve the issue of finding the perfect name for the operation :-P

@straight-shoota
Copy link
Member

@AlexWayfer No, I meant renaming #values_at to something else (for example #fetch_all), not just dropping the _at suffix.

@AlexWayfer
Copy link
Contributor Author

@AlexWayfer No, I meant renaming #values_at to something else (for example #fetch_all), not just dropping the _at suffix.

Oh, OK, I understood, thanks.

It may be fetch + at = #fetch_at. 😅

This is incorrect, I think. #fetch_from may be more correct.

@AlexWayfer
Copy link
Contributor Author

Just for notice: checks fail not because of code.

@hugoabonizio
Copy link
Contributor

This is valid Ruby (2.5) code:

def [](idx, &block)
  puts idx + block.call
end

self[1] { 2 }

https://repl.it/repls/GreedyConsciousJava

@asterite
Copy link
Member

@hugoabonizio Cool! Let's add that to Crystal too.

@asterite
Copy link
Member

asterite commented Jul 20, 2018

Here's an initial diff, if someone wants to take care of this:

diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr
index ecdf0db3c..4e52e658d 100644
--- a/spec/compiler/parser/parser_spec.cr
+++ b/spec/compiler/parser/parser_spec.cr
@@ -124,6 +124,10 @@ module Crystal
     it_parses "foo[] /2", Call.new(Call.new("foo".call, "[]"), "/", 2.int32)
     it_parses "foo[1] /2", Call.new(Call.new("foo".call, "[]", 1.int32), "/", 2.int32)
     it_parses "[1] /2", Call.new(([1.int32] of ASTNode).array, "/", 2.int32)
+    it_parses "foo[1] { 2 }", Call.new("foo".call, "[]", args: [1.int32] of ASTNode, block: Block.new([] of Var, 2.int32))
+    it_parses "foo[1]? { 2 }", Call.new("foo".call, "[]?", args: [1.int32] of ASTNode, block: Block.new([] of Var, 2.int32))
+    assert_syntax_error "foo[1, &x] { 2 }", "block already specified with &"
+    assert_syntax_error "foo[1, &x]? { 2 }", "block already specified with &"
 
     it_parses "!1", Not.new(1.int32)
     it_parses "- 1", Call.new(1.int32, "-")
diff --git a/spec/std/indexable_spec.cr b/spec/std/indexable_spec.cr
index 344e5bda9..e2110bebb 100644
--- a/spec/std/indexable_spec.cr
+++ b/spec/std/indexable_spec.cr
@@ -114,4 +114,12 @@ describe Indexable do
     return_value.should eq(indexable)
     last_element.should eq(3)
   end
+
+  {% if compare_version(Crystal::VERSION, "2.5.1") > 0 %}
+    it "does [] with block" do
+      indexable = SafeIndexable.new(5)
+      (indexable[1] { 2 }).should eq(1)
+      (indexable[10] { 2 }).should eq(2)
+    end
+  {% end %}
 end
diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr
index 6d2d509f2..6bfef1b81 100644
--- a/src/compiler/crystal/syntax/parser.cr
+++ b/src/compiler/crystal/syntax/parser.cr
@@ -715,13 +715,6 @@ module Crystal
           @wants_regex = false
           next_token
 
-          if call_args
-            args = call_args.args
-            block = call_args.block
-            block_arg = call_args.block_arg
-            named_args = call_args.named_args
-          end
-
           if @token.type == :"?"
             method_name = "[]?"
             next_token_skip_space
@@ -730,6 +723,16 @@ module Crystal
             skip_space
           end
 
+          if call_args
+            args = call_args.args
+            block = call_args.block
+            block_arg = call_args.block_arg
+            named_args = call_args.named_args
+          end
+
+          curly_block = parse_curly_block(block_arg)
+          block = curly_block if curly_block.is_a?(Block)
+
           atomic = Call.new(atomic, method_name, args: (args || [] of ASTNode), block: block, block_arg: block_arg, named_args: named_args, name_column_number: column_number).at(location)
           atomic.name_size = 0 if atomic.is_a?(Call)
           atomic
diff --git a/src/indexable.cr b/src/indexable.cr
index d5dc7df4a..84149f609 100644
--- a/src/indexable.cr
+++ b/src/indexable.cr
@@ -73,6 +73,18 @@ module Indexable(T)
     at(index)
   end
 
+  # Returns the element at the given *index*, if in bounds,
+  # otherwise executes the given block and returns its value.
+  #
+  # ```
+  # a = [:foo, :bar]
+  # a[0] { :baz } # => :foo
+  # a[2] { :baz } # => :baz
+  # ```
+  def [](index : Int)
+    at(index) { yield }
+  end
+
   # Returns the element at the given *index*.
   #
   # Negative indices can be used to start counting from the end of the array.

Adding the [] methods with a block variant is currently supported, it's just the [] { ... } syntax that's missing. So we can add all of the functionality for the next release. I just don't have time to implement all of the block variants (Hash, String, and many others would need them). Eventually we could remove the at and fetch variants, though I'm not sure because we still need the variant that accept a second argument as the default value (instead of a block with the default value).

@straight-shoota
Copy link
Member

/cc #6324

@RX14
Copy link
Contributor

RX14 commented Jul 21, 2018

@asterite a[x]? || "default value" is short enough, honestly. a[x] { "default value" } if your value type contains nil.

@asterite
Copy link
Member

@RX14 I guess that's fine. We would eliminate at and fetch, and just keep [] and []?, which is much simpler (although very different than Ruby with all its aliases).

If someone want to go for it, you can use my diff above as a start :-)

@AlexWayfer
Copy link
Contributor Author

@asterite a[x]? || "default value" is short enough

Doesn't work for falsey values, I guess.

We would eliminate at and fetch, and just keep [] and []?, which is much simpler (although very different than Ruby with all its aliases).

With blocks?

@hugoabonizio Cool! Let's add that to Crystal too.

Let's add the ability to define #[](arg, &block) method?

Array#[] in Ruby doesn't receive blocks:

image

@asterite
Copy link
Member

Doesn't work for falsey values, I guess.

That's where you would use the block variant.

Array#[] in Ruby doesn't receive blocks

Yes, in Ruby 2.5.1, although it seems they forgot to document it.

@AlexWayfer
Copy link
Contributor Author

Yes, in Ruby 2.5.1, although it seems they forgot to document it.

No, they didn't. It's valid for some reason, but doesn't work as expected.

https://repl.it/repls/SmartQuestionableLegacy

@ysbaddaden
Copy link
Contributor

Having the triplet [], [] {} and []? and removing fetch and at methods sounds good to me!

@straight-shoota
Copy link
Member

I don't like this idea at first sight.

foo.fetch(key, default) is a simple and easy to understand interface. Replacing that with foo[key]? || default or foo[key] { default } seems like a step backwards. The first one accuratly describes it's purpose, but whether it can be used depends on the return type. This is bad. If for some reason the return type changes to include falsey values, you'd have to first notice this doesn't work anymore and change it everywhere.
Well, we could maybe just use foo[key] { default } everywhere.

But IMHO foo.fetch(key, default) (or even more expressive: foo.fetch(key, default: default) is way better. Please consider these consequences before removing it.

@j8r
Copy link
Contributor

j8r commented Jul 22, 2018

#[key, default] is also an option.
At least when using signs there is no wording debate 😄

@AlexWayfer
Copy link
Contributor Author

Is it OK with change unsafe_at to unsafe_fetch?

@AlexWayfer yes +1

Source

Let's remove fetch with just one argument.

Source.

No we don't have fetch with one argument, and we have unsafe_fetch with just one argument. Is it OK?

@AlexWayfer
Copy link
Contributor Author

Conflicts resolved.

def [](key)
fetch(key)
fetch(key) do
if (block = @block) && key.is_a?(K)
Copy link
Contributor

@Sija Sija Sep 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part below seems wrong to me:

if ... key.is_a?(K)
else
  raise KeyError.new "Missing hash key: #{key.inspect}"
end

Why wrong key type should trigger such error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it was so (look below). This code just moved.

And, I guess, because Ruby works like that:

If the key can't be found, there are several options: With no other arguments, it will raise a KeyError exception;

https://devdocs.io/ruby~2.5/hash#method-i-fetch

Example: https://repl.it/@AlexWayfer/UnlinedGlossyQuotient

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, I guess, because Ruby works like that:

Ruby doesn't have strong typing, and so this specific edge-case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sija If the is_a?(K) check is not there, invoking the block won't compile. I added that check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@asterite Would that be wrong thing to do ™? is this check is present inside of other Hash methods?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean. There's an issue about restricting these methods and it's a PITA to use, so it as never done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, it looked odd to me to have two of those guards as a logic for handling the case of missing key, but your explanation makes total sense, thanks!

@AlexWayfer AlexWayfer force-pushed the stdlib/replace/Indexable#at/#fetch branch from 775f2a1 to cdc06a1 Compare September 6, 2018 21:13
@AlexWayfer AlexWayfer force-pushed the stdlib/replace/Indexable#at/#fetch branch from cdc06a1 to ec67e39 Compare September 27, 2018 20:20
@AlexWayfer
Copy link
Contributor Author

Conflicts resolved again.

Add more implementations similar to `Hash#fetch`.

No we have:

* `[key]` (convenience shortcut for `fetch(key) { raise }`)
* `[key]?` (convenience shortcut for `fetch(key) { nil }`)
* `fetch(key, default)`
* `fetch(key) { default }`
@AlexWayfer AlexWayfer force-pushed the stdlib/replace/Indexable#at/#fetch branch from ec67e39 to 00f9271 Compare September 27, 2018 22:20
@AlexWayfer
Copy link
Contributor Author

Sorry, fails fixed.

@delef
Copy link
Contributor

delef commented Oct 25, 2018

Is it possible to add this PR to 0.27.0?

@RX14
Copy link
Contributor

RX14 commented Oct 26, 2018

@bcardiff I think this would be nice in 0.27.0 too

Copy link
Member

@bcardiff bcardiff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also removes Hash#fetch with no block and make Hash#[] the only way to access without fall back value. I just needed to view the change since the initial description does not highlight this change.

I think there is a missing AlwaysInline. After that is done we can merge this before 0.27

src/indexable.cr Show resolved Hide resolved
Co-Authored-By: AlexWayfer <alex.wayfer@gmail.com>
@RX14 RX14 added this to the 0.27.0 milestone Oct 28, 2018
@RX14 RX14 merged commit 9d6fc90 into crystal-lang:master Oct 28, 2018
@AlexWayfer AlexWayfer deleted the stdlib/replace/Indexable#at/#fetch branch October 28, 2018 21:02
jacksonrayhamilton added a commit to jacksonrayhamilton/prax.cr that referenced this pull request Nov 17, 2018
ysbaddaden pushed a commit to ysbaddaden/prax.cr that referenced this pull request Nov 18, 2018
davidkellis pushed a commit to davidkellis/linalg that referenced this pull request Dec 17, 2018
Per crystal-lang/crystal#6296, calls to Slice#unsafe_at need to be replaced with a call to Slice#unsafe_fetch.
dscottboggs added a commit to dscottboggs/bindgen that referenced this pull request Dec 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet