Kepintaran Bias adalah Memegang Anda Kembali: Sudah Masa untuk Merangkul Fungsi Panah

Saya mengajar JavaScript untuk hidup. Baru-baru ini saya telah berjalan-jalan di sekitar kurikulum saya untuk mengajar fungsi panah yang lebih cepat - dalam beberapa pelajaran pertama. Saya memajangnya lebih awal dalam kurikulum kerana ia adalah kemahiran yang sangat berharga, dan pelajar mengambil kari dengan anak panah jauh lebih cepat daripada yang saya fikir mereka akan.

Jika mereka dapat memahaminya dan memanfaatkannya lebih awal, mengapa tidak mengajarnya lebih awal?

Nota: Kursus saya tidak direka untuk orang yang tidak pernah menyentuh garis kod sebelum ini. Kebanyakan pelajar menyertai selepas menghabiskan sekurang-kurangnya beberapa bulan pengkodan - sendiri, dalam bootcamp, atau profesional. Walau bagaimanapun, saya telah melihat banyak pemaju muda dengan pengalaman sedikit atau tidak memilih topik ini dengan cepat.

Saya telah melihat sekumpulan pelajar mendapat kebiasaan bekerja dengan fungsi panah curried dalam rentang satu pelajaran selama 1 jam. (Sekiranya anda seorang ahli "Belajar JavaScript dengan Eric Elliott", anda boleh menonton pelajaran 55-minit ES6 Curry & Komposisi sekarang).

Melihat betapa cepatnya pelajar mengambilnya dan mula memegang kuasa kari mereka yang baru ditemui, saya selalu terkejut apabila saya menyiarkan fungsi anak panah curried di Twitter, dan Twitterverse bertindak balas dengan kemarahan kerana memikirkan mengenakan kod "tidak dapat dibaca" pada orang yang perlu mengekalkannya.

Mula-mula, beri saya memberi anda contoh apa yang kita bicarakan. Kali pertama saya melihat tindak balas tersebut adalah respons Twitter kepada fungsi ini:

rahsia rah = msg => () => msg;

Saya terkejut apabila orang di Twitter menuduh saya cuba mengelirukan orang. Saya menulis fungsi itu untuk menunjukkan betapa mudahnya untuk menyatakan fungsi yang curried dalam ES6. Ini adalah aplikasi praktikal yang paling mudah dan ungkapan penutupan yang boleh saya fikirkan dalam JavaScript. (Berkaitan: "Apakah Penutupan?").

Ia bersamaan dengan ungkapan fungsi berikut:

rahsia rahsia = fungsi (msg) {
  fungsi kembali () {
    kembali msg;
  };
};

rahsia () adalah fungsi yang mengambil mesej dan mengembalikan fungsi baru yang mengembalikan mesej. Ia mengambil kesempatan daripada penutupan untuk menetapkan nilai msg kepada apa jua nilai yang anda rahasiakan ().

Berikut adalah cara anda menggunakannya:

const mySecret = rahsia ('hi');
rahsia saya(); // 'hi'

Ternyata, "anak panah berganda" adalah orang yang keliru. Saya yakin bahawa ini adalah fakta:

Dengan kebiasaan, fungsi anak panah dalam talian adalah cara yang paling mudah dibaca untuk menyatakan fungsi yang dipuji dalam JavaScript.

Ramai orang telah memperdebatkan saya bahawa bentuk yang lebih panjang lebih mudah dibaca daripada bentuk yang lebih pendek. Mereka sebahagiannya betul, tetapi kebanyakannya salah. Ia lebih tegas, dan lebih jelas, tetapi tidak mudah dibaca - sekurang-kurangnya, tidak kepada orang yang biasa dengan fungsi anak panah.

Keberatan yang saya lihat di Twitter hanya tidak berlegar dengan pengalaman pembelajaran yang lancar yang saya nikmati. Dalam pengalaman saya, pelajar mengambil fungsi anak panah curried seperti ikan mengambil ke air. Dalam masa beberapa hari belajar, mereka adalah satu dengan anak panah. Mereka mengikat mereka dengan mudah untuk menangani pelbagai cabaran pengkodan.

