Bagaimana Anda bisa melakukan sesuatu yang bermanfaat tanpa status bisa berubah?

265

Saya telah membaca banyak hal tentang pemrograman fungsional akhir-akhir ini, dan saya bisa memahaminya, tetapi satu hal yang tidak bisa saya ingat adalah coding stateless. Bagi saya, menyederhanakan pemrograman dengan menghapus status yang bisa berubah adalah seperti "menyederhanakan" mobil dengan menghapus dasbor: produk jadi mungkin lebih sederhana, tetapi semoga berhasil membuatnya berinteraksi dengan pengguna akhir.

Hampir setiap aplikasi pengguna yang saya anggap melibatkan status sebagai konsep inti. Jika Anda menulis dokumen (atau posting SO), status berubah dengan setiap input baru. Atau jika Anda bermain video game, ada banyak variabel keadaan, dimulai dengan posisi semua karakter, yang cenderung bergerak terus-menerus. Bagaimana Anda bisa melakukan sesuatu yang bermanfaat tanpa melacak perubahan nilai?

Setiap kali saya menemukan sesuatu yang membahas masalah ini, itu ditulis dalam bahasa fungsional yang benar-benar teknis yang mengasumsikan latar belakang FP yang berat yang tidak saya miliki. Apakah ada yang tahu cara untuk menjelaskan hal ini kepada seseorang dengan pemahaman yang baik dan solid tentang imperatif coding tetapi siapa yang n00b lengkap di sisi fungsional?

EDIT: Banyak balasan sejauh ini tampaknya berusaha meyakinkan saya tentang keuntungan dari nilai-nilai yang tidak berubah. Saya mendapatkan bagian itu. Masuk akal. Yang tidak saya mengerti adalah bagaimana Anda bisa melacak nilai-nilai yang harus berubah, dan berubah secara konstan, tanpa variabel yang bisa berubah.

Mason Wheeler
sumber
2
Lihat ini: stackoverflow.com/questions/844536/…
Sasha Chedygov
1
Pendapat pribadi saya yang sederhana adalah bahwa itu seperti kekuatan dan uang. Hukum pengembalian yang semakin menurun berlaku. Jika Anda cukup kuat, mungkin ada sedikit insentif untuk menjadi sedikit lebih kuat tetapi tidak ada salahnya untuk melakukannya (dan beberapa orang melakukannya dengan penuh semangat). Hal yang sama berlaku untuk negara yang bisa berubah global. Merupakan preferensi pribadi saya untuk menerima bahwa ketika keterampilan pengkodean saya berkembang, ada baiknya untuk membatasi jumlah keadaan global yang bisa berubah dalam kode saya. Ini mungkin tidak pernah sempurna tetapi bagus untuk bekerja untuk meminimalkan negara yang bisa berubah secara global.
AturSams
Seperti halnya uang, poin yang akan dicapai adalah menginvestasikan lebih banyak waktu ke dalamnya, tidak lagi sangat berguna dan prioritas lainnya akan naik ke atas. Jika misalnya, Anda mencapai kekuatan sebesar mungkin (per metafora saya) itu mungkin tidak melayani tujuan yang berguna dan bahkan bisa menjadi beban. Tetapi masih baik untuk berjuang menuju tujuan yang mungkin tidak dapat dicapai dan menginvestasikan sumber daya moderat ke dalamnya.
AturSams
7
Secara singkat, dalam FP, fungsi tidak pernah mengubah keadaan. Akhirnya mereka akan mengembalikan sesuatu yang menggantikan keadaan saat ini. Tetapi negara tidak pernah dimodifikasi (bermutasi) di tempat.
jinglesthula
Ada cara untuk mendapatkan status penuh tanpa mutasi (menggunakan tumpukan dari apa yang saya mengerti), tetapi pertanyaan ini dalam arti di samping intinya (meskipun itu hebat). Sulit untuk berbicara tentang secara ringkas, tapi di sini ada posting yang diharapkan menjawab pertanyaan Anda medium.com/@jbmilgrom/… . TLDR adalah bahwa semantik dari bahkan program fungsional stateful tidak dapat diubah, namun komunikasi b / w berjalan dari fungsi program ditangani.
jbmilgrom

Jawaban:

166

Atau jika Anda bermain video game, ada banyak variabel keadaan, dimulai dengan posisi semua karakter, yang cenderung bergerak terus-menerus. Bagaimana Anda bisa melakukan sesuatu yang bermanfaat tanpa melacak perubahan nilai?

Jika Anda tertarik, inilah serangkaian artikel yang menjelaskan pemrograman game dengan Erlang.

Anda mungkin tidak akan menyukai jawaban ini, tetapi Anda tidak akan mendapatkan program fungsional sampai Anda menggunakannya. Saya dapat memposting sampel kode dan mengatakan "Di sini, tidakkah Anda melihat " - tetapi jika Anda tidak memahami sintaksis dan prinsip-prinsip yang mendasarinya, maka mata Anda hanya berkaca-kaca. Dari sudut pandang Anda, sepertinya saya melakukan hal yang sama dengan bahasa imperatif, tetapi hanya mengatur semua jenis batasan untuk sengaja membuat pemrograman lebih sulit. Menurut saya, Anda baru saja mengalami paradoks Blub .

