なじみのバイアスがあなたを阻んでいます:今度は矢印の機能を採用する時です

「アンカー」— Actor212 —(CC BY-NC-ND 2.0)

私はJavaScriptを生かして教えています。最近、私はカリキュラムをシャッフルして、カレー矢印の機能をより早く、最初の数レッスンで教えました。非常に貴重なスキルであるため、カリキュラムの早い段階で変更しました。生徒は、思ったよりもずっと速く矢印を使ったカレーを習得しました。

彼らがそれを理解し、より早くそれを利用できるならば、なぜそれをより早く教えませんか?

注:私のコースは、これまでコード行に触れたことのない人向けではありません。ほとんどの学生は、少なくとも数か月のコーディングを経て参加します。自分で、ブートキャンプで、または専門的に。しかし、経験がほとんどない、またはまったくない多くのジュニア開発者が、これらのトピックをすばやく取り上げています。

1時間のレッスンで、多くの学生がカレー矢印の機能に慣れ親しんでいるのを見てきました。 (「Eric ElliottでJavaScriptを学ぶ」のメンバーなら、今すぐ55分間のES6カレーと作曲のレッスンを見ることができます)。

生徒がすぐにそれを拾い上げて新しいカレーの力を使い始めるのを見て、カリー化された矢印関数をTwitterに投稿すると、いつも驚かされます。Twitterverseは、その「判読不能な」コードをそれを維持する必要がある人々。

まず、私たちが話していることの例を挙げましょう。バックラッシュに最初に気付いたのは、この関数に対するTwitterの応答でした。

const secret = msg =>()=> msg;

Twitterの人々が人々を混乱させようとしていると非難したとき、私はショックを受けました。この関数は、カリー化された関数をES6で簡単に表現できることを示すために作成しました。 JavaScriptで考えることができる最も単純で実用的なクロージャーのアプリケーションと表現です。 (関連:「クロージャとは?」)。

次の関数式と同等です。

const secret = function(msg){
  return関数(){
    return msg;
  };
};

secret()は、msgを取り、msgを返す新しい関数を返す関数です。クロージャーを利用して、msgの値をsecret()に渡す値に固定します。

使用方法は次のとおりです。

const mySecret = secret( 'hi');
私の秘密(); // 'こんにちは'

結局のところ、「二重矢印」は人々を混乱させたものです。これは事実だと確信しています。

精通しているので、インライン矢印関数は、カリー化された関数をJavaScriptで表現する最も読みやすい方法です。

多くの人は、長い形式のほうが短い形式より読みやすいと私に主張しています。彼らは部分的には正しいが、ほとんどは間違っている。より詳細で、より明確ですが、読みやすくはありません-少なくとも、矢印関数に精通している人には。

Twitterで私が見た異議は、生徒たちが楽しんでいたスムーズな学習体験を考えているだけではありませんでした。私の経験では、生徒は魚が水にかかるようなカレー矢印機能を利用します。それらを学ぶ数日以内に、彼らは矢印の付いたものです。彼らはあらゆる種類のコーディングの課題に取り組むために楽にそれらを投げます。

矢印機能が学習、読み取り、または理解するのに「難しい」という兆候は見られません。1時間の数回のレッスンと学習セッションで学習するための初期投資を行った後です。

彼らは今まで見たことのないカレー矢印関数を簡単に読んで、何が起こっているのか説明してくれます。私が彼らに挑戦するとき、彼らは自然に彼ら自身を書きます。

言い換えれば、カレー矢印の機能に慣れるとすぐに、彼らは何の問題もありません。彼らはあなたがこの文を読んでいるのと同じくらい簡単にそれらを読みます—そして彼らの理解はより少ないバグでより単純なコードに反映されます。

一部の人々がレガシー関数式を読むのが「簡単」に見えると考える理由

なじみのバイアスは、より良いオプションを認識しているにもかかわらず、自己破壊的な決定を下すことにつながる、測定可能な人間の認知バイアスです。快適さと習慣からより良いパターンを知っているにもかかわらず、私たちは同じ古いパターンを使い続けています。

