Cara menulis program Java yang bermanfaat tanpa menggunakan variabel yang bisa berubah


12

Saya sedang membaca sebuah artikel tentang pemrograman fungsional di mana penulis menyatakan

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

Perhatikan bahwa ia tidak memiliki variabel. Memang, ia tidak lebih dari tiga fungsi dan satu konstanta. Cobalah menulis kuadrat bilangan bulat di Jawa tanpa menggunakan variabel. Oh, mungkin ada cara untuk melakukannya, tetapi tentu saja itu tidak alami, dan itu tidak akan sebagus program saya di atas.

Apakah mungkin untuk mencapai ini di Jawa? Andaikan Anda diminta untuk mencetak kuadrat dari 15 bilangan bulat pertama, dapatkah Anda menulis untuk atau sementara loop tanpa menggunakan variabel?

Pemberitahuan mod

Pertanyaan ini bukan kontes kode golf. Kami mencari jawaban yang menjelaskan konsep-konsep yang terlibat (idealnya tanpa mengulangi jawaban sebelumnya), dan bukan hanya untuk sepotong kode lagi.


19
Misalnya fungsional Anda tidak menggunakan variabel di dalam, tetapi bahasa tidak semua itu di belakang layar. Anda telah secara efektif mendelegasikan bagian yang tidak menyenangkan kepada seseorang yang Anda percaya telah melakukannya dengan benar.
Blrfl

12
@ Bllfl: Argumen "di belakang layar" membunuh semua perdebatan berbasis bahasa, karena setiap bagian dari kode akhirnya diterjemahkan ke kode mesin x86. kode x86 tidak berorientasi objek, tidak prosedural, tidak fungsional, bukan apa-apa, tetapi kategori ini adalah tag berharga untuk bahasa pemrograman. Lihatlah bahasanya, bukan implementasinya.
thiton

10
@thiton Tidak setuju. Apa yang dikatakan Blrfl adalah bahwa fungsi-fungsi itu mungkin menggunakan variabel yang ditulis dalam bahasa pemrograman yang sama . Tidak perlu pergi level rendah di sini. Cuplikan hanya menggunakan fungsi perpustakaan. Anda dapat dengan mudah menulis kode yang sama di Jawa, lihat: squaresOf(integers()).take(25)(menulis fungsi-fungsi itu dibiarkan sebagai latihan untuk pembaca; kesulitannya terletak pada set yang tak terbatas untuk integers(), tapi itu masalah untuk Jawa karena evaluasi yang bersemangat, tidak ada hubungannya dengan variabel)
Andres F.

6
Kutipan itu membingungkan dan menyesatkan, tidak ada keajaiban di sana, hanya gula sintaksis .
yannis

2
@thiton Saya sarankan Anda mempelajari lebih lanjut tentang bahasa FP, tetapi meskipun demikian, cuplikan kode tidak mendukung (atau menolak) pernyataan bahwa "variabel" tidak diperlukan (yang saya anggap maksud Anda "variabel yang dapat diubah", karena yang lain jenis umum di FP). Cuplikan hanya menunjukkan fungsi perpustakaan yang bisa diimplementasikan di Jawa juga, kecuali masalah malas / bersemangat yang offtopic di sini.
Andres F.

Jawaban:


31

Apakah mungkin untuk menerapkan contoh seperti itu di Jawa tanpa menggunakan pembaruan yang merusak? Iya. Namun, seperti yang disebutkan oleh @Thiton dan artikelnya, itu akan menjadi jelek (tergantung selera seseorang). Salah satu caranya adalah menggunakan rekursi; inilah contoh Haskell yang melakukan hal serupa:

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

Catatan 1) kurangnya mutasi, 2) penggunaan rekursi, dan 3) kurangnya loop. Poin terakhir sangat penting - bahasa fungsional tidak memerlukan konstruksi perulangan yang dibangun ke dalam bahasa, karena rekursi dapat digunakan untuk sebagian besar (semua?) Kasus di mana loop digunakan di Jawa. Berikut adalah serangkaian makalah terkenal yang menunjukkan bagaimana panggilan fungsi yang sangat ekspresif dapat terjadi.


Saya menemukan artikel yang tidak memuaskan dan ingin membuat beberapa poin tambahan:

Artikel itu adalah penjelasan yang sangat buruk dan membingungkan tentang pemrograman fungsional dan manfaatnya. Saya akan sangat merekomendasikan sumber lain untuk belajar tentang pemrograman fungsional.

