Değişken değişkenler kullanmadan faydalı Java programları nasıl yazılır


12

Yazarın belirttiği fonksiyonel programlama hakkında bir makale okuyordum

(take 25 (squares-of (integers)))

Değişkenleri olmadığına dikkat edin. Gerçekten de, üç fonksiyondan ve bir sabitten daha fazlası yoktur. Değişken kullanmadan Java'daki tamsayıların karelerini yazmayı deneyin. Oh, muhtemelen bunu yapmanın bir yolu var, ama kesinlikle doğal değil ve yukarıdaki programım kadar iyi okumaz.

Java ile bunu başarmak mümkün mü? İlk 15 tamsayının karelerini yazdırmanız gerektiğini varsayarsak, değişkenleri kullanmadan bir for veya while döngüsü yazabilir misiniz?

Mod bildirimi

Bu soru değil bir kod golf yarışma. Sadece başka bir kod parçası için değil, (daha önceki cevapları tekrar etmeden) ilgili kavramları açıklayan cevaplar arıyoruz.


19
Sizin fonksiyonel örnek yapar içeride kullanım değişkenleri, ama dil perde arkasında her şeyi yapar. Hoş olmayan kısımları doğru bir şekilde yaptığını düşündüğünüz birine etkili bir şekilde devrettiniz.
Blrfl

12
@Blrfl: "Sahne arkası" argümanı tüm dil tabanlı tartışmaları öldürür, çünkü her kod parçası nihayetinde x86 makine koduna çevrilir. x86 kodu nesneye yönelik değildir, yordamsal değildir, işlevsel değildir, hiçbir şey değildir, ancak bu kategoriler programlama dilleri için değerli etiketlerdir. Uygulamaya değil, dile bakın.
thiton

10
@thiton Kabul edilmedi. Blrfl'in söylediği, bu işlevlerin muhtemelen aynı programlama dilinde yazılmış değişkenleri kullanmasıdır . Burada düşük seviyeye gitmeye gerek yok. Parçacık yalnızca kütüphane işlevlerini kullanıyor. Aynı kodu Java'da kolayca yazabilirsiniz, bkz: squaresOf(integers()).take(25)(bu işlevleri yazmak okuyucu için bir alıştırma olarak bırakılır; zorluk sonsuz sette yatmaktadır integers(), ancak bu istekli değerlendirmesi nedeniyle Java için bir sorundur, bununla ilgisi yoktur. değişkenleri)
Andres F.

6
Bu alıntı kafa karıştırıcı ve yanıltıcı, orada sihir yok, sadece sözdizimsel şeker .
yannis

2
@thiton FP dilleri hakkında daha fazla bilgi edinmenizi öneririm, ancak yine de kod snippet'i "değişkenlerin" gerekli olmadığı iddiasını desteklemez (veya reddeder), çünkü tür FP yaygındır). Parçacık sadece Java'da da uygulanabilecek kütüphane işlevlerini gösterir, burada offtopik olan tembel / istekli problemleri engeller.
Andres F.

Yanıtlar:


31

Yıkıcı güncellemeler kullanmadan böyle bir örneği Java'da uygulamak mümkün müdür? Evet. Bununla birlikte, @Thiton ve makalenin kendisinin de belirttiği gibi, çirkin olacak (kişinin zevkine bağlı olarak). Bunun bir yolu özyineleme kullanmaktır; İşte benzer bir şey yapan bir Haskell örneği:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

Not 1) mutasyon eksikliği, 2) özyineleme kullanımı ve 3) döngü eksikliği. Son nokta çok önemlidir - fonksiyonel diller, dilde yerleşik döngü yapılarına ihtiyaç duymaz, çünkü özyineleme Java'da döngülerin kullanıldığı çoğu (tümü?) Vaka için kullanılabilir. İşte çok etkileyici işlev çağrılarının ne kadar etkileyici olabileceğini gösteren iyi bilinen bir dizi makale.


Makaleyi tatmin edici bulmadım ve birkaç ek nokta daha yapmak istiyorum:

Bu makale fonksiyonel programlamanın ve faydalarının çok zayıf ve kafa karıştırıcı bir açıklamasıdır. Fonksiyonel programlamayı öğrenmek için diğer kaynakları şiddetle tavsiye ediyorum .

