Apakah ada alasan mengapa kita tidak dapat mengulang pada "Rentang terbalik" di ruby?

104

Saya mencoba mengulangi ke belakang dengan menggunakan Range dan each:

(4..0).each do |i|
  puts i
end
==> 4..0

Iterasi melalui 0..4penulisan angka. Di Rentang lain r = 4..0tampaknya baik-baik saja r.first == 4,, r.last == 0.

Tampaknya aneh bagi saya bahwa konstruksi di atas tidak memberikan hasil yang diharapkan. Apa alasannya? Apa situasi ketika perilaku ini masuk akal?

fifigyuri
sumber
Saya tidak hanya tertarik pada bagaimana mewujudkan iterasi ini, yang jelas tidak didukung, tetapi mengapa mengembalikan kisaran 4..0 itu sendiri. Apa niat para desainer bahasa? Mengapa, dalam situasi apa ini bagus? Saya juga melihat perilaku serupa di konstruksi ruby ​​lain, dan masih belum bersih saat berguna.
fifigyuri
1
Rentang itu sendiri dikembalikan oleh konvensi. Karena .eachpernyataan tidak mengubah apa pun, tidak ada "hasil" yang dihitung untuk dikembalikan. Jika demikian, Ruby biasanya mengembalikan objek aslinya jika berhasil dan nilsaat error. Ini memungkinkan Anda menggunakan ekspresi seperti ini sebagai kondisi pada ifpernyataan.
bta

Jawaban:

99

Rentang hanyalah itu: sesuatu yang ditentukan oleh awal dan akhir, bukan oleh isinya. "Iterasi" pada suatu rentang tidak benar-benar masuk akal dalam kasus umum. Pertimbangkan, misalnya, bagaimana Anda akan "mengulang" rentang yang dihasilkan oleh dua tanggal. Apakah Anda akan mengulang di siang hari? berdasarkan bulan? menurut tahun? menurut minggu? Itu tidak terdefinisi dengan baik. IMO, fakta bahwa itu diizinkan untuk rentang maju harus dilihat sebagai metode kenyamanan saja.

Jika Anda ingin mengulangi rentang seperti itu, Anda selalu dapat menggunakan downto:

$ r = 10..6
=> 10..6

$ (r.first).downto(r.last).each { |i| puts i }
10
9
8
7
6

Berikut adalah beberapa pemikiran lain dari orang lain tentang mengapa sulit untuk mengizinkan iterasi dan secara konsisten menangani rentang terbalik.

John Feminella
sumber
10
Saya pikir melakukan iterasi pada rentang dari 1 hingga 100 atau dari 100 hingga 1 secara intuitif berarti menggunakan langkah 1. Jika seseorang menginginkan langkah yang berbeda, ubah defaultnya. Demikian pula bagi saya (setidaknya) iterasi dari 1 Januari hingga 16 Agustus berarti melangkah demi hari. Saya pikir sering kali ada sesuatu yang umumnya bisa kita sepakati, karena kita secara intuitif bermaksud seperti itu. Terima kasih atas jawaban Anda, juga tautan yang Anda berikan bermanfaat.
fifigyuri
3
Saya masih berpikir bahwa mendefinisikan pengulangan "intuitif" untuk banyak rentang sulit dilakukan secara konsisten, dan saya tidak setuju bahwa pengulangan tanggal dengan cara itu secara intuitif menyiratkan langkah yang sama dengan 1 hari - bagaimanapun juga, hari itu sendiri sudah merupakan kisaran waktu (dari tengah malam hingga tengah malam). Misalnya, siapa yang mengatakan bahwa "1 Januari hingga 18 Agustus" (tepatnya 20 minggu) tidak menyiratkan pengulangan minggu, bukan hari? Mengapa tidak mengulang per jam, menit, atau detik?
John Feminella
8
The .eachada berlebihan, 5.downto(1) { |n| puts n }bekerja dengan baik. Juga, alih-alih semua hal terakhir yang terakhir, lakukan saja (6..10).reverse_each.
mk12
@ Mk12: 100% setuju, saya hanya mencoba untuk menjadi super eksplisit demi Rubyist yang lebih baru. Mungkin itu terlalu membingungkan.
John Feminella
Saat mencoba menambahkan tahun ke dalam formulir, saya menggunakan:= f.select :model_year, (Time.zone.now.year + 1).downto(Time.zone.now.year - 100).to_a
Eric Norcross
92

Bagaimana dengan (0..1).reverse_eachyang mengulang rentang ke belakang?

Marko Taponen
sumber
1
Itu membangun array sementara, setidaknya di 2.4.1 ruby-doc.org/core-2.4.1/Enumerable.html#method-i-reverse_each
kolen
1
Ini berhasil bagi saya untuk menyortir rentang tanggal (Date.today.beginning_of_year..Date.today.yesterday).reverse_eachTerima kasih
daniel
18