Awalnya saya skeptis, tetapi saya melompat ke kereta pemrograman fungsional beberapa tahun yang lalu dan jatuh cinta padanya. Trik dengan pemrograman fungsional adalah mampu mengenali pola, penugasan variabel tertentu, dan memindahkan status imperatif ke stack. Misalnya untuk-loop, menjadi rekursi:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Tidak terlalu cantik, tapi kami mendapat efek yang sama tanpa mutasi. Tentu saja, jika memungkinkan, kami ingin menghindari pengulangan dan abaikan saja:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Metode Seq.iter akan menghitung melalui koleksi dan memanggil fungsi anonim untuk setiap item. Sangat berguna :)

Saya tahu, mencetak angka tidak terlalu mengesankan. Namun, kami dapat menggunakan pendekatan yang sama dengan game: tahan semua status di tumpukan dan buat objek baru dengan perubahan dalam panggilan rekursif. Dengan cara ini, setiap frame adalah snapshot stateless dari game, di mana setiap frame hanya menciptakan objek baru dengan perubahan yang diinginkan dari objek stateless apa pun yang perlu diperbarui. Kodesemu untuk ini mungkin:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

Versi imperatif dan fungsional identik, tetapi versi fungsional jelas tidak menggunakan keadaan bisa berubah. Kode fungsional membuat semua status disimpan di stack - hal yang menyenangkan tentang pendekatan ini adalah bahwa, jika terjadi kesalahan, debugging mudah, yang Anda butuhkan hanyalah jejak stack.

Ini menskala hingga sejumlah objek dalam game, karena semua objek (atau koleksi objek terkait) dapat dirender dalam utasnya sendiri.

Hampir setiap aplikasi pengguna yang saya anggap melibatkan status sebagai konsep inti.

Dalam bahasa fungsional, alih-alih mengubah keadaan objek, kami cukup mengembalikan objek baru dengan perubahan yang kita inginkan. Ini lebih efisien daripada kedengarannya. Struktur data, misalnya, sangat mudah direpresentasikan sebagai struktur data yang tidak berubah. Tumpukan, misalnya, terkenal mudah diimplementasikan:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Kode di atas membuat dua daftar yang tidak dapat diubah, menambahkannya bersama untuk membuat daftar baru, dan menambahkan hasilnya. Tidak ada keadaan yang bisa berubah digunakan di mana saja dalam aplikasi. Itu terlihat agak besar, tapi itu hanya karena C # adalah bahasa yang verbose. Berikut program yang setara di F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Tidak perlu diubah untuk membuat dan memanipulasi daftar. Hampir semua struktur data dapat dengan mudah dikonversi menjadi padanan fungsionalnya. Saya menulis sebuah halaman di sini yang menyediakan implementasi tumpukan, antrian, tumpukan kiri, pohon merah-hitam, daftar malas. Tidak satu pun cuplikan kode yang berisi status apa pun yang dapat berubah. Untuk "bermutasi" pohon, saya membuat yang baru dengan simpul baru yang saya inginkan - ini sangat efisien karena saya tidak perlu membuat salinan dari setiap simpul di pohon, saya dapat menggunakan kembali yang lama di baru saya pohon.

Menggunakan contoh yang lebih signifikan, saya juga menulis parser SQL ini yang benar-benar stateless (atau setidaknya kode saya stateless, saya tidak tahu apakah lexing library yang mendasari adalah stateless).

Pemrograman stateless sama ekspresif dan kuatnya dengan pemrograman stateful, hanya perlu sedikit latihan untuk melatih diri Anda untuk mulai berpikir tanpa kewarganegaraan. Tentu saja, "pemrograman stateless bila memungkinkan, pemrograman stateful jika diperlukan" tampaknya menjadi moto dari sebagian besar bahasa fungsional yang tidak murni. Tidak ada salahnya kembali pada perumpamaan ketika pendekatan fungsional tidak sebersih atau seefisien itu.

Juliet
sumber
7
Saya suka contoh Pacman. Tapi itu bisa menyelesaikan satu masalah hanya untuk mengangkat masalah lain: Bagaimana jika sesuatu yang lain mengacu pada objek Pacman yang ada? Maka itu tidak akan mengumpulkan sampah dan diganti; alih-alih, Anda berakhir dengan dua salinan objek, salah satunya tidak valid. Bagaimana Anda menangani masalah ini?
Mason Wheeler
9
Jelas Anda perlu membuat "sesuatu yang lain" baru dengan objek Pacman baru;) Tentu saja, jika kita mengambil rute itu terlalu jauh, kita akhirnya menciptakan grafik objek untuk seluruh dunia kita setiap kali ada perubahan. Pendekatan yang lebih baik dijelaskan di sini ( prog21.dadgum.com/26.html ): daripada memiliki objek memperbarui diri mereka sendiri dan semua dependensi mereka, jauh lebih mudah bagi mereka untuk mengirimkan pesan tentang keadaan mereka ke loop peristiwa yang menangani semua memperbarui. Ini membuatnya lebih mudah untuk memutuskan objek apa dalam grafik yang perlu diperbarui, dan mana yang tidak.
Juliet
6
@ Juliet, saya punya satu keraguan - dalam pola pikir saya yang benar-benar penting, rekursi harus berakhir di beberapa titik, jika tidak Anda akhirnya akan menghasilkan stack overflow. Dalam contoh pacman rekursif, bagaimana tumpukan disimpan di teluk - apakah objek secara implisit muncul di awal fungsi?
BlueStrat
9
@BlueStrat - pertanyaan yang bagus ... jika itu "panggilan ekor" ... yaitu panggilan rekursif adalah hal terakhir dalam fungsi ... maka sistem tidak perlu membuat bingkai tumpukan baru ... itu bisa gunakan kembali yang sebelumnya. Ini adalah optimasi umum untuk bahasa pemrograman fungsional. en.wikipedia.org/wiki/Tail_call
reteptilian
4
@MichaelOsofsky, saat berinteraksi dengan database dan API, selalu ada 'dunia luar' yang memiliki status untuk berkomunikasi. Dalam hal ini, Anda tidak dapat berfungsi 100%. Penting untuk menjaga kode 'tidak berfungsi' ini diisolasi dan diabstraksi sehingga hanya ada satu entri dan satu pintu keluar ke dunia luar. Dengan cara ini Anda dapat menjaga sisa kode Anda berfungsi.
Chielt
76