Saya tidak nampak apa-apa tanda bahawa fungsi anak panah adalah "keras" untuk mereka belajar, membaca, atau memahami - apabila mereka telah membuat pelaburan awal untuk belajar mereka selama beberapa pelajaran selama 1 jam dan sesi belajar.

Mereka mudah membaca fungsi panah curried yang belum pernah mereka lihat sebelumnya dan terangkan kepada saya apa yang sedang berlaku. Mereka secara semula jadi menulis sendiri apabila saya memberikan cabaran kepada mereka.

Dalam erti kata lain, sebaik sahaja mereka menjadi akrab dengan melihat fungsi panah curried, mereka tidak mempunyai masalah dengan mereka. Mereka membaca mereka semudah anda membaca ayat ini - dan pemahaman mereka tercermin dalam kod yang lebih mudah dengan kurang bug.

Mengapa Beberapa Orang Berfikir Ungkapan Fungsi Legacy Lihat "Mudah" Baca

Kecenderungan pengertian adalah kecenderungan kognitif manusia yang dapat diukur yang membawa kita untuk membuat keputusan sendiri merosakkan walaupun menyedari pilihan yang lebih baik. Kami terus menggunakan corak lama yang sama walaupun mengetahui corak yang lebih baik daripada keselesaan dan kebiasaan.

Anda boleh belajar lebih banyak tentang kecenderungan kebiasaan (dan banyak cara lain yang menipu diri kita sendiri) dari buku yang sangat baik, "Projek yang Mencegah: Persahabatan yang Menukar Pikiran Kita". Buku ini perlu dibaca untuk setiap pemaju perisian, kerana ia mendorong anda untuk berfikir secara lebih kritis dan menguji andaian untuk mengelakkan terjadinya perangkap kognitif - dan kisah bagaimana perangkap kognitif yang ditemui adalah benar-benar baik juga .

Ekspresi Fungsi Legacy Mungkin Menyebabkan Bugs dalam Kod Anda

Hari ini saya menulis semula fungsi anak panah curian dari ES6 ke ES5 supaya saya dapat menerbitkannya sebagai modul sumber terbuka yang boleh digunakan oleh orang dalam pelayar lama tanpa penukaran. Versi ES5 mengejutkan saya.

Versi ES6 adalah hanya 4 baris mudah, ringkas, dan elegan.

Saya fikir, ini adalah fungsi yang akan membuktikan kepada Twitter bahawa fungsi anak panah lebih unggul, dan orang harus meninggalkan fungsi warisan mereka seperti kebiasaan buruk mereka.

Jadi saya tweet:

Berikut adalah teks fungsi, sekiranya imej tidak berfungsi untuk anda:

/ // curried with arrows
const composeMixins = (... mixins) => (
  contoh = {},
  campuran = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => campuran (... mixins) (misalnya);
/ vs gaya ES5
var composeMixins = function () {
  var mixins = [] .slice.call (hujah);
  fungsi kembali (contoh, campuran) {
    jika (instance) instance = {};
    jika (! campuran) {
      campuran = fungsi () {
        var fns = [] .slice.call (hujah);
        fungsi kembali (x) {
          kembali fns.reduce (fungsi (acc, fn) {
            kembali fn (acc);
          }, x);
        };
      };
    }
    kembali mix.apply (null, mixins) (instance);
  };
};

Fungsi yang dimaksudkan ialah pembungkus mudah di sekitar paip (), utiliti pengaturcaraan berfungsi standard yang biasa digunakan untuk mengarang fungsi. Fungsi paip () ada di lodash sebagai aliran / aliran, di Ramda sebagai R.pipe (), dan juga mempunyai pengendali sendiri dalam beberapa bahasa pengaturcaraan fungsional.

Ia harus biasa kepada semua orang yang biasa dengan pengaturcaraan fungsional. Begitu juga kebergantungan utamanya: Kurangkan.

Dalam kes ini, ia digunakan untuk mengarang campuran berfungsi, tetapi itu terperinci yang tidak relevan (dan keseluruhan catatan blog lain). Inilah butir-butir penting:

Fungsi ini mengambil sejumlah campuran berfungsi dan mengembalikan fungsi yang menerapkannya satu demi satu dalam saluran paip - seperti garis perakitan. Setiap campuran berfungsi mengambil contoh sebagai input, dan mengambil beberapa barang ke atasnya sebelum menyampaikannya ke fungsi seterusnya dalam perancangan.

Jika anda meninggalkan contoh, objek baru akan dicipta untuk anda.

Kadang-kadang kita mungkin mahu menyusun campuran secara berbeza. Sebagai contoh, anda mungkin mahu mengepos () bukannya paip () untuk membalikkan urutan keutamaan.

Sekiranya anda tidak perlu menyesuaikan tingkah laku, anda hanya akan meninggalkan lalai sahaja, dan mendapatkan kelakuan paip standard ().

Hanya Fakta

Pendapat mengenai kebolehbacaan selainnya, inilah fakta objektif yang berkaitan dengan contoh ini:

  • Saya mempunyai pengalaman bertahun-tahun dengan ekspresi fungsi ES5 dan ES6, anak panah atau sebaliknya. Kecenderungan pengertian bukanlah pemboleh ubah dalam data ini.
  • Saya menulis versi ES6 dalam beberapa saat. Ia mengandungi bug sifar (yang saya tahu - ia melepasi semua ujian unit).
  • Saya mengambil masa beberapa minit untuk menulis versi ES5. Sekurang-kurangnya satu pesanan magnitud lebih banyak masa. Minit vs detik. Saya kehilangan kedudukan saya dalam lekapan fungsi dua kali. Saya menulis 3 pepijat, semua yang saya perlu debug dan menetapkan. Dua daripadanya terpaksa menggunakan console.log () untuk mencari tahu apa yang berlaku.
  • Versi ES6 adalah 4 baris kod.
  • Versi ES5 adalah 21 baris panjang (17 sebenarnya mengandungi kod).
  • Walaupun kelamburannya yang membosankan, versi ES5 sebenarnya kehilangan sedikit kesetiaan maklumat yang tersedia dalam versi ES6. Ia lebih lama, tetapi kurang berkomunikasi, baca untuk butirannya.
  • Versi ES6 mengandungi 2 spread untuk parameter fungsi. Versi ES5 menghilangkan spread, dan sebaliknya menggunakan objek hujah tersirat, yang menyakitkan kebolehbacaan tandatangan fungsi (penurunan kesetiaan 1).
  • Versi ES6 mentakrifkan lalai untuk campuran dalam tandatangan fungsi supaya anda dapat melihat dengan jelas bahawa ia adalah nilai untuk parameter. Versi ES5 mengaburkan butiran itu dan sebaliknya menyembunyikannya di dalam badan fungsi. (downgrade kesetiaan 2).
  • Versi ES6 hanya mempunyai 2 peringkat lekukan, yang membantu menjelaskan struktur bagaimana ia dibaca. Versi ES5 mempunyai 6, dan tahap bersarang tidak jelas dan bukannya membantu pembacaan struktur fungsi (penurunan kesetiaan 3).

Dalam versi ES5, paip () menduduki sebahagian besar badan fungsi - jadi banyak yang sedikit gila untuk menentukan ia sebaris. Ia benar-benar perlu dipecah menjadi fungsi berasingan untuk menjadikan versi ES5 boleh dibaca:

var pipe = function () {
  var fns = [] .slice.call (hujah);
  fungsi kembali (x) {
    kembali fns.reduce (fungsi (acc, fn) {
      kembali fn (acc);
    }, x);
  };
};
var composeMixins = function () {
  var mixins = [] .slice.call (hujah);
  fungsi kembali (contoh, campuran) {
    jika (instance) instance = {};
    jika campuran (! campuran) = paip;
    kembali mix.apply (null, mixins) (instance);
  };
};

Ini seolah-olah lebih mudah dibaca dan difahami oleh saya.

Mari lihat apa yang berlaku apabila kita menggunakan "pengoptimuman" pembacaan yang sama dengan versi ES6:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);
const composeMixins = (... mixins) => (
  contoh = {},
  campurkan = paip
) => campuran (... mixins) (misalnya);

