Bagaimana saya dapat _read_ kode JavaScript fungsional?

9

Saya percaya saya telah mempelajari beberapa / banyak / sebagian besar konsep dasar yang mendasari pemrograman fungsional dalam JavaScript. Namun, saya memiliki masalah khusus membaca kode fungsional, bahkan kode yang saya tulis, dan bertanya-tanya apakah ada yang bisa memberi saya petunjuk, tips, praktik terbaik, terminologi, dll. Yang dapat membantu.

Ambil kode di bawah ini. Saya menulis kode ini. Ini bertujuan untuk menetapkan kesamaan persen antara dua objek, antara katakan {a:1, b:2, c:3, d:3}dan {a:1, b:1, e:2, f:2, g:3, h:5}. Saya menghasilkan kode sebagai jawaban atas pertanyaan ini pada Stack Overflow . Karena saya tidak yakin persis berapa persen kesamaan yang ditanyakan oleh poster itu, saya memberikan empat jenis:

  • persen kunci dalam objek 1 yang dapat ditemukan di 2,
  • persentase nilai dalam objek 1 yang dapat ditemukan di ke-2, termasuk duplikat,
  • persentase nilai dalam objek 1 yang dapat ditemukan di ke-2, tanpa duplikat diizinkan, dan
  • persentase {key: value} berpasangan di objek 1 yang dapat ditemukan di objek ke-2.

Saya mulai dengan kode yang cukup penting, tetapi segera menyadari bahwa ini adalah masalah yang cocok untuk pemrograman fungsional. Secara khusus, saya menyadari bahwa jika saya dapat mengekstrak satu atau tiga fungsi untuk masing-masing dari empat strategi di atas yang menentukan jenis fitur yang ingin saya bandingkan (misalnya kunci, atau nilai, dll.), Maka saya mungkin menjadi mampu mengurangi (mengampuni permainan kata-kata) sisa kode menjadi unit berulang. Anda tahu, menjaganya tetap KERING. Jadi saya beralih ke pemrograman fungsional. Saya cukup bangga dengan hasilnya, saya pikir itu cukup elegan, dan saya pikir saya mengerti apa yang saya lakukan dengan cukup baik.

Namun, bahkan setelah menulis kode sendiri dan memahami setiap bagian dari itu selama konstruksi, ketika saya sekarang melihat kembali, saya terus menjadi lebih bingung tentang cara membaca setengah garis tertentu, serta cara "grok" apa yang dilakukan oleh setengah baris kode tertentu. Saya menemukan diri saya membuat panah mental untuk menghubungkan bagian-bagian yang berbeda yang dengan cepat terdegradasi menjadi berantakan spageti.

Jadi, adakah yang bisa memberi tahu saya cara "membaca" beberapa bit kode yang lebih berbelit-belit dengan cara yang ringkas dan berkontribusi pada pemahaman saya tentang apa yang saya baca? Saya kira bagian yang paling membuat saya adalah mereka yang memiliki beberapa panah gemuk berturut-turut dan / atau bagian yang memiliki beberapa tanda kurung berturut-turut. Sekali lagi, pada intinya, pada akhirnya saya bisa mengetahui logikanya, tetapi (saya harap) ada cara yang lebih baik untuk dilakukan dengan cepat dan jelas dan langsung "mengambil" serangkaian pemrograman JavaScript fungsional.

Jangan ragu menggunakan baris kode apa pun dari bawah, atau bahkan contoh lainnya. Namun, jika Anda ingin beberapa saran awal dari saya, berikut adalah beberapa. Mulailah dengan yang cukup sederhana. Dari dekat akhir kode, ada ini yang dilewatkan sebagai parameter ke fungsi: obj => key => obj[key]. Bagaimana seseorang membaca dan memahaminya? Sebuah contoh lagi adalah salah satu fungsi penuh dari dekat awal: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. Bagian terakhir mapmembuat saya khususnya.

Harap dicatat, pada saat ini dalam waktu saya tidak mencari referensi untuk Haskell atau notasi abstrak simbolis atau dasar-dasar currying, dll Apa yang saya sedang cari adalah kalimat bahasa Inggris yang saya diam-diam bisa mulut sambil melihat baris kode. Jika Anda memiliki referensi yang secara khusus membahas hal itu, bagus, tetapi saya juga tidak mencari jawaban yang mengatakan saya harus membaca beberapa buku teks dasar. Saya telah melakukan itu dan saya mendapatkan (setidaknya sejumlah besar) logika. Perhatikan juga, saya tidak perlu jawaban lengkap (meskipun upaya seperti itu akan diterima): Bahkan jawaban singkat yang memberikan cara yang elegan untuk membaca satu baris tertentu dari kode yang merepotkan akan dihargai.