Jawaban singkat: Anda tidak bisa.

Jadi apa masalahnya tentang ketidakberdayaan?

Jika Anda berpengalaman dalam bahasa imperatif, maka Anda tahu bahwa "global itu buruk". Mengapa? Karena mereka memperkenalkan (atau berpotensi untuk memperkenalkan) beberapa dependensi yang sangat sulit untuk dilepaskan dalam kode Anda. Dan ketergantungan tidak baik; Anda ingin kode Anda menjadi modular . Bagian program tidak memengaruhi bagian lain sesedikit mungkin. Dan FP membawa Anda ke cawan suci modularitas: tidak ada efek samping sama sekali . Anda hanya memiliki f (x) = y. Masukkan x in, keluar y. Tidak ada perubahan pada x atau yang lainnya. FP membuat Anda berhenti memikirkan keadaan, dan mulai berpikir dalam hal nilai. Semua fungsi Anda hanya menerima nilai dan menghasilkan nilai baru.

Ini memiliki beberapa keunggulan.

Pertama, tidak ada efek samping berarti program yang lebih sederhana, lebih mudah untuk dipertimbangkan. Tidak perlu khawatir bahwa memperkenalkan bagian baru dari program akan mengganggu dan merusak bagian yang sudah ada dan berfungsi.

Kedua, ini membuat program dapat diparalelkan secara sepele (paralelisasi efisien adalah masalah lain).

Ketiga, ada beberapa kemungkinan keuntungan kinerja. Katakanlah Anda memiliki fungsi:

double x = 2 * x

Sekarang Anda memasukkan nilai 3 ke dalam, dan Anda mendapatkan nilai 6 keluar. Setiap saat. Tetapi Anda dapat melakukannya dengan keharusan juga, bukan? Ya. Tetapi masalahnya adalah bahwa secara imperatif, Anda dapat melakukan lebih banyak lagi . Dapat saya lakukan:

int y = 2;
int double(x){ return x * y; }

tetapi saya juga bisa melakukannya

int y = 2;
int double(x){ return x * (y++); }

Compiler imperatif tidak tahu apakah saya akan memiliki efek samping atau tidak, yang membuatnya lebih sulit untuk dioptimalkan (yaitu double 2 tidak perlu 4 setiap kali). Yang fungsional tahu saya tidak akan - karenanya, ia dapat mengoptimalkan setiap kali ia melihat "ganda 2".

Sekarang, meskipun menciptakan nilai-nilai baru setiap kali tampaknya sangat boros untuk jenis nilai yang kompleks dalam hal memori komputer, tidak harus begitu. Karena, jika Anda memiliki f (x) = y, dan nilai x dan y "sebagian besar sama" (mis. Pohon yang hanya berbeda dalam beberapa daun) maka x dan y dapat berbagi bagian memori - karena tidak satu pun dari mereka akan bermutasi .

Jadi, jika hal yang tidak dapat diubah ini begitu hebat, mengapa saya menjawab bahwa Anda tidak dapat melakukan sesuatu yang berguna tanpa kondisi yang dapat berubah. Nah, tanpa bisa berubah-ubah, seluruh program Anda akan menjadi fungsi f (x) = y raksasa. Dan hal yang sama berlaku untuk semua bagian dari program Anda: hanya fungsi, dan fungsi dalam arti "murni" pada saat itu. Seperti yang saya katakan, ini berarti f (x) = y setiap waktu. Jadi mis. ReadFile ("myFile.txt") perlu mengembalikan nilai string yang sama setiap kali. Tidak terlalu bermanfaat.

Karena itu, setiap FP menyediakan beberapa cara untuk bermutasi. Bahasa fungsional "Murni" (mis. Haskell) melakukan ini menggunakan konsep yang agak menyeramkan seperti monad, sedangkan bahasa "tidak murni" (mis. ML) memungkinkan ini secara langsung.

Dan tentu saja, bahasa fungsional datang dengan sejumlah barang lain yang membuat pemrograman lebih efisien, seperti fungsi kelas satu dll.