Bagian yang paling membingungkan tentang artikel ini adalah bahwa ia tidak menyebutkan bahwa ada dua kegunaan untuk pernyataan penugasan di Jawa (dan sebagian besar bahasa utama lainnya):

  1. mengikat nilai ke nama: final int MAX_SIZE = 100;

  2. pembaruan destruktif: int a = 3; a += 1; a++;

Pemrograman fungsional menghindari yang kedua, tetapi merangkul yang pertama (contoh: let-ekspresi, parameter fungsi, defineition tingkat atas ) . Ini adalah hal yang sangat penting untuk memahami, karena jika tidak artikel tersebut sepertinya konyol dan mungkin meninggalkan Anda bertanya-tanya, apa take, squares-ofdan integersjika tidak variabel?

Selain itu, contohnya tidak ada artinya. Ini tidak menunjukkan implementasi take, squares-ofatau integers. Sejauh yang kita tahu, mereka diimplementasikan menggunakan variabel yang bisa berubah. Seperti yang dikatakan @Martin, Anda dapat dengan sepele menulis contoh ini di Java.

Sekali lagi, saya akan merekomendasikan menghindari artikel ini dan orang lain menyukainya jika Anda benar-benar ingin belajar tentang pemrograman fungsional. Tampaknya ditulis lebih dengan tujuan mengejutkan dan menyinggung daripada mengajarkan konsep dan dasar-dasar. Sebaliknya, mengapa tidak memeriksa salah satu kertas favorit saya sepanjang masa , oleh John Hughes. Hughes mencoba untuk mengatasi beberapa masalah yang sama dengan artikel tersebut (walaupun Hughes tidak berbicara tentang konkurensi / paralelisasi); inilah sebuah teaser:

Makalah ini merupakan upaya untuk menunjukkan kepada komunitas yang lebih besar dari programmer (nonfungsional) programmer arti penting dari pemrograman fungsional, dan juga untuk membantu programmer fungsional mengeksploitasi kelebihannya secara penuh dengan memperjelas apa keuntungannya.

[...]

Kami akan berdebat dalam sisa makalah ini bahwa bahasa fungsional menyediakan dua jenis lem baru yang sangat penting. Kami akan memberikan beberapa contoh program yang dapat dimodulasi dengan cara baru dan dengan demikian dapat disederhanakan. Ini adalah kunci kekuatan pemrograman fungsional - memungkinkan peningkatan modularisasi. Ini juga merupakan tujuan yang harus diusahakan oleh programmer fungsional - modul yang lebih kecil dan lebih sederhana dan lebih umum, direkatkan dengan lem baru yang akan kami jelaskan.


10
+1 untuk "Saya akan merekomendasikan menghindari artikel ini dan yang lain menyukainya jika Anda benar-benar ingin belajar tentang pemrograman fungsional. Tampaknya lebih banyak ditulis dengan tujuan mengejutkan dan menyinggung daripada mengajarkan konsep dan dasar-dasar."

3
Setengah alasan orang tidak melakukan FP adalah karena mereka tidak mendengar / belajar sesuatu tentang hal itu di uni, dan setengah lainnya karena ketika mereka melihat ke dalamnya mereka menemukan artikel yang membuat mereka berdua tidak mendapat informasi dan berpikir itu semua untuk beberapa yang fantastis bermain tentang alih-alih menjadi pendekatan yang masuk akal dengan manfaat. +1 untuk memberi sumber informasi yang lebih baik
Jimmy Hoffa

3
Letakkan jawaban Anda pada pertanyaan di puncak mutlak jika Anda mau jadi lebih langsung ke pertanyaan dan mungkin pertanyaan ini akan tetap terbuka kemudian (dengan jawaban yang berfokus pada pertanyaan langsung)
Jimmy Hoffa

2
Maaf untuk nitpick, tapi saya tidak mengerti mengapa Anda memilih kode haskell ini. Saya sudah membaca LYAH dan teladan Anda sulit bagi saya untuk grok. Saya juga tidak melihat kaitannya dengan pertanyaan awal. Mengapa Anda tidak menggunakan saja take 25 (map (^2) [1..])sebagai contoh?
Daniel Kaplan