優れた本「The Undoing Project:A Friendsship that Our Minds」から、親しみやすさのバイアス(および私たちが自分をだます他の多くの方法)についてより多くを学ぶことができます。この本は、すべてのソフトウェア開発者が読む必要があります。さまざまな認知トラップに陥ることを避けるために、より批判的に考え、仮定をテストすることをお勧めします。そして、これらの認知トラップがどのように発見されたかの話も本当に良いです。

レガシー関数式はおそらくコードのバグの原因です

今日、カリー化された矢印関数をES6からES5に書き直して、オープンソースモジュールとして公開できるようにしました。これは、人々が古いブラウザーでトランスコンパイルせずに使用できるものです。 ES5バージョンは私に衝撃を与えました。

ES6バージョンは、シンプルで短く、エレガントで、わずか4行でした。

確かに、これは矢印機能が優れていること、そして人々が悪い習慣のようにレガシー機能を放棄すべきであることをTwitterに証明する機能だと思った。

だから私はつぶやいた:

画像が機能しない場合の機能のテキストは次のとおりです。

//矢印でカレー
const composeMixins =(... mixins)=>(
  インスタンス= {}、
  mix =(... fns)=> x => fns.reduce((acc、fn)=> fn(acc)、x)
)=> mix(... mixins)(instance);
// vs ES5スタイル
var composeMixins = function(){
  var mixins = [] .slice.call(arguments);
  リターン関数(インスタンス、ミックス){
    if(!instance)instance = {};
    if(!mix){
      ミックス=関数(){
        var fns = [] .slice.call(arguments);
        リターン関数(x){
          return fns.reduce(function(acc、fn){
            return fn(acc);
          }、 バツ);
        };
      };
    }
    return mix.apply(null、mixins)(instance);
  };
};

問題の関数は、関数を構成するために一般的に使用される標準の関数型プログラミングユーティリティであるpipe()の単純なラッパーです。 pipe()関数はlodashにはlodash / flowとして、RamdaにはR.pipe()として存在し、いくつかの関数型プログラミング言語に独自の演算子さえあります。

関数型プログラミングに精通している人なら誰でも知っているはずです。その主な依存関係がそうであるように:Reduce。

この場合、機能的なミックスインを作成するために使用されていますが、それは無関係な詳細(および他のブログ記事全体)です。重要な詳細は次のとおりです。

この関数は、任意の数の機能ミックスインを取得し、それらをパイプラインで次々に適用する関数を返します(アセンブリーラインなど)。各機能ミックスインは、インスタンスを入力として受け取り、パイプライン内の次の関数に渡す前に、いくつかのものをタックします。

インスタンスを省略すると、新しいオブジェクトが作成されます。

ミックスインの構成を変えたい場合があります。たとえば、優先順位を逆にするためにpipe()の代わりにcompose()を渡すことができます。

動作をカスタマイズする必要がない場合は、デフォルトのままにして、標準のpipe()動作を取得します。

事実だけ

読みやすさについてのご意見はともかく、この例に関する客観的な事実は次のとおりです。

  • ES5とES6の両方の関数式、矢印などで複数年の経験があります。このデータでは、親密度バイアスは変数ではありません。
  • ES6バージョンは数秒で書きました。バグはありませんでした(私は知っていますが、すべてのユニットテストに合格しています)。
  • ES5バージョンを書くのに数分かかりました。少なくとも1桁以上の時間。分対秒。関数のインデントの場所を2回失った。 3つのバグを作成しましたが、そのすべてをデバッグして修正する必要がありました。そのうち2つは、何が起こっているのかを把握するためにconsole.log()に頼らなければなりませんでした。
  • ES6バージョンは4行のコードです。
  • ES5バージョンは21行です(17行には実際にコードが含まれています)。
  • 退屈な冗長性にもかかわらず、ES5バージョンは、ES6バージョンで利用可能な情報忠実度の一部を実際に失います。はるかに長くなりますが、通信は少なくなりますので、詳細を読んでください。
  • ES6バージョンには、関数パラメーターの2つのスプレッドが含まれています。 ES5バージョンはスプレッドを省略し、代わりに暗黙的な引数オブジェクトを使用します。これにより、関数シグネチャの可読性が損なわれます(忠実度ダウングレード1)。
  • ES6バージョンでは、関数シグネチャでミックスのデフォルトが定義されているため、パラメーターの値であることが明確にわかります。 ES5バージョンはその詳細を覆い隠し、代わりに関数本体の奥深くに隠します。 (忠実度ダウングレード2)。
  • ES6バージョンには2レベルのインデントしかないため、読み方の構造を明確にするのに役立ちます。 ES5バージョンには6があり、関数の構造を読みやすくするのではなく、ネストレベルがわかりにくくなります(忠実度のダウングレード3)。