Makale ile ilgili en kafa karıştırıcı kısım, Java'da (ve diğer birçok ana dilde) atama ifadeleri için iki kullanımdan bahsetmediğidir:

  1. bir değeri bir ada bağlama: final int MAX_SIZE = 100;

  2. yıkıcı güncelleme: int a = 3; a += 1; a++;

Fonksiyonel programlama ikinciden kaçınır, ancak ilkini kucaklar (örnekler: let-ifadeler, fonksiyon parametreleri, üst düzey defineitions) . Aksi makale sadece aptalca görünüyor ve merak terk edebilir, ne çünkü bu, kavramak için çok önemli bir noktadır take, squares-ofve integersdeğişkenler değilse?

Ayrıca, örnek anlamsızdır. Bu uygulamalarını göstermiyor take, squares-ofya integers. Bildiğimiz kadarıyla, değişken değişkenler kullanılarak uygulanırlar. @Martin'in söylediği gibi, bu örneği Java'da önemsiz bir şekilde yazabilirsiniz.

Bir kez daha, fonksiyonel programlama hakkında gerçekten öğrenmek istiyorsanız, bu makaleden ve benzerlerinden kaçınmanızı tavsiye ederim. Kavram ve temelleri öğretmekten ziyade şok edici ve rahatsız edici bir amaç ile yazılmış gibi görünüyor. Bunun yerine, neden John Hughes'un tüm zamanların en sevdiğim gazetelerinden birine göz atmıyorsun? Hughes makalenin kapsadığı aynı sorunlardan bazılarını çözmeye çalışır (Hughes eşzamanlılık / paralelleşme hakkında konuşmasa da); İşte bir iltifat:

Bu çalışma, daha büyük (işlevsel olmayan) programcı topluluğuna işlevsel programlamanın önemini gösterme ve ayrıca işlevsel programcıların bu avantajları açıklığa kavuşturarak avantajlarını sonuna kadar kullanmalarına yardımcı olma girişimidir.

[...]

Bu yazının geri kalanında işlevsel dillerin iki yeni, çok önemli tutkal türü sunduğunu tartışacağız. Yeni şekillerde modüle edilebilecek ve böylece basitleştirilebilecek bazı program örnekleri vereceğiz. Bu, işlevsel programlamanın gücünün anahtarıdır - geliştirilmiş modülerleştirmeye izin verir. Ayrıca, işlevsel programcıların çabalaması gereken hedeftir - tarif edeceğimiz yeni yapıştırıcılar ile yapıştırılmış daha küçük ve daha basit ve daha genel modüller.


10
+1, "İşlevsel programlama hakkında gerçekten öğrenmek istiyorsanız, bu makaleden ve benzerlerinden kaçınmanızı tavsiye ederim. Kavram ve temelleri öğretmek yerine şok edici ve rahatsız edici bir amaç ile daha çok yazılmış gibi görünüyor."

3
İnsanların FP yapmamalarının yarısı, üniversitede bunun hakkında hiçbir şey duymadıkları / öğrenmedikleri ve diğer yarısı, içine baktıklarında, onları hem bilgisiz bırakan hem de bunların biraz hayalperest olduğunu düşünen makaleler bulmalarıdır. faydaları ile düşünülmüş ve gerekçeli bir yaklaşım olmaktan ziyade oynamaktır. Daha iyi bilgi kaynakları için +1
Jimmy Hoffa

3
Eğer soruya doğrudan cevap verirseniz ve belki de bu soru o zaman açık kalacaktır (doğrudan soru odaklı bir cevapla) soruya cevabınızı mutlak tepeye koyun
Jimmy Hoffa

2
Nitpick için üzgünüm, ama neden bu haskell kodunu seçtiğinizi anlamıyorum. LYAH okudum ve senin örnek almam zor. Orijinal soru ile olan ilişkisini de görmüyorum. Neden sadece take 25 (map (^2) [1..])örnek olarak kullanmadınız ?
Daniel Kaplan

