このブログは更新を終了しました。移転先はこちらです。

2023-08-05

NTA-DIY:2ヶ月目④~よくわからん三銃士~

 それなりのツールを作ろうとすると、それなりに調べ物が多岐にわたることになりますが、そうなると「よくわからないあれをまた見かけた」ということをしばしば体験します。

 自分の目的を速やかに達成したいが、自分の理解が追いついていなくてちょっと避けているものがどうしてもついて回ってくる、という状態です。

 そこでちょちょっと調べて「なるほどそういう意味ねー」となれば話は早いのですが、そうならないから逃げ回って結局遭遇してということを繰り返すことになります。


 二ヶ月目のこの頃に困っていたのは主に三つの概念です。ずばり、アロー関数コールバック関数非同期処理です。JavaScriptをわかる人はこれらが如何に避けがたいものかおわかりかと思います。実際に何か作ろうとするならば、それが簡単なプログラムであっても扱わざるを得ないものたちです。

 当時困っていたもの三銃士という話なので、三つの粒度はバラバラです。理解の難易度も実際には大きく差がありますが、当時はどれも同程度に馴染み難いものでした。急に抽象度が上がったような感覚になるのです。

 数学で言えば、平方根、指数対数、三角関数、微積分、行列などの単元に入った時の感覚でしょうか。もちろんわかる人はすんなりわかるのですが、そのステップアップに失敗して数学で挫折したという人も多いでしょう。既に自分の中にある理解の組み合わせが通用せず、正面からの格闘を求められているような感覚です。

 これらについて誤りなく説明できる自信はないですし、解説は世の中にいくらでもあるのでここでは詳しく説明しませんが、混乱している人に寄り添うために自分の混乱を元に書けることを書いておきたいと思います。最初はこうだよね、というお話です。


アロー関数


 アロー関数というのは、関数の書き方の一種で、function~で定義するより簡潔な見た目をした構文です。

// 普通の関数宣言  
function add(a, b){
  return a + b;
}
// アロー関数
const add = (a, b) => a + b;

 「=>」の部分から「アロー(=矢印)関数」という名前がついています。

 アロー関数は単に「関数宣言を簡単に書けるようにした」というだけのものではなく、挙動に色々違いがあります。その違いはとても重要で、そこに躓く人も少なくないと思いますが、私の場合はそれ以前にまずこの書き方の見た目に混乱してしまいました。

 上記の例だと引数の部分に括弧がついているので辛うじて関数っぽさがなくもないですが、例えば以下のように書かれるとわけがわかりません。

const f = a => b => a + b;

 プログラミング脳になっていないと、パッと見た限りでは「fにaを代入、と思いきや矢印がある、けどaはすぐ出てこなくてなんか急にb出てくる」みたいな見え方をします。ちなみにこれは普通の関数宣言で書くと以下のようになります。

function f(a){
  return function(b){
    return a + b;
  }
}
console.log(f(1)(2)); // 3

 「関数を返す関数」というのも初学者の「わけわからないポイント」のひとつですが、とりあえずfunctionの形にしてみれば、括弧なし引数の矢印矢印の連鎖よりかは意味に想像がつくことでしょう。シンプルすぎるアロー関数はかえって理解に苦労することがあります。


 で、「アロー関数ってなんなんだ」という感覚になった時、「アロー関数とは」などと検索をかけても「functionとの違い」を細かく解説されるばかりで、「なんなんだ」感はいまいち解消されなかったりします。「なんなんだ」とは思っているのですが、「なんなのか」を説明されても困るのです。(解説記事が悪いのではなく、「なんなんだ」と思って「アロー関数とは」と調べざるを得ないところに難点があります。求めているものではないものの山に突っ込んでしまうのです。)

 じゃあこれを乗り越えるにはどうしたらいいかということですが、もし私のように「パッと見た時に意味が取りにくくて困惑する」という意味で「なんなんだ」になっているのだとしたら、私に言える答えはひとつです。アロー関数を書きまくるしかありません。それまでfunctionで書いていたものをとりあえずアロー関数で書いてみる。頭の理解をどうにかするのではなく、目が慣れるまで書く。ルートやインテグラルやシグマに目が慣れるまで問題集を解くのと同じです。

 見た目に困惑しなくなった頃に、functionとの違いが気になり始めると思います。そうなったら本格的にアロー関数について勉強することになるでしょう。

 ちなみに私が「おおよそアロー関数をわかった」と思えるようになったのは五月末あたりです(functionとの違いも含めて理解したのがそのくらいということです)。ツールを作る上で、理解しなくてもどうにかなるものの理解は後回しにしてまず作る、というスタンスでやっていたのもあって、なんだかんだ三ヶ月くらい曖昧なままでいたことになります。