2
@tieTYT pertanyaan yang bagus - terima kasih telah menunjukkan ini. Alasan saya menggunakan contoh itu adalah karena itu menunjukkan bagaimana menghasilkan daftar angka menggunakan rekursi dan menghindari variabel yang bisa berubah. Maksud saya adalah agar OP melihat kode itu dan berpikir tentang bagaimana melakukan sesuatu yang serupa di Jawa. Untuk mengatasi cuplikan kode Anda, apa itu [1..]? Itu fitur keren bawaan Haskell, tetapi tidak menggambarkan konsep di balik pembuatan daftar semacam itu. Saya yakin contoh dari Enumkelas (yang membutuhkan sintaks) juga membantu, tetapi terlalu malas untuk menemukannya. Jadi unfoldr,. :)

27

Kamu tidak akan Variabel adalah inti dari pemrograman imperatif, dan jika Anda mencoba memprogram secara imperatif tanpa menggunakan variabel, Anda hanya akan membuat semua orang kesakitan. Dalam paradigma pemrograman yang berbeda, gaya berbeda, dan konsep yang berbeda membentuk dasar Anda. Variabel di Jawa, bila digunakan dengan baik dengan cakupan kecil, tidak jahat. Meminta program Java tanpa variabel seperti meminta program Haskell tanpa fungsi, jadi Anda tidak memintanya, dan Anda tidak membiarkan diri Anda tertipu melihat pemrograman imperatif lebih rendah karena menggunakan variabel.

Jadi, cara Java adalah:

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

dan jangan biarkan diri Anda dibodohi untuk menulisnya dengan cara yang lebih rumit karena kebencian terhadap variabel.


5
"Kebencian terhadap variabel"? Ooookay ... Apa yang Anda baca tentang Pemrograman Fungsional? Bahasa apa yang sudah Anda coba? Tutorial yang mana?
Andres F.

8
@AndresF .: Lebih dari dua tahun kursus di Haskell. Saya tidak mengatakan FP itu buruk. Namun, ada kecenderungan dalam banyak diskusi FP-vs-IP (seperti artikel tertaut) untuk mengutuk penggunaan entitas bernama ditugaskan kembali (variabel AKA), dan untuk mengutuk tanpa alasan atau data yang baik. Kecaman yang tidak masuk akal dibenci dalam buku saya. Dan kebencian membuat kode yang sangat buruk.
thiton

10
"Kebencian variabel" adalah kausal terlalu menyederhanakan en.wikipedia.org/wiki/Fallacy_of_the_single_cause ada banyak manfaat untuk pemrograman stateless yang bahkan bisa memiliki di Jawa, meskipun saya setuju dengan jawaban Anda bahwa di Jawa biaya akan terlalu tinggi dalam kompleksitas ke program dan menjadi non-idiomatik. Saya masih tidak akan pergi dengan tangan melambaikan gagasan bahwa pemrograman tanpa negara itu baik dan menyatakan keadaan adalah buruk karena beberapa tanggapan emosional daripada sikap, dipikirkan dengan baik orang-orang datang karena pengalaman.
Jimmy Hoffa

2
Sejalan dengan apa yang dikatakan @JimmyHoffa, saya akan merujuk Anda ke John Carmack pada topik pemrograman gaya fungsional dalam bahasa imperatif (C ++ dalam kasusnya) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers

5
Kecaman yang tidak masuk akal bukanlah kebencian, dan menghindari keadaan yang bisa berubah juga tidak masuk akal.
Michael Shaw

21

Yang paling sederhana yang bisa saya lakukan dengan rekursi adalah fungsi dengan satu parameter. Ini bukan Java-esque, tetapi ini berfungsi:

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
+1 untuk mencoba menjawab pertanyaan dengan contoh Java.
KChaloux

Saya akan menurunkan ini untuk presentasi gaya kode golf (lihat pemberitahuan Mod ) tetapi tidak dapat memaksa diri untuk menekan panah karena kode ini sangat cocok dengan pernyataan yang dibuat dalam jawaban favorit saya : "1) kurangnya mutasi, 2) penggunaan rekursi, dan 3) kurangnya loop "
nyamuk

3
@gnat: Jawaban ini diposting sebelum pemberitahuan Mod. Saya tidak mencari gaya yang hebat, saya mencari kesederhanaan, dan memuaskan pertanyaan awal OP; untuk menggambarkan bahwa adalah mungkin untuk melakukan hal-hal seperti di Jawa.
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner yakin; ini tidak akan menghentikan saya dari DVing (karena Anda seharusnya mampu mengedit untuk mematuhi) - itu adalah pasangan yang sangat sempurna yang melakukan keajaiban. Bagus, sangat bagus, cukup mendidik - terima kasih
nyamuk