2
@tieTYT iyi soru - bunu işaret ettiğiniz için teşekkürler. Bu örneği kullanmamın nedeni, özyineleme kullanarak ve değişken değişkenlerden kaçınarak bir sayı listesinin nasıl oluşturulacağını göstermesidir. Amacım OP'nin bu kodu görmesi ve Java'da benzer bir şeyin nasıl yapılacağını düşünmesiydi. Kod snippet'inize hitap etmek için nedir [1..]? Bu Haskell'de yerleşik olan harika bir özellik, ancak böyle bir liste oluşturmanın arkasındaki kavramları göstermiyor. Eminim Enumsınıf (ki sözdizimi gerektirir) örnekleri de yararlı, ama onları bulmak için çok tembel. Böylece unfoldr,. :)

27

Yapmazsın. Değişkenler zorunlu programlamanın merkezinde yer alır ve değişkenleri kullanmadan zorunlu olarak programlamaya çalışırsanız, sadece herkesin kıçından acı çekmesine neden olursunuz. Farklı programlama paradigmalarında, stiller farklıdır ve farklı kavramlar temelinizi oluşturur. Java'da bir değişken, küçük bir kapsamla iyi kullanıldığında kötü değildir. Değişkenler olmadan bir Java programı istemek, işlevleri olmayan bir Haskell programı istemek gibidir, bu yüzden bunu istemezsiniz ve değişkenleri kullandığı için zorunlu programlamayı daha aşağı olarak görmeye aldanmanıza izin vermezsiniz.

Java yolu şöyle olur:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

ve değişkenlerin nefreti nedeniyle bunu daha karmaşık bir şekilde yazmanıza izin vermeyin.


5
"Değişkenlerden nefret" mi? Ooookay ... Fonksiyonel Programlama hakkında ne okudunuz? Hangi dilleri denediniz? Hangi öğreticiler?
Andres F.

8
@AndresF .: Haskell'de iki yıldan fazla kurs. FP'nin kötü olduğunu söylemiyorum. Bununla birlikte, birçok FP-IP görüşmesinde (bağlantılı makale gibi) yeniden atanabilir adlandırılmış varlıkların (AKA değişkenleri) kullanımını kınama ve iyi bir neden veya veri olmadan kınama eğilimi vardır. Kitabımda mantıksız kınama nefret ediliyor. Ve nefret gerçekten kötü bir kod yaratır.
thiton

10
"Değişkenlerin nefreti" nedensel aşırı basitleştirmedir en.wikipedia.org/wiki/Fallacy_of_the_single_cause Java'da bile vatansız durum programlamanın birçok faydası vardır, ancak Java'da maliyetin karmaşıklığının çok yüksek olacağı cevabına katılıyorum program ve deyimsel olmayan. Vatansız programlamanın iyi ve durumsal olduğu fikrini, insanların deneyimler nedeniyle ortaya çıktığı gerekçeli, iyi düşünülmüş bir duruştan ziyade bazı duygusal tepkiler kadar kötü olduğu fikrini elden kaçırmam.
Jimmy Hoffa

2
@JimmyHoffa'nın söylediklerine göre, sizi zorunlu dilde fonksiyonel tarzda programlama (onun durumunda C ++) konusunda John Carmack'e yönlendireceğim ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers

5
Mantıksız kınama nefret değildir ve değişebilir durumdan kaçınmak mantıksız değildir.
Michael Shaw

21

Özyineleme ile yapabileceğim en basit, tek parametreli bir işlevdir. Çok Java esque değil, ama işe yarıyor:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
Bir Java örneğiyle soruyu gerçekten yanıtlamaya çalıştığınız için +1.
KChaloux

Ben kod golf tarzı sunum için bu aşağıya ( Mod uyarısı bakın ) ama o aşağı kendi ok cevap basmaya zorlayamaz çünkü bu kod benim favori cevap yapılan ifadeler mükemmel eşleşir : "1) mutasyon eksikliği, 2) kullanımı özyineleme ve 3) döngü eksikliği "
gnat

3
@gnat: Bu cevap Mod bildiriminden önce yayınlanmıştır. Harika bir tarz için gitmiyordum, basitlik içindeydim ve OP'nin orijinal sorusunu tatmin ediyordum; o göstermek için olan Java böyle şeyler yapmak mümkün.
Hayal kırıklığına

@FrustratedWithFormsDesigner emin; bu beni DVing'den alıkoymaz ( uymak için düzenleme yapabilmeniz gerekiyor ) - sihri yapan çarpıcı derecede mükemmel bir eşleşme . Aferin, gerçekten aferin, oldukça eğitici - teşekkür ederim
gnat