oggy
sumber
2
<< readFile ("myFile.txt") perlu mengembalikan nilai string yang sama setiap kali. Tidak terlalu berguna. >> Saya kira itu berguna selama Anda menyembunyikan global, sebuah sistem file. Jika Anda menganggapnya sebagai parameter kedua dan membiarkan proses lain mengembalikan referensi baru ke sistem file setiap kali mereka memodifikasinya dengan filesystem2 = tulis (filesystem1, fd, pos, "string"), dan biarkan semua proses bertukar referensi mereka ke sistem file , kita bisa mendapatkan gambaran yang lebih bersih dari sistem operasi.
belut ghEEz
@eelghEEz, ini adalah pendekatan yang sama yang dilakukan Datomic ke basis data.
Jason
1
+1 untuk perbandingan yang jelas dan ringkas antara paradigma. Salah satu saran adalah int double(x){ return x * (++y); }karena yang sekarang masih akan menjadi 4, meskipun masih memiliki efek samping yang tidak ++y
diiklankan
@eelghEEz Saya tidak yakin dengan alternatif, sungguh, apakah ada orang lain? Untuk memperkenalkan informasi ke dalam konteks FP (murni), Anda "melakukan pengukuran", misalnya "di timestamp X, suhunya Y". Jika seseorang menanyakan suhu, mereka mungkin secara implisit berarti X = sekarang, tetapi mereka tidak mungkin meminta suhu sebagai fungsi universal waktu, bukan? FP berurusan dengan keadaan tidak berubah, dan Anda harus membuat keadaan tidak berubah - dari sumber internal dan eksternal - dari yang bisa berubah. Indeks, cap waktu, dll. Berguna tetapi ortogonal untuk berubah-ubah - seperti VCS adalah untuk mengontrol versi itu sendiri.
John P
29

Perhatikan bahwa mengatakan pemrograman fungsional tidak memiliki 'status' sedikit menyesatkan dan mungkin menjadi penyebab kebingungan. Ini jelas tidak memiliki 'keadaan yang bisa berubah', tetapi masih bisa memiliki nilai yang dimanipulasi; mereka tidak bisa diubah di tempat (mis. Anda harus membuat nilai baru dari nilai lama).

Ini adalah penyederhanaan berlebihan, tetapi bayangkan Anda memiliki bahasa OO, di mana semua properti di kelas ditetapkan hanya sekali dalam konstruktor, semua metode adalah fungsi statis. Anda masih bisa melakukan hampir semua perhitungan dengan meminta metode mengambil objek yang berisi semua nilai yang mereka butuhkan untuk perhitungannya dan kemudian mengembalikan objek baru dengan hasilnya (mungkin bahkan instance baru dari objek yang sama).

Mungkin sulit untuk menerjemahkan kode yang ada ke dalam paradigma ini, tetapi itu karena itu benar-benar memerlukan cara berpikir yang sama sekali berbeda tentang kode. Sebagai efek samping meskipun dalam banyak kasus Anda mendapatkan banyak kesempatan untuk paralelisme secara gratis.

Tambahan: (Mengenai hasil edit Anda tentang cara melacak nilai-nilai yang perlu diubah) Nilai-nilai itu
akan disimpan dalam struktur data yang tidak dapat diubah tentu saja ...

Ini bukan 'solusi' yang disarankan, tetapi cara termudah untuk melihat bahwa ini akan selalu berhasil adalah Anda dapat menyimpan nilai-nilai yang tidak dapat diubah ini ke dalam struktur peta (kamus / hashtable) seperti, dikunci oleh 'nama variabel'.

Jelas dalam solusi praktis Anda akan menggunakan pendekatan yang lebih waras, tetapi ini menunjukkan bahwa kasus terburuk jika tidak ada yang berhasil Anda bisa 'mensimulasikan' keadaan yang bisa berubah dengan peta yang Anda bawa melalui pohon doa Anda.

jerigen
sumber
2
OK, saya mengubah judulnya. Namun, jawaban Anda tampaknya mengarah ke masalah yang lebih buruk. Jika saya harus membuat ulang setiap objek setiap kali sesuatu dalam keadaannya berubah, saya akan menghabiskan semua waktu CPU saya melakukan apa-apa selain membangun objek. Saya berpikir tentang pemrograman game di sini, di mana Anda memiliki banyak hal yang bergerak di layar (dan di luar layar) sekaligus, yang harus dapat berinteraksi satu sama lain. Seluruh mesin memiliki satu set framerate: semua yang akan Anda lakukan, Anda harus lakukan dalam X jumlah milidetik. Tentunya ada cara yang lebih baik daripada terus-menerus mendaur ulang seluruh benda?
Mason Wheeler
4
Keindahannya adalah bahwa imutabilitas ada pada bahasa, bukan pada implementasinya. Dengan beberapa trik, Anda dapat memiliki status imutable dalam bahasa sementara implementasinya sebenarnya mengubah status yang ada. Lihat misalnya ST monad Haskell.
CesarB
4
@ Alasan: Intinya kompiler dapat lebih baik memutuskan di mana (utas) aman untuk mengubah keadaan di tempat daripada yang Anda bisa.
jerryjvl
Saya pikir untuk permainan Anda harus menghindari kekal untuk bagian mana pun di mana kecepatan tidak masalah. Meskipun bahasa yang tidak berubah dapat mengoptimalkan untuk Anda, tidak ada yang lebih cepat daripada memodifikasi memori yang dilakukan CPU dengan cepat. Jadi jika ternyata ada 10 atau 20 tempat di mana Anda perlu perintah saya pikir Anda harus menghindari sama sekali tidak berubah kecuali jika Anda dapat memodulasi itu untuk area yang sangat terpisah seperti menu game. Dan logika permainan pada khususnya bisa menjadi tempat yang bagus untuk menggunakan kekekalan karena saya merasa itu bagus untuk melakukan pemodelan kompleks sistem murni seperti aturan bisnis.
LegendLength
@LegendLength Anda menentang diri sendiri.
Ixx
18