Iterasi pada rentang di Ruby dengan eachmemanggil succmetode pada objek pertama dalam rentang tersebut.

$ 4.succ
=> 5

Dan 5 berada di luar jangkauan.

Anda dapat mensimulasikan iterasi terbalik dengan peretasan ini:

(-4..0).each { |n| puts n.abs }

John menunjukkan bahwa ini tidak akan berhasil jika span 0. Ini akan:

>> (-2..2).each { |n| puts -n }
2
1
0
-1
-2
=> -2..2

Tidak bisa mengatakan saya sangat menyukai salah satu dari mereka karena mereka agak mengaburkan maksudnya.

Jonas Elfström
sumber
2
Tidak, tetapi dengan mengalikan dengan -1 daripada menggunakan .abs Anda bisa.
Jonas Elfström
12

Menurut buku "Programming Ruby", objek Range menyimpan dua endpoint dari range dan menggunakan .succanggota untuk menghasilkan nilai antara. Bergantung pada jenis tipe data yang Anda gunakan dalam rentang Anda, Anda selalu dapat membuat subkelas Integerdan mendefinisikan ulang .succanggotanya sehingga berfungsi seperti iterator terbalik (Anda mungkin juga ingin mendefinisikan ulang .nextjuga).

Anda juga dapat mencapai hasil yang Anda cari tanpa menggunakan Range. Coba ini:

4.step(0, -1) do |i|
    puts i
end

Ini akan melangkah dari 4 menjadi 0 dalam langkah -1. Namun, saya tidak tahu apakah ini akan berhasil untuk apa pun kecuali argumen Integer.

bta
sumber
11

Cara lainnya adalah (1..10).to_a.reverse

Sergey Kishenin
sumber
5

Anda bahkan dapat menggunakan forloop:

for n in 4.downto(0) do
  print n
end

yang mencetak:

4
3
2
1
0
bakerstreet221b
sumber
3

jika daftarnya tidak terlalu besar. saya pikir [*0..4].reverse.each { |i| puts i } cara paling sederhana.

marocchino
sumber
2
IMO umumnya baik untuk menganggapnya besar. Saya pikir itu adalah keyakinan dan kebiasaan yang benar untuk diikuti secara umum. Dan karena iblis tidak pernah tidur, saya tidak percaya pada diri saya sendiri bahwa saya ingat di mana saya mengulang-ulang array. Tapi Anda benar, jika kita memiliki 0 dan 4 konstan, iterasi pada array mungkin tidak menimbulkan masalah.
fifigyuri
1

Seperti yang dikatakan bta, alasannya adalah Range#eachmengirim succke awalnya, lalu ke hasil succpanggilan itu, dan seterusnya sampai hasilnya lebih besar dari nilai akhir. Anda tidak bisa mendapatkan dari 4 ke 0 dengan menelepon succ, dan pada kenyataannya Anda sudah memulai lebih dari yang terakhir.

Membuang
sumber
1

Saya menambahkan satu sama lain kemungkinan bagaimana mewujudkan iterasi atas Range terbalik. Saya tidak menggunakannya, tapi itu kemungkinan. Agak berisiko untuk menambal objek inti ruby ​​monyet.

class Range

  def each(&block)
    direction = (first<=last ? 1 : -1)
    i = first
    not_reached_the_end = if first<=last
                            lambda {|i| i<=last}
                          else
                            lambda {|i| i>=last}
                          end
    while not_reached_the_end.call(i)
      yield i
      i += direction
    end
  end
end
fifigyuri
sumber
0

Ini berhasil untuk kasus penggunaan malas saya

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]
forforf
sumber
0

OP menulis

Tampaknya aneh bagi saya bahwa konstruksi di atas tidak memberikan hasil yang diharapkan. Apa alasannya? Apa situasi ketika perilaku ini masuk akal?

bukan 'Bisakah itu dilakukan?' tetapi untuk menjawab pertanyaan yang tidak ditanyakan sebelum sampai pada pertanyaan yang sebenarnya ditanyakan:

$ irb
2.1.5 :001 > (0..4)
 => 0..4
2.1.5 :002 > (0..4).each { |i| puts i }
0
1
2
3
4
 => 0..4
2.1.5 :003 > (4..0).each { |i| puts i }
 => 4..0
2.1.5 :007 > (0..4).reverse_each { |i| puts i }
4
3
2
1
0
 => 0..4
2.1.5 :009 > 4.downto(0).each { |i| puts i }
4
3
2
1
0
 => 4

Karena reverse_each diklaim untuk membangun seluruh array, downto jelas akan lebih efisien. Fakta bahwa seorang perancang bahasa bahkan dapat mempertimbangkan untuk mengimplementasikan hal-hal seperti itu agak terkait dengan jawaban atas pertanyaan sebenarnya seperti yang ditanyakan.