16

Dalam contoh fungsional Anda, kami tidak melihat bagaimana squares-ofdan takefungsi diimplementasikan. Saya bukan ahli Java, tapi saya cukup yakin kita bisa menulis fungsi-fungsi itu untuk mengaktifkan pernyataan seperti ini ...

squares_of(integers).take(25);

yang tidak begitu jauh berbeda.


6
Nitpick: squares-ofbukan nama yang valid di Java ( squares_ofis though). Tetapi sebaliknya, poin bagus yang menunjukkan bahwa contoh artikel itu buruk.

Saya menduga bahwa artikel itu integermalas menghasilkan bilangan bulat, dan takefungsinya mengambil 25 squared-ofangka integer. Singkatnya, Anda harus memiliki integerfungsi untuk menghasilkan bilangan bulat malas hingga tak terbatas.
OnesimusUnbound

Agak gila untuk memanggil sesuatu seperti (integer)fungsi - suatu fungsi masih sesuatu yang memetakan argumen ke suatu nilai. Ternyata itu (integer)bukan fungsi, tetapi hanya nilai. Seseorang bahkan dapat melangkah lebih jauh dengan mengatakan bahwa itu integeradalah variabel yang terikat pada untaian angka yang tak terbatas.
Ingo

6

Di Jawa Anda bisa melakukan ini (terutama bagian daftar tak terbatas) dengan iterator. Dalam contoh kode berikut, jumlah yang dipasok ke Takekonstruktor dapat tinggi secara sewenang-wenang.

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());
    }
}

Atau dengan metode pabrik rantai:

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

Di mana SquaresOf, Takedan IntegersluaskanNumbers

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
Ini menunjukkan keunggulan paradigma OO daripada fungsional; dengan desain OO yang tepat Anda bisa meniru paradigma fungsional tetapi Anda tidak bisa meniru paradigma OO dalam gaya fungsional.
m3th0dman

3
@ m3th0dman: Dengan desain OO yang tepat Anda mungkin dapat setengah meniru FP, sama seperti bahasa apa pun yang memiliki string, daftar, dan / atau kamus bisa setengah meniru meniru OO. Kesetaraan Turing bahasa tujuan umum berarti bahwa dengan upaya yang cukup, bahasa apa pun dapat mensimulasikan fitur lainnya.
cHao

Perhatikan bahwa iterator gaya Java seperti in while (test.hasNext()) System.out.println(test.next())akan menjadi no-no di FP; iterator secara inheren bisa berubah.
cHao

1
@ cHao Saya hampir tidak percaya bahwa enkapsulasi atau polimorfisme sejati dapat ditiru; juga Java (dalam contoh ini) tidak dapat benar-benar meniru bahasa fungsional karena evaluasi yang ketat. Saya juga percaya bahwa iterator dapat ditulis secara rekursif.
m3th0dman

@ m3th0dman: Polimorfisme tidak akan sulit sama sekali untuk meniru; bahkan bahasa C dan bahasa assembly dapat melakukannya. Cukup buat metode menjadi bidang dalam objek atau deskriptor kelas / vtable. Dan enkapsulasi dalam persembunyian data tidak sepenuhnya dibutuhkan; setengah bahasa di luar sana tidak menyediakannya, ketika objek Anda tidak berubah, tidak masalah apakah orang bisa melihat isi perutnya. semua yang diperlukan adalah pembungkusan data , yang dapat dengan mudah disediakan oleh bidang metode yang disebutkan di atas.
cHao

6

Versi pendek:

Agar gaya penugasan tunggal berfungsi dengan baik di Jawa, Anda perlu (1) semacam infrastruktur ramah-abadi, dan (2) dukungan tingkat kompiler atau runtime untuk eliminasi panggilan ekor.

Kami dapat menulis banyak infrastruktur, dan kami dapat mengatur berbagai hal untuk menghindari pengisian tumpukan. Tetapi selama setiap panggilan membutuhkan frame stack, akan ada batasan berapa banyak rekursi yang dapat Anda lakukan. Simpan iterables Anda kecil dan / atau malas, dan Anda seharusnya tidak memiliki masalah besar. Setidaknya sebagian besar masalah yang akan Anda hadapi tidak membutuhkan pengembalian sejuta hasil sekaligus. :)