Saya pikir ada sedikit kesalahpahaman. Program fungsional murni memiliki status. Perbedaannya adalah bagaimana keadaan itu dimodelkan. Dalam pemrograman fungsional murni, state dimanipulasi oleh fungsi-fungsi yang mengambil beberapa state dan mengembalikan state berikutnya. Sequencing through state kemudian dicapai dengan melewati state melalui serangkaian fungsi murni.

Bahkan negara yang bisa berubah global dapat dimodelkan dengan cara ini. Di Haskell, misalnya, program adalah fungsi dari Dunia ke Dunia. Artinya, Anda melewati seluruh alam semesta , dan program mengembalikan alam semesta baru. Namun dalam praktiknya, Anda hanya perlu melewati bagian-bagian alam semesta tempat program Anda benar-benar tertarik. Dan program sebenarnya mengembalikan urutan tindakan yang berfungsi sebagai instruksi untuk lingkungan operasi tempat program berjalan.

Anda ingin melihat ini dijelaskan dalam hal pemrograman imperatif. OK, mari kita lihat beberapa pemrograman imperatif yang sangat sederhana dalam bahasa fungsional.

Pertimbangkan kode ini:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Kode imperatif cukup standar rawa. Tidak melakukan sesuatu yang menarik, tetapi itu OK untuk ilustrasi. Saya pikir Anda akan setuju bahwa ada keadaan yang terlibat di sini. Nilai variabel x berubah seiring waktu. Sekarang, mari kita ubah notasi sedikit dengan menciptakan sintaks baru:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Masukkan tanda kurung untuk memperjelas apa artinya ini:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Jadi, Anda lihat, state dimodelkan oleh urutan ekspresi murni yang mengikat variabel bebas dari ekspresi berikut.

Anda akan menemukan bahwa pola ini dapat memodelkan segala jenis keadaan, bahkan IO.

Apocalisp
sumber
Apakah itu seperti Monad?
CMCDragonkai
Apakah Anda mempertimbangkan ini: A bersifat deklaratif pada level 1 B adalah deklaratif pada level 2, menganggap A sebagai keharusan. C bersifat deklaratif pada level 3, ia menganggap B sebagai keharusan. Saat kami meningkatkan lapisan abstraksi, ia selalu menganggap bahasa yang lebih rendah pada lapisan abstraksi lebih penting daripada itu sendiri.
CMCDragonkai
14

Inilah cara Anda menulis kode tanpa status dapat diubah : alih-alih menempatkan status berubah ke variabel yang dapat diubah, Anda memasukkannya ke dalam parameter fungsi. Dan alih-alih menulis loop, Anda menulis fungsi rekursif. Jadi misalnya kode penting ini:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

menjadi kode fungsional ini (sintaksis mirip Skema):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

atau kode Haskellish ini

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Mengenai mengapa programmer fungsional suka melakukan ini (yang tidak Anda tanyakan), semakin banyak potongan program Anda yang tidak memiliki kewarganegaraan, semakin banyak cara untuk menyatukan potongan-potongan tanpa mengalami kerusakan . Kekuatan paradigma kewarganegaraan tidak terletak pada kewarganegaraan (atau kemurnian) semata , tetapi kemampuan yang diberikannya kepada Anda untuk menulis fungsi-fungsi yang kuat dan dapat digunakan kembali dan menggabungkannya.

Anda dapat menemukan tutorial yang bagus dengan banyak contoh di makalah John Hughes, Why Functional Programming Matters .

Norman Ramsey
sumber
13

Itu hanya cara berbeda untuk melakukan hal yang sama.

Pertimbangkan contoh sederhana seperti menambahkan angka 3, 5, dan 10. Bayangkan Anda berpikir untuk melakukan itu dengan terlebih dahulu mengubah nilai 3 dengan menambahkan 5 ke dalamnya, kemudian menambahkan 10 ke angka "3", kemudian mengeluarkan nilai saat ini dari " 3 "(18). Ini kelihatannya benar-benar konyol, tetapi pada intinya cara pemrograman imperatif berbasis negara sering dilakukan. Memang, Anda dapat memiliki banyak "3" yang berbeda yang memiliki nilai 3, namun berbeda. Semua ini tampak aneh, karena kita sudah begitu mendarah daging dengan gagasan yang sangat masuk akal, sehingga angkanya tidak berubah.

Sekarang pikirkan tentang menambahkan 3, 5, dan 10 ketika Anda mengambil nilai menjadi tidak berubah. Anda menambahkan 3 dan 5 untuk menghasilkan nilai lain, 8, lalu Anda menambahkan 10 ke nilai itu untuk menghasilkan nilai lain lagi, 18.

Ini adalah cara yang setara untuk melakukan hal yang sama. Semua informasi yang diperlukan ada di kedua metode, tetapi dalam bentuk yang berbeda. Dalam satu informasi ada sebagai negara dan dalam aturan untuk mengubah negara. Di lain informasi ada dalam data yang tidak berubah dan definisi fungsional.

Baji
sumber
10

Saya terlambat ke diskusi, tetapi saya ingin menambahkan beberapa poin untuk orang-orang yang berjuang dengan pemrograman fungsional.

  1. Bahasa fungsional mempertahankan pembaruan keadaan yang sama persis dengan bahasa imperatif tetapi mereka melakukannya dengan meneruskan status yang diperbarui ke panggilan fungsi berikutnya . Berikut ini adalah contoh yang sangat sederhana untuk melakukan perjalanan menuruni garis angka. Status Anda adalah lokasi Anda saat ini.

