Rust: Masalah pelacakan untuk penjepit RFC

Dibuat pada 26 Agu 2017  ·  101Komentar  ·  Sumber: rust-lang/rust

Masalah pelacakan untuk https://github.com/rust-lang/rfcs/pull/1961

PR di sini: #44097 #58710
Stabilisasi PR: https://github.com/rust-lang/rust/pull/77872

MELAKUKAN:

  • [x] Minta RFC melewati periode komentar terakhir
  • [x] Terapkan RFC
  • [ ] Stabilkan
B-unstable C-tracking-issue Libs-Tracked T-libs

Komentar yang paling membantu

Sepertinya kita berada di tempat yang sangat buruk jika metode apa pun yang cukup diinginkan orang untuk mendefinisikan sifat ekstensi tidak akan pernah dapat ditambahkan ke perpustakaan standar.

Semua 101 komentar

Harap dicatat: Ini merusak Servo dan Pathfinder.

cc @rust-lang/libs, ini adalah kasus yang mirip dengan min / max , di mana ekosistem sudah menggunakan nama clamp , dan karenanya menambahkannya telah menyebabkan ambiguitas . Ini diperbolehkan kerusakan per kebijakan semver, tetapi tetap menyebabkan rasa sakit di bagian hilir.

Pencalonan untuk pertemuan triase pada hari Selasa.

Adakah pemikiran sementara itu?

Saya agak dengan @bluss yang satu ini karena akan lebih baik untuk tidak mengulanginya. "Clamp" mungkin adalah nama yang bagus, tetapi bisakah kita menghindarinya dengan memilih nama yang berbeda?

restrict
clamp_to_range
min_max (Karena ini seperti menggabungkan min dan max.)
Ini mungkin berhasil. Bisakah kita menggunakan kawah untuk menentukan seberapa buruk dampak clamp sebenarnya? clamp dikenal baik di beberapa bahasa dan perpustakaan.

Jika kami pikir kami mungkin perlu mengganti nama, mungkin yang terbaik adalah segera mengembalikan PR, dan kemudian menguji lebih hati-hati dengan kawah dll. @Xaeroxe , siap untuk itu?

Tentu. Saya belum pernah menggunakan kawah sebelumnya, tapi saya bisa belajar.

@Xaeroxe ah maaf, maksud saya mengembalikan PR dengan cepat. (Saya sedang berlibur hari ini sehingga Anda mungkin membutuhkan orang lain di lib , seperti @alexcrichton , untuk membantu mendaratkannya).

Saya sedang mempersiapkan PR sekarang. Bersenang-senanglah di liburan Anda!

Bisakah clamp_to_range(min, max) terdiri dari clamp_to_min(min) dan clamp_to_max(max) (dengan pernyataan tambahan bahwa min <= max ), tetapi fungsi-fungsi itu juga dapat dipanggil secara independen?

Saya kira ide itu mengamanatkan RFC.

Saya harus mengatakan meskipun saya telah bekerja untuk mendapatkan fungsi 4 baris ke perpustakaan std selama 6 bulan sekarang. Aku agak lelah. Fungsi yang sama digabung menjadi num dalam 2 hari dan itu cukup baik untuk saya. Jika ada orang lain yang benar-benar menginginkan ini di perpustakaan std, silakan, tetapi saya belum siap untuk 6 bulan lagi.

Saya buka kembali ini agar pencalonan @aturon sebelumnya tetap terlihat.

Saya pikir ini harus masuk seperti yang tertulis atau panduan tentang perubahan apa yang dapat dilakukan harus diperbarui untuk menghindari pemborosan waktu orang di masa depan.

Sudah sangat jelas sejak awal bahwa ini dapat menyebabkan kerusakan yang terjadi. Secara pribadi, saya membandingkannya dengan ord_max_min yang merusak banyak hal:

Dan tanggapannya adalah "Fungsi Ord::min telah ditambahkan [...] Tim libs memutuskan hari ini bahwa ini adalah kerusakan yang diterima". Dan itu adalah fitur TMTOWTDI dengan nama yang lebih umum, sedangkan clamp belum ada di std dalam bentuk yang berbeda.

Rasanya, secara subjektif, bagi saya bahwa jika RFC ini dikembalikan, aturan sebenarnya adalah "Anda pada dasarnya tidak dapat menempatkan metode baru pada sifat-sifat di std, kecuali mungkin Iterator ".

Anda juga tidak dapat benar-benar menempatkan metode baru pada tipe yang sebenarnya. Pertimbangkan situasi di mana seseorang memiliki "sifat ekstensi" untuk tipe di std. Sekarang std mengimplementasikan metode sifat ekstensi yang disediakan sebagai metode aktual pada jenis ini. Kemudian ini mencapai stabil, tetapi metode baru ini masih di belakang bendera fitur. Kompiler kemudian akan mengeluh bahwa metode tersebut berada di belakang flag fitur dan tidak dapat digunakan dengan rantai alat stabil, alih-alih kompiler memilih metode sifat ekstensi seperti sebelumnya dan dengan demikian menyebabkan kerusakan pada kompiler stabil.

Ini juga perlu diperhatikan: Ini bukan hanya masalah perpustakaan standar. Sintaks pemanggilan metode membuatnya sangat sulit untuk menghindari memperkenalkan perubahan yang melanggar hampir di mana saja di ekosistem.

(meta) Hanya menyalin komentar saya di irlo sini.