Perhatikan juga, karena program harus benar-benar memengaruhi perubahan yang terlihat agar layak untuk dijalankan, Anda tidak dapat membuat semuanya berubah. Anda dapat, bagaimanapun, menjaga sebagian besar barang-barang Anda sendiri tidak berubah, menggunakan subset kecil dari perumpamaan penting (stream, misalnya) hanya pada titik-titik kunci tertentu di mana alternatif akan terlalu berat.


Versi panjang:

Sederhananya, program Java tidak dapat sepenuhnya menghindari variabel jika ingin melakukan sesuatu yang layak dilakukan. Anda dapat menampungnya , dan dengan demikian membatasi mutabilitas hingga tingkat yang sangat besar, tetapi desain bahasa dan API yang sangat - bersama dengan kebutuhan untuk akhirnya mengubah sistem yang mendasarinya - membuat kekekalan total menjadi tidak mungkin.

Java dirancang sejak awal sebagai bahasa imperatif , berorientasi objek .

  • Bahasa imperatif hampir selalu bergantung pada variabel yang bisa berubah dari beberapa jenis. Mereka cenderung mendukung iterasi daripada rekursi, misalnya, dan hampir semua konstruksi berulang - bahkan while (true)dan for (;;)! - sangat tergantung pada variabel di suatu tempat yang berubah dari iterasi ke iterasi.
  • Bahasa berorientasi objek cukup banyak membayangkan setiap program sebagai grafik objek yang saling mengirim pesan, dan dalam hampir semua kasus, merespons pesan-pesan itu dengan memutasikan sesuatu.

Hasil akhir dari keputusan desain tersebut adalah bahwa tanpa variabel yang dapat berubah, Java tidak memiliki cara untuk mengubah keadaan apa pun - bahkan sesuatu yang sederhana seperti mencetak "Halo dunia!" ke layar melibatkan aliran output, yang melibatkan menempel byte dalam buffer yang bisa berubah .

Jadi, untuk semua tujuan praktis, kita dibatasi untuk membuang variabel dari kode kita sendiri . OK, kita bisa melakukan itu. Hampir. Pada dasarnya yang kita butuhkan adalah mengganti hampir semua iterasi dengan rekursi, dan semua mutasi dengan panggilan rekursif mengembalikan nilai yang diubah. seperti itu ...

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()));
}

Pada dasarnya, kita membangun daftar tertaut, di mana setiap node adalah daftar itu sendiri. Setiap daftar memiliki "kepala" (nilai saat ini) dan "ekor" (sublist yang tersisa). Sebagian besar bahasa fungsional melakukan sesuatu yang mirip dengan ini, karena itu sangat bisa diterima untuk kekekalan efisien. Operasi "berikutnya" hanya mengembalikan buntut, yang biasanya diteruskan ke tingkat berikutnya dalam setumpuk panggilan rekursif.

Sekarang, ini adalah versi yang sangat disederhanakan dari barang ini. Tapi itu cukup bagus untuk menunjukkan masalah serius dengan pendekatan ini di Jawa. Pertimbangkan kode ini:

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

Meskipun kami hanya membutuhkan 25 int untuk hasilnya, squares_oftidak tahu itu. Ini akan mengembalikan kuadrat dari setiap angka di integers. Rekursi 20 juta level menyebabkan masalah yang cukup besar di Jawa.

Lihat, bahasa fungsional Anda biasanya melakukan keanehan seperti ini, memiliki fitur yang disebut "eliminasi panggilan ekor". Apa itu artinya, ketika kompiler melihat tindakan terakhir kode adalah untuk memanggil dirinya sendiri (dan mengembalikan hasilnya jika fungsi tidak batal), ia menggunakan bingkai stack panggilan saat ini alih-alih mengatur yang baru dan melakukan "lompatan" sebagai gantinya dari "panggilan" (sehingga ruang stack yang digunakan tetap konstan). Singkatnya, ia berjalan sekitar 90% dari jalan menuju mengubah rekursi ekor menjadi iterasi. Itu bisa berurusan dengan milyaran int tanpa meluap tumpukan. (Ini pada akhirnya masih kehabisan memori, tetapi merakit daftar milyaran int akan tetap mengacaukan Anda dengan memori pada sistem 32-bit.)