Saya kira bagian dari pertanyaan ini adalah: Dapatkah saya membaca kode fungsional secara linear, Anda tahu, dari kiri ke kanan dan dari atas ke bawah? Atau apakah seseorang cukup dipaksa untuk membuat gambaran mental kabel spaghetti seperti pada halaman kode yang jelas tidak linier? Dan jika seseorang harus melakukan itu, kita masih harus membaca kodenya, jadi bagaimana kita mengambil teks linear dan memasang spageti?

Setiap tips akan sangat dihargai.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25
Andrew Willems
sumber

Jawaban:

18

Anda sebagian besar mengalami kesulitan membacanya karena contoh khusus ini tidak terlalu mudah dibaca. Tidak ada pelanggaran yang dimaksudkan, sebagian besar sampel yang Anda temukan di Internet sangat mengecewakan. Banyak orang hanya bermain-main dengan pemrograman fungsional pada akhir pekan dan tidak pernah benar-benar harus berurusan dengan mempertahankan kode fungsional produksi jangka panjang. Saya akan menulis lebih seperti ini:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Untuk beberapa alasan banyak orang memiliki ide ini di kepala mereka bahwa kode fungsional harus memiliki "tampilan" estetika dari ekspresi bersarang besar. Catatan meskipun versi saya agak menyerupai kode imperatif dengan semua titik koma, semuanya tidak berubah, sehingga Anda bisa mengganti semua variabel dan mendapatkan satu ekspresi besar jika Anda mau. Ini memang sama "fungsional" dengan versi spageti, tetapi dengan lebih mudah dibaca.

Di sini ekspresi dipecah menjadi potongan-potongan yang sangat kecil dan diberi nama yang bermakna bagi domain. Nesting dihindari dengan menarik fungsi umum seperti mapObjke dalam fungsi bernama. Lambdas dicadangkan untuk fungsi yang sangat singkat dengan tujuan konteks yang jelas.

Jika Anda menemukan kode yang sulit dibaca, perbaiki ulang kode itu hingga lebih mudah. Butuh beberapa latihan, tetapi bermanfaat. Kode fungsional dapat dibaca sebagai keharusan. Bahkan, lebih sering, karena biasanya lebih ringkas.

Karl Bielefeldt
sumber
Jelas tidak ada pelanggaran yang dilakukan! Sementara saya masih akan mempertahankan bahwa saya tahu beberapa hal tentang pemrograman fungsional, mungkin pernyataan saya dalam pertanyaan tentang seberapa banyak yang saya tahu sedikit berlebihan. Saya benar-benar seorang pemula. Jadi melihat bagaimana usaha saya ini dapat ditulis ulang dengan cara yang jelas tapi tetap fungsional seperti emas ... terima kasih. Saya akan mempelajari kembali penulisan Anda dengan cermat.
Andrew Willems
1
Saya pernah mendengarnya mengatakan bahwa memiliki rantai panjang dan / atau metode bersarang menghilangkan variabel perantara yang tidak perlu. Sebaliknya, jawaban Anda memecah rantai saya / bersarang menjadi pernyataan yang berdiri sendiri antara menggunakan variabel perantara yang dinamai dengan baik. Saya menemukan kode Anda lebih mudah dibaca dalam kasus ini, tapi saya ingin tahu seberapa umum Anda mencoba menjadi. Apakah Anda mengatakan bahwa rantai metode yang panjang dan / atau bersarang yang dalam sering atau bahkan selalu merupakan anti-pola yang harus dihindari, atau adakah saat ketika mereka membawa manfaat yang signifikan? Dan apakah jawaban untuk pertanyaan itu berbeda untuk pengkodean fungsional versus imperatif?
Andrew Willems
3
Ada situasi tertentu di mana menghilangkan variabel perantara dapat menambah kejelasan. Misalnya, dalam FP Anda hampir tidak pernah menginginkan indeks menjadi array. Terkadang juga tidak ada nama yang bagus untuk hasil antara. Namun, dalam pengalaman saya, kebanyakan orang cenderung melakukan kesalahan terlalu jauh ke arah lain.
Karl Bielefeldt
6

Saya belum melakukan banyak pekerjaan yang sangat fungsional dalam Javascript (yang akan saya katakan ini - kebanyakan orang berbicara tentang Javascript fungsional mungkin menggunakan peta, filter dan pengurangan, tetapi kode Anda mendefinisikan sendiri fungsi tingkat yang lebih tinggi , yang merupakan agak lebih maju dari itu), tetapi saya telah melakukannya di Haskell, dan saya pikir setidaknya beberapa pengalaman diterjemahkan. Saya akan memberi Anda beberapa petunjuk untuk hal-hal yang telah saya pelajari:

Menentukan jenis fungsi sangat penting. Haskell tidak mengharuskan Anda untuk menentukan jenis fungsi, tetapi memasukkan jenis dalam definisi membuatnya lebih mudah dibaca. Meskipun Javascript tidak mendukung pengetikan eksplisit dengan cara yang sama, tidak ada alasan untuk tidak memasukkan definisi tipe dalam komentar, misalnya:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Dengan sedikit latihan dalam bekerja dengan definisi tipe seperti ini, mereka membuat makna dari suatu fungsi menjadi lebih jelas.

Penamaan itu penting, bahkan mungkin lebih daripada dalam pemrograman prosedural. Banyak program fungsional ditulis dalam gaya yang sangat singkat yang berat pada konvensi (misalnya konvensi bahwa 'xs' adalah daftar / larik dan 'x' adalah item di dalamnya sangat meresap), tetapi kecuali Anda mengerti gaya itu mudah saya sarankan lebih banyak penamaan verbose. Melihat nama-nama spesifik yang Anda gunakan, "getX" agak tidak jelas, dan karenanya "getXs" tidak terlalu banyak membantu. Saya akan memanggil "getXs" sesuatu seperti "applyToProperties", dan "getX" mungkin akan menjadi "propertyMapper". "getPctSameXs" kemudian akan menjadi "persenPropertiesSameWith" ("dengan").

Hal penting lainnya adalah menulis kode idiomatik . Saya perhatikan bahwa Anda menggunakan sintaks a => b => some-expression-involving-a-and-buntuk menghasilkan fungsi kari. Ini menarik, dan bisa berguna dalam beberapa situasi, tetapi Anda tidak melakukan apa pun di sini yang mendapat manfaat dari fungsi kari dan itu akan menjadi Javascript yang lebih idiomatis untuk menggunakan fungsi multi-argumen tradisional sebagai gantinya. Melakukan hal itu dapat membuatnya lebih mudah untuk melihat apa yang sedang terjadi dalam sekejap. Anda juga menggunakan const name = lambda-expressionuntuk mendefinisikan fungsi, di mana akan lebih idiomatis untuk digunakan function name (args) { ... }. Saya tahu mereka sedikit berbeda secara semantik, tetapi kecuali jika Anda mengandalkan perbedaan itu, saya sarankan menggunakan varian yang lebih umum bila memungkinkan.

Jules
sumber
5
+1 untuk tipe! Hanya karena bahasanya tidak memilikinya, tidak berarti Anda tidak perlu memikirkannya . Beberapa sistem dokumentasi untuk ECMAScript memiliki jenis bahasa untuk merekam jenis fungsi. Beberapa IDE ECMAScript juga memiliki jenis bahasa (dan biasanya, mereka juga memahami bahasa jenis untuk sistem dokumentasi utama), dan mereka bahkan dapat melakukan pemeriksaan tipe yang belum sempurna dan petunjuk heuristik menggunakan anotasi jenis tersebut .
Jörg W Mittag
Anda telah memberi saya banyak hal untuk dikunyah: ketik definisi, nama yang bermakna, menggunakan idiom ... terima kasih! Hanya beberapa dari banyak komentar yang mungkin: Saya tidak bermaksud menulis bagian-bagian tertentu sebagai fungsi kari; mereka baru saja berevolusi seperti itu ketika saya refactored kode saya selama menulis. Saya dapat melihat sekarang bagaimana itu tidak diperlukan, dan bahkan hanya menggabungkan parameter dari dua fungsi menjadi dua parameter untuk fungsi tunggal tidak hanya lebih masuk akal tetapi langsung membuat bit pendek setidaknya lebih mudah dibaca.
Andrew Willems
@ JörgWMittag, terima kasih atas komentar Anda tentang pentingnya jenis dan untuk tautan ke jawaban lain yang Anda tulis. Saya menggunakan WebStorm dan tidak menyadari bahwa, menurut cara saya membaca jawaban Anda yang lain, WebStorm tahu bagaimana menafsirkan anotasi seperti-jsdoc. Saya berasumsi dari komentar Anda bahwa jsdoc dan WebStorm dapat digunakan bersama untuk menjelaskan kode fungsional, bukan hanya keharusan, tetapi saya harus mempelajari lebih jauh untuk benar-benar mengetahuinya. Saya telah bermain dengan jsdoc sebelum dan sekarang saya tahu bahwa WebStorm dan saya dapat bekerja sama di sana, saya berharap saya akan menggunakan fitur / pendekatan itu lebih banyak.
Andrew Willems
@ Jules, hanya untuk memperjelas fungsi kari yang saya maksudkan dalam komentar saya di atas: Seperti yang Anda maksudkan, setiap instance obj => key => ...dapat disederhanakan (obj, key) => ...karena nantinya getX(obj)(key)juga dapat disederhanakan get(obj, key). Sebaliknya, fungsi kari lain (getX, filter = vals => vals) => (objA, objB) => ...,, tidak dapat dengan mudah disederhanakan, setidaknya dalam konteks kode lainnya seperti yang ditulis.
Andrew Willems