Jika kita setuju bahwa #44438 dibenarkan,

  1. Kami mungkin perlu mempertimbangkan kembali apakah jenis-kerusakan-inferensi-kerusakan yang dijamin benar-benar dapat diabaikan sebagai XIB.

    Saat ini perubahan inferensi tipe dianggap dapat diterima oleh RFC 1105 dan 1122 karena seseorang selalu dapat menggunakan UFCS atau cara lain untuk memaksa suatu tipe. Tetapi komunitas tidak terlalu menyukai kerusakan yang disebabkan oleh #42496 ( Ord::{min, max} ). Selain itu, #41336 (percobaan pertama T += &T ) ditutup "hanya" karena 8 jenis regresi inferensi.

  2. Setiap kali kita menambahkan metode, harus ada jalur kawah untuk memastikan namanya belum ada.

    Perhatikan bahwa menambahkan metode bawaan dapat menyebabkan kegagalan inferensi juga — #41793 disebabkan oleh penambahan metode bawaan {f32, f64}::from_bits , yang bertentangan dengan metode ieee754::Ieee754::from_bits dalam sifat hilir.

  3. Ketika peti hilir tidak menentukan #![feature(clamp)] , kandidat Ord::clamp tidak boleh dipertimbangkan (peringatan yang kompatibel di masa mendatang masih dapat dikeluarkan) kecuali ini adalah solusi unik. Ini akan memungkinkan pengenalan metode sifat baru yang tidak "melanggar insta", tetapi masalahnya masih akan kembali saat menstabilkan.

Sepertinya kita berada di tempat yang sangat buruk jika metode apa pun yang cukup diinginkan orang untuk mendefinisikan sifat ekstensi tidak akan pernah dapat ditambahkan ke perpustakaan standar.

Maks/menit mencapai titik yang sangat buruk sehubungan dengan penggunaan nama metode umum pada sifat umum. Hal yang sama tidak perlu diterapkan pada penjepit.

Saya masih ingin mengatakan ya, tetapi @sfackler apakah kita benar-benar harus menambahkan metode pada sifat yang begitu umum diterapkan, oleh beragam jenis? Kita harus berhati-hati saat menambahkan api dari semua jenis yang telah membeli sifat yang ada.

Dengan datangnya spesialisasi, kami tidak kehilangan apa pun dengan menempatkan metode ekstensi dalam sifat ekstensi.

Satu bagian yang mengganggu adalah jika metode std baru merusak kode Anda: itu akan muncul jauh sebelum Anda benar-benar dapat menggunakannya, karena tidak stabil. Selain itu tidak terlalu buruk jika konflik dengan metode yang memiliki arti yang sama.

Saya pikir memberikan fungsi ini nama yang berbeda untuk menghindari kerusakan adalah solusi yang buruk. Saat berfungsi, ini mengoptimalkan tidak merusak beberapa peti (yang semuanya memilih untuk setiap malam) alih-alih mengoptimalkan keterbacaan kode apa pun di masa mendatang menggunakan fitur ini.

Saya memiliki beberapa kekhawatiran yang beberapa di antaranya tidak perlu khawatir.

  • nama dan bayangan tidak ideal tetapi berhasil
  • untuk vektor numerik dan matriks saya pikir max/min/clamp tidak ideal tetapi ini diselesaikan dengan tidak menggunakan Ord sama sekali. Ndarray ingin melakukan klem argumen elementwise dan generik (skalar atau array), tetapi Ord tidak digunakan oleh kami atau pustaka serupa. Jadi jangan khawatir.
  • Jenis senyawa yang ada yang tidak numerik: BtreeMap akan mendapatkan metode penjepit dengan perubahan ini. Apakah itu masuk akal secara umum? Bisakah itu menerapkan makna yang masuk akal untuk itu selain dari default?
  • mode panggilan berdasarkan nilai tidak sesuai dengan setiap implementasi. Sekali lagi, BtreeMap. Haruskah penjepit menggunakan 3 peta dan mengembalikan salah satunya?

jenis senyawa

Saya pikir itu sama masuk akalnya dengan BtreeSet<BtreeSet<impl Ord>>::range . Tetapi ada kasus tertentu yang bahkan bisa membantu, seperti Vec<char> .

mode panggilan berdasarkan nilai

Ketika ini muncul di RFC, jawabannya hanya menggunakan Cow .

Tentu saja, bisa seperti ini , untuk menggunakan kembali penyimpanan:

    fn clamp<T>(mut self, low: &T, high: &T) -> Self
        where T: ?Sized + ToOwned<Owned=Self> + Ord, Self : Borrow<T>
    {
        assert!(low <= high);
        if self.borrow() < &low {
            low.clone_into(&mut self);
        } else if self.borrow() >= &high {
            high.clone_into(&mut self);
        }
        self
    }

Yang https://github.com/rust-lang/rfcs/pull/2111 mungkin membuat panggilan ergonomis.

Tim lib membahas hal ini selama triase beberapa hari yang lalu dan kesimpulannya adalah bahwa kita harus melakukan kawah untuk melihat apa kerusakan di seluruh ekosistem untuk perubahan ini. Hasil dari itu akan menentukan tindakan apa yang harus diambil tepat pada masalah ini.

Ada sejumlah kemungkinan fitur bahasa di masa mendatang yang dapat kami tambahkan untuk memudahkan penambahan api seperti ini seperti ciri-ciri berprioritas rendah atau menggunakan ciri-ciri ekstensi dengan cara yang lebih beraroma. Namun, kami tidak ingin memblokir ini pada kemajuan seperti itu.

Apakah kawah pernah terjadi untuk fitur ini?

Saya berencana untuk menghidupkan kembali metode clamp() setelah #48552 digabungkan. Namun, RangeInclusive akan distabilkan sebelum itu, artinya alternatif berbasis rentang sekarang layak untuk dipertimbangkan (yang sebenarnya adalah proposal asli, tetapi ditarik kembali karena ..= sangat tidak stabil ):

// Current
trait Ord {
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}
assert_eq!(9.clamp(6, 7), 7);


// Alternative
trait Ord {
    fn clamp(self, range: RangeInclusive<Self>) -> Self { ... }
}
assert_eq!(9.clamp(6..=7), 7);

RangeInclusive stabil juga membuka kemungkinan lain, seperti membalikkan keadaan (yang memungkinkan beberapa kemungkinan menarik dengan autoref, dan menghindari tabrakan nama sama sekali):

impl<T: Ord + Clone> RangeInclusive<T> {
    fn clamp(&self, mut x: T) -> T {
        if x < self.start { x.clone_from(&self.start); }
        else if x > self.end { x.clone_from(&self.end); }
        x
    } 
} 

    assert_eq!((1..=10).clamp(11), 10);

    let strings = String::from("aa")..=String::from("b");
    assert_eq!(strings.clamp(String::from("a")), "aa");
    assert_eq!(strings.clamp(String::from("aaa")), "aaa");