16

İşlevsel örneğinizde squares-ofve takeişlevlerinin nasıl uygulandığını görmüyoruz . Ben bir Java uzmanı değilim, ama eminim ki böyle bir ifadeyi etkinleştirmek için bu işlevleri yazabiliriz ...

squares_of(integers).take(25);

ki bu çok farklı değil.


6
Nitpick: squares-ofJava'da geçerli bir ad değil ( squares_ofolsa da). Ancak aksi halde, makalenin örneğinin zayıf olduğunu gösteren iyi bir nokta.

Makalenin integertembel bir şekilde tamsayılar oluşturduğundan ve takefonksiyonun 25 squared-ofrakamını aldığından şüpheleniyorum integer. Kısacası, integersonsuza tembel olarak tamsayılar üretme fonksiyonuna sahip olmalısınız .
OnesimusUnbound

(integer)Bir işlev gibi bir şeyi çağırmak biraz deliliktir - bir işlev hala bir argümanı bir değerle eşleyen bir şeydir . Bunun (integer)bir işlev değil, sadece bir değer olduğu ortaya çıkıyor . Hatta sonsuz sayıda sayıya bağlı integerbir değişken olduğunu söyleyebiliriz .
Ingo

6

Java'da bunu (özellikle sonsuz liste bölümü) yineleyicilerle yapabilirsiniz. Aşağıdaki kod örneğinde, kurucuya verilen sayı Takeisteğe bağlı olarak yüksek olabilir.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Veya zincirlenebilir fabrika yöntemleri ile:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Nerede SquaresOf, Takeve IntegersgenişletmekNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
Bu, OO paradigmasının işlevsel olana göre üstünlüğünü gösterir; uygun OO tasarımı ile fonksiyonel paradigmayı taklit edebilirsiniz ancak OO paradigmasını fonksiyonel bir tarzda taklit edemezsiniz.
m3th0dman

3
@ m3th0dman: Uygun OO tasarımıyla , tıpkı dizeleri, listeleri ve / veya sözlükleri olan herhangi bir dilin OO'yu yarı-kıyasıya taklit edebileceği gibi FP'yi yarı yarıya taklit edebilirsiniz. Genel amaçlı dillerin Turing denkliği, yeterli çaba gösterildiğinde, herhangi bir dilin başkalarının özelliklerini simüle edebileceği anlamına gelir.
cHao

Java tarzı yineleyicilerin FP'de while (test.hasNext()) System.out.println(test.next())hayır olacağını unutmayın ; yineleyiciler doğası gereği değiştirilebilir.
cHao

1
@cHao Gerçek kapsülleme veya polimorfizmin taklit edilebileceğine inanmıyorum; ayrıca Java (bu örnekte) sıkı istekli değerlendirme nedeniyle işlevsel bir dili gerçek anlamda taklit edemez. Ayrıca yineleyicilerin özyinelemeli olarak yazılabileceğine inanıyorum.
m3th0dman

@ m3th0dman: Polimorfizmi taklit etmek hiç de zor olmazdı; C ve montaj dili bile bunu yapabilir. Yöntemi nesnede bir alan veya sınıf tanımlayıcı / vtable yapın. Ve veri gizleme anlamında kapsülleme kesinlikle gerekli değildir; Dışarıdaki dillerin yarısı bunu sağlamaz, nesneniz değişmez olduğunda, insanların bağırsaklarını yine de görüp göremeyeceği önemli değildir. gerekli olan tek şey, yukarıda bahsedilen yöntem alanlarının kolayca sağlayabildiği veri kaydırmadır .
cHao

6

Kısa versiyon:

Tek atama stilinin Java'da güvenilir bir şekilde çalışmasını sağlamak için (1) bir tür değişmez dostu altyapıya ve (2) kuyruk çağrısı ortadan kaldırılması için derleyici veya çalışma zamanı düzeyinde desteğe ihtiyacınız vardır.

Altyapının çoğunu yazabiliriz ve yığını doldurmaktan kaçınmak için şeyler düzenleyebiliriz. Ancak, her çağrı bir yığın çerçevesi aldıkça, ne kadar özyineleme yapabileceğiniz konusunda bir sınır olacaktır. Yinelenebilir öğelerinizi küçük ve / veya tembel tutun ve büyük sorunlarınız olmamalıdır. Karşılaşacağınız sorunların en azından bir kerede bir milyon sonuç döndürmesini gerektirmez. :)