Seperti pengoptimuman ES5, versi ini lebih terperinci (ia menambah pemboleh ubah baru yang tidak ada sebelum ini). Tidak seperti versi ES5, versi ini tidak dapat dibaca dengan lebih ketara selepas menggubal definisi paip. Lagipun, ia sudah mempunyai nama berubah yang jelas diberikan kepadanya dalam tandatangan fungsi: campuran.

Takrif campuran telah terkandung di dalam barisannya sendiri, yang menjadikannya tidak mungkin bagi para pembaca untuk keliru tentang di mana ia berakhir dan seluruh fungsi tersebut terus berlanjut.

Sekarang kita mempunyai 2 pembolehubah yang mewakili perkara yang sama bukannya 1. Adakah kita mendapat banyak? Tidak jelas, tidak.

Jadi mengapa versi ES5 jelas lebih baik dengan fungsi yang sama digerakkan?

Kerana versi ES5 jelas lebih rumit. Sumber kerumitan itu adalah pokok perkara ini. Saya menegaskan bahawa sumber kerumitan itu berakar ke bunyi sintaks, dan bunyi sintaks itu mengaburkan maksud fungsi itu, tidak membantu.

Mari beralih gear dan elakkan beberapa pemboleh ubah. Mari gunakan ES6 untuk kedua-dua contoh, dan hanya bandingkan fungsi arrow vs ekspresi fungsi warisan:

var composeMixins = function (... mixins) {
  fungsi kembali (
    contoh = {},
    campuran = fungsi (... fns) {
      fungsi kembali (x) {
        kembali fns.reduce (fungsi (acc, fn) {
          kembali fn (acc);
        }, x);
      };
    }
  ) {
    campuran kembali (... mixins) (misalnya);
  };
};

Ini kelihatan lebih mudah dibaca kepada saya. Apa yang kita ubah ialah kita mengambil kesempatan daripada sintaks parameter dan bakinya. Sudah tentu, anda perlu terbiasa dengan sintaks dan rehat lalai agar versi ini lebih mudah dibaca, tetapi walaupun anda tidak, saya fikir ia jelas bahawa versi ini masih kurang berantakan.

Ini membantu banyak, tetapi masih jelas kepada saya bahawa versi ini masih cukup berantakan bahawa paip abstrak () ke dalam fungsinya sendiri tentu akan membantu:

const pipe = function (... fns) {
  fungsi kembali (x) {
    kembali fns.reduce (fungsi (acc, fn) {
      kembali fn (acc);
    }, x);
  };
};
/ // Ungkapan fungsi legacy
const composeMixins = function (... mixins) {
  fungsi kembali (
    contoh = {},
    campurkan = paip
  ) {
    campuran kembali (... mixins) (misalnya);
  };
};

Itu lebih baik, bukan? Sekarang bahawa tugasan campuran hanya menduduki satu baris, struktur fungsi lebih jelas - tetapi masih terdapat banyak sintaks bunyi untuk rasa saya. Dalam composeMixins (), ia tidak jelas kepada saya sekilas di mana satu fungsi berakhir dan yang lain bermula.

Daripada memanggil badan fungsi, kata kunci berfungsi seolah-olah secara visual menggabungkan dengan pengenal di sekitarnya. Terdapat fungsi bersembunyi dalam fungsi saya! Di manakah tanda tandatangan akhir dan badan berfungsi bermula? Saya dapat memikirkannya jika saya melihat dengan teliti, tetapi ia tidak jelas kepada saya.

Bagaimana jika kita boleh menyingkirkan kata kunci fungsi, dan memanggil nilai kembali dengan menunjuk secara visual kepada mereka dengan anak panah besar lemak => dan bukannya menulis kata kunci kembali yang menggabungkan dengan pengecam sekitarnya?

Ternyata, kita boleh, dan inilah yang kelihatan seperti:

const composeMixins = (... mixins) => (
  contoh = {},
  campurkan = paip
) => campuran (... mixins) (misalnya);

Kini perlu jelas apa yang berlaku. composeMixins () adalah fungsi yang mengambil sejumlah campuran dan mengembalikan fungsi yang mengambil dua parameter pilihan, misalnya, dan campuran. Ia mengembalikan hasil contoh pipa melalui campuran campuran.

