Project

General

Profile

Actions

Feature #21386

open

Introduce `Enumerable#join_map`

Added by matheusrich (Matheus Richard) about 1 month ago. Updated about 15 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:122345]

Description

Problem

The pattern .map { ... }.join(sep) is extremely common in Ruby codebases:

users.map(&:name).join(", ")

It’s expressive but repetitive (both logically and computationally). This pattern allocates an intermediate array and does two passes over the collection.

Real-world usage is widespread:

Proposal

Just like filter_map exists to collapse a common map + compact, this
proposal introduces Enumerable#join_map, which maps and joins in a single
pass.

users.join_map(", ", &:name)

A Ruby implementation could look like this:

module Enumerable
  def join_map(sep = "")
    return "" unless block_given?

    str = +""
    first = true

    each do |item|
      str << sep unless first
      str << yield(item).to_s
      first = false
    end

    str
  end
end

The name join_map follows the precedent of filter_map, emphasizing the final
operation (join) over the intermediate (map).

Prior Art

Some other languages have similar functionality, but with different names or implementations:

Elixir

Elixir has this via the Enum.map_join/3 function:

Enum.map_join([1, 2, 3], &(&1 * 2))
"246"

Enum.map_join([1, 2, 3], " = ", &(&1 * 2))
"2 = 4 = 6"

Crystal

Crystal, on the other hand, uses Enumerable#join with a block:

[1, 2, 3].join(", ") { |i| -i } # => "-1, -2, -3"

Kotlin

Kotlin has a similar function called joinToString that can take a transformation function:

val chars = charArrayOf('a', 'b', 'c')
println(chars.joinToString() { it.uppercaseChar().toString() }) // A, B, C 

Related issues 1 (1 open0 closed)

Related to Ruby - Feature #21455: Add a block argument to Array#joinOpenActions
Actions #1

Updated by mame (Yusuke Endoh) 8 days ago

Updated by nobu (Nobuyoshi Nakada) 1 day ago

(Prateek Choudhary) wrote in #note-2:

PR: https://github.com/ruby/ruby/pull/13792

This difference is intentional?

[1,2,3].map {|n|[n]}.join(",") #=> "1,2,3"
[1,2,3].join_map(",") {|n|[n]} #=> "[1],[2],[3]"

Updated by nobu (Nobuyoshi Nakada) 1 day ago

This code would show the difference more clearly.

[[1,2],3].map {|n|[n]}.join("|") #=> "1|2|3"
[[1,2],3].join_map("|") {|n|[n]} #=> "[[1, 2]]|[3]"

Updated by matheusrich (Matheus Richard) about 19 hours ago

My expectation is that join_map would behave like map + join

Updated by [email protected] (Prateek Choudhary) about 15 hours ago · Edited

nobu (Nobuyoshi Nakada) wrote in #note-4:

This code would show the difference more clearly.

[[1,2],3].map {|n|[n]}.join("|") #=> "1|2|3"
[[1,2],3].join_map("|") {|n|[n]} #=> "[[1, 2]]|[3]"

Hmm. I missed considering that behavior. In this example though, just the map would return wrapped arrays:

[[1,2],3].map {|n|[n]} #=> [[[1, 2]], [3]]

So somehow doing a map and join is doing more of a flat_map + join.
I can correct the PR to mimic that behavior.

EDIT: The PR has been updated.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like1Like0