Scala görevlendirmesinin atanan değerden ziyade Birim'e göre değerlendirilmesinin motivasyonu nedir?


84

Scala görevlendirmesinin atanan değerden ziyade Birim'e göre değerlendirilmesinin motivasyonu nedir?

G / Ç programlamasında yaygın bir model, aşağıdaki gibi şeyler yapmaktır:

while ((bytesRead = in.read(buffer)) != -1) { ...

Ancak Scala'da bu mümkün değil çünkü ...

bytesRead = in.read(buffer)

.. bytesRead'in yeni değerini değil, Birim'i döndürür.

İşlevsel bir dilin dışında bırakmak ilginç bir şey gibi görünüyor. Neden böyle yapıldığını merak ediyorum?


David Pollack, Martin Odersky'nin cevabına bıraktığı yorumla hemen hemen onaylanan bazı ilk elden bilgiler yayınladı. Sanırım Pollack'ın cevabını güvenle kabul edebilirsiniz.
Daniel C. Sobral

Yanıtlar:


89

Atamaların birim yerine atanan değeri döndürmesini savundum. Martin ve ben bunun üzerinde ileri geri gittik, ancak onun argümanı, yığına zamanın% 95'ini çıkarmak için bir değer koymanın bayt kodlarının boşa harcanması ve performans üzerinde olumsuz bir etkiye sahip olduğuydu.


7
Scala derleyicisinin atamanın değerinin gerçekten kullanılıp kullanılmadığına bakamamasının ve buna göre verimli bayt kodu üretememesinin bir nedeni var mı?
Matt R

43
Ayarlayıcıların varlığında bu o kadar kolay değildir: Her pasör bir sonuç döndürmek zorundadır, bu da yazmak için bir acıdır. Daha sonra, derleyicinin bunu optimize etmesi gerekir, bu da aramalar arasında yapılması zordur.
Martin Odersky

1
İddianız mantıklı, ancak java & C # buna karşı. Sanırım üretilen bayt koduyla garip bir şey yapıyorsunuz, o zaman Scala'daki bir atama nasıl sınıf dosyasına derlenir ve Java'ya geri derlenmiş gibi görünür?
Phương Nguyenễn

3
@ PhươngNguyễn Fark, Tekdüzen Erişim İlkesidir. C # / Java ayarlayıcılarda (genellikle) geri döner void. Scala'da foo_=(v: Foo), Fooatama yaparsa geri dönmelidir .
Alexey Romanov

5
@Martin Odersky: şuna ne dersiniz: ayarlayıcılar kalır void( Unit), ödevler x = valueeşdeğerine çevrilir x.set(value);x.get(value); derleyici get, değer kullanılmamışsa -call'ları optimize etme aşamalarını ortadan kaldırır . Yeni bir ana sürümde (geriye dönük uyumsuzluk nedeniyle) hoş bir değişiklik olabilir ve kullanıcılar için daha az rahatsızlık olabilir. Ne düşünüyorsun?
Eugen Labun

20

Gerçek nedenler hakkında içeriden bilgi alamıyorum, ancak şüphem çok basit. Scala, programcıların doğal olarak anlamak için tercih etmeleri için yan etkili döngülerin kullanımını garip hale getirir.

Bunu birçok şekilde yapar. Örneğin, forbir değişkeni bildirdiğiniz ve değiştirdiğiniz bir döngünüz yok. Bir whiledöngüdeki durumu (kolayca) mutasyona uğratamazsınız , aynı zamanda koşulu test edersiniz, bu da mutasyonu genellikle ondan hemen önce ve sonunda tekrarlamanız gerektiği anlamına gelir. Bir whileblok içinde bildirilen değişkenler whiletest koşulundan görünmez , bu da do { ... } while (...)daha az kullanışlı hale getirir . Ve bunun gibi.

Çözüm:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

Değeri ne olursa olsun.

Alternatif bir açıklama olarak, belki Martin Odersky, bu tür kullanımdan kaynaklanan çok çirkin birkaç hatayla yüzleşmek zorunda kaldı ve bunu kendi dilinden yasaklamaya karar verdi.

DÜZENLE

David Pollack , yanıtını Martin Odersky'nin kendisinin de Pollack tarafından ortaya konan performansla ilgili konular argümanına güvenerek yorumladığı gerçeğiyle açıkça onaylanan bazı gerçek gerçeklerle yanıtladı .


3
Yani muhtemelen fordöngü versiyonu olacaktır: for (bytesRead <- in.read(buffer) if (bytesRead) != -1hayır olmadığından işe yaramaz dışında büyük olan foreachve withFilterkullanılabilir!
oxbow_lakes

12

Bu, Scala'nın daha "resmi olarak doğru" bir tip sistemine sahip olmasının bir parçası olarak gerçekleşti. Resmi olarak konuşursak, atama tamamen yan etkiye sahip bir ifadedir ve bu nedenle geri dönmelidir Unit. Bunun bazı güzel sonuçları var; Örneğin:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

state_=Yöntemi döndürür Unit(bir ayarlayıcı için beklenen) tam atama döner, çünkü Unit.

Bir akışı veya benzerini kopyalamak gibi C tarzı desenler için bu özel tasarım kararının biraz zahmetli olabileceğine katılıyorum. Ancak, aslında genel olarak nispeten problemsizdir ve tip sisteminin genel tutarlılığına gerçekten katkıda bulunur.


Teşekkürler Daniel. Tutarlılık, hem atamaların hem de ayarlayıcıların değeri döndürmesi olsaydı, bunu tercih ederim! (Yapamamaları için hiçbir sebep yok.) Henüz "tamamen yan etkiye sahip bir ifade" gibi kavramların nüanslarını karıştırmadığımdan şüpheleniyorum.
Graham Lea

2
@Graham: Ama o zaman, tutarlılığı takip etmeli ve tüm ayarlayıcılarınızda ne kadar karmaşık olurlarsa olsunlar, belirledikleri değeri döndürdüklerinden emin olmalısınız. Bu bazı durumlarda karmaşık, bazı durumlarda ise yanlış olabilir diye düşünüyorum. (Hata durumunda ne döndürürdünüz? Null? - değil. Yok mu? - o zaman türünüz Seçenek [T] olacaktır.) Bence bununla tutarlı olmak zor.
Debilski

7

Belki de bu komut-sorgu ayırma ilkesinden kaynaklanmaktadır?

CQS, yan etkileri olan veya olmayan (yani nesneyi değiştiren) nesne yöntemleri arasında bariz bir ayrım yarattığı için OO ve fonksiyonel programlama stillerinin kesişme noktasında popüler olma eğilimindedir. CQS'yi değişken atamalara uygulamak, her zamankinden daha ileri götürür, ancak aynı fikir geçerlidir.

CQS yararlıdır neden kısa bir gösterimidir: bir hipotetik hibrid F / OO dili göz önünde Listyöntemleri vardır sınıfı Sort, Append, Firstve Length. Zorunlu OO tarzında, şöyle bir işlev yazmak isteyebilir:

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

Daha işlevsel bir tarzda ise, şunun gibi bir şey yazılması daha olasıdır:

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

Bunlar aynı şeyi yapmaya çalışıyor gibi görünüyor , ancak ikisinden biri yanlış ve yöntemlerin davranışları hakkında daha fazla bilgi sahibi olmadan hangisi olduğunu bilemiyoruz.

Bununla birlikte, CQS'yi kullanarak , listeyi değiştirir Appendve Sortdeğiştirirlerse, birim türünü döndürmeleri gerektiği konusunda ısrarcı oluruz , böylece yapmamamız gerektiğinde ikinci formu kullanarak hatalar oluşturmamızı engelleriz. Yan etkilerin varlığı bu nedenle yöntem imzasında da örtük hale gelir.


4

Sanırım bu, programı / dili yan etkilerden uzak tutmak için.

Tanımladığınız şey, genel durumda kötü bir şey olarak kabul edilen bir yan etkinin kasıtlı olarak kullanılmasıdır.


Heh. Scala yan etkisiz mi? :) Ayrıca, val a = b = 1( valönünde "büyülü" hayal edin b) vs. gibi bir durum düşünün val a = 1; val b = 1;.

Bunun yan etkilerle hiçbir ilgisi yok, en azından burada anlatılan anlamıyla: Yan etki (bilgisayar bilimi)
Feuermurmel

4

Bir atamayı boole ifadesi olarak kullanmak en iyi stil değildir. Aynı anda iki şeyi gerçekleştirirsiniz, bu da sıklıkla hatalara yol açar. Ve Scalas kısıtlamasıyla "==" yerine "=" nin yanlışlıkla kullanılması önlenir.


2
Bunun saçma bir sebep olduğunu düşünüyorum! OP'nin yayınladığı gibi, kod hala derlenir ve çalışır: sadece makul olarak beklediğiniz şeyi yapmaz. Bir tane daha gotcha, bir eksik değil!
oxbow_lakes

1
If (a = b) gibi bir şey yazarsanız derlemez. Yani en azından bu hatadan kaçınılabilir.
deamon

1
OP, '==' yerine '=' kullanmadı, ikisini de kullandı. Atamanın daha sonra kullanılabilecek bir değer döndürmesini bekler, örneğin başka bir değerle karşılaştırmak için (örnekte -1)
IttayD

@deamon: a ve b boole ise (en azından Java'da) derlenecektir. İf (a = true) kullanarak bu tuzağa düşen yeni başlayanlar gördüm. Daha basit if (a) 'yı tercih etmek için bir neden daha (ve daha anlamlı bir ad kullanıyorsanız daha net!).
PhiLho

2

Bu arada: Başlangıçtaki while-trick'i Java'da bile aptalca buluyorum. Neden böyle bir şey olmasın?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

Verilmiş, ödev iki kez görünüyor, ancak en azından bytesRead ait olduğu kapsamda ve komik atama hileleriyle oynamıyorum ...


1
Hile oldukça yaygın olsa da, genellikle bir arabellekten okuyan her uygulamada görünür. Ve her zaman OP'nin versiyonuna benziyor.
TWiStErRob

0

Yönlendirme için bir referans türünüz olduğu sürece bunun için bir geçici çözüm bulabilirsiniz. Deneyimsiz bir uygulamada, rastgele türler için aşağıdakileri kullanabilirsiniz.

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

Ardından, ref.valuedaha sonra referansa erişmek için kullanmanız gereken kısıtlama altında, whileyükleminizi şu şekilde yazabilirsiniz:

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

ve karşı denetimi yazmak bytesReadzorunda kalmadan daha örtük bir şekilde yapabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.