ES5バージョンでは、pipe()が関数本体の大部分を占めています。インラインで定義するのは少し気が狂うほどです。 ES5のバージョンを読みやすくするには、実際には別の関数に分割する必要があります。

var pipe = function(){
  var fns = [] .slice.call(arguments);
  リターン関数(x){
    return fns.reduce(function(acc、fn){
      return fn(acc);
    }、 バツ);
  };
};
var composeMixins = function(){
  var mixins = [] .slice.call(arguments);
  リターン関数(インスタンス、ミックス){
    if(!instance)instance = {};
    if(!mix)mix = pipe;
    return mix.apply(null、mixins)(instance);
  };
};

これは明らかに私にとって読みやすく理解しやすいようです。

同じ読みやすさの「最適化」をES6バージョンに適用するとどうなるか見てみましょう。

const pipe =(... fns)=> x => fns.reduce((acc、fn)=> fn(acc)、x);
const composeMixins =(... mixins)=>(
  インスタンス= {}、
  ミックス=パイプ
)=> mix(... mixins)(instance);

ES5の最適化と同様に、このバージョンはより冗長です(以前にはなかった新しい変数が追加されます)。 ES5バージョンとは異なり、このバージョンは、パイプの定義を抽象化した後ではそれほど読みやすくありません。結局のところ、関数シグネチャで変数名が既に明確に割り当てられています:mix。

ミックスの定義は既に独自の行に含まれていたため、読者がどこで終了して残りの機能が続くかについて混乱することはほとんどありません。

これで、1ではなく同じものを表す2つの変数ができました。明らかに、いいえ。

では、なぜ同じ機能を抽象化したES5バージョンが明らかに優れているのでしょうか?

ES5バージョンは明らかにもっと複雑だからです。その複雑さの原因は、この問題の核心です。複雑さの原因はシンタックスノイズに要約され、シンタックスノイズは関数の意味を曖昧にしており、助けにはならないと断言します。

ギアをシフトして、いくつかの変数を削除しましょう。両方の例でES6を使用し、矢印関数と従来の関数式のみを比較します。

var composeMixins = function(... mixins){
  リターン関数(
    インスタンス= {}、
    ミックス=関数(... fns){
      リターン関数(x){
        return fns.reduce(function(acc、fn){
          return fn(acc);
        }、 バツ);
      };
    }
  ){
    return mix(... mixins)(instance);
  };
};

これは私にとって非常に読みやすいようです。変更したのは、休息とデフォルトのパラメーター構文を利用していることだけです。もちろん、このバージョンを読みやすくするためには、残りの構文とデフォルトの構文に精通する必要がありますが、そうでない場合でも、このバージョンがまだ混雑していないことは明らかです。

これは大いに役立ちましたが、このバージョンはまだ十分に散らかっており、独自の関数にpipe()を抽象化すると明らかに役立つことは明らかです:

const pipe = function(... fns){
  リターン関数(x){
    return fns.reduce(function(acc、fn){
      return fn(acc);
    }、 バツ);
  };
};
//レガシー関数式
const composeMixins = function(... mixins){
  リターン関数(
    インスタンス= {}、
    ミックス=パイプ
  ){
    return mix(... mixins)(instance);
  };
};

いいですねミックスの割り当てが1行のみを占めるようになったため、関数の構造ははるかに明確になりましたが、それでも私の好みにはあまりにも多くの構文ノイズがあります。 composeMixins()では、ある機能が終了して別の機能が開始する場所が一目でわかりません。

