Trong Ruby, có phương thức Array nào kết hợp giữa 'select' và 'map' không?


95

Tôi có một mảng Ruby chứa một số giá trị chuỗi. Tôi cần phải:

  1. Tìm tất cả các phần tử phù hợp với một số vị ngữ
  2. Chạy các phần tử phù hợp thông qua một phép chuyển đổi
  3. Trả về kết quả dưới dạng một mảng

Ngay bây giờ giải pháp của tôi trông như thế này:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Có phương thức Array hoặc Enumerable nào kết hợp lựa chọn và ánh xạ vào một câu lệnh logic duy nhất không?


5
Hiện tại không có phương pháp nào, nhưng có một đề xuất để thêm một phương pháp vào Ruby: bug.ruby-lang.org/issues/5663
stefankolb

Các Enumerable#grepphương pháp thực hiện chính xác những gì được yêu cầu và đã được trong Ruby trong hơn mười năm. Nó có một đối số vị từ và một khối chuyển đổi. @hirolau đưa ra câu trả lời đúng duy nhất cho câu hỏi này.
inopinatus

2
Ruby 2.7 đang giới thiệu filter_mapcho mục đích chính xác này. Thêm thông tin ở đây .
SRack

Câu trả lời:


114

Tôi thường sử dụng mapcompactcùng với các tiêu chí lựa chọn của tôi như một hậu tố if. compactthoát khỏi nils.

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 

1
Ah-ha, tôi đang cố gắng tìm cách bỏ qua các lỗ không do khối bản đồ của tôi trả về. Cảm ơn!
Seth Petry-Johnson,

Không sao, tôi thích nhỏ gọn. nó không phô trương ngồi ngoài đó và làm công việc của nó. Tôi cũng thích phương pháp này để xâu chuỗi các hàm có thể liệt kê cho các tiêu chí lựa chọn đơn giản vì nó rất dễ khai báo.
Jed Schneider,

4
Tôi không chắc liệu map+ compactcó thực sự hoạt động tốt hơn hay không injectvà đã đăng kết quả điểm chuẩn của tôi lên một chủ đề liên quan: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton

3
điều này sẽ loại bỏ tất cả các nils, cả nils gốc và những nils không đạt tiêu chí của bạn. Vì vậy, hãy chú ý
dùng1143669

1
Nó không hoàn toàn loại bỏ chaining mapselect, nó chỉ là compactmột trường hợp đặc biệt rejectmà các công trình trên nils và thực hiện phần nào tốt hơn do đã được thực hiện trực tiếp trong C.
Joe Atzberger

53

Bạn có thể sử dụng reducecho việc này, chỉ yêu cầu một thẻ:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

Nói cách khác, hãy khởi tạo trạng thái thành những gì bạn muốn (trong trường hợp của chúng tôi, một danh sách trống để điền: [] :), sau đó luôn đảm bảo trả về giá trị này với các sửa đổi cho từng phần tử trong danh sách ban đầu (trong trường hợp của chúng tôi là phần tử đã sửa đổi đẩy lên danh sách).

Đây là cách hiệu quả nhất vì nó chỉ lặp lại danh sách với một lần vượt qua ( map+ selecthoặccompact yêu cầu hai lần vượt qua).

Trong trường hợp của bạn:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

20
Không có each_with_objectý nghĩa hơn một chút? Bạn không phải trả về mảng ở cuối mỗi lần lặp lại khối. Bạn có thể đơn giản làm my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha

@henrebotha Có lẽ nó đúng. Tôi đến từ một nền tảng chức năng, đó là lý do tại sao tôi tìm thấy reduceđầu tiên 😊
Adam Lindberg

33

Ruby 2.7+

Hiện có!

Ruby 2.7 đang giới thiệu filter_map cho mục đích chính xác này. Nó mang tính thành ngữ và biểu diễn, và tôi hy vọng nó sẽ sớm trở thành tiêu chuẩn.

Ví dụ:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

Đây là một bài đọc hay về chủ đề này .

Hy vọng điều đó hữu ích cho ai đó!


1
Bất kể tôi nâng cấp thường xuyên như thế nào, một tính năng thú vị luôn có trong phiên bản tiếp theo.
mlt

Đẹp. Một vấn đề có thể là do filter, selectfind_allđồng nghĩa, giống như mapcollecthiện tại, nên có thể khó nhớ tên của phương pháp này. Có filter_map, select_collect, find_all_maphoặc filter_collect?
Eric Duminil

19

Một cách khác để tiếp cận vấn đề này là sử dụng câu hỏi mới (liên quan đến câu hỏi này) Enumerator::Lazy:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