Ayrıca, programın çalışmaya değer olması için görünür değişiklikleri gerçekten etkilemesi gerektiğinden, her şeyi değiştirilemez hale getiremezsiniz . Bununla birlikte, temel alternatiflerin küçük bir alt kümesini (örneğin, akarsuları) kullanarak, yalnızca alternatiflerin çok zahmetli olacağı belirli noktalarda kendi eşyalarınızın büyük çoğunluğunu değişmez tutabilirsiniz.


Uzun versiyon:

Basitçe söylemek gerekirse, bir Java programı, yapmaya değer bir şey yapmak istiyorsa değişkenlerden tamamen kaçınamaz. Bunları içerebilir ve böylece değişebilirliği büyük ölçüde kısıtlayabilirsiniz, ancak dilin ve API'nin tasarımı - sonuçta altta yatan sistemi değiştirme ihtiyacıyla - toplam değişmezliği mümkün kılmaz.

Java en başından zorunlu , nesne yönelimli bir dil olarak tasarlanmıştır .

  • Zorunlu diller neredeyse her zaman bir tür değişebilir değişkenlere bağlıdır. Örneğin, yinelemeden yinelemeyi ve neredeyse tüm yinelemeli yapıları - hatta while (true)ve for (;;)! - tamamen yinelemeden yinelemeye dönüşen bir değişkene bağımlıdır.
  • Nesne yönelimli diller, hemen hemen her programı birbirine mesaj gönderen nesnelerin bir grafiği olarak görürler ve neredeyse tüm durumlarda bu mesajlara bir şeyler değiştirerek yanıt verir.

Bu tasarım kararlarının sonucu, değişken değişkenler olmadan Java'nın hiçbir şeyin durumunu değiştirmenin bir yolu olmadığıdır - hatta "Merhaba dünya!" ekrana olarak bayt yapışma içerir Bir çıkış akımının, içerir kesilebilir tamponu.

Yani, tüm pratik amaçlar için, değişkenleri kendi kodumuzdan çıkarmakla sınırlıyız . Tamam, bunu yapabiliriz. Neredeyse. Temel olarak ihtiyacımız olan şey, neredeyse tüm yinelemeyi özyineleme ile değiştirmek ve yinelenen çağrılarla değiştirilen değeri döndüren tüm mutasyonları değiştirmek. böyle ...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

Temel olarak, her bir düğümün kendi içinde bir liste olduğu bağlantılı bir liste oluştururuz. Her liste bir "kafa" (geçerli değer) ve bir "kuyruk" (kalan alt liste) içerir. Çoğu işlevsel dil buna benzer bir şey yapar, çünkü verimli değişmezliğe çok uygundur. Bir "sonraki" işlemi sadece özyinelemeli çağrılar yığını içinde bir sonraki seviyeye geçirilen kuyruğu döndürür.

Şimdi, bu son derece basitleştirilmiş bir versiyonudur. Ancak Java'daki bu yaklaşımda ciddi bir sorun göstermek için yeterince iyi. Bu kodu düşünün:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Sonuç için sadece 25 inte ihtiyacımız olmasına rağmen, squares_ofbunu bilmiyor. Her sayının karesini geri döndürecek integers. 20 milyon seviyeli derin özyineleme Java'da oldukça büyük sorunlara neden olur.

Gördüğünüz gibi, tipik olarak böyle tuhaflık yapacağınız işlevsel diller, "kuyruk çağrısının kaldırılması" özelliğine sahiptir. Bunun anlamı, derleyici kodun son eyleminin kendisini çağırmasını (ve işlevin geçersiz olmaması durumunda sonucu döndürmesini) gördüğünde, yenisini ayarlamak yerine geçerli çağrının yığın çerçevesini kullanır ve bunun yerine "atlama" yapar bir "çağrı" (böylece yığın alanı sabit kalır). Kısacası, kuyruk tekrarını yinelemeye dönüştürmenin yolunun yaklaşık% 90'ı. Bu milyar ints ile yığını aşmadan baş edebilirdi. (Nihayetinde bellek tükenecekti, ancak bir milyar inçlik bir liste oluşturmak, 32 bitlik bir sistemde sizi yine de hafızaya karıştıracak.)

