Equivalent of &.method for {|obj| obj.method}, but for {|obj| method(obj)}?

I find it very convenient, as I’m sure many do, to use &.method in map and other methods that require a block, when the block is a simple method call, for example: list.map(&.size) instead of list.map { |e| e.size }

But I miss this convenience when I need to do the reverse: call a method and pass the argument to the block. For example, list.map { |e| my_format(e) }.

Has it ever been considered to make a syntactic shortcut for this case? Maybe like list.map(^.my_format) (though I don’t care about the specific syntax). Would there be downsides?

I’ve never contributed to the crystal code itself, but if there’s a chance this would be accepted, I might try my hand at a pull request to add it.

Thoughts?

Yes, checkout https://github.com/crystal-lang/crystal/pull/9218 and Compiler: add implicit block arguments (_1, _2, etc.) by asterite · Pull Request #9216 · crystal-lang/crystal · GitHub.

Compiler: add short block syntax &(..., &1) by asterite · Pull Request #9218 · crystal-lang/crystal · GitHub is basically where it was left. Tho I don’t think that RFC was ever created. Probably would be a good starting point tho.

Thanks! I’ll look at them.

Hmm. As an aside, where can I learn more about &->?

All of the commentary is helpful, and I can’t say I disagree with anyone’s thoughts on it.

Honestly, in my own opinion, map { |file| File.open(file, "r") } is better written exactly that way. It feels to me that the shortcut should be for the most common case, which I think is to only have a single argument. I can keep track of one argument to one method in my head when I’m reading someone else’s code (or my own code the day after I wrote it); more than that feels like it deserves real parameter names.

I am a very experienced perl programmer, I’ve used it since Perl 4, and I love the language, but the thing I like about Crystal is that it encourages me to use named arguments, and never have $_[13] creep into my code. (That’s perl for 13th positional parameter, for those who don’t know the language.)

The basic appeal to me of .map(&.to_i) is not just that it’s concise, but also that there’s no whitespace, so it feels like a single expression. That’s what I’d love to have for the method parameter case.

For that reason I don’t love &foo(&1). It isn’t much shorter than writing it out as {|x|foo(x)}, except for the whitespace the formatter will put into it. And &foo looks a lot like &.foo, which might confuse future me.

The it option like &foo(it) is fine, but I agree with some comments that it doesn’t stand out enough as a placeholder. What happens if I have it=1; x.map(&foo(it))?

That’s why I thought maybe ^.foo or something similar would make me happy. It’s visually unambiguous, there’s only one possible parameter, so no creeping into $_[13] territory, and it covers a what is probably 30-40% of my uses of #map and #each and similar operations on enumerables.

When I get time™ I might look at opening a new RFC for this simple case, with a single argument. And if the community goes a different direction, that’s fine, but hopefully it’ll be possible to conserve this concise, single-parameter case.

(And I’m torn on whether I’d like to see ^.foo.bar be allowed, and expanded to { |obj| foo(obj).bar }… it feels like a common case for me, but in reality, how often does it exist?)

1 Like

Aha… regarding &->, I looked at the crystal source, and it exists in one place: https://github.com/crystal-lang/crystal/blob/5ed476a41adf0719fc540e6c072a521b4c8ed3ec/src/compiler/crystal/syntax/lexer.cr#L444

And that’s enough to tell me what it does. :)

Yes, it’s basically combining two feature of the lang into something that looks like a single operator. I.e. converting a method into a Proc and then passing that to a method that accepts a block.

It’s talked about a bit in Block forwarding - Crystal.

1 Like

@plambert

I agree with your reasoning for using named parameters. As I see it, the block parameters isn’t for the compilers sake, but for those that read the code, and key is just better than &1.

But I’m not a fan of ^.foo. To me, it reads backwards as a method call on ^. It’s probably not lexally possible, but I’d much prefer arr.each puts(^) to keep the proper order of the method call.

But I think it’s a bit microoptimizing the arr.each { |x| puts(x) } case. There’s something to be said for consistency.

Times like these make me wish we have universal function call syntax. So |e| my_format(e) can just be e.my_format

@Xen I cannot disagree. Admittedly, this is all caused by my not liking whitespace in what seems like it is a single “simple” expression.

For example,

arr.map(Math.log2(^)).map(Math.sqrt(^))

looks like a single expression to me. While

arr.map { |v| Math.log2(v) }.map { |v| Math.sqrt(v) }

looks a lot busier and more complicated–for me it’s harder to parse quickly.

But it’s clearly a personal preference–if there’s not much support for this, I won’t be the least bit offended.

I’m not familiar with this; how would this work? How would you call a method on e if e.my_format calls my_format(e) on the default receiver?

This issue is potentially related

2 Likes

Funny, to me it’s the other way around. It’s basically like word spacing, I know runes didn’t have spacing, butI’dratherhaveafewspacestomakethingsmorereadable.

Besides,

arr.map { |v| Math.log2(v) }.map { |v| Math.sqrt(v) }

Isn’t just a single expression, but a series of operations. But hey, I write 2 * 3 / 4 too…

Syntactically, there’s no difference between a method on an object vs a function that accepts it as the first argument