Các .lazyphương thức trả về một Enumerator lười biếng. Gọi .selecthoặc .maptrên một điều tra viên lười biếng trả về một điều tra viên lười biếng khác. Chỉ một khi bạn gọi .uniq, nó thực sự buộc người điều tra và trả về một mảng. Vì vậy, điều hiệu quả xảy ra là của bạn .select.mapcác cuộc gọi được kết hợp thành một - bạn chỉ lặp lại @linesmột lần để thực hiện cả hai .select.map .

Bản năng của tôi là reducephương pháp của Adam sẽ nhanh hơn một chút, nhưng tôi nghĩ điều này dễ đọc hơn nhiều.


Hệ quả chính của việc này là không có đối tượng mảng trung gian nào được tạo cho mỗi lần gọi phương thức tiếp theo. Trong @lines.select.maptrường hợp bình thường , selecttrả về một mảng sau đó được sửa đổi bởi map, một lần nữa trả về một mảng. Để so sánh, đánh giá lười biếng chỉ tạo mảng một lần. Điều này hữu ích khi đối tượng bộ sưu tập ban đầu của bạn lớn. Thiết bị cho phép bạn làm việc với điều tra viên vô hạn - ví dụ random_number_generator.lazy.select(&:odd?).take(10).


4
Để mỗi người của họ. Với loại giải pháp của mình, tôi có thể nhìn vào tên phương thức và ngay lập tức biết rằng tôi sẽ chuyển đổi một tập hợp con của dữ liệu đầu vào, làm cho nó trở thành duy nhất và sắp xếp nó. reducenhư một chuyển đổi "làm mọi thứ" luôn cảm thấy khá lộn xộn đối với tôi.
henrebotha

2
@henrebotha: Thứ lỗi cho tôi nếu tôi đã hiểu sai ý của bạn, nhưng đây là một điểm rất quan trọng: không đúng khi nói rằng "bạn chỉ lặp lại @linesmột lần để làm cả hai .select.map". Sử dụng .lazykhông có nghĩa là các hoạt động được xâu chuỗi trên một liệt kê lười biếng sẽ bị "thu gọn" thành một lần lặp duy nhất. Đây là một sự hiểu lầm phổ biến về các hoạt động chuỗi wrt đánh giá lười biếng trên một tập hợp. (Bạn có thể kiểm tra điều này bằng cách thêm một putscâu lệnh vào đầu khối selectmaptrong ví dụ đầu tiên. Bạn sẽ thấy rằng chúng in cùng một số dòng)
pje

1
@henrebotha: và nếu bạn loại bỏ .lazynó, nó sẽ in cùng một số lần. Đó là quan điểm của tôi — mapkhối của bạn và khối của bạn selectđược thực thi cùng một số lần trong các phiên bản lười biếng và háo hức. Phiên bản lười biếng không "kết hợp cuộc gọi của bạn .select.map"
pje

1
@pje: Trên thực tế, lazy kết hợp chúng vì một phần tử không đạt selectđiều kiện sẽ không được chuyển đến map. Nói cách khác: chi tiêu trước lazygần tương đương với việc thay thế selectmapbằng một khối duy nhất reduce([])và "thông minh" biến selectkhối thành điều kiện tiên quyết để đưa vào reducekết quả của.
henrebotha

1
@henrebotha: Tôi nghĩ đó là một sự tương tự gây hiểu lầm cho việc đánh giá lười biếng nói chung, bởi vì sự lười biếng không thay đổi độ phức tạp về thời gian của thuật toán này. Đây là quan điểm của tôi: trong mọi trường hợp, một bản đồ chọn-sau đó sẽ luôn thực hiện cùng một số lượng tính toán như phiên bản háo hức của nó. Nó không tăng tốc bất cứ thứ gì, nó chỉ thay đổi thứ tự thực hiện của mỗi lần lặp — hàm cuối cùng trong chuỗi "kéo" các giá trị cần thiết từ các hàm trước theo thứ tự ngược lại.
pje

13

Nếu bạn có một selectcó thể sử dụng casetoán tử ( ===), greplà một lựa chọn thay thế tốt:

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

Nếu chúng ta cần logic phức tạp hơn, chúng ta có thể tạo lambdas:

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]

Nó không phải là một sự thay thế - đó là câu trả lời đúng duy nhất cho câu hỏi.
inopinatus

@inopinatus: Không còn nữa . Tuy nhiên, đây vẫn là một câu trả lời tốt. Tôi không nhớ đã nhìn thấy grep với một khối nào khác.
Eric Duminil

