Учет списка в Ruby

Чтобы сделать эквивалент Python-списков, я делаю следующее:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

Есть ли лучший способ сделать это... возможно, с помощью одного вызова метода?

Ответ 1

Если вы действительно этого хотите, вы можете создать метод Array # comprehend следующим образом:

class Array
  def comprehend(&block)
    return self if block.nil?
    self.collect(&block).compact
  end
end

some_array = [1, 2, 3, 4, 5, 6]
new_array = some_array.comprehend {|x| x * 3 if x % 2 == 0}
puts new_array

Печать

6
12
18

Я, вероятно, просто сделаю так, как вы это сделали.

Ответ 2

Как "бой":

some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact

Слегка чище, по крайней мере, на мой вкус, и в соответствии с быстрым эталонным тестом примерно на 15% быстрее, чем ваша версия...

Ответ 3

Я сделал быстрый тест, сравнивающий три альтернативы, и map-compact действительно кажется лучшим вариантом.

Тест производительности (Rails)

require 'test_helper'
require 'performance_test_help'

class ListComprehensionTest < ActionController::PerformanceTest

  TEST_ARRAY = (1..100).to_a

  def test_map_compact
    1000.times do
      TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
    end
  end

  def test_select_map
    1000.times do
      TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
    end
  end

  def test_inject
    1000.times do
      TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
    end
  end

end

Результаты

/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
ListComprehensionTest#test_inject (1230 ms warmup)
           wall_time: 1221 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_map_compact (860 ms warmup)
           wall_time: 855 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_select_map (961 ms warmup)
           wall_time: 955 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.
Finished in 66.683039 seconds.

15 tests, 0 assertions, 0 failures, 0 errors

Ответ 4

Я обсуждал эту тему с Рейном Хенрихсом, который говорит мне, что наилучшим решением является

map { ... }.compact`

Это имеет смысл, потому что он избегает создания промежуточных массивов, как при неизменном использовании Enumerable#inject, и позволяет избежать роста массива, что вызывает выделение. Это как общее, как любое из других, если ваша коллекция не может содержать нулевые элементы.

Я не сравнивал это с

select {...}.map{...}

Возможно, реализация Ruby C Enumerable#select тоже очень хороша.

Ответ 5

Кажется, есть некоторая путаница среди программистов Ruby в этой теме относительно того, что такое понимание списка. Каждый ответ предполагает, что какой-то существующий массив преобразуется. Но способность распознавания списков лежит в массиве, создаваемом "на лету" со следующим синтаксисом:

squares = [x**2 for x in range(10)]

Следующим будет аналог в Ruby (единственный адекватный ответ в этом потоке, AFAIC):

a = Array.new(4).map{rand(2**49..2**50)} 

В приведенном выше случае я создаю массив случайных целых чисел, но блок может содержать что угодно. Но это будет понимание списка Ruby.

Ответ 6

Альтернативное решение, которое будет работать в каждой реализации и работать в O (n) вместо O (2n), равно:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}

Ответ 7

Я только что опубликовал comprehend gem для RubyGems, который позволяет вам сделать это:

require 'comprehend'

some_array.comprehend{ |x| x * 3 if x % 2 == 0 }

Это написано на C; массив проходит только один раз.

Ответ 8

Enumerable имеет метод grep, первым аргументом которого может быть предикат proc, а второй необязательный второй аргумент - функция отображения; поэтому следующие работы:

some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}

Это не так читаемо, как пара других предложений (мне нравится anoiaque simple select.map или histocrat comprehend gem), но его сильные стороны состоят в том, что он уже является частью стандартной библиотеки и является однопроходным, t включает создание временных промежуточных массивов и не требует значения вне пределов, например nil, используемого в предложениях compact.

Ответ 9

[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

Это работает для меня. Это также чисто. Да, это то же самое, что и map, но я думаю, что collect делает код более понятным.


select(&:even?).map()

на самом деле выглядит лучше, увидев его ниже.

Ответ 10

Это более краткий:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}

Ответ 11

Как упоминал Педро, вы можете объединить прикованные вызовы к Enumerable#select и Enumerable#map, избегая обхода по выбранным элементам. Это верно, потому что Enumerable#select является специализацией fold или inject. Я отправил поспешное введение в тему на субредате Ruby.

Взаимодействующие преобразования Array могут быть утомительными, поэтому, возможно, кто-то может сыграть с реализацией Robert Gamble comprehend, чтобы сделать этот шаблон select/map более красивым.

Ответ 12

Что-то вроде этого:

def lazy(collection, &blk)
   collection.map{|x| blk.call(x)}.compact
end

Назовите его:

lazy (1..6){|x| x * 3 if x.even?}

Что возвращает:

=> [6, 12, 18]

Ответ 13

Другое решение, но, возможно, не самое лучшее

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

или

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }

Ответ 14

Это один из способов подойти к этому:

c = -> x do $*.clear             
  if x['if'] && x[0] != 'f' .  
    y = x[0...x.index('for')]    
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif x['if'] && x[0] == 'f'
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif !x['if'] && x[0] != 'f'
    y = x[0...x.index('for')]
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  else
    eval(x.split[3]).to_a
  end
end 

поэтому в основном мы конвертируем строку в правильный синтаксис ruby для цикла тогда мы можем использовать синтаксис Python в строке, чтобы сделать:

c['for x in 1..10']
c['for x in 1..10 if x.even?']
c['x**2 for x in 1..10 if x.even?']
c['x**2 for x in 1..10']

# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [2, 4, 6, 8, 10]
# [4, 16, 36, 64, 100]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

или, если вам не нравится, как выглядит строка, или что нужно использовать лямбду, мы можем отказаться от попытки отразить синтаксис Python и сделать что-то вроде этого:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]

Ответ 15

Я думаю, что самое общее понимание справки будет следующим:

some_array.select{ |x| x * 3 if x % 2 == 0 }

Так как Ruby позволяет поместить условное выражение после выражения, мы получим синтаксис, аналогичный версии Python для понимания списка. Кроме того, поскольку метод select не содержит ничего, что приравнивается к false, все значения nil удаляются из результирующего списка и не требуется вызов в компактный, как это было бы, если бы мы использовали map или collect вместо этого.