ゆるおたノート

Tomorrow is another day.

【GoogleAppsScript】コールバック関数を使わずに表現する方法を考えてみた話。

<2019/10/28 更新>

最近、コールバック関数なるテクニックを学びました。
配列の要素たちに対して、反復メソッド(に指定した処理)を繰り返し適用・処理してくれるというものです。

これを使うと、何段階かの処理をシンプルに表現できます。ちょー便利。

代表的なのは、forEachメソッド(またの名をforEach文)*1でしょうか?

ただ、その代わり実際の処理が見えなくなりやすいので、コードを共有するときはノンプログラマーの壁になってしまうのだそうです。

(今のところGASツールの共有どころか「コーディングのコの字も出てこない」部署に居ますが…笑)*2
いつかチームで自動化に取り組む時のために、そして自分の勉強のために、より基礎的な(?)for文を使って処理内容の再現に挑戦してみました。

サンプル

GAS本より、下記のサンプルを拝借しました。

(p.182) ▼サンプル 7-4-7 reduceメソッドとreduceRightメソッド

function myFunction_7_4_7() {
  var array1 = ['Bob', 'Tom', 'Jay', 'Dan'];

  //引数array1の先頭の要素から取り出して、順番に連結する
  var result1 = array1.reduce(function(str, name, index, array) {
    return str + name;
  });
  Logger.log(result1);
  
  //引数array1の末尾の要素から取り出して、順番に連結する
  var result2 = array1.reduceRight(function(str, name, index, array) {
    return str + name;
  });
  Logger.log(result2);
}

今回は、これを書き換えてみます。
(たぶん反復メソッドの中で簡単な方なので…)

まずはお勉強 ~反復メソッドとコールバック関数~

基本構文

配列.反復メソッド(コールバック関数, 反復の初期値);

これを少しずつ分解して考えてみます。

分解して考えてみる

引き数

反復メソッドの引き数は、以下の通りです。

反復メソッド(コールバック関数, 反復の初期値)
  • Function callback:コールバック関数
    反復に必要な処理を、「関数リテラル」で指定します。省略不可。

  • Optional Object initialValue:反復の初期値
    コールバック関数の始めの処理に、最初の引き数として使用されます。
    「Optional」、つまり省略可です。
    省略すると配列オブジェクトの最初の要素(インデックスは0)が使用されます。
    ただし、配列が空だとエラーになるので要注意。

戻り値

「コールバック関数の戻り値」で「アレコレした結果」を返します。

◆reduceメソッド、reduceRightメソッド

reduceメソッドは、配列の要素を「先頭から昇順」で取り出して、「左から」順に1つの値へまとめて返します。

先頭の要素から順に1つの値へまとめる

逆に、reduceRightメソッドは「末尾から降順」に取り出して「右から」順に並べて返します。

コールバック関数の中身

これらのメソッドでは、コールバック関数の中で以下の4つを引き数に取ります。

function(ひとつ前の結果, 現在の値, 現在のインデックス, 呼び出されている配列) {
  return 処理;
}
  • previousValue (accumulator)
    ひとつ前の処理結果、または初期の値(initialValue)

  • currentValue
    現在処理されている要素の値

  • Optional index
    現在処理されている要素のインデックス。省略可。

  • Optional array
    呼び出されている配列。省略可。

例えば、今回のサンプルのうちreduceメソッドで考えてみます。

var array1 = ['Bob', 'Tom', 'Jay', 'Dan'];

// 関数リテラルでコールバック関数を定義
function(str, name, index, array1) {
  return str + name;
}

コールバック関数の中で、各引き数の値は下記のように変遷します。

// 1回目
function('', 'Bob', 0, array1) {
  return '' + 'Bob'; //'Bob'
}

// 2回目
function('Bob', 'Tom', 1, array1) {
  return 'Bob' + 'Tom'; //'BobTom'
}

// 3回目
function('BobTom', 'Jay', 2, array1) {
  return 'BobTom' + 'Jay'; //'BobTomJay'
}

// 4回目
function('BobTomJay', 'Dan', 3, array1) {
  return 'BobTomJay' + 'Dan'; //'BobTomJayDan'
}

このように、コールバック関数は、反復メソッドの呼び出し元である「Arrayオブジェクト」から、各引き数の中身として勝手に要素を引っ張ってきてくれます。

そのため、引き数とは言っても自分で値を持っているので、コールバック関数の各引き数の中身はこちらで定義出来ないようです。
名前は自由に変えられるのにな…

改めて、構文。

これらを踏まえて今回のメソッドを書き換えてみると、構文は次のようになります。

配列.反復メソッド(function(ひとつ前の結果, 現在の値, 現在のインデックス, 呼び出されている配列) {
  return 処理;
}, 反復の初期値);

入れ子だからカッコが多くてなんかややこしいですけど…

書き換えてみた。

さて、本題です。

サンプル(再掲)

(p.182) ▼サンプル 7-4-7 reduceメソッドとreduceRightメソッド

function myFunction_7_4_7() {
  var array1 = ['Bob', 'Tom', 'Jay', 'Dan'];

  //引数array1の先頭の要素から取り出して、順番に連結する
  var result1 = array1.reduce(function(str, name, index, array) {
    return str + name;
  });
  Logger.log(result1);
  
  //引数array1の末尾の要素から取り出して、順番に連結する
  var result2 = array1.reduceRight(function(str, name, index, array) {
    return str + name;
  });
  Logger.log(result2);
}