https://play.rust-lang.org/?gist=38def79ba2f3f8380197918377dc66f5&version=nightly

Saya belum memutuskan apakah saya pikir itu lebih baik, meskipun ...

Saya akan menggunakan nama yang berbeda jika digunakan sebagai metode jangkauan.

Tentunya saya akan menikmati fitur ini lebih cepat daripada nanti, apa pun bentuknya.

Apa statusnya saat ini?
Bagi saya tampaknya ada konsensus, bahwa menambahkan penjepit ke RangeInclusive mungkin merupakan alternatif yang lebih baik.
Jadi seseorang harus menulis RFC?

RFC penuh mungkin tidak diperlukan pada saat ini. Hanya keputusan ejaan mana yang harus dipilih:

  1. value.clamp(min, max) (ikuti RFC apa adanya)
  2. value.clamp(min..=max)
  3. (min..=max).clamp(value)

Opsi 2 atau 3 akan memungkinkan penjepitan parsial yang lebih mudah. Anda dapat melakukan value.clamp(min..) atau value.clamp(..=max) , tanpa perlu metode khusus clamp_to_start atau clamp_to_end .

@egilburg : kita sudah memiliki metode khusus tersebut: clamp_to_start adalah max dan clamp_to_end adalah min :wink:

Konsistensinya bagus.

@egilburg Rust tidak mendukung kelebihan beban langsung. Agar opsi 2 berfungsi dengan saran Anda, kami memerlukan sifat baru yang diterapkan untuk RangeInclusive , RangeToInclusive dan RangeFrom , yang terasa cukup berat.

Saya pikir, opsi 3 itu adalah opsi terbaik.

1 atau 2 adalah yang paling tidak mengejutkan. Saya akan tetap menggunakan 1 karena banyak kode yang harus dilakukan untuk mengganti implementasi lokal dengan yang std.

Saya pikir kita harus merencanakan untuk menggunakan _semua_ jenis rentang* atau _tidak ada_ di antaranya.

Tentu saja, itu lebih sulit untuk hal-hal seperti Range daripada RangeInclusive . Tapi ada sesuatu yang menyenangkan tentang (0.0..1.0).clamp(2.0_f32) => 0.99999994_f32 .

@kennytm Jadi jika saya akan membuka permintaan tarik dengan opsi 3 menurut Anda apakah itu akan digabungkan?
Atau apa yang Anda pikirkan tentang bagaimana untuk melanjutkan selanjutnya?

@EdorianDark Untuk ini kita perlu bertanya @rust-lang/libs

Saya pribadi menyukai opsi 2, dengan RangeInclusive saja. Seperti yang disebutkan "penjepitan parsial" sudah ada dengan min dan max .

Saya setuju dengan @SimonSapin , meskipun saya juga akan baik-baik saja dengan opsi 1. Dengan opsi 3, saya kemungkinan tidak akan menggunakan fungsi tersebut karena tampaknya mundur bagi saya. Dalam bahasa/perpustakaan lain dengan penjepit yang disurvei @kennytm sebelumnya , 5 dari 7 (semua kecuali Swift dan Qt) memiliki nilai terlebih dahulu, lalu rentangnya.

Clamp sekarang di master lagi!

Saya senang, meskipun saya masih mencoba mencari tahu perubahan apa yang membuat ini dapat diterima sekarang, padahal tidak di #44097

Kami sekarang memiliki periode peringatan karena #48552, alih-alih langsung memutus inferensi bahkan sebelum stabil.

Itu berita bagus, terima kasih!

@kennytm Saya hanya ingin berterima kasih atas kerja keras yang Anda lakukan untuk mewujudkan #48552, dan @EdorianDark terima kasih atas minat Anda dalam hal ini dan mengimplementasikannya. Sangat menyenangkan melihat ini akhirnya bergabung.

https://rust.godbolt.org/z/JmLWJi

pub fn clamped(a: f32) -> f32 {
   a.clamp(0.,255.)
}

Dikompilasi ke:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm1, xmm0
  vmovss xmm1, dword ptr [rip + .LCPI0_0]
  vminss xmm0, xmm1, xmm0

yang tidak terlalu buruk ( vmaxss dan vminss digunakan), tetapi:

pub fn maxmined(a: f32) -> f32 {
   (0f32).max(a).min(255.)
}

menggunakan satu instruksi lebih sedikit:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm0, xmm1
  vminss xmm0, xmm0, dword ptr [rip + .LCPI1_0]

Apakah itu melekat pada implementasi penjepit, atau hanya permainan optimasi LLVM?

@kornelski clamp ing NAN seharusnya mempertahankan NAN , yang maxmined tidak, karena max / min mempertahankan _non_- NAN .

Akan sangat bagus untuk menemukan implementasi yang keduanya memenuhi harapan NAN dan lebih pendek. Dan itu akan baik untuk doctests untuk menunjukkan penanganan NAN. Sepertinya PR asli memiliki beberapa:

https://github.com/rust-lang/rust/blob/b762283e57ff71f6763effb9cfc7fc0c7967b6b0/src/libstd/f32.rs#L1089 -L1094

Mengapa klem mengapung panik jika min atau maks adalah NaN? Saya akan mengubah pernyataan dari assert!(min <= max) menjadi assert!(!(min > max)) , sehingga minimum atau maksimum NaN tidak akan berpengaruh, seperti pada metode max dan min.

NAN untuk min atau max di penjepit kemungkinan menunjukkan kesalahan pemrograman, dan kami pikir lebih baik panik lebih cepat daripada mungkin memasukkan data yang tidak dijepit ke IO. Jika Anda tidak menginginkan batas atas atau bawah, fungsi ini bukan untuk Anda.

Anda selalu dapat menggunakan INF dan -INF jika Anda tidak menginginkan batas atas atau bawah, bukan? Yang juga masuk akal secara matematis, tidak seperti NaN. Tetapi sebagian besar waktu lebih baik menggunakan max dan min untuk itu.