8

Tôi không chắc là có. Các mô-đun Enumerable , mà thêm selectmap , không hiển thị một.

Bạn sẽ được yêu cầu vượt qua hai khối để select_and_transform phương thức, điều này sẽ khiến IMHO hơi không trực quan.

Rõ ràng, bạn có thể chuỗi chúng lại với nhau, dễ đọc hơn:

transformed_list = lines.select{|line| ...}.map{|line| ... }

3

Câu trả lời đơn giản:

Nếu bạn có n bản ghi, và bạn muốn selectmapdựa trên điều kiện thì

records.map { |record| record.attribute if condition }.compact

Ở đây, thuộc tính là bất kỳ thứ gì bạn muốn từ bản ghi và điều kiện bạn có thể đặt bất kỳ kiểm tra nào.

nhỏ gọn là để loại bỏ số không không cần thiết ra khỏi điều kiện đó nếu


1
Bạn có thể sử dụng tương tự với điều kiện trừ khi quá. Như bạn tôi đã hỏi.
Sk. Irfan

2

Không, nhưng bạn có thể làm như thế này:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

Hoặc thậm chí tốt hơn:

lines.inject([]) { |all, line| all << line if check_some_property; all }

14
reject(&:nil?)về cơ bản giống như compact.
Jörg W Mittag,

Vâng, vì vậy phương pháp tiêm thậm chí còn tốt hơn.
Daniel O'Hara,

2

Tôi nghĩ rằng cách này dễ đọc hơn, vì chia nhỏ các điều kiện bộ lọc và giá trị được ánh xạ trong khi vẫn rõ ràng rằng các hành động được kết nối với nhau:

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

Và, trong trường hợp cụ thể của bạn, hãy loại bỏ resulttất cả các biến cùng nhau:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end

1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

Trong Ruby 1.9 và 1.8.7, bạn cũng có thể xâu chuỗi và quấn các trình vòng lặp bằng cách không truyền khối cho chúng:

enum.select.map {|bla| ... }

Nhưng nó không thực sự khả thi trong trường hợp này, vì các loại khối trả về giá trị của selectmapkhông khớp với nhau. Nó có ý nghĩa hơn cho một cái gì đó như thế này:

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS, điều tốt nhất bạn có thể làm là ví dụ đầu tiên.

Đây là một ví dụ nhỏ:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

Nhưng những gì bạn thực sự muốn là ["A", "B", "C", "D"].


Tôi đã thực hiện một tìm kiếm rất ngắn trên web vào đêm qua cho "phương pháp chuỗi trong Ruby" và có vẻ như nó không được hỗ trợ tốt. Tho, chắc mình cũng nên thử rồi ... còn nữa, sao bạn nói các kiểu đối số khối không khớp nhau vậy? Trong ví dụ của tôi, cả hai khối đều lấy một dòng văn bản từ mảng của tôi, phải không?
Seth Petry-Johnson

@Seth Petry-Johnson: Vâng, xin lỗi, ý tôi là giá trị trả về. selecttrả về giá trị Boolean-ish quyết định có giữ phần tử hay không, maptrả về giá trị đã chuyển đổi. Bản thân giá trị được chuyển đổi có thể sẽ là trung thực, vì vậy tất cả các phần tử được chọn.
Jörg W Mittag,

1

Bạn nên thử sử dụng thư viện Rearmed Ruby của tôi, trong đó tôi đã thêm phương thức Enumerable#select_map. Đây là một ví dụ:

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

select_maptrong thư viện này chỉ thực hiện cùng một select { |i| ... }.map { |i| ... }chiến lược từ nhiều câu trả lời ở trên.
Jordan Sitkin

1

Nếu bạn không muốn tạo hai mảng khác nhau, bạn có thể sử dụng compact!nhưng hãy cẩn thận về nó.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

Thật thú vị, compact!có một loại bỏ nil tại chỗ. Giá trị trả về của compact!là cùng một mảng nếu có thay đổi nhưng nil nếu không có nils.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

Sẽ là một lớp lót.


0

Phiên bản của bạn:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Phiên bản của tôi:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

Điều này sẽ thực hiện 1 lần lặp (ngoại trừ sắp xếp) và có thêm phần thưởng là giữ tính duy nhất (nếu bạn không quan tâm đến uniq, thì chỉ cần tạo kết quả là một mảng và results.push(line) if ...


-1

Đây là một ví dụ. Nó không giống với vấn đề của bạn, nhưng có thể là những gì bạn muốn hoặc có thể đưa ra manh mối cho giải pháp của bạn:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.