Java bunu çoğu durumda yapmaz. (Derleyiciye ve çalışma zamanına bağlıdır, ancak Oracle'ın uygulaması bunu yapmaz.) Özyinelemeli bir işleve yapılan her çağrı, yığın çerçevesinin bellek değerini tüketir. Çok fazla tüketirseniz yığın taşması olur. Yığının taşması, programın ölümünü garanti eder. Bu yüzden bunu yapmamaya dikkat etmeliyiz.

Bir yarı geçici çözüm ... tembel değerlendirme. Yığın sınırlamalarımız hala var, ancak bunlar üzerinde daha fazla kontrol sahibi olduğumuz faktörlere bağlanabilir. Sadece 25 dönmek için bir milyon int hesaplamak zorunda değiliz. :)

Şimdi bize tembel bir değerlendirme altyapısı inşa edelim. (Bu kod bir süre önce test edildi, ancak o zamandan beri biraz değiştirdim; sözdizimi hatalarını değil, fikri okuyun. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(Bu, Java'da gerçekten geçerli olsaydı, en azından yukarıdaki gibi bir kodun zaten API'nın bir parçası olacağını unutmayın.)

Bir altyapı mevcut olduğunda, değişken değişkenlere ihtiyaç duymayan ve en azından daha küçük miktarlarda girdi için kararlı olan kod yazmak oldukça önemsizdir.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Bu çoğunlukla işe yarar, ancak taşma yığınlarına hala yatkındır. take2 milyar int denemek ve onlar üzerinde bir şeyler yapmak. : P En azından 64+ GB RAM standart hale gelene kadar bir istisna atar. Sorun şu ki, bir programın yığını için ayrılan belleğinin miktarı o kadar büyük değil. Genellikle 1 ila 8 MiB arasındadır. (Daha büyük isteyebilir ama hepsi çok ne kadar sormak fark etmez - aramak take(1000000000, someInfiniteSequence), sen olacaktır . İstisna olsun) bir alanda biz daha iyi yapamaz Neyse ki, tembel değerlendirmesiyle, zayıf nokta kontrolü . Ne kadar olduğumuza dikkat etmeliyiz take().

Yığın kullanımımız doğrusal olarak arttığından, hala ölçeklendirme konusunda birçok sorun yaşayacaktır. Her çağrı bir elemanı ele alır ve geri kalanını başka bir çağrıya iletir. Şimdi bunu düşündüğümde, çekebileceğimiz bir hile var, bu da bizi biraz daha fazla alan yaratabilir: çağrı zincirini bir çağrı ağacına dönüştürün . Bunun gibi bir şey düşünün:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

workWithtemelde işi iki yarıya böler ve her yarıyı kendisine başka bir çağrıya atar. Her çağrı, çalışma listesinin boyutunu bir değil yarı yarıya azalttığından, bu doğrusal olarak değil logaritmik olarak ölçeklendirilmelidir.

Sorun, bu fonksiyon bir giriş istiyor - ve bağlantılı bir listeyle, uzunluk elde etmek tüm listenin çaprazlanmasını gerektirir. Ancak bu kolayca çözülebilir; basitçe umurumda değil kaç tane giriş var. :) Yukarıdaki kod Integer.MAX_VALUEsayı gibi bir şeyle çalışır , çünkü bir null işlemi yine de durdurur. Sayım çoğunlukla oradadır, bu yüzden sağlam bir taban kasamız var. Integer.MAX_VALUEBir listede birden fazla giriş olmasını bekliyorsanız , workWithdönüş değerini kontrol edebilirsiniz - sonunda boş olmalıdır. Aksi takdirde, geri ödeme.

Unutmayın, bu söylediğiniz kadar öğeye dokunur. Tembel değil; hemen işini yapar. Bunu sadece eylemler için yapmak istersiniz - yani, tek amacı kendisini bir listedeki her öğeye uygulamak olan şeyler. Şu anda düşündüğüm gibi, bana göre diziler doğrusal tutulursa çok daha az karmaşık olurdu; bir sorun olmamalı, çünkü diziler zaten kendilerini çağırmıyorlar - sadece onları tekrar çağıran nesneler yaratıyorlar.