Satu lagi perkara ... jika kita menggunakan pengoptimuman yang sama untuk paip (), ia secara ajaib berubah menjadi satu-liner:

const pipe = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x);

Dengan definisi itu dalam satu baris, kelebihan mengalihkannya ke fungsinya sendiri tidak jelas. Ingat, fungsi ini wujud sebagai utiliti di Lodash, Ramda, dan sekumpulan perpustakaan lain, tetapi adakah ia benar-benar bernilai overhead mengimport perpustakaan lain?

Adakah ia patut menariknya ke garisannya sendiri? Mungkin. Mereka sebenarnya mempunyai dua fungsi yang berbeza, dan memisahkan mereka menjadikannya lebih jelas.

Sebaliknya, dengan itu dalam talian menjelaskan jenis dan jangkaan penggunaan apabila anda melihat tandatangan parameter. Inilah yang berlaku apabila kita menciptanya secara dalam talian:

const composeMixins = (... mixins) => (
  contoh = {},
  campuran = (... fns) => x => fns.reduce ((acc, fn) => fn (acc), x)
) => campuran (... mixins) (misalnya);

Sekarang kita kembali kepada fungsi asal. Sepanjang perjalanan, kami tidak membuang apa-apa makna. Malah, dengan mengisytiharkan parameter dan nilai lalai secara inline, kami menambah maklumat tentang bagaimana fungsi digunakan, dan apakah nilai parameter yang mungkin kelihatan seperti itu.

Semua kod tambahan dalam versi ES5 hanyalah bunyi bising. Bunyi sintaks. Ia tidak memberikan apa-apa tujuan berguna kecuali untuk menyesuaikan diri dengan orang yang tidak dikenali dengan fungsi panah curried.

Sebaik sahaja anda mendapat kebolehsampan yang mencukupi dengan fungsi panah curried, ia harus jelas bahawa versi asal lebih mudah dibaca kerana terdapat sintaks yang kurang banyak untuk tersesat.

Ia juga kurang kerap kesilapan, kerana terdapat banyak kawasan permukaan yang kurang untuk menyembunyikannya.

Saya mengesyaki terdapat banyak bug yang bersembunyi dalam fungsi warisan yang akan ditemui dan dihapuskan jika anda menaik taraf kepada fungsi anak panah.

Saya juga berpendapat bahawa pasukan anda akan menjadi lebih produktif jika anda belajar untuk memeluk dan memihak lebih banyak sintaks ringkas yang terdapat dalam ES6.

Walaupun benar bahawa kadang-kadang perkara lebih mudah difahami jika ia dibuat jelas, ia juga benar bahawa sebagai peraturan umum, kod kurang lebih baik.

Jika kod kurang dapat mencapai perkara yang sama dan berkomunikasi lebih banyak, tanpa mengorbankan apa-apa makna, itu secara objektifnya lebih baik.

Kunci untuk mengetahui perbezaannya adalah makna. Sekiranya lebih banyak kod gagal untuk menambah makna, kod tersebut tidak boleh wujud. Konsep itu sangat asas, ia adalah garis panduan gaya yang terkenal untuk bahasa semulajadi.

Garis panduan gaya yang sama berlaku untuk kod sumber. Rayakannya, dan kod anda akan menjadi lebih baik.

Pada penghujung hari, cahaya dalam kegelapan. Sebagai tindak balas kepada lagi tweet lain yang mengatakan versi ES6 kurang dibaca:

Masa untuk mengenali ES6, kari, dan komposisi fungsi.

Langkah seterusnya

"Belajar JavaScript dengan Eric Elliott" ahli boleh menonton pelajaran ES6 Curry & Komposisi selama 55 minit sekarang.

Jika anda bukan ahli, anda hilang!

Eric Elliott adalah pengarang "Aplikasi JavaScript Pengaturcaraan" (O'Reilly), dan "Pelajari JavaScript dengan Eric Elliott". Beliau telah menyumbang kepada pengalaman perisian untuk Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, dan artis rakaman teratas termasuk Usher, Frank Ocean, Metallica, dan banyak lagi.

Dia menghabiskan sebahagian besar waktunya di San Francisco Bay Area dengan wanita paling cantik di dunia.