Untuk menjawab pertanyaan seperti yang sebenarnya ditanyakan ...

Alasannya karena Ruby adalah bahasa yang selalu mengejutkan. Beberapa kejutan memang menyenangkan, tetapi ada banyak perilaku yang benar-benar rusak. Meskipun beberapa dari contoh berikut ini dikoreksi oleh rilis yang lebih baru, masih banyak lainnya, dan mereka tetap sebagai dakwaan pada pola pikir dari desain aslinya:

nil.to_s
   .to_s
   .inspect

menghasilkan "" tetapi

nil.to_s
#  .to_s   # Don't want this one for now
   .inspect

menghasilkan

 syntax error, unexpected '.', expecting end-of-input
 .inspect
 ^

Anda mungkin mengharapkan << dan mendorong menjadi sama untuk menambahkan ke array, tapi

a = []
a << *[:A, :B]    # is illegal but
a.push *[:A, :B]  # isn't.

Anda mungkin berharap 'grep' berperilaku seperti baris perintah Unix yang setara, tetapi ia tidak === cocok dengan not = ~, terlepas dari namanya.

$ echo foo | grep .
foo
$ ruby -le 'p ["foo"].grep(".")'
[]

Berbagai metode secara tidak terduga merupakan alias untuk satu sama lain, jadi Anda harus mempelajari banyak nama untuk hal yang sama - misalnya finddan detect- bahkan jika Anda memang menyukai kebanyakan pengembang dan hanya menggunakan satu atau yang lain. Hal yang sama berlaku untuk size,, countdan length, kecuali untuk kelas yang mendefinisikan masing-masing secara berbeda, atau tidak mendefinisikan satu atau dua sama sekali.

Kecuali seseorang telah menerapkan sesuatu yang lain - seperti metode inti taptelah didefinisikan ulang di berbagai pustaka otomatisasi untuk menekan sesuatu di layar. Semoga berhasil mencari tahu apa yang terjadi, terutama jika beberapa modul yang dibutuhkan oleh modul lain telah menggunakan modul lain untuk melakukan sesuatu yang tidak terdokumentasi.

Objek variabel lingkungan, ENV tidak mendukung 'merge', jadi Anda harus menulis

 ENV.to_h.merge('a': '1')

Sebagai bonus, Anda bahkan dapat mendefinisikan ulang konstanta Anda atau orang lain jika Anda berubah pikiran tentang apa yang seharusnya.

android.weasel
sumber
Ini tidak menjawab pertanyaan dengan cara, bentuk, atau bentuk apa pun. Ini hanyalah ocehan tentang hal-hal yang tidak disukai penulis tentang Ruby.
Jörg W Mittag
Diperbarui untuk menjawab pertanyaan yang tidak ditanyakan, selain jawaban yang sebenarnya ditanyakan. kata-kata kasar: kata kerja 1. berbicara atau berteriak panjang lebar dengan cara yang penuh amarah. Jawaban aslinya tidak marah atau bersemangat: itu adalah tanggapan yang dipertimbangkan dengan contoh.
android.weasel
@ JörgWMittag Pertanyaan asli juga mencakup: Tampaknya aneh bagi saya bahwa konstruksi di atas tidak memberikan hasil yang diharapkan. Apa alasannya? Apa situasi ketika perilaku ini masuk akal? jadi dia mencari alasan, bukan solusi kode.
android.weasel
Sekali lagi, saya gagal untuk melihat bagaimana perilaku grepdengan cara apa pun, bentuk atau bentuk terkait dengan fakta bahwa iterasi pada rentang kosong adalah tanpa operasi. Saya juga tidak melihat bagaimana fakta bahwa iterasi pada rentang kosong adalah no-op dalam bentuk atau bentuk apapun yang "mengejutkan tanpa henti" dan "benar-benar rusak".
Jörg W Mittag
Karena kisaran 4..0 memiliki maksud yang jelas dari [4, 3, 2, 1, 0] tetapi secara mengejutkan bahkan tidak memunculkan peringatan. Ini mengejutkan OP dan mengejutkan saya, dan pasti mengejutkan banyak orang lainnya. Saya membuat daftar contoh lain dari perilaku mengejutkan. Saya bisa mengutip lebih banyak, jika Anda suka. Begitu sesuatu menunjukkan lebih dari sejumlah perilaku mengejutkan, itu mulai melayang ke wilayah 'rusak'. Mirip dengan bagaimana konstanta memunculkan peringatan saat ditimpa, terutama saat metode tidak.
android.weasel
0

Bagi saya cara yang paling sederhana adalah:

[*0..9].reverse

Cara lain untuk mengulang pencacahan:

(1..3).reverse_each{|v| p v}
AgentIvan
sumber