3

Daha önce Java'da lisp benzeri bir dil için bir tercüman oluşturmaya çalıştım (birkaç yıl önce ve tüm kod sourceforge'da CVS'de olduğu gibi kayboldu) ve Java uteri yineleyicileri fonksiyonel programlama için biraz ayrıntılı listelerde.

Burada, geçerli değeri almak ve bir sonraki öğeden başlayarak diziyi almak için ihtiyacınız olan iki işlemi içeren bir dizi arayüzüne dayanan bir şey var. Bunlar şemadaki işlevlerden sonra baş ve kuyruk olarak adlandırılır.

Listenin tembel bir şekilde oluşturulduğu anlamına geldiği için , Seqveya Iteratorarayüzleri gibi bir şey kullanmak önemlidir . IteratorArayüz böylece daha az fonksiyonel programlamaya uygundur, değişmez bir nesne olamaz - Eğer bir işlev geçmesine değeri onun tarafından değiştirilmiş ise, söyleyemem, sen fonksiyonel programlamanın önemli avantajlarından biri kaybeder.

Açıkçası integerstüm tamsayıların bir listesi olmalı, bu yüzden sıfırdan başladım ve dönüşümlü olarak pozitif ve negatif olanları geri verdim.

Karelerin iki sürümü vardır - biri özel bir dizi oluşturur, diğeri map'işlev' alır - Java 7'de lambda yoktur, bu yüzden bir arabirim kullandım - ve sırayla her öğeye uygular.

Noktası square ( int x )işlevi çağırmak için ihtiyacı ortadan kaldırmak için sadece head()normalde bir nihai değişkene değer koyarak, ancak programda hiçbir değişkenler, sadece işlev parametreleri vardır bu fonksiyon araçları ekleyerek bu yapardı - iki kez.

Bu tür bir programlama için Java'nın ayrıntısı, tercümanımın ikinci sürümünü C99'a yazmamı sağladı.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

Değişken değişkenler kullanmadan faydalı Java programları nasıl yazılır .

Teorik olarak Java'da sadece özyineleme kullanarak ve değişken değişkenler olmadan hemen hemen her şeyi uygulayabilirsiniz.