コールバック関数


 次はコールバック関数です。コールバック関数とは引数として渡される関数のことです。以下のような関数があった時、引数funcに入れている関数fugaがコールバック関数になります。

function hoge(func){
  const q = confirm('実行しますか?');
  if(q) func();
}
function fuga(){
   alert('Hello world!');
}
hoge(fuga);

 ちなみにこの場合hogeの方は高階関数と言うようですが、今のところ「高階関数」というワードがすごく重要と思ったことはありません。

 で、コールバック関数の定義としてはこうなのですが、問題は「コールバック関数の引数に値が渡される」という状況です。上記の例ではfuncに入る関数には何も渡されていないので話が簡単に見えるかと思いますが、値を渡して使う場合の方が多いのではないかと思います。

 さて少し話が枝分かれしますが、コールバック関数に最初に直面するタイミングというのは、自分で高階関数を書こうとする時ではなく、コールバック関数を引数とするメソッドを使う必要が生じた時だろうと思います。具体的にはArray.prototype.find()などです。配列の中から条件に合うデータを探し出す時にコールバック関数が必須になるのです。

 簡単な日記データからデータを取り出す例を書いてみると、たとえばこんな感じです。

const data = [
  { date: '2023-08-02', text: 'すごく暑かった。' },
  { date: '2023-08-03', text: '夜にアイス食べた。' },
  { date: '2023-08-04', text: '今日も暑かった。' },
]
const today = data.find((obj) => obj.date === '2023-08-04'); // 2023-08-04のデータを返す
const soHot = data.filter((obj) => obj.text.includes('暑かった')); // 2023-08-02と2023-08-04のデータを含む配列を返す

 実際にはDateオブジェクトを使って今日の日付を指定したりしますが、とりあえずfindとfilterに注目してもらいたいので直に値を入れました。

 コールバックわからんちんな人もこの例を見るとfindやfilterの括弧の中にどう書けばいいのかなんとなくわかるのではないかと思います。これを「コールバック関数を引数に渡している」感が出るように書くと例えばこうなります。

function callback1(obj){
   return obj.date === '2023-08-04';
}
const today = data.find(callback1);

function callback2(obj){
  return obj.text.includes('暑かった');
}
const soHot = data.filter(callback2);

 アロー関数で一気に書いてしまっていたのを別途整理したわけですが、逆に意味がわからなくなったかもしれません。コールバック関数として渡した関数がfindやfilterメソッドの中でどう使われるのか想像できないからでしょう。(ちなみに「obj」という文字列の部分は別に何でも好きに決めていいのですが、何でもいいのだというのは関数を分けて書いてみると明らかでしょう。)

 findなどのメソッドが具体的にどういうコードで出来ているかわかりませんが、メソッドの中で「引数に渡したコールバック関数に配列の各要素を渡し、その返り値がtrueかfalseかを判定して条件分岐」という処理が行われているはずです。

 各メソッドは、引数として渡される関数(コールバック関数)の引数にどんなものをどの順番で渡して処理するかが決まっています。Array.prototype.findなら、配列arrのi番目を判定するとして、引数の一つ目はarr[i]、二つ目はi、三つ目はarrです。つまり、それらを私たちはコールバック関数の中で活用することができるということです。そして各メソッドは私たちが渡したコールバック関数の引数に順番にこれらを入れて実行してみて、trueなら次の処理を実行、というようなことをしていくわけです。if(callback(arr[i], i, arr)){}みたいなことです(あくまでイメージです)。


 しかし最初はそのことがよくわかっていませんでした。「data.find((obj) => obj.date === '2023-08-04');」みたいな文は「こう書くもの」としておまじないのように書いており、これが「関数を渡している」のだという感覚があまりありませんでした。いや、わかってみればこれはどう見たって関数を渡していますし、関数じゃなきゃなんなんだよという話なのですが、関数に関数を渡すという概念がわかっていなかった時は関数を渡した結果それがどうなるのか想像できなかったので、「これがなんなのかよくわからないが、とりあえずこう書く」という理解でfindやfilterを使っていたわけです。この時点でアロー関数自体馴染んでいなかったというのは手前で書いた通りです。

 しばらくして、自分で関数を書いていて「任意の関数をその中で実行させたい」と思う場面が来ました。その時当たり前に「引数として関数を渡して、それを実行すればいいんだろう」と思いました。そして試して期待通りに動いた時、はたと「コールバック関数を必要とするメソッドってつまりこれじゃん」と気がつきました。なぜその瞬間までわからなかったのかわかりません。その瞬間が訪れたのは確か三月末のことです。

 たまたま自分で同じ形のものを作って初めて「あれってこれじゃん」と気づいて理解が一気に進んだというのは、他のことでもいくつかあります。メソッドの説明を読んだだけではよくわからなかったのが、後からまさにその挙動を欲するタイミングができた時に、自分でわざわざ複雑なコードを作ってから「もっと簡単にならないのか?」と思って「あのメソッドってこれじゃん!」と気づいたりします。説明を読んだだけではすんなり理解できないがゆえに多分遠回りをしているのですが、わかった時には具体性をもってわかっているので(まさに目の前に実例があるわけです)、もうそれ以後はポンポン使えるということになります。