Java tidak melakukan itu, dalam banyak kasus. (Itu tergantung pada kompiler dan runtime, tetapi implementasi Oracle tidak melakukannya.) Setiap panggilan ke fungsi rekursif memakan memori stack. Gunakan terlalu banyak, dan Anda mendapatkan stack overflow. Meluap tumpukan semua tetapi menjamin kematian program. Jadi kita harus memastikan untuk tidak melakukannya.

Satu semi-solusi ... evaluasi malas. Kami masih memiliki batasan tumpukan, tetapi mereka dapat dikaitkan dengan faktor-faktor yang memiliki kendali lebih besar. Kami tidak harus menghitung satu juta ints hanya untuk mengembalikan 25. :)

Jadi mari kita bangun beberapa infrastruktur evaluasi malas. (Kode ini sudah diuji beberapa waktu lalu, tapi saya sudah memodifikasinya sejak itu; baca idenya, bukan kesalahan sintaks. :))

// 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()); }
    };
}

(Perlu diingat bahwa jika ini benar-benar layak di Jawa, kode setidaknya seperti di atas sudah akan menjadi bagian dari API.)

Sekarang, dengan infrastruktur yang tersedia, agak sepele untuk menulis kode yang tidak perlu variabel yang dapat diubah dan setidaknya stabil untuk jumlah input yang lebih kecil.

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);
}

Ini sebagian besar berfungsi, tetapi masih cenderung untuk menumpuk kelebihan. Coba take2 miliar int dan lakukan beberapa tindakan pada mereka. : P Pada akhirnya akan mengeluarkan pengecualian, setidaknya hingga 64+ GB RAM menjadi standar. Masalahnya adalah, jumlah memori program yang dicadangkan untuk tumpukannya tidak terlalu besar. Biasanya antara 1 dan 8 MiB. (Anda dapat meminta yang lebih besar, tetapi tidak masalah seberapa banyak yang Anda minta - Anda menelepon take(1000000000, someInfiniteSequence), Anda akan mendapatkan pengecualian.) Untungnya, dengan evaluasi yang malas, titik lemah ada di area yang dapat kita kontrol lebih baik . Kita hanya harus berhati-hati tentang berapa banyak kita take().

Masih akan ada banyak masalah yang ditingkatkan, karena penggunaan tumpukan kami meningkat secara linear. Setiap panggilan menangani satu elemen dan meneruskan sisanya ke panggilan lain. Sekarang saya berpikir tentang hal itu, ada satu trik yang bisa kita tarik yang mungkin memberi kita sedikit lebih banyak ruang kepala: mengubah rantai panggilan menjadi pohon panggilan. Pertimbangkan sesuatu yang lebih seperti ini:

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);
}

workWithpada dasarnya memecah pekerjaan menjadi dua bagian, dan menetapkan masing-masing setengah untuk panggilan lain untuk dirinya sendiri. Karena setiap panggilan mengurangi ukuran daftar kerja menjadi setengah daripada satu, ini harus skala logaritma daripada linear.

Masalahnya adalah, fungsi ini membutuhkan input - dan dengan daftar yang ditautkan, mendapatkan panjang memerlukan melintasi seluruh daftar. Namun itu mudah dipecahkan; hanya tidak peduli berapa banyak entri ada. :) Kode di atas akan berfungsi dengan sesuatu seperti Integer.MAX_VALUEhitungan, karena null tetap menghentikan pemrosesan. Hitungannya sebagian besar ada sehingga kami memiliki alas yang kuat. Jika Anda mengantisipasi memiliki lebih dari Integer.MAX_VALUEentri dalam daftar, maka Anda dapat memeriksa workWithnilai kembali - itu harus nol pada akhirnya. Kalau tidak, kambuh.

Perlu diingat, ini menyentuh elemen sebanyak yang Anda katakan. Itu tidak malas; ia melakukan hal itu dengan segera. Anda hanya ingin melakukannya untuk tindakan - yaitu, hal-hal yang tujuan utamanya adalah untuk diterapkan sendiri ke setiap elemen dalam daftar. Seperti yang saya pikirkan sekarang, menurut saya urutannya akan jauh lebih rumit jika dijaga tetap linier; seharusnya tidak menjadi masalah, karena sekuens tidak menyebut diri mereka sendiri - mereka hanya membuat objek yang memanggil mereka lagi.


3