Pertama cara imperatif (dalam kodesemu)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Sekarang dengan cara fungsional (dalam pseudocode). Saya sangat bergantung pada operator ternary karena saya ingin orang-orang dari latar belakang imperatif benar-benar dapat membaca kode ini. Jadi, jika Anda tidak menggunakan operator ternary banyak (saya selalu menghindarinya di hari-hari penting saya) di sini adalah cara kerjanya.

predicate ? if-true-expression : if-false-expression

Anda dapat mengaitkan ekspresi terner dengan meletakkan ekspresi terner baru sebagai ganti ekspresi salah

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Maka dengan itu dalam pikiran, inilah versi fungsionalnya.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Ini adalah contoh sepele. Jika ini menggerakkan orang-orang di dunia permainan, Anda harus memperkenalkan efek samping seperti menggambar posisi objek saat ini di layar dan memperkenalkan sedikit penundaan dalam setiap panggilan berdasarkan seberapa cepat objek bergerak. Tetapi Anda masih tidak perlu keadaan bisa berubah.

  1. Pelajarannya adalah bahasa-bahasa fungsional "bermutasi" dengan memanggil fungsi dengan parameter yang berbeda. Jelas ini tidak benar-benar bermutasi variabel apa pun, tapi itulah cara Anda mendapatkan efek yang sama. Ini berarti Anda harus terbiasa berpikir secara rekursif jika Anda ingin melakukan pemrograman fungsional.

  2. Belajar berpikir secara rekursif tidaklah sulit, tetapi butuh latihan dan toolkit. Bagian kecil dalam buku "Learn Java" tempat mereka menggunakan rekursi untuk menghitung faktorial tidak memotongnya. Anda memerlukan perangkat keterampilan seperti membuat proses berulang dari rekursi (inilah sebabnya rekursi ekor sangat penting untuk bahasa fungsional), kelanjutan, invarian, dll. Anda tidak akan melakukan pemrograman OO tanpa belajar tentang pengubah akses, antarmuka, dll. Hal yang sama untuk pemrograman fungsional.

Rekomendasi saya adalah melakukan Little Schemer (perhatikan bahwa saya mengatakan "lakukan" dan bukan "membaca") dan kemudian lakukan semua latihan di SICP. Setelah selesai, Anda akan memiliki otak yang berbeda dari ketika Anda mulai.

Just Another Justin
sumber
8

Sebenarnya cukup mudah untuk memiliki sesuatu yang tampak seperti keadaan bisa berubah bahkan dalam bahasa tanpa keadaan bisa berubah.

Pertimbangkan fungsi dengan tipe s -> (a, s). Menerjemahkan dari sintaks Haskell, itu berarti fungsi yang mengambil satu parameter tipe " s" dan mengembalikan sepasang nilai, tipe " a" dan " s". Jika sadalah tipe negara kita, fungsi ini mengambil satu negara dan mengembalikan negara baru, dan mungkin nilai (Anda selalu dapat mengembalikan "unit" alias (), yang setara dengan " void" di C / C ++, sebagai " a" Tipe). Jika Anda mem-chain beberapa panggilan fungsi dengan tipe seperti ini (mengembalikan status dari satu fungsi dan meneruskannya ke fungsi berikutnya), Anda memiliki status "bisa berubah-ubah" (sebenarnya Anda berada di setiap fungsi yang membuat negara baru dan mengabaikan yang lama ).

Mungkin lebih mudah untuk dipahami jika Anda membayangkan keadaan yang bisa berubah sebagai "ruang" tempat program Anda dijalankan, dan kemudian pikirkan dimensi waktu. Pada t1 instan, "ruang" berada dalam kondisi tertentu (misalnya, beberapa lokasi memori memiliki nilai 5). Pada t2 instan kemudian, ia berada dalam kondisi yang berbeda (misalnya lokasi memori sekarang memiliki nilai 10). Setiap "irisan" waktu ini adalah suatu keadaan, dan itu tidak berubah (Anda tidak dapat kembali pada waktunya untuk mengubahnya). Jadi, dari sudut pandang ini, Anda beralih dari ruangwaktu penuh dengan panah waktu (keadaan berubah-ubah Anda) ke satu set irisan ruangwaktu (beberapa negara tidak berubah), dan program Anda hanya memperlakukan setiap irisan sebagai nilai dan menghitung masing-masing dari mereka sebagai fungsi yang diterapkan pada yang sebelumnya.

OK, mungkin itu tidak mudah dimengerti :-)