その関数キーワードは、関数の本体を呼び出すのではなく、周囲の識別子と視覚的に融合しているようです。私の機能に隠れている機能があります!パラメータシグネチャはどこで終了し、関数本体はどこから始まりますか?よく見るとわかりますが、視覚的には明らかではありません。

関数キーワードを取り除いて、周囲の識別子と調和するreturnキーワードを書く代わりに、大きな太い矢印=>で視覚的にポイントすることで戻り値を呼び出すことができたらどうでしょうか?

判明しましたが、次のようになります。

const composeMixins =(... mixins)=>(
  インスタンス= {}、
  ミックス=パイプ
)=> mix(... mixins)(instance);

これで何が起こっているかが明確になりますcomposeMixins()は、任意の数のミックスインを受け取り、2つのオプションのパラメーター、インスタンス、およびミックスを受け取る関数を返す関数です。構成されたミックスインを介してインスタンスをパイプ処理した結果を返します。

もう1つ…同じ最適化をpipe()に適用すると、魔法のように1ライナーに変換されます。

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

その定義を1行にすると、それを独自の機能に抽象化する利点はあまり明確ではありません。この関数はLodash、Ramda、および他の多くのライブラリのユーティリティとして存在しますが、別のライブラリをインポートするオーバーヘッドの価値は本当にありますか?

それを独自の行に引き出す価値さえありますか?多分。これらは実際には2つの異なる機能であり、それらを分離することでより明確になります。

一方、インラインにすることで、パラメーターシグネチャを確認したときに、タイプと使用方法が明確になります。インラインで作成すると次のようになります。

const composeMixins =(... mixins)=>(
  インスタンス= {}、
  mix =(... fns)=> x => fns.reduce((acc、fn)=> fn(acc)、x)
)=> mix(... mixins)(instance);

元の機能に戻りました。途中で、意味を捨てませんでした。実際、パラメーターとデフォルト値をインラインで宣言することにより、関数の使用方法とパラメーターの値がどのようになるかについての情報を追加しました。

ES5バージョンの余分なコードはすべてノイズでした。構文ノイズ。カレー矢印機能に不慣れな人を順応させること以外は、有用な目的には役立ちませんでした。

カリー化された矢印関数について十分な知識が得られたら、失われる構文がはるかに少ないため、元のバージョンの方が読みやすいことは明らかです。

また、バグが隠れる表面積がはるかに少ないため、エラーが発生しにくくなります。

矢印関数にアップグレードする場合に発見され、排除されるレガシー関数に隠れているバグがたくさんあると思います。

また、ES6で利用可能な簡潔な構文をより多く取り入れ、支持することを学べば、チームの生産性が大幅に向上すると思います。

明示的にすれば理解しやすいこともありますが、一般的なルールとして、コードが少ないほど良いことも事実です。

意味を犠牲にすることなく、同じことを達成し、より多くの通信を行うことができるコードが少ない場合、客観的には優れています。

違いを知る鍵は意味です。コードを追加しても意味を追加できない場合、そのコードは存在しないはずです。この概念は非常に基本的であり、自然言語の有名なスタイルガイドラインです。

同じスタイルのガイドラインがソースコードに適用されます。それを受け入れれば、コードはより良くなります。

一日の終わりに、暗闇の中の光。 ES6バージョンは読みにくいと言っているさらに別のツイートへの回答:

ES6、カレー、および機能構成に慣れる時間です。

次のステップ

「エリックエリオットでJavaScriptを学ぶ」メンバーは、今すぐ55分間のES6カレーと作曲のレッスンを見ることができます。

あなたがメンバーでない場合、あなたは逃しています!

エリックエリオットは「JavaScriptアプリケーションのプログラミング」(O’Reilly)、および「エリックエリオットでJavaScriptを学ぶ」の著者です。彼は、Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC、およびUsher、Frank Ocean、Metallicaなどのトップレコーディングアーティストのソフトウェアエクスペリエンスに貢献してきました。

彼はほとんどの時間をサンフランシスコ湾岸地域で世界で最も美しい女性と過ごしています。