Saya sebelumnya telah mencoba membuat interpreter untuk bahasa mirip-lisp di Jawa, (beberapa tahun yang lalu dan semua kode hilang seperti di CVS di sourceforge), dan Java util iterators agak verbose untuk pemrograman fungsional pada daftar.

Berikut ini sesuatu yang didasarkan pada antarmuka urutan, yang hanya memiliki dua operasi yang Anda butuhkan untuk mendapatkan nilai saat ini dan mendapatkan urutan mulai dari elemen berikutnya. Ini dinamai kepala dan ekor setelah fungsi dalam skema.

Sangat penting untuk menggunakan sesuatu seperti Seqatau Iteratorantarmuka karena artinya daftar dibuat dengan malas. The Iteratorantarmuka tidak dapat menjadi objek abadi, sehingga kurang cocok untuk pemrograman fungsional - jika Anda tidak bisa mengatakan jika nilai Anda masuk ke fungsi telah diubah olehnya, Anda kehilangan salah satu keuntungan utama dari pemrograman fungsional.

Jelas integersharus menjadi daftar semua bilangan bulat, jadi saya mulai dari nol dan secara berurutan mengembalikan yang positif dan negatif.

Ada dua versi kotak - satu membuat urutan kustom, yang lain menggunakan mapyang mengambil 'fungsi' - Java 7 tidak memiliki lambda jadi saya menggunakan antarmuka - dan menerapkannya ke setiap elemen dalam urutan pada gilirannya.

Inti dari square ( int x )fungsi ini hanya untuk menghapus kebutuhan untuk memanggil head()dua kali - biasanya saya akan melakukan ini dengan meletakkan nilai ke dalam variabel akhir, tetapi menambahkan fungsi ini berarti tidak ada variabel dalam program, hanya parameter fungsi.

Verbositas Java untuk pemrograman semacam ini membuat saya menulis versi kedua penerjemah saya di C99.

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

Cara menulis program Java yang bermanfaat tanpa menggunakan variabel yang bisa berubah.

Secara teori Anda dapat mengimplementasikan hampir semua hal di Jawa menggunakan hanya rekursi dan tidak ada variabel yang bisa berubah.

Dalam praktek:

  • Bahasa Jawa tidak dirancang untuk ini. Banyak konstruksi dirancang untuk mutasi, dan sulit digunakan tanpa mutasi. (Misalnya, Anda tidak dapat menginisialisasi array Java panjang variabel tanpa mutasi.)

  • Ditto untuk perpustakaan. Dan jika Anda membatasi diri Anda pada kelas perpustakaan yang tidak menggunakan mutasi di balik sampulnya, itu bahkan lebih sulit. (Anda bahkan tidak dapat menggunakan String ... lihat bagaimana hashcodepenerapannya.)

  • Implementasi Mainstream Java tidak mendukung pengoptimalan tail-call. Itu berarti bahwa versi algoritma rekursif cenderung menumpuk ruang "lapar". Dan karena tumpukan thread Java tidak tumbuh, Anda perlu mengalokasikan tumpukan besar ... atau risiko StackOverflowError.

Gabungkan ketiga hal ini, dan Java sebenarnya bukan pilihan yang layak untuk menulis program yang berguna (yaitu non-sepele) tanpa variabel yang dapat diubah.

(Tapi hei, tidak apa-apa. Ada bahasa pemrograman lain yang tersedia untuk JVM, beberapa di antaranya mendukung pemrograman fungsional.)


2

Saat kita mencari contoh konsep, saya katakan mari kita mengesampingkan Jawa dan mencari pengaturan yang berbeda namun akrab di mana untuk menemukan versi konsep yang akrab. Pipa UNIX agak mirip dengan chaining fungsi malas.

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

Di Linux ini artinya, beri saya byte yang masing-masing terdiri dari bit palsu dan bukan benar, sampai saya kehilangan nafsu makan; ubah setiap byte tersebut menjadi karakter baris baru; nomor setiap baris yang dibuat; dan menghasilkan kuadrat dari angka itu. Selain itu saya memiliki selera untuk 25 baris dan tidak lebih.

Saya mengklaim bahwa seorang programmer tidak akan sakit untuk menulis pipa Linux dengan cara itu. Linux scripting shell yang relatif normal.