Mungkin tampak tidak efisien untuk secara eksplisit mewakili keseluruhan kondisi program sebagai nilai, yang harus dibuat hanya untuk dibuang saat berikutnya (hanya setelah yang baru dibuat). Untuk beberapa algoritma mungkin alami, tetapi ketika tidak, ada trik lain. Alih-alih keadaan sebenarnya, Anda dapat menggunakan keadaan palsu yang tidak lebih dari penanda (sebut saja jenis keadaan palsu ini State#). Keadaan palsu ini ada dari sudut pandang bahasa, dan dilewatkan seperti nilai lainnya, tetapi kompilator sepenuhnya menghilangkannya saat membuat kode mesin. Ini hanya berfungsi untuk menandai urutan eksekusi.

Sebagai contoh, misalkan kompiler memberi kita fungsi-fungsi berikut:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

Menerjemahkan dari deklarasi mirip Haskell ini, readRefmenerima sesuatu yang menyerupai pointer atau handle ke nilai tipe " a", dan status palsu, dan mengembalikan nilai tipe " a" yang ditunjukkan oleh parameter pertama dan status palsu baru. writeRefserupa, tetapi sebaliknya mengubah nilai yang ditunjuk.

Jika Anda memanggil readRefdan meneruskannya, negara palsu dikembalikan oleh writeRef(mungkin dengan panggilan lain ke fungsi yang tidak terkait di tengah; nilai-nilai negara ini membuat "rantai" panggilan fungsi), itu akan mengembalikan nilai tertulis. Anda dapat menelepon writeReflagi dengan pointer / handle yang sama dan itu akan menulis ke lokasi memori yang sama - tetapi, karena secara konseptual mengembalikan negara baru (palsu), negara (palsu) masih dapat diubah (yang baru telah "dibuat "). Compiler akan memanggil fungsi dalam urutan yang harus dipanggil jika ada variabel keadaan nyata yang harus dihitung, tetapi satu-satunya keadaan yang ada adalah keadaan penuh (bisa berubah) dari perangkat keras nyata.

(Mereka yang tahu Haskell akan memperhatikan saya menyederhanakan banyak hal dan membatalkan beberapa detail penting. Bagi mereka yang ingin melihat lebih detail, lihat Control.Monad.Statedari mtl, dan pada ST sdan IO(alias ST RealWorld) monad.)

Anda mungkin bertanya-tanya mengapa melakukannya dengan cara bundaran (bukan hanya memiliki keadaan yang bisa berubah dalam bahasa). Keuntungan sebenarnya adalah Anda telah memverifikasi status program Anda. Apa yang sebelumnya adalah implisit (keadaan program Anda bersifat global, memungkinkan untuk hal-hal seperti tindakan di kejauhan ) sekarang eksplisit. Fungsi yang tidak menerima dan mengembalikan negara tidak dapat memodifikasinya atau dipengaruhi olehnya; mereka "murni". Bahkan lebih baik, Anda dapat memiliki utas negara terpisah, dan dengan sedikit jenis sihir, mereka dapat digunakan untuk menanamkan perhitungan imperatif dalam yang murni, tanpa membuatnya tidak murni ( STmonad di Haskell adalah yang biasanya digunakan untuk trik ini; yang State#saya sebutkan di atas sebenarnya adalah GHC State# s, digunakan oleh penerapannya STdanIO Monad).

CesarB
sumber
7

Pemrograman fungsional menghindari keadaan dan menekankanKegunaan. Tidak pernah ada yang namanya keadaan, meskipun negara itu mungkin benar-benar sesuatu yang abadi atau dimasukkan ke dalam arsitektur dari apa yang Anda kerjakan. Pertimbangkan perbedaan antara server web statis yang hanya memuat file dari sistem file versus program yang mengimplementasikan kubus Rubik. Yang pertama akan diimplementasikan dalam hal fungsi yang dirancang untuk mengubah permintaan menjadi permintaan jalur file menjadi respons dari konten file itu. Hampir tidak ada keadaan yang diperlukan di luar sedikit konfigurasi (sistem file 'negara' benar-benar di luar ruang lingkup program. Program ini bekerja dengan cara yang sama terlepas dari keadaan apa file tersebut berada). Dalam yang terakhir, Anda perlu memodelkan kubus dan implementasi program Anda tentang bagaimana operasi pada kubus itu mengubah keadaannya.

Jherico
sumber
Ketika saya lebih anti-fungsional saya bertanya-tanya bagaimana bisa lebih baik ketika sesuatu seperti hard drive bisa berubah. Kelas-kelas c # saya semuanya bisa berubah-ubah dan secara logis dapat mensimulasikan hard drive atau perangkat lain. Sedangkan dengan fungsional ada ketidakcocokan antara model dan mesin yang sebenarnya mereka pemodelan. Setelah mempelajari lebih jauh tentang fungsional, saya menyadari manfaat yang Anda peroleh bisa melebihi masalah itu. Dan jika secara fisik dimungkinkan untuk menciptakan hard drive yang membuat salinannya sendiri, itu sebenarnya akan berguna (seperti yang sudah dilakukan jurnal).
LegendLength
5

Selain jawaban hebat yang diberikan orang lain, pikirkan tentang kelas-kelas Integerdan Stringdi Jawa. Contoh dari kelas-kelas ini tidak berubah, tetapi itu tidak membuat kelas tidak berguna hanya karena instance mereka tidak dapat diubah. Kekekalan memberi Anda keamanan. Anda tahu jika Anda menggunakan contoh String atau Integer sebagai kunci untuk Map, kunci tidak dapat diubah. Bandingkan ini dengan Datekelas di Jawa:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Anda telah diam-diam mengubah kunci di peta Anda! Bekerja dengan objek yang tidak dapat diubah, seperti dalam Pemrograman Fungsional, jauh lebih bersih. Lebih mudah untuk memikirkan tentang apa efek samping yang terjadi - tidak ada! Ini berarti lebih mudah bagi programmer, dan juga lebih mudah untuk optimizer.

Eddie
sumber
2
Saya mengerti itu, tetapi itu tidak menjawab pertanyaan saya. Ingatlah bahwa program komputer adalah model dari beberapa peristiwa atau proses di dunia nyata, jika Anda tidak dapat mengubah nilai-nilai Anda, lalu bagaimana Anda memodelkan sesuatu yang berubah?
Mason Wheeler
Nah, Anda tentu dapat melakukan hal-hal yang berguna dengan kelas Integer dan String. Ini tidak seperti kekekalannya yang berarti Anda tidak dapat memiliki keadaan yang bisa berubah.
Eddie
@Mason Wheeler - Dengan memahami bahwa sesuatu dan keadaannya adalah dua "hal" yang berbeda. Apa pacman itu tidak berubah dari waktu A ke waktu B. Di mana pacman adalah tidak berubah. Ketika Anda berpindah dari waktu A ke waktu B, Anda mendapatkan kombinasi baru dari negara + pacman ... yang sama dengan negara pacman, berbeda. Status tidak diubah ... keadaan berbeda.
RHSeeger
4

Untuk aplikasi yang sangat interaktif seperti game, Functional Reactive Programming adalah teman Anda: jika Anda dapat merumuskan properti dunia game Anda sebagai nilai yang bervariasi waktu (dan / atau aliran acara), Anda siap! Rumus-rumus ini kadang-kadang bahkan lebih alami dan menunjukkan maksud daripada bermutasi, misalnya untuk bola yang bergerak, Anda dapat langsung menggunakan hukum terkenal x = v * t . Dan yang lebih baik, aturan permainan yang ditulis sedemikian rupa menyusun lebih baik daripada abstraksi berorientasi objek. Sebagai contoh, dalam hal ini, kecepatan bola bisa juga merupakan nilai waktu yang bervariasi, yang tergantung pada aliran peristiwa yang terdiri dari tabrakan bola. Untuk pertimbangan desain yang lebih konkret, lihat Making Games in Elm .

thSoft
sumber
4

Menggunakan beberapa kreativitas dan pencocokan pola, game stateless telah dibuat:

serta demo yang bergulir:

dan visualisasi:

Paul Sweatte
sumber
3

Begitulah cara FORTRAN bekerja tanpa blok UMUM: Anda akan menulis metode yang memiliki nilai yang Anda berikan dan variabel lokal. Itu dia.

Pemrograman berorientasi objek membawa kita keadaan dan perilaku bersama, tapi itu ide baru ketika saya pertama kali menemukannya dari C ++ pada tahun 1994.

Ya ampun, saya adalah seorang programmer fungsional ketika saya adalah seorang insinyur mekanik dan saya tidak mengetahuinya!

Duffymo
sumber
2
Saya tidak setuju bahwa ini adalah sesuatu yang dapat Anda pin pada OO. Bahasa sebelum OO mendorong status penggabungan dan algoritme. OO hanya menyediakan cara yang lebih baik untuk mengelolanya.
Jason Baker
"Didorong" - mungkin. OO menjadikannya bagian eksplisit dari bahasa. Anda dapat melakukan enkapsulasi dan menyembunyikan informasi dalam bahasa C, tetapi saya akan mengatakan bahwa bahasa OO membuatnya jauh lebih mudah.
duffymo
2

Ingatlah: bahasa fungsional sudah lengkap. Oleh karena itu, tugas apa pun yang berguna yang Anda lakukan dalam bahasa imperatif dapat dilakukan dalam bahasa fungsional. Pada akhirnya, saya pikir ada sesuatu yang bisa dikatakan tentang pendekatan hybrid. Bahasa-bahasa seperti F # dan Clojure (dan saya yakin orang lain) mendorong desain tanpa kewarganegaraan, tetapi memungkinkan mutabilitas jika diperlukan.

Jason Baker
sumber
Hanya karena dua bahasa Turing selesai tidak berarti mereka dapat melakukan tugas yang sama. Artinya adalah mereka dapat melakukan perhitungan yang sama. Brainfuck sudah selesai, tapi saya cukup yakin itu tidak bisa berkomunikasi melalui TCP stack
RHSeeger
2
Tentu bisa. Mengingat akses yang sama ke perangkat keras seperti katakanlah C, itu bisa. Itu tidak berarti bahwa itu akan praktis, tetapi kemungkinan ada di sana.
Jason Baker
2

Anda tidak dapat memiliki bahasa fungsional murni yang bermanfaat. Akan selalu ada tingkat ketidakstabilan yang harus Anda hadapi, IO adalah salah satu contohnya.

Pikirkan bahasa fungsional hanya sebagai alat lain yang Anda gunakan. Ini bagus untuk hal-hal tertentu, tetapi tidak untuk yang lain. Contoh gim yang Anda berikan mungkin bukan cara terbaik untuk menggunakan bahasa fungsional, setidaknya layar akan memiliki status bisa berubah yang tidak dapat Anda lakukan dengan FP. Cara Anda memikirkan masalah dan jenis masalah yang Anda selesaikan dengan FP akan berbeda dari yang biasa Anda gunakan dengan pemrograman imperatif.

Naik.
sumber
1

Dengan menggunakan banyak rekursi.

Tic Tac Toe dalam F # (Bahasa fungsional.)

Spencer Ruport
sumber
Tentu saja, rekursi ekor dapat diimplementasikan dengan sangat efisien, karena kompiler dapat mengubahnya menjadi satu lingkaran.
Eddie
-3

Ini sangat sederhana. Anda dapat menggunakan variabel sebanyak yang Anda inginkan dalam pemrograman fungsional ... tetapi hanya jika mereka variabel lokal (berisi fungsi-fungsi di dalam). Jadi cukup bungkus kode Anda dalam fungsi, berikan nilai bolak-balik di antara fungsi-fungsi tersebut (sebagai parameter yang lulus dan nilai yang dikembalikan) ... dan hanya itu yang ada di sana!

Ini sebuah contoh:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);
John Doe
sumber
John, bahasa apa ini?