Uygulamada:

  • Java dili bunun için tasarlanmamıştır. Birçok yapı mutasyon için tasarlanmıştır ve onsuz kullanımı zordur. (Örneğin, değişken uzunluklu bir Java dizisini mutasyon olmadan başlatamazsınız.)

  • Kütüphaneler için aynen. Ve kendinizi kapak altında mutasyon kullanmayan kütüphane sınıflarıyla sınırlandırırsanız, daha da zorlaşır. (String'i bile kullanamazsınız ... nasıl hashcodeuygulandığına bir göz atın .)

  • Ana Java uygulamaları kuyruk çağrısı optimizasyonunu desteklemez. Bu, algoritmaların özyinelemeli sürümlerinin yığın alanı "aç" olma eğiliminde olduğu anlamına gelir. Java iş parçacığı yığınları büyümediğinden, büyük yığınları önceden konumlandırmanız gerekir ... veya risk StackOverflowError.

Bu üç şeyi birleştirdiğinizde Java, değişken değişkenleri olmayan yararlı (önemsiz olmayan) programlar yazmak için gerçekten uygun bir seçenek değildir .

(Ama hey, sorun değil. JVM için bazıları işlevsel programlamayı destekleyen başka programlama dilleri de var.)


2

Kavramların bir örneğini ararken, Java'yı bir kenara bırakalım ve kavramların tanıdık bir versiyonunu bulmak için farklı ama tanıdık bir ayar arayalım. UNIX boruları, zincirleme tembel işlevlere oldukça benzer.

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

Linux'ta bu, iştahımı kaybedene kadar her biri gerçek bitlerden ziyade yanlış bitlerden oluşan baytlar ver; bu baytların her birini bir yeni satır karakterine dönüştürmek; böylece oluşturulan her satırın numarası; ve bu sayının karesini üretiyoruz. Üstelik 25 hatta iştahım yok.

Bir programcının bu şekilde bir Linux boru hattı yazması tavsiye edilmediğini iddia ediyorum. Nispeten normal Linux kabuk komut dosyasıdır.

Bir programcının aynı şeyi Java'da benzer şekilde yazmaya çalışmasının tavsiye edilmediğini iddia ediyorum. Bunun nedeni, yazılım projelerinin ömür boyu maliyetinde önemli bir faktör olarak yazılım bakımdır. Görünüşte bir Java programı olanı sunarak bir sonraki programcıyı karıştırmak istemiyoruz, ancak aslında Java platformunda zaten var olan işlevselliği özenle çoğaltarak özel bir kerelik bir dilde etkili bir şekilde yazılmıştır.

Öte yandan, "Java" paketlerimizden bazıları aslında Clojure ve Scala gibi fonksiyonel veya nesne / fonksiyonel dillerden birinde yazılmış Java Sanal Makine paketleri ise bir sonraki programcının daha kabul edilebilir olabileceğini iddia ediyorum. Bunlar, fonksiyonları zincirleme ile kodlanacak ve Java yöntemi çağrılarının normal şekilde Java'dan çağrılacak şekilde tasarlanmıştır.

Daha sonra, yine de, bir Java programcısının yerlerde fonksiyonel programlamadan ilham alması iyi bir fikir olabilir.

Son zamanlarda en sevdiğim teknik, değişmez, başlatılmamış bir dönüş değişkeni ve tek bir çıkış kullanmaktı, böylece bazı fonksiyonel dil derleyicilerinin yaptığı gibi Java, işlevin gövdesinde ne olursa olsun, her zaman sadece bir tane sağladığımı kontrol eder. geri dönüş değeri. Misal:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


Ben yaklaşık% 70 bazı Java zaten zaten dönüş değeri kontrol yok zaten. Denetim geçersiz olmayan bir işlevin sonundan düşebiliyorsa, "eksik dönüş bildirimi" hakkında bir hata almalısınız.
cHao

Benim açımdan: Eğer int result = -n; if (n < 1) { result = 0 } return result;iyi derler gibi kodlar ve derleyicinin benim örneğimdeki işleve eşdeğer yapmak niyetinde olup olmadığı hakkında hiçbir fikrim yok. Belki bu örnek tekniğin yararlı görünmesini sağlamak için çok basittir, ancak çok sayıda dalı olan bir işlevde, hangi yolun izlendiğine bakılmaksızın sonucun tam olarak bir kez atandığını açıkça belirtmenin iyi olduğunu hissediyorum.
minopret

if (n < 1) return 0; else return -n;Yine de derseniz , hiç sorun yaşamazsınız ... ve bunun yanı sıra daha basit. :) Bana öyle geliyor ki, bu durumda, "bir dönüş" kuralı aslında dönüş değerinin ne zaman ayarlandığını bilmeme sorununa neden olur ; Aksi takdirde, yalnızca geri döndürebilirsiniz ve Java, diğer yolların ne zaman bir değer döndürmeyeceğini belirleyebilir, çünkü değerin hesaplanmasını artık gerçek döndürülmesinden ayırmazsınız.
cHao

Veya cevabınızın örneği için if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao

Daha yeni anlarımı Java'daki OnlyOneReturn kuralını savunarak harcamak istemediğime karar verdim. Dışarı gidiyor. Fonksiyonel programlama uygulamalarından etkilenen savunmayı düşündüğüm bir Java kodlama uygulamasını düşündüğümde ve düşünürsem, bu örneğin yerine geçeceğim. O zamana kadar, örnek yok.
minopret

0

Bunu bulmanın en kolay yolu, aşağıdakileri Frege derleyicisine beslemek ve oluşturulan java koduna bakmak olacaktır:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

Birkaç gün sonra düşüncelerimin bu cevaba döndüğünü gördüm. Benim önerim, Scala'da fonksiyonel programlama parçalarını uygulamaktı. Eğer Scala'yı gerçekten Haskell'i aklımızda tuttuğumuz noktalara uygulamayı düşünürsek (ve sanırım sadece bir blog ben değilim ).
minopret

@minopret Bu gerçekten de Frege'nin tüccar olduğu - Haskell'i tanımaya ve sevmeye ve henüz JVM'ye ihtiyaç duyan insanlar. Bir gün eminim Frege en azından ciddi bir şekilde düşünecek kadar olgun olacak.
Ingo
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.