処理の流れ

一応書いておくと、処理の流れとしては下記のとおりです。

  1. reduce関数に配列を渡す。
  2. joinElements関数で要素を連結する。
  3. インデックスの昇順で2.を繰り返し、結果をResultに繋いでいく。
  4. 連結の結果をRewriteMethods関数に返して、ログ出力する。
  5. reduceRight関数も、インデックスの降順で2.~4.を行う。

コード

※変数名は便宜的に少し変えています。

/* -------------------------------メイン------------------------------- */

function RewriteMethods() {
  var testArray = ['Bob', 'Tom', 'Jay', 'Dan'];

  var reduceResult = '';
  reduceResult = reduce(testArray);
  Logger.log(reduceResult);

  var reduceRightResult = '';
  reduceRightResult = reduceRight(testArray);
  Logger.log(reduceRightResult);
}

/* ----------------------------反復メソッド---------------------------- */

//繰り返しを昇順に設定して連結
function reduce(Array) {
  var Result1 = ''; //念のため初期化
  
  for (var i = 0; i < Array.length - 1; i++) {
    var index1 = i;
    var str1 = Array[index1]; //ひとつ前の処理結果
    var name1 = Array[index1 + 1]; //現在の値
    
    Result1 = joinElements(str1, name1, Result1);
  }
  return Result1; //コールバック関数の戻り値
}
//同じく降順に設定して連結
function reduceRight(Array) {
  var Result2 = ''; //念のため初期化
  
  for (var j = Array.length - 1; j > 0; j--) {
    var index2 = j;
    var str2 = Array[index2]; //ひとつ前の処理結果
    var name2 = Array[index2 -1]; //現在の値

    Result2 = joinElements(str2, name2, Result2);
  }
  return Result2; //コールバック関数の戻り値
}

/* ---------------------------コールバック関数------------------------- */

//ひとつ前の処理結果に、現在の値(要素)を繋げる
function joinElements(str, name, currentResult) {
  if (currentResult.indexOf(str, 1) <= 0) {
    return str + name;
  } else {
    return currentResult + name;
  }
}

感想と展望

今回は関数で置き換えてみました。
ただ、ここまで書いてから気付いたことですが、本来は「Arrayクラス」のメソッドなので関数ではなくクラスとprototypeプロパティを使った方が良いのでは?と思いました。
リファレンスでもそうなっていますね。(後述)

でも、初級者間で共有するという目的の場合は、クラスを使ってしまうと本末転倒なので、今回のコードの方が使えるかなと思います。

また、調べながら記事を書いていて、調べれば調べるほど疑問や気になることが増えて、どんどん「ワケワカラン!」になりました。(これも後述)
結局、すべては解決出来てない状態で書いてます。悪しからず…

そして個人的には、英語の勉強も兼ねて英語のリファレンスを読んでみたかったのですが、GASのリファレンスでは該当の部分を見つけられませんでした…
探し方が悪いのか、そもそもコンテンツが違うのかな…?

今後のためには調べ方も勉強する必要がありそうです。めんどくさい。

疑問が解消したら、少しずつここにも書き加えていきたいと思います。

<2019/10/28 追記>
GASのリファレンスは、「GAS特有のクラス」についてのドキュメントでした(恥)。
JavaScriptの文法は別途調べよう、ということですね。

余談

仮引数は一部省略可

上記の例では、コールバック関数の中でindexarrayは処理に使われていません。
これらはOptionalなので、このように書いても同じように動きます。

// コールバック関数
function(str, name) {
  return str + name;
}

逆に、敢えて宣言しておけば、function()の処理の中でそれぞれを使うこともできます。
例えばこんな感じ。

// コールバック関数
function(str, name, index, array1) {
  return String(index) + str + name;
}

ただし、indexはcurrentValue(※)の添え字なので、「0ではなく1から出力される」ことは注意が必要です。
※↑のサンプルコードではname

混乱してきたら、return文の1行前に↓を追加して実行し、ログを確認してみてください。

Logger.log('(%s) %s, %s', index, name, array);

値の動きを追えるのでオススメです。

ブラウザ依存

GASの親分であるJavaScriptの話ですが、Webブラウザによってはこのメソッドが対応していないこともあるそうです。

リファレンスに自分で実装する方法も載っているので、必要な方は参考にされると良いかと思います。
(書き換えを先に自分で試してから説明とか調べたので知らなかったのですが、多分コレが今回やろうとしてることの正解ですね…)

※GASはGoogleさんが標準装備してくれてるので、ココは関係ないです!
わーい、覚えることが減ったぞー。

型の疑問…

試しに、スクリプトエディタでreduceメソッドを途中まで入力してみると、下記のようにヒントが出ます。

initialValueも戻り値も「値」なのに型はObjectなんですよね…なぜだろう?

reduce(Function callback, Object initialValue) : Objcet

これは「(評価前でまだ型が分からないので)なにがしかのオブジェクト」って意味なのかな???🤔

参照

今回お世話になったのは、下記の書籍とWebサイトです。

書籍

  • みんな大好きGASの教科書!サンプルの出典元です。

リファレンス

*1:VBAで言うと「For Each文」が似てますね。

*2:<2019/10/28 追記>
転職してコーディングの仕事を少し頂けるようになりました!