非同期処理


 最後は非同期処理です。なんというか、名前からして意味がよくわかりません。「非同期」ということは「同期していない」ということだというのはわかりますが、そもそも「同期」している処理って何? という話です。Aの存在を認識していないのに急に「非Aとは」と言われてもなんのこっちゃです。

 一言で言うと、「手前のものが終わるのを待って次に行く」のが同期処理で、「手前のものが終わるのを待たないで(よそに投げておいて)次のことを始めてしまう」のが非同期処理です(多分)。「同期」という単語とプログラム的な意味内容が脳内でいまいちバシッとリンクしませんでしたが、とりあえずそういうことだと理解しておかねばなりません。ファイルの読込など規模の大きい処理が必要な時、それが終わるのを待ってから次となるとリソースがもったいないので、他の処理を進めておいてから規模の大きい処理をやる、というような形にする必要が生じるわけです。

 とても難易度が高い概念なのですが、ツールを作るとなるとファイルの読込というのが不可避で、そうなると非同期処理を使うことになります。というのは、ファイルの内容を取得するのに使うfetch APIが非同期処理の形をしているからです。非同期的に処理したいとか考えるまでもなく、そういう形式になっているのでその形を理解しなければ使えないのです。

 一口に非同期処理と言っても、非同期処理にする書き方はいくつかあるので非同期処理とはこの形という話にはなりませんが、とりあえずファイル読込に使うfetch()が必要になるので、それについて書いていきます。

fetch('./test.txt')
.then(response => response.text())
.then(text => {
  // 引数textには「test.txt」の中身の文字列が入っている
  console.log(text);
})

fetch('./data.json')
.then(response => response.json())
.then(json => {
  // 引数jsonには「data.json」の中身がオブジェクトまたは配列に変換されて入っている
  console.log(json);
})

 なんじゃこりゃあ。「.then」って何? responseには一体何が入っていて何が処理されてるの? というようなことが、初学者にはわかりません。同期処理か非同期処理かという話より、構文の見た目の異質さに戸惑ってしまいます。これまた数学の微積分のごとく、それまでになかった概念と直面した瞬間です。習ってないのでわからないのは当然です。

 これを正しく理解するにはPromiseチェーンというのがわかる必要があります。そんなにべらぼうに難解な話ではないのですが、何しろアロー関数の意味すらあやふやな状態でやっているのです。説明を理解するのはちょっと大変です。全部理解するまで頑張っていたらツール作りが進みません。

 なので私の脳みそを非同期処理にしてしまいましょう。Promiseチェーンの理解という規模の大きい処理は後回しにして、まず目下のツール作りを先に進めてしまうのです。ツール作りの合間にキューが空いたらPromiseチェーンに取り組むことにします。二ヶ月目のこの時点では、とりあえずおまじないを書けるようになれば良しとしました。

 肝心なのは、自分が書いた処理がfetchでデータを読み込んでから行われる必要があるということです。

fetch('./data.json')
.then(response => response.json())
.then(json => {
  // 自分の作った処理がここで実行される必要がある
  hoge(json);
  fuga(json);
})

 そうなると、ツールで実行する処理というのは関数で綺麗に整理しておかないと大変です。このthenの中にずらーっと書いていってもまあ動きますが、きちんとまとめておいてthenの中に関数をいくつかぽんと置いて実行すると見通しが良くなると感じます。それまではべたべた直に書いて上から順に実行していた箇所が多かったわけですが、これを機に関数の使い方が少し変わっていきました。

 ちなみにPromiseチェーンを(ある程度)理解したのは結構後のことです。自分用のデジタルノートアプリを作るだけなら、fetchでローカルファイルの中身やAPIにアクセスした結果を取ってこれればそれで大体事足りました。


 その後あれこれ試みる中でこの三つに苦手意識を感じなくなった時に、JavaScriptでのプログラミングはぐっと自由さを増したように思います。苦手意識がなくなるのは割と唐突で、ふと「あれ、わかったぞ」という感覚がありました。最初は苦手でも、その周辺をうろうろしていればそのうちどうにかなるものですね。



このシリーズ記事の概要はこちら→ノートテイキングアプリDIY体験記