Saya mengklaim bahwa seorang programmer akan sakit menyarankan untuk mencoba menulis hal yang sama dengan cara yang sama di Jawa. Alasannya adalah pemeliharaan perangkat lunak sebagai faktor utama dalam biaya seumur hidup proyek perangkat lunak. Kami tidak ingin membingungkan programmer berikutnya dengan menghadirkan apa yang seolah-olah sebuah program Java tetapi sebenarnya ditulis dengan efek dalam bahasa sekali pakai khusus dengan menduplikasi fungsionalitas rumit yang sudah ada di platform Java.

Di sisi lain, saya mengklaim bahwa programmer selanjutnya bisa lebih menerima jika beberapa paket "Java" kami sebenarnya adalah paket Java Virtual Machine yang ditulis dalam salah satu bahasa fungsional atau objek / fungsional seperti Clojure dan Scala. Ini dirancang untuk dikodekan dengan fungsi chaining bersama dan dipanggil dari Jawa dengan cara biasa panggilan metode Java.

Kemudian lagi, itu masih bisa menjadi ide yang baik bagi seorang programmer Java untuk mengambil inspirasi dari pemrograman fungsional, di beberapa tempat.

Baru-baru ini teknik favorit saya adalah menggunakan variabel return yang tidak diinisialisasi dan keluar tunggal sehingga, seperti yang dilakukan oleh beberapa kompiler bahasa fungsional, Java memeriksa bahwa apa pun yang terjadi dalam tubuh fungsi, saya selalu menyediakan satu dan hanya satu nilai pengembalian. Contoh:

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;
}


Saya sekitar 70% Jawa tertentu sudah melakukan pengecekan nilai pengembalian. Anda harus mendapatkan kesalahan tentang "pernyataan pengembalian yang hilang" jika kontrol dapat jatuh dari fungsi non-batal.
cHao

Maksud saya: Jika Anda mengkodekannya karena int result = -n; if (n < 1) { result = 0 } return result;dikompilasi dengan baik dan kompiler tidak tahu apakah Anda bermaksud membuatnya setara dengan fungsi dalam contoh saya. Mungkin contoh itu terlalu sederhana untuk membuat teknik terlihat membantu, tetapi dalam fungsi dengan banyak cabang saya merasa senang untuk menjelaskan bahwa hasilnya diberikan tepat sekali terlepas dari jalur apa yang diikuti.
minopret

Namun, jika Anda mengatakan if (n < 1) return 0; else return -n;, Anda berakhir tanpa masalah ... dan lebih sederhana lagi. :) Tampak bagi saya bahwa dalam kasus itu, aturan "satu pengembalian" sebenarnya membantu menyebabkan masalah tidak mengetahui kapan nilai pengembalian Anda ditetapkan; jika tidak, Anda bisa mengembalikannya dan Java akan lebih bisa menentukan kapan jalur lain mungkin tidak mengembalikan nilai, karena Anda tidak lagi menceraikan perhitungan nilai dari pengembalian yang sebenarnya.
cHao

Atau, untuk contoh jawaban Anda if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;,.
cHao

Saya baru saja memutuskan saya tidak ingin menghabiskan waktu lagi dalam hidup saya membela aturan OnlyOneReturn di Jawa. Begitulah. Kapan dan jika saya memikirkan praktik pengkodean Java yang saya rasa ingin dipertahankan yang dipengaruhi oleh praktik pemrograman fungsional, saya akan memasukkan pengganti untuk contoh itu. Sampai saat itu, tidak ada contoh.
minopret

0

Cara termudah untuk mengetahui hal itu adalah memberi makan yang berikut ke kompiler Frege , dan lihat kode java yang dihasilkan:

module Main where

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

Setelah beberapa hari saya menemukan pikiran saya kembali ke jawaban ini. Setelah semua bagian dari saran saya adalah untuk mengimplementasikan bagian-bagian pemrograman fungsional di Scala. Jika kita mempertimbangkan untuk menerapkan Scala di tempat-tempat di mana kita benar-benar memiliki Haskell dalam pikiran (dan saya pikir saya bukan satu-satunya blog.zlemma.com/2013/02/20/… ), bukankah kita setidaknya mempertimbangkan Frege?
minopret

@minopret Ini memang ceruk Frege yang tragis - orang-orang yang telah mengenal dan mencintai Haskell namun membutuhkan JVM. Saya yakin suatu hari nanti Frege akan cukup matang untuk mendapatkan pertimbangan yang serius.
Ingo
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.