@Xaeroxe Terima kasih atas implementasinya.

Mungkin ini bisa masuk di edisi berikutnya, apakah akan merusak kode stabil?

Satu hal yang perlu dipertimbangkan IMO secara lebih rinci adalah penjepitan satu sisi f32 / f64 . Diskusi tampaknya telah menyentuh topik ini secara singkat tetapi tidak benar-benar mempertimbangkannya secara rinci.

Dalam kebanyakan kasus, jika input ke klem satu sisi adalah NAN, hasilnya akan lebih berguna untuk menjadi NAN daripada hasilnya menjadi batas klem. Jadi, fungsi f32::min dan f64::max yang ada tidak berfungsi dengan baik untuk kasus penggunaan ini. Kami membutuhkan fungsi terpisah untuk penjepitan satu sisi. (Lihat rust-num/num-traits #122.)

Alasan mengapa saya mengangkat ini adalah karena ini mempengaruhi desain dua sisi clamp , karena akan lebih baik jika klem dua sisi dan satu sisi memiliki antarmuka yang konsisten. Beberapa pilihan adalah:

  1. input.clamp(min, max) , input.clamp_min(min) , dan input.clamp_max(max)
  2. input.clamp(min..=max) , input.clamp(min..) , input.clamp(..=max)
  3. input.clamp(min, max) , input.clamp(min, std::f64::INFINITY) , input.clamp(std::f64::NEG_INFINITY, max)

Dengan implementasi saat ini ( min dan max sebagai parameter terpisah f32 / f64 ), kita harus memilih opsi 1, yang menurut saya sangat masuk akal , atau opsi 3, yang IMO-nya terlalu bertele-tele. Kita hanya harus menyadari bahwa pengorbanannya adalah harus menambahkan fungsi clamp_min dan clamp_max atau mengharuskan pengguna untuk menulis infinity positif/negatif.

Perlu juga dicatat bahwa kami dapat menyediakan

impl f32 {
    pub fn clamp<T>(self, bounds: T) -> f32
    where
        T: RangeBounds<f32>,
    {
         // ...
    }
}

// and for f64

karena untuk f32 / f64 kita sebenarnya tahu bagaimana menangani batas eksklusif, tidak seperti Ord . Tentu saja, maka kita mungkin ingin mengubah Ord::clamp untuk mengambil argumen RangeInclusive untuk konsistensi. Sepertinya tidak ada pendapat yang kuat tentang apakah lebih suka dua argumen atau argumen RangeInclusive tunggal untuk Ord::clamp .

Jika ini sudah menjadi masalah yang diselesaikan, jangan ragu untuk mengabaikan komentar saya. Saya hanya ingin mengangkat hal-hal ini karena saya tidak melihatnya dalam diskusi sebelumnya.

Triase: API di bawah saat ini tidak stabil dan mengarah ke sini. Apakah ada masalah yang perlu dipertimbangkan selain penanganan NaN? Apakah perlu menstabilkan Ord::clamp terlebih dahulu tanpa memblokirnya pada penanganan NaN?

``` karat
sifat pub Ord: Persamaan + PartialOrd{
// …
fn clamp(self, min: Self, max: Self) -> Self dimana Self: Ukuran {…}
}
impl f32 {
pub fn clamp(self, min: f32, max: f32) -> f32 {…}
}
impl f64 {
pub fn clamp(self, min: f64, max: f64) -> f64 {…}
}

@SimonSapin Saya akan dengan senang hati menstabilkan semuanya secara pribadi

+1, ini melalui RFC penuh dan saya tidak berpikir ada materi apa pun yang muncul sejak saat itu. Misalnya, penanganan NaN muncul secara rinci di IRLO dan dalam diskusi RFC .

Baiklah, itu terdengar cukup adil.

@rfcbot penggabungan

Anggota tim @SimonSapin telah mengusulkan untuk menggabungkan ini. Langkah selanjutnya adalah peninjauan oleh anggota tim yang ditandai lainnya:

  • [x] @Amanieu
  • [ ] @Kimundi
  • [x] @SimonSapin
  • [x] @alexcrichton
  • [x] @dtolnay
  • [ ] @sfackler
  • [ ] @tanpa perahu

Tidak ada kekhawatiran saat ini terdaftar.

Setelah mayoritas peninjau menyetujui (dan paling banyak 2 persetujuan belum diselesaikan), ini akan memasuki periode komentar terakhirnya. Jika Anda menemukan masalah besar yang belum diangkat pada titik mana pun dalam proses ini, silakan angkat bicara!

Lihat dokumen ini untuk info tentang perintah apa yang dapat diberikan oleh anggota tim yang ditandai kepada saya.

Apakah ada keputusan yang dibuat tentang x.clamp(7..=13) vs x.clamp(7, 13) ? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 menyebutkan yang pertama mungkin lebih baik untuk konsistensi dengan potensi masa depan f64::clamp .

Saya akan mengatakan itu adalah resolusi yang sangat disayangkan karena .min dan .max sering menyebabkan bug saat Anda menggunakan .min(...) untuk menentukan batas atas dan .max(...) untuk menentukan batas bawah. Ini sangat membingungkan dan saya telah melihat begitu banyak bug dengan ini. .clamp(..1.0) dan .clamp(0.0..) jauh lebih jelas.

@CryZe membuat min = batas atas, max = batas bawah , Anda masih harus melakukan senam mental untuk mengingat mana yang harus digunakan. Beban kognitif ini akan lebih baik dihabiskan untuk masalah apa pun yang Anda coba selesaikan.

Saya tahu x.clamp(y, z) lebih diharapkan, tapi mungkin ini kesempatan untuk berinovasi ;)

Saya bereksperimen sedikit dengan rentang di tahap awal, dan bahkan menunda RFC beberapa bulan sehingga kami bisa bereksperimen dengan rentang inklusif. (Ini dimulai sebelum mereka stabil)

Saya menemukan bahwa tidak mungkin menerapkan penjepit untuk rentang eksklusif pada angka titik mengambang. Hanya mendukung beberapa jenis rentang tetapi tidak yang lain terlalu mengejutkan hasilnya, jadi meskipun saya telah menunda RFC beberapa bulan untuk bereksperimen dengan cara ini, saya akhirnya memutuskan rentang bukanlah solusinya.

@m-ou-se Lihat pembahasannya mulai dari #44095 (komentar) dan juga #58710 (ulasan).

Sunting: seperti yang ditunjukkan di bawah ini, diskusi dalam permintaan tarik (#58710) berisi lebih banyak diskusi tentang keputusan desain daripada masalah pelacakan. Sangat disayangkan bahwa ini tidak dikomunikasikan di sini, yang merupakan tempat diskusi desain biasanya berlangsung, tetapi telah didiskusikan.

Hanya mendukung beberapa jenis jangkauan tetapi tidak yang lain terlalu mengejutkan hasilnya

Rust sudah memperlakukan beberapa rentang secara berbeda dari yang lain (misalnya menggunakannya untuk iterasi), jadi hanya mengizinkan beberapa rentang sebagai argumen clamp tampaknya tidak mengejutkan saya sama sekali.

Inilah analisis yang paling membantu: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

@Xaeroxe Hanya mendukung beberapa jenis rentang tetapi tidak yang lain terlalu mengejutkan hasilnya

Jika Anda memikirkan hal ini sebelum distabilkan, apakah waktu dan penggunaan umum telah mengubah pendapat Anda, atau apakah menurut Anda masih demikian?

Saya berpendapat bahwa rentang eksklusif tidak boleh diterapkan untuk float, karena mereka akan memiliki perilaku yang berbeda dengan bilangan bulat (kisaran 0..10 termasuk batas bawah dan tidak termasuk batas atas, jadi mengapa rentang hipotetis 0.0...10.0 mengecualikan keduanya?). Saya tidak berpikir itu akan mengejutkan, setidaknya bagi saya.

@varkor Tapi ini kemudian diubah setelah satu komentar di ulasan, tanpa diskusi tentang masalah pelacakan.

Ini mungkin terlihat terlalu konfrontatif, coba sesuatu seperti "ketika saya melihat-lihat percakapan, saya tidak menemukan argumen yang meyakinkan mengapa kita tidak boleh menggunakan rentang, dapatkah seseorang mengarahkan saya ke sana?".

Saya menduga argumen yang Anda cari ada di sini: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

EDIT @Xaeroxe mengalahkan saya untuk itu :)

memiliki waktu dan penggunaan umum mengubah pendapat Anda

Sejauh ini belum, tetapi rentang adalah sesuatu yang jarang saya gunakan dalam pengkodean harian saya. Saya terbuka untuk dibujuk oleh contoh kode dan API yang ada dengan dukungan rentang parsial. Namun, bahkan jika kami menyelesaikan pertanyaan itu, masih ada beberapa poin bagus lainnya yang diajukan scottmcm dalam komentar RFC yang perlu ditangani. Misalnya, Step tidak diimplementasikan pada banyak tipe seperti Ord , apakah perubahan sintaksis kecil ini layak untuk menghilangkan tipe tersebut? Selanjutnya, apakah ada kasus penggunaan untuk klem rentang non-inklusif? Sejauh yang saya tahu, tidak ada bahasa atau kerangka kerja lain yang merasa perlu untuk mendukung penjepit jangkauan eksklusif, jadi apa manfaat yang kita peroleh darinya? Rentang jauh lebih sulit untuk diterapkan dengan cara yang memuaskan, dan datang dengan banyak kerugian dan sedikit manfaat.

Jika saya menerapkan ini menggunakan rentang, itu akan terlihat seperti ini.

Jadi ada beberapa alasan saya pikir kita tidak harus pergi dengan pendekatan ini.

  1. Pemilihan rentang yang diperlukan cukup baru sehingga memerlukan sifat baru, dan secara khusus mengecualikan rentang yang paling umum, Range .

  2. Kita sudah sejauh ini dalam proses RFC dan satu-satunya hal yang std dapatkan dari ini adalah cara lain untuk menulis .max() atau .min() . Saya tidak benar-benar ingin mengatur kembali RFC ke awal proses untuk mengimplementasikan sesuatu yang sudah bisa kita lakukan di Rust.

  3. Ini menggandakan jumlah percabangan yang terjadi dalam fungsi untuk mengakomodasi kasus penggunaan yang kami masih belum yakin ada. Saya tidak bisa menampilkan ini di benchmark.

Kebutuhan untuk operasi penjepitan satu sisi

... satu-satunya hal yang std dapatkan dari ini adalah cara lain untuk menulis .max() atau .min() .

Poin utama yang saya coba sampaikan adalah bahwa saya telah melihat kesetaraan yang nyata antara .min() / .max() dan klem satu sisi muncul beberapa kali dalam diskusi, tetapi operasinya tidak setara untuk angka floating point dengan cara yang seharusnya mereka tangani NAN .

Misalnya, pertimbangkan input.max(0.) sebagai ekspresi untuk menjepit angka negatif ke nol. Jika input bukan- NAN , itu berfungsi dengan baik. Namun, ketika input adalah NAN , itu dievaluasi menjadi 0. . Ini hampir tidak pernah menjadi perilaku yang diinginkan; penjepitan satu sisi harus mempertahankan nilai NAN . (Lihat komentar ini dan ini .) Kesimpulannya, .max() bekerja dengan baik untuk mengambil dua angka yang lebih besar, tetapi tidak bekerja dengan baik untuk penjepitan satu sisi.

Jadi, kita membutuhkan operasi penjepitan satu sisi (terpisah dari .min() / .max() ) untuk bilangan floating-point. Yang lain membuat argumen yang baik untuk kegunaan operasi penjepitan satu sisi untuk jenis titik-mengambang juga. Pertanyaan selanjutnya adalah bagaimana kita ingin mengekspresikan operasi tersebut.

Bagaimana mengekspresikan operasi penjepitan satu sisi

.clamp() dengan INFINITY

Dengan kata lain, jangan menambahkan operasi penjepitan satu sisi; cukup beri tahu pengguna untuk menggunakan .clamp() dengan batas INFINITY atau NEG_INFINITY . Misalnya, beri tahu pengguna untuk menulis input.clamp(0., std::f64::INFINITY) .

Ini sangat bertele-tele, yang akan mendorong pengguna untuk menggunakan .min() / .max() jika mereka tidak mengetahui nuansa penanganan NAN . Selain itu, itu tidak membantu untuk T: Ord , dan IMO itu kurang jelas daripada alternatifnya.

.clamp_min() dan .clamp_max()

Salah satu opsi yang masuk akal adalah menambahkan metode .clamp_min() dan .clamp_max() , yang tidak memerlukan perubahan apa pun pada implementasi yang diusulkan saat ini. Saya pikir ini adalah pendekatan yang masuk akal; Saya hanya ingin memastikan bahwa kami sadar bahwa kami harus menggunakan pendekatan ini jika kami menstabilkan implementasi clamp saat ini diusulkan.

Argumen rentang

Pilihan lain adalah membuat clamp mengambil argumen range. @Xaeroxe telah menunjukkan satu cara untuk mengimplementasikan ini, tetapi implementasi itu memang memiliki beberapa kelemahan, seperti yang dia sebutkan. Cara alternatif untuk menulis implementasi mirip dengan cara mengiris saat ini diterapkan (sifat SliceIndex ). Ini menyelesaikan semua keberatan yang saya lihat dalam diskusi kecuali kekhawatiran tentang penyediaan implementasi untuk subset dari tipe rentang dan kompleksitas tambahan. Saya setuju bahwa itu menambah beberapa kerumitan, tetapi IMO itu tidak jauh lebih buruk daripada menambahkan .clamp_min() / .clamp_max() . Untuk Ord , saya akan menyarankan sesuatu seperti ini:

pub trait Ord: Eq + PartialOrd<Self> {
    // ...

    fn clamp<B>(self, bounds: B) -> B::Output
    where
        B: Clamp<Self>,
    {
        bounds.clamp(self)
    }
}

pub trait Clamp<T> {
    type Output;
    fn clamp(self, input: T) -> Self::Output;
}

impl<T> Clamp<T> for RangeFull {
    type Output = T;
    fn clamp(self, input: T) -> T {
        input
    }
}

impl<T: Ord> Clamp<T> for RangeFrom<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input < self.start {
            self.start
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeToInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input > self.end {
            self.end
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        assert!(self.start <= self.end);
        let mut x = input;
        if x < self.start { x = self.start; }
        if x > self.end { x = self.end; }
        x
    }
}

Beberapa pemikiran tentang ini:

  • Kami dapat menambahkan implementasi untuk rentang eksklusif di mana T: Ord + Step .
  • Kita dapat mempertahankan sifat Clamp hanya setiap malam, mirip dengan sifat SliceIndex .
  • Untuk mendukung f32 / f64 , kita bisa

    1. Santai implementasinya ke T: PartialOrd . (Saya tidak yakin mengapa implementasi clamp ini ada di Ord bukannya PartialOrd . Mungkin saya melewatkan sesuatu dalam diskusi? Sepertinya PartialOrd akan cukup.)

    2. atau tulis implementasi khusus untuk f32 dan f64 . (Jika diinginkan, kami selalu dapat beralih ke opsi i nanti tanpa mengubah perubahan.)

    lalu tambahkan

    impl f32 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
    impl f64 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
  • Kami dapat menerapkan Clamp untuk rentang eksklusif dengan f32 / f64 nanti jika diinginkan. ( @scottmcm berkomentar bahwa ini tidak langsung karena std sengaja tidak memiliki f32 / f64 operasi pendahulunya. Saya tidak yakin mengapa std tidak memiliki operasi itu; mungkin masalah dengan angka denormal? Bagaimanapun, itu bisa diatasi nanti.)

    Bahkan jika kita tidak menambahkan implementasi Clamp untuk rentang eksklusif dengan f32 / f64 , saya tidak setuju bahwa ini akan terlalu mengejutkan. Seperti yang ditunjukkan @varkor , Rust sudah memperlakukan berbagai jenis rentang secara berbeda untuk tujuan Copy dan Iterator / IntoIterator . (IMO, ini adalah kutil std , tetapi setidaknya satu contoh di mana jenis rentang diperlakukan secara berbeda.) Plus, jika seseorang mencoba menggunakan rentang eksklusif, pesan kesalahan akan mudah dimengerti ( "sifat terikat std::ops::Range<f32>: Clamp<f32> tidak puas").

  • Saya telah membuat Output jenis terkait untuk fleksibilitas maksimum untuk menambahkan lebih banyak implementasi di masa mendatang, tetapi itu tidak sepenuhnya diperlukan.

Pada dasarnya, pendekatan ini memungkinkan kita fleksibilitas sebanyak yang kita inginkan sehubungan dengan batas sifat. Ini juga memungkinkan untuk memulai dengan set implementasi Clamp berguna minimal, dan menambahkan lebih banyak implementasi nanti tanpa merusak perubahan.

Membandingkan pilihan

Pendekatan "gunakan .clamp() dengan INFINITY " memiliki kelemahan substansial, seperti yang disebutkan di atas.

Pendekatan " .clamp " + .clamp_min() + .clamp_max() memiliki kelemahan sebagai berikut:

  • Ini lebih bertele-tele, misalnya input.clamp_min(0) daripada input.clamp(0..) .
  • Itu tidak mendukung rentang eksklusif.
  • Kami tidak dapat menambahkan lebih banyak implementasi .clamp() di masa mendatang (tanpa menambahkan lebih banyak metode). Misalnya, kami tidak dapat mendukung penjepitan nilai u32 dengan batas u8 , yang merupakan fitur yang diminta dari diskusi RFC . Contoh khusus itu mungkin lebih baik ditangani dengan fungsi .saturating_into() , tetapi mungkin ada contoh lain di mana lebih banyak implementasi penjepitan akan berguna.
  • Seseorang mungkin bingung antara .min() , .max() , .clamp_min() , dan .clamp_max() untuk penjepitan satu sisi. (Menjepit dengan .clamp_min() mirip dengan menggunakan .max() , dan menjepit dengan .clamp_max() mirip dengan menggunakan .min() .) Kita sebagian besar dapat menghindari masalah ini dengan memberi nama operasi penjepitan satu sisi .clamp_lower() / .clamp_upper() atau .clamp_to_start() / .clamp_to_end() bukannya .clamp_min() / .clamp_max() , meskipun itu bahkan lebih verbose ( input.clamp_lower(0) versus input.clamp(0..) ).

Pendekatan argumen jangkauan memiliki kelemahan sebagai berikut:

  • Implementasinya lebih kompleks daripada menambahkan .clamp_min() / .clamp_max() .
  • Jika kami memutuskan untuk tidak atau tidak dapat menerapkan Clamp untuk jenis rentang eksklusif, ini mungkin mengejutkan.

Saya tidak memiliki pendapat yang kuat tentang pendekatan " .clamp + .clamp_min() + .clamp_max() versus pendekatan argumen rentang. Ini adalah pertukaran.

@Xaeroxe Ini menggandakan jumlah percabangan yang terjadi dalam fungsi untuk mengakomodasi kasus penggunaan yang kami masih belum yakin ada. Saya tidak bisa menampilkan ini di benchmark.

Mungkin cabang tambahan akan dioptimalkan oleh LLVM?

Pada penjepitan satu sisi

Karena penjepitan sudah termasuk di kedua sisi, seseorang dapat menentukan min/maks di kiri/kanan untuk mendapatkan perilaku penjepitan satu sisi saja. Saya pikir itu bisa diterima, dan bisa dibilang lebih bagus daripada .clamp((Bound::Unbounded, Inclusive(3.2))) mana tidak ada tipe Range* yang cocok:

x.clamp(i32::MIN, 10);
x.clamp(-f32::INFINITY, 10.0);

Tidak ada kerugian kinerja, karena LLVM secara sepele dapat mengoptimalkan sisi mati: https://rust.godbolt.org/z/l_uBLO

Sintaks rentang akan keren, tetapi clamp cukup mendasar sehingga dua argumen terpisah baik-baik saja dan mudah dimengerti.

Mungkin penanganan min / max NaN dapat diperbaiki dengan sendirinya, misalnya dengan mengubah implementasi metode bawaan f32 ? Atau mengkhususkan PartialOrd::min/max ? (dengan bendera edisi, dengan asumsi Rust berhasil menemukan cara untuk beralih hal-hal di libstd).

@scottmcm Anda harus memeriksa RangeToInclusive .

Setelah merenungkan ini lagi, saya sadar bahwa stabil itu selamanya, jadi kita tidak boleh mempertimbangkan "mengatur ulang proses RFC" sebagai alasan untuk tidak melakukan perubahan.

Untuk itu, saya ingin kembali ke pola pikir yang saya miliki saat menerapkan ini. Clamp secara konseptual beroperasi pada rentang, jadi masuk akal untuk menggunakan kosakata yang sudah dimiliki Rust untuk mengekspresikan rentang. Itu adalah reaksi sentakan lutut saya, dan itu tampaknya menjadi reaksi bagi banyak orang lainnya. Jadi mari kita ulangi argumen untuk tidak melakukannya dengan cara ini, dan lihat apakah kita bisa membantahnya.

  • Pemilihan rentang yang diperlukan cukup baru sehingga memerlukan sifat baru, dan secara khusus mengecualikan rentang yang paling umum, Range .

    • Menggunakan implementasi baru yang disediakan oleh @jturner314 kami sekarang memiliki kemampuan untuk menambahkan lebih banyak batasan pada jenis Range* , seperti Ord + Step untuk mengembalikan nilai dengan benar untuk rentang eksklusif. Jadi, meskipun penjepit rentang eksklusif seringkali tidak terlalu dibutuhkan, kami sebenarnya dapat menerima seluruh rentang rentang di sini, tanpa mengorbankan antarmuka rentang yang tidak memiliki batasan teknis ini.
  • Kita bisa menggunakan Infinity/Min/Max untuk penjepitan satu sisi.

    • Itu benar, dan sebagian besar mengapa perubahan ini bukanlah mandat yang kuat menurut saya. Saya hanya punya satu jawaban untuk ini, dan itu adalah sintaks Range* melibatkan lebih sedikit karakter dan lebih sedikit impor untuk kasus penggunaan ini.

Sekarang kami telah membantah alasan untuk tidak melakukan ini, komentar ini tidak memiliki motivasi apa pun untuk melakukan perubahan, karena opsinya tampak setara. Mari temukan beberapa motivasi untuk melakukan perubahan. Saya hanya punya satu alasan, yaitu bahwa pendapat umum di utas ini tampaknya bahwa pendekatan berbasis rentang meningkatkan semantik bahasa. Tidak hanya untuk penjepit rentang ujung ganda yang inklusif, tetapi juga untuk fungsi seperti .min() dan .max() .

Saya ingin tahu apakah pemikiran ini memiliki daya tarik dengan orang lain yang mendukung menstabilkan RFC apa adanya.

Saya pikir akan lebih baik untuk meninggalkan Clamp dalam bentuk saat ini, karena sekarang sangat mirip dengan bahasa lain.
Ketika saya mengerjakan pull request #58710 saya mencoba menggunakan implementasi berbasis Range.
Tapi rust-lang/rfcs#1961 (komentar) ) meyakinkan saya, bahwa itu lebih baik dalam bentuk standar.

Saya pikir logis untuk memiliki atribut #[must_use] pada fungsi tersebut, agar tidak membingungkan orang yang tidak terbiasa dengan cara kerja angka karat. Artinya, saya dapat dengan mudah melihat seseorang menulis kode (salah) berikut:

let mut x: f64 = some_number_source();
x.clamp(0.0, 1.0);
//Proceeds to assume that 0.0 <= x <= 1.0

Secara umum, rust mengambil pendekatan (number).method() untuk numerik (sedangkan bahasa lain menggunakan Math.Method(number) ), tetapi bahkan ketika mengingat hal ini, itu akan menjadi asumsi logis bahwa ini dapat mengubah number . Ini lebih merupakan kualitas hidup daripada apa pun.

Atribut [must_use] telah ditambahkan baru-baru ini .
@ Xaeroxe Apakah Anda menemukan sesuatu untuk penjepit berbasis jangkauan?
Saya pikir fungsi seperti sekarang ini paling cocok dengan fungsi numerik karat lainnya dan ingin mulai menstabilkannya lagi.

Saat ini saya tidak melihat alasan untuk menggunakan penjepit berbasis rentang. Ya, mari tambahkan atribut must_use dan bekerja menuju stabilisasi.

@SimonSapin @scottmcm Bisakah kita memulai kembali proses stabilisasi?

Seperti yang dikatakan @jturner314 , akan sangat bagus untuk memiliki penjepit di PartialOrd, bukan Ord, sehingga juga dapat digunakan pada pelampung.

Kami memiliki f32::clamp dan f64::clamp dalam masalah ini.

Inilah yang saya coba lakukan:

use num_traits::float::FloatCore;

struct Foo<T> (T);

impl<T: FloatCore> Foo<T> {
    fn foo(&self) -> T {
        self.0.clamp(1, 10)
    }
}

fn main() {
    let foo = Foo(15.3);
    println!("{}", foo.foo())
}

Tautan ke taman bermain.

PartialOrd bukan sifat float-only. Memiliki metode khusus float tidak membuat penjepit tersedia untuk tipe PartialOrd kustom.

Implementasi saat ini membutuhkan Eq , meskipun tidak menggunakannya.

Perhatian utama dengan PartialOrd adalah bahwa ia memberikan jaminan yang lebih lemah, yang pada gilirannya melemahkan jaminan penjepit. Pengguna yang menginginkan ini berada di PartialOrd mungkin tertarik dengan fungsi lain yang saya tulis https://docs.rs/num/0.2.1/num/fn.clamp.html

Apa jaminan ini?

Harapan yang cukup wajar adalah jika x.clamp(a, b) == x maka a <= x && x <= b . Ini tidak dijamin dengan PartialCmp mana x mungkin tidak dapat dibandingkan dengan a atau b .

Datang ke sini hari ini mencari clamp() samar-samar diingat dan membaca diskusi dengan penuh minat.

Saya akan menyarankan menggunakan "trik opsi" sebagai kompromi antara mengizinkan rentang sewenang-wenang dan memiliki beberapa fungsi bernama. Saya tahu ini tidak populer bagi sebagian orang, tetapi tampaknya menangkap semantik yang diinginkan dengan baik di sini:

#![allow(unstable_name_collisions)]

pub trait Clamp: Sized {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>;
}

impl Clamp for f32 {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>,
    {
        let below = match lower.into() {
            None => self,
            Some(lower) => self.max(lower),
        };
        match upper.into() {
            None => below,
            Some(upper) => below.min(upper),
        }
    }
}

#[test]
fn test_clamp() {
    assert_eq!(1.0, f32::clamp(2.0, -1.0, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, 1.0));
    assert_eq!(1.0, f32::clamp(2.0, None, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, None));
    assert_eq!(2.0, f32::clamp(2.0, -1.0, None));
    assert_eq!(-2.0, f32::clamp(-2.0, None, 1.0));
}

Jika ini disertakan dalam std implementasi blanket juga dapat disertakan untuk T: Ord , yang akan mencakup kekhawatiran yang muncul tentang implementasi PartialOrd .

Mengingat bahwa mendefinisikan fungsi clamp() dalam kode pengguna saat ini menghasilkan peringatan kompiler tentang tabrakan nama yang tidak stabil secara default, saya pikir nama "clamp" baik-baik saja untuk fungsi ini.

Saya pikir, bahwa clamp(a,b,c) harus berperilaku sama seperti min(max(a,b), c) .
Karena max dan min tidak diterapkan untuk PartialOrd clamp .
Masalah dengan NaN sudah dibahas .

@EdorianDark saya setuju. min, maks juga hanya membutuhkan PartialOrd.

@noonien min dan max didefinisikan sejak Rust 1.0 dan mereka membutuhkan Ord dan memiliki definisi untuk f32 dan f64 .
Ini bukan tempat yang tepat untuk membahas fungsi ini.
Di sini kita hanya dapat menjaga bahwa min , max dan clamp berperilaku sebanding dan tidak mengherankan.
Sunting: Saya tidak suka situasi dengan PartialOrd dan lebih suka float implement Ord , tetapi ini tidak mungkin diubah lagi setelah 1.0.

Ini telah bergabung dan tidak stabil selama sekitar satu setengah tahun sekarang. Bagaimana perasaan kita tentang menstabilkan ini?

Saya akan senang menstabilkan ini!

Jika konflik nama metode clamp terdengar seperti masalah, saya menyarankan untuk mengubah resolusi nama pada satu titik di https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812, dan itu akan membantu dengan ini juga.

@Xaeroxe Saya pikir prosesnya adalah mengirimkan PR stabilisasi dan meminta konsensus tim lib tentang itu. Tampaknya t-libs kelebihan beban dan tidak dapat mengikuti hal-hal non-fcped.

@matklad sebenarnya proposal FCP sudah dimulai tahun lalu di https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 , tetapi macet karena ada satu kotak centang yang tersisa.

Dalam hal ini, saya pikir melakukan ping setahun sekali pada suatu masalah cukup dapat ditoleransi.

@Kimundi
@sfackler
@tanpa perahu

https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 masih menunggu perhatian Anda

Tim libs telah berubah sedikit sejak FCP dimulai. Apa pendapat Anda semua tentang memulai FCP baru di PR stabilisasi? Rasanya itu tidak akan memakan waktu lebih lama daripada menunggu kotak centang yang tersisa di sini.

@LukasKalbertodt baik-baik saja oleh saya, apakah Anda keberatan

Membatalkan FCP di sini, karena FCP itu sekarang terjadi pada PR stabilisasi: https://github.com/rust-lang/rust/pull/77872#issuecomment -722982535

@fcpbot batal

uh

@rfcbot batal

@m-ou-se proposal dibatalkan.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat