Bagaimana cara memperbaiki program OO menjadi program fungsional?

26

Saya mengalami kesulitan menemukan sumber daya tentang cara menulis program dengan gaya fungsional. Topik paling maju yang bisa saya temukan dibahas secara online adalah menggunakan pengetikan struktural untuk mengurangi hierarki kelas; kebanyakan hanya berurusan dengan cara menggunakan peta / lipat / kurangi / dll untuk mengganti loop imperatif.

Apa yang ingin saya temukan adalah diskusi mendalam tentang implementasi OOP dari program non-sepele, keterbatasannya, dan bagaimana cara memperbaikinya dalam gaya fungsional. Bukan hanya algoritma atau struktur data, tetapi sesuatu dengan beberapa peran dan aspek yang berbeda - video game mungkin. Ngomong-ngomong saya membaca Pemrograman Fungsional Dunia Nyata oleh Tomas Petricek, tetapi saya masih menginginkan lebih.

Asik
sumber
6
saya pikir itu tidak mungkin. Anda harus mendesain ulang (dan menulis ulang) semuanya lagi.
Bryan Chen
18
-1, posting ini bias oleh asumsi yang salah bahwa OOP dan gaya fungsional bertentangan. Itu sebagian besar konsep ortogonal, dan IMHO itu mitos bahwa mereka tidak. "Fungsional" lebih menentang "Prosedural", dan kedua gaya dapat digunakan bersama dengan OOP.
Doc Brown
11
@DocBrown, OOP terlalu bergantung pada status yang bisa berubah. Objek stateless tidak cocok dengan praktik desain OOP saat ini.
SK-logic
9
@ SK-logic: kuncinya bukan objek stateless, tetapi objek yang tidak dapat diubah. Dan bahkan ketika objek bisa berubah, mereka sering dapat digunakan di bagian fungsional dari sistem selama mereka tidak berubah dalam konteks yang diberikan. Selain itu, saya kira Anda tahu benda dan penutupan dapat dipertukarkan. Jadi ini semua menunjukkan bahwa OOP dan "fungsional" tidak bertentangan.
Doc Brown
12
@DocBrown: Saya pikir konstruksi bahasa adalah orthogonal, sedangkan pola pikir cenderung berbenturan. Orang-orang OOP cenderung bertanya "benda apa dan bagaimana mereka berkolaborasi?"; orang fungsional cenderung bertanya "apa data saya, dan bagaimana saya ingin mengubahnya?". Itu bukan pertanyaan yang sama, dan mengarah pada jawaban yang berbeda. Saya juga berpikir Anda salah membaca pertanyaan. Ini bukan "OOP air liur dan aturan FP, bagaimana saya menyingkirkan OOP?", Itu "Saya mendapatkan OOP dan saya tidak mendapatkan FP, apakah ada cara untuk mengubah program OOP menjadi fungsional, sehingga saya bisa mendapatkan beberapa wawasan? ".
Michael Shaw

Jawaban:

31

Definisi Pemrograman Fungsional

Pengantar The Joy of Clojure mengatakan yang berikut:

Pemrograman fungsional adalah salah satu istilah komputasi yang memiliki definisi amorf. Jika Anda bertanya kepada 100 programmer tentang definisi mereka, Anda kemungkinan akan menerima 100 jawaban berbeda ...

Pemrograman fungsional menyangkut dan memfasilitasi penerapan dan komposisi fungsi ... Agar suatu bahasa dianggap fungsional, pengertian fungsinya harus kelas satu. Fungsi kelas satu dapat disimpan, diteruskan, dan dikembalikan sama seperti bagian data lainnya. Di luar konsep inti ini, [definisi FP mungkin meliputi] kemurnian, kekekalan, rekursi, kemalasan, dan transparansi referensial.

Pemrograman dalam Scala 2nd Edition hlm. 10 memiliki definisi berikut:

Pemrograman fungsional dipandu oleh dua ide utama. Gagasan pertama adalah bahwa fungsi adalah nilai kelas satu ... Anda dapat meneruskan fungsi sebagai argumen ke fungsi lain, mengembalikannya sebagai hasil dari fungsi, atau menyimpannya dalam variabel ...

Gagasan utama kedua pemrograman fungsional adalah bahwa operasi suatu program harus memetakan nilai input ke nilai output daripada mengubah data yang ada.

Jika kami menerima definisi pertama, maka satu-satunya hal yang perlu Anda lakukan untuk membuat kode Anda "fungsional" adalah mengubah loop Anda ke dalam. Definisi kedua termasuk kekekalan.

Fungsi Kelas Pertama

Bayangkan Anda saat ini mendapatkan Daftar Penumpang dari objek Bus Anda dan Anda mengulanginya mengurangi rekening bank setiap penumpang dengan jumlah ongkos bus. Cara fungsional untuk melakukan tindakan yang sama akan memiliki metode pada Bus, mungkin disebut forEachPassenger yang mengambil fungsi dari satu argumen. Kemudian Bus akan beralih dari penumpangnya namun yang terbaik dilakukan dan kode klien Anda yang menetapkan ongkos untuk perjalanan akan dimasukkan ke dalam fungsi dan diteruskan ke untuk setiap Penumpang. Voila! Anda menggunakan pemrograman fungsional.

Imperatif:

for (Passenger p : Bus.getPassengers()) {
    p.debit(fare);
}

Fungsional (menggunakan fungsi anonim atau "lambda" di Scala):

myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })

Versi Scala yang lebih manis:

myBus = myBus.forEachPassenger(_.debit(fare))

Fungsi Non-kelas

Jika bahasa Anda tidak mendukung fungsi kelas satu, ini bisa menjadi sangat jelek. Di Java 7 atau sebelumnya, Anda harus menyediakan antarmuka "Objek Fungsional" seperti ini:

// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
    public void accept(T t);
}

Kemudian kelas Bus menyediakan iterator internal:

public void forEachPassenger(Consumer<Passenger> c) {
    for (Passenger p : passengers) {
        c.accept(p);
    }
}

Akhirnya, Anda meneruskan objek fungsi anonim ke Bus:

// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
    }
}

Java 8 memungkinkan variabel lokal untuk ditangkap lingkup fungsi anonim, tetapi dalam versi sebelumnya, varibales tersebut harus dinyatakan final. Untuk menyiasatinya, Anda mungkin perlu membuat kelas pembungkus MutableReference. Berikut adalah kelas khusus integer yang memungkinkan Anda menambahkan penghitung lingkaran ke kode di atas:

public static class MutableIntWrapper {
    private int i;
    private MutableIntWrapper(int in) { i = in; }
    public static MutableIntWrapper ofZero() {
        return new MutableIntWrapper(0);
    }
    public int value() { return i; }
    public void increment() { i++; }
}

final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
        count.increment();
    }
}

System.out.println(count.value());

Bahkan dengan keburukan ini, kadang-kadang bermanfaat untuk menghilangkan logika yang rumit dan berulang dari loop yang tersebar di seluruh program Anda dengan menyediakan iterator internal.

Keburukan ini telah diperbaiki di Java 8, tetapi penanganan pengecualian yang diperiksa di dalam fungsi kelas satu masih benar-benar jelek dan Java masih membawa asumsi bisa berubah-ubah dalam semua koleksinya. Yang membawa kita ke tujuan lain yang sering dikaitkan dengan FP:

Kekekalan

Josh Bloch's Item 13 adalah "Prefer Immutability." Meskipun pembicaraan sampah umum bertentangan, OOP dapat dilakukan dengan objek yang tidak dapat diubah, dan melakukan hal itu membuatnya jauh lebih baik. Misalnya, String di Java tidak dapat diubah. StringBuffer, OTOH harus bisa berubah untuk membangun String yang tidak berubah. Beberapa tugas, seperti bekerja dengan buffer secara inheren membutuhkan kemampuan berubah-ubah.

Kemurnian

Setiap fungsi setidaknya harus dapat dipindah-pindahkan - jika Anda memberikan parameter input yang sama (dan seharusnya tidak memiliki input selain argumen aktualnya), ia harus menghasilkan output yang sama setiap kali tanpa menyebabkan "efek samping" seperti mengubah keadaan global, melakukan / O, atau melempar pengecualian.

Dikatakan bahwa dalam Pemrograman Fungsional, "beberapa kejahatan biasanya diperlukan untuk menyelesaikan pekerjaan." Kesucian 100% umumnya bukan tujuan. Meminimalkan efek sampingnya.

Kesimpulan

Sungguh, dari semua ide di atas, ketidakmampuan telah menjadi kemenangan tunggal terbesar dalam hal aplikasi praktis untuk menyederhanakan kode saya - apakah OOP, atau FP. Melewati fungsi ke iterator adalah kemenangan terbesar kedua. The Java 8 Lambdas dokumentasi memiliki penjelasan terbaik mengapa. Rekursi sangat bagus untuk memproses pohon. Kemalasan memungkinkan Anda bekerja dengan koleksi tak terbatas.

Jika Anda menyukai JVM, saya sarankan Anda melihat Scala dan Clojure. Keduanya adalah interpretasi mendalam tentang Pemrograman Fungsional. Scala adalah tipe-aman dengan sintaksis mirip-C, meskipun ia benar-benar memiliki banyak sintaksis yang sama dengan Haskell seperti halnya dengan C. Clojure bukan tipe-aman dan itu adalah Lisp. Saya baru-baru ini memposting perbandingan Java, Scala dan Clojure sehubungan dengan satu masalah refactoring tertentu. Perbandingan Logan Campbell menggunakan Game of Life termasuk Haskell dan juga Clojure yang diketik.

PS

Jimmy Hoffa menunjukkan bahwa kelas Bus saya bisa berubah. Daripada memperbaiki yang asli, saya pikir ini akan menunjukkan jenis refactoring pertanyaan ini. Ini dapat diperbaiki dengan membuat setiap metode pada Bus sebuah pabrik untuk menghasilkan Bus baru, setiap metode pada Penumpang sebuah pabrik untuk menghasilkan Penumpang baru. Jadi saya telah menambahkan jenis kembali ke segala sesuatu yang berarti saya akan menyalin java.util.function.Function Java 8 bukan antarmuka konsumen:

public interface Function<T,R> {
    public R apply(T t);
    // Note: I'm leaving out Java 8's compose() method here for simplicity
}

Kemudian di Bus:

public Bus mapPassengers(Function<Passenger,Passenger> c) {
    // I have to use a mutable collection internally because Java
    // does not have immutable collections that return modified copies
    // of themselves the way the Clojure and Scala collections do.
    List<Passenger> newPassengers = new ArrayList(passengers.size());
    for (Passenger p : passengers) {
        newPassengers.add(c.apply(p));
    }
    return Bus.of(driver, Collections.unmodifiableList(passengers));
}

Akhirnya, objek fungsi anonim mengembalikan keadaan yang dimodifikasi (bus baru dengan penumpang baru). Ini mengasumsikan bahwa p.debit () sekarang mengembalikan Penumpang kekal baru dengan lebih sedikit uang daripada yang asli:

Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
    @Override
    public Passenger apply(final Passenger p) {
        return p.debit(fare);
    }
}

Semoga sekarang Anda dapat membuat keputusan sendiri tentang seberapa fungsional Anda ingin membuat bahasa imperatif Anda, dan memutuskan apakah akan lebih baik untuk mendesain ulang proyek Anda menggunakan bahasa fungsional. Di Scala atau Clojure, koleksi dan API lainnya dirancang untuk memudahkan pemrograman fungsional. Keduanya memiliki interop Java yang sangat bagus, sehingga Anda dapat mencampur dan mencocokkan bahasa. Bahkan, untuk interoperabilitas Java, Scala mengkompilasi fungsi kelas pertamanya ke kelas anonim yang hampir kompatibel dengan antarmuka fungsional Java 8. Anda dapat membaca tentang detail di Scala in Depth sect. 1.3.2 .

GlenPeterson
sumber
Saya menghargai upaya, organisasi, dan komunikasi yang jelas dalam jawaban ini; tapi saya harus mengambil sedikit masalah dengan beberapa hal teknis. Salah satu kunci seperti yang disebutkan di dekat bagian atas adalah komposisi fungsi, ini kembali ke mengapa sebagian besar fungsi enkapsulasi di dalam objek tidak menghasilkan tujuan: Jika suatu fungsi berada di dalam objek, itu harus ada di sana untuk bertindak pada objek itu; dan jika itu bertindak pada objek itu harus berubah itu internal. Sekarang saya akan memaafkan bahwa tidak semua orang memerlukan transparansi referensial atau kekekalan, tetapi jika itu mengubah objek di tempatnya tidak perlu lagi mengembalikannya
Jimmy Hoffa
Dan segera setelah fungsi tidak mengembalikan nilai, tiba-tiba fungsi tidak dapat dikomposisi dengan yang lain, dan Anda kehilangan semua abstraksi komposisi fungsional. Anda dapat memiliki fungsi mengubah objek di tempat dan kemudian mengembalikan objek, tetapi jika melakukan ini mengapa tidak hanya membuat fungsi mengambil objek sebagai parameter dan membebaskannya dari batas-batas objek induknya? Dibebaskan dari objek induk, itu akan dapat bekerja pada tipe lain juga, yang merupakan bagian penting dari FP yang Anda lewatkan: Ketik abstraksi. forEachPasenger Anda hanya bekerja terhadap penumpang ...
Jimmy Hoffa
1
Alasan Anda abstrak hal-hal untuk memetakan dan mengurangi, dan fungsi-fungsi ini tidak terikat untuk berisi objek adalah agar mereka dapat digunakan pada berbagai jenis melalui polimorfisme parametrik. Ini adalah penggabungan berbagai abstraksi yang tidak Anda temukan dalam bahasa OOP yang benar-benar mendefinisikan FP dan mendorongnya untuk memiliki nilai. Bukan karena kemalasan, transparansi referensial, imutabilitas, atau bahkan sistem tipe HM diperlukan untuk membuat FP, hal-hal itu lebih merupakan efek samping dari menciptakan bahasa yang ditujukan untuk komposisi fungsional di mana fungsi dapat abstrak atas jenis secara umum
Jimmy Hoffa
@JimmyHoffa Anda membuat kritik yang sangat adil atas contoh saya. Saya tergoda untuk berubah-ubah oleh antarmuka Java8 Consumer. Juga, definisi chouser / fogus dari FP tidak termasuk immutability dan saya menambahkan definisi Odersky / Spoon / Venners nanti. Saya meninggalkan contoh aslinya, tetapi menambahkan versi baru yang tidak dapat diubah di bawah bagian "PS" di bagian bawah. Itu jelek. Tapi saya pikir itu menunjukkan fungsi yang bekerja pada objek untuk menghasilkan objek baru daripada mengubah internal aslinya. Komentar yang bagus!
GlenPeterson
1
Percakapan ini berlanjut di Papan Tulis: chat.stackexchange.com/transcript/message/11702383#11702383
GlenPeterson
12

Saya punya pengalaman pribadi "menyelesaikan" ini. Pada akhirnya, saya tidak menemukan sesuatu yang murni fungsional, tetapi saya menemukan sesuatu yang saya sukai. Begini cara saya melakukannya:

  • Konversi semua status eksternal ke parameter fungsi. EG: jika metode objek memodifikasi x, buatlah agar metode tersebut dilewatkan dan xbukannya menelepon this.x.
  • Hapus perilaku dari objek.
    1. Jadikan data objek dapat diakses publik
    2. Ubah semua metode menjadi fungsi yang dipanggil objek.
    3. Memiliki kode klien yang memanggil objek memanggil fungsi baru dengan mengirimkan data objek. EG: Konversikan x.methodThatModifiesTheFooVar()menjadifooFn(x.foo)
    4. Hapus metode asli dari objek
  • Ganti sebanyak loop berulang yang Anda bisa dengan tinggi fungsi rangka suka map, reduce, filter, dll

Saya tidak bisa menyingkirkan keadaan yang bisa berubah. Itu terlalu non-idiomatis dalam bahasa saya (JavaScript). Tetapi, dengan membuat semua status masuk dan / atau dikembalikan, setiap fungsi dimungkinkan untuk diuji. Ini berbeda dari OOP di mana pengaturan negara akan memakan waktu terlalu lama atau pemisahan dependensi sering memerlukan modifikasi kode produksi terlebih dahulu.

Juga, saya bisa saja salah tentang definisi, tetapi saya pikir fungsi saya secara transparan transparan: Fungsi saya akan memiliki efek yang sama dengan input yang sama.

Edit

Seperti yang Anda lihat di sini , tidak mungkin membuat objek yang benar-benar tidak dapat diubah dalam JavaScript. Jika Anda rajin dan mengendalikan siapa yang memanggil kode Anda, Anda dapat melakukannya dengan selalu membuat objek baru alih-alih memutasikan yang sekarang. Itu tidak sepadan dengan usaha saya.

Tetapi jika Anda menggunakan Java, Anda dapat menggunakan teknik ini untuk membuat kelas Anda tidak berubah.

Daniel Kaplan
sumber
+1 Bergantung pada apa yang sebenarnya Anda coba lakukan, ini mungkin sejauh Anda benar-benar dapat pergi tanpa membuat perubahan desain yang akan melampaui hanya "refactoring".
Evicatos
@Evicatos: Saya tidak tahu, jika JavaScript memiliki dukungan yang lebih baik untuk keadaan tidak berubah, saya pikir solusi saya akan berfungsi seperti Anda akan mendapatkan dalam bahasa fungsional yang dinamis seperti Clojure. Apa contoh sesuatu yang akan membutuhkan sesuatu di luar sekadar refactoring?
Daniel Kaplan
Saya pikir menyingkirkan negara yang bisa berubah akan memenuhi syarat. Saya tidak berpikir itu hanya masalah dukungan yang lebih baik dalam bahasa, saya pikir beralih dari tidak bisa berubah ke dasarnya akan selalu memerlukan perubahan arsitektur mendasar yang pada dasarnya merupakan penulisan ulang. Ymmv tergantung pada definisi Anda tentang refactoring.
Evicatos
@Evicatos lihat hasil edit saya
Daniel Kaplan
1
@tTYT ya, itu menyedihkan tentang JS yang bisa berubah-ubah, tapi setidaknya Clojure dapat dikompilasi ke JavaScript: github.com/clojure/clojurescript
GlenPeterson
3

Saya tidak berpikir itu benar-benar mungkin untuk memperbaiki program sepenuhnya - Anda harus mendesain ulang dan mengimplementasikannya dalam paradigma yang benar.

Saya telah melihat kode refactoring didefinisikan sebagai "teknik disiplin untuk merestrukturisasi tubuh kode yang ada, mengubah struktur internalnya tanpa mengubah perilaku eksternalnya".

Anda dapat membuat hal-hal tertentu lebih fungsional, tetapi pada intinya Anda masih memiliki program berorientasi objek. Anda tidak bisa hanya mengubah sedikit demi sedikit untuk menyesuaikannya dengan paradigma yang berbeda.

Uri
sumber
Saya akan menambahkan bahwa tanda pertama yang baik adalah berusaha untuk transparansi referensial. Setelah Anda memilikinya, Anda mendapatkan ~ 50% dari manfaat pemrograman fungsional.
Daniel Gratzer
3

Saya pikir seri artikel ini persis seperti yang Anda inginkan:

Retrogames Murni Fungsional

http://prog21.dadgum.com/23.html Bagian 1

http://prog21.dadgum.com/24.html Bagian 2

http://prog21.dadgum.com/25.html Bagian 3

http://prog21.dadgum.com/26.html Bagian 4

http://prog21.dadgum.com/37.html Tindak lanjut

Ringkasannya adalah:

Penulis menyarankan loop utama dengan efek samping (efek samping harus terjadi di suatu tempat, kan?) Dan sebagian besar fungsi mengembalikan catatan kecil yang tidak dapat diubah yang merinci bagaimana mereka mengubah keadaan permainan.

Tentu saja ketika menulis program dunia nyata Anda akan mencampur dan mencocokkan beberapa gaya pemrograman, menggunakan masing-masing gaya yang paling membantu. Namun itu adalah pengalaman belajar yang baik untuk mencoba menulis sebuah program dengan cara yang paling fungsional / tidak berubah dan juga menulisnya dengan cara yang paling spaghetti, hanya menggunakan variabel global :-) (lakukan sebagai percobaan, bukan dalam produksi, silakan)

marcus
sumber
2

Anda mungkin harus mengubah semua kode Anda ke dalam karena OOP dan FP memiliki dua pendekatan yang berlawanan untuk mengatur kode.

OOP mengatur kode seputar tipe (kelas): kelas yang berbeda dapat mengimplementasikan operasi yang sama (metode dengan tanda tangan yang sama). Sebagai hasilnya, OOP lebih tepat ketika rangkaian operasi tidak banyak berubah sementara tipe baru dapat ditambahkan sangat sering. Sebagai contoh, pertimbangkan sebuah perpustakaan GUI di mana setiap widget memiliki seperangkat tetap metode ( hide(), show(), paint(), move(), dan sebagainya) tapi widget baru bisa ditambahkan sebagai perpustakaan diperpanjang. Dalam OOP mudah untuk menambahkan tipe baru (untuk antarmuka yang diberikan): Anda hanya perlu menambahkan kelas baru dan mengimplementasikan semua metode (perubahan kode lokal). Di sisi lain, menambahkan operasi (metode) baru ke antarmuka mungkin memerlukan perubahan semua kelas yang mengimplementasikan antarmuka itu (meskipun warisan dapat mengurangi jumlah pekerjaan).

FP mengatur kode di sekitar operasi (fungsi): setiap fungsi mengimplementasikan beberapa operasi yang dapat memperlakukan berbagai jenis dengan cara yang berbeda. Ini biasanya dicapai dengan mengirimkan jenis melalui pencocokan pola atau mekanisme lainnya. Sebagai konsekuensinya, FP lebih sesuai ketika rangkaian tipe stabil dan operasi baru ditambahkan lebih sering. Ambil contoh satu set tetap format gambar (GIF, JPEG, dll) dan beberapa algoritma yang ingin Anda terapkan. Setiap algoritma dapat diimplementasikan oleh fungsi yang berperilaku berbeda sesuai dengan jenis gambar. Menambahkan algoritma baru itu mudah karena Anda hanya perlu mengimplementasikan fungsi baru (perubahan kode lokal). Menambahkan format (tipe) baru memerlukan modifikasi semua fungsi yang telah Anda implementasikan sejauh ini untuk mendukungnya (perubahan non-lokal).

Intinya: OOP dan FP pada dasarnya berbeda dalam cara mereka mengatur kode, dan mengubah desain OOP menjadi desain FP akan melibatkan perubahan semua kode Anda untuk mencerminkan hal ini. Ini bisa menjadi latihan yang menarik. Lihat juga catatan kuliah ini untuk buku SICP yang dikutip oleh mikemay, khususnya slide 13.1.5 hingga 13.1.10.

Giorgio
sumber