ゆるおたノート

Tomorrow is another day.

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

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

VBAではFor Each文によくお世話になっていますが、GASやJavaScriptでは、forEachメソッド(またの名をforEach文)を始めとして、コールバック関数ならこれを3~4行で表現できます。ちょー便利。

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

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

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

基本構文

まず、基本構文はこちら。

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

引き数と戻り値

引き数

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

  • Function callback:コールバック関数
    反復に必要な処理を、関数リテラルで指定します。省略不可。
  • OptionalObject initialValue:反復の初期値
    コールバック関数の始めに、最初の引き数として使用されます。
    Optional、つまり省略可です。
    省略すると、配列オブジェクトの最初の要素(インデックス0)が使用されますが、配列が空だとエラーになります。
戻り値

戻り値としては、コールバック関数の戻り値をアレコレした結果を返します。

reduceメソッド、reduceRightメソッド

今回は、おなじみのGAS本から、(たぶん反復メソッドの中でforEachメソッドの次に簡単そうな笑)reduceメソッドとreduceRightメソッドを取り上げます。

reduceメソッドは、配列の要素を昇順で取り出し、関数によって左から順に1つの値へまとめて返します。
同じく、reduceRightメソッドは降順に取り出して、右から順にして返します。

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

previousValue (accumulator) ひとつ前の処理結果、または初期の値(initialValue)
currentValue 現在処理されている要素の値
Optionalindex 現在処理されている要素のインデックス
Optionalarray 呼び出されている配列

ただ、引き数とは言っても自分で値を持っているので、名前は決められても中身の定義は出来ないようです。
reduceメソッド(やreduceRightメソッド)を使う配列から、勝手に値を引っ張ってくるイメージです。

また、indexarrayOptionalなので、これらを省略しても動きます。
後述のサンプルでも、省略して使えました。

構文

これらを踏まえて、今回のメソッドの構文はこちらです。

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

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

ちなみに。

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

f:id:yuricks7:20181108211154p:plain

initialValueもそうですが、戻り値も「値」なのにObjectなのですね。…なぜ??

あと、GASの親分であるJavaScriptとしては、ブラウザによってはこのメソッドが対応していないこともあるそうです。
自分で実装する方法が載っているので、必要な方は後述のリファレンスを参考にしてください。
(書き換えを先に自分で試してから説明とか調べたので知らなかったのですが、多分コレが今回やろうとしてることの正解ですね…)

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

サンプル

さて、本題の書き換えですが。

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);
}

仮引数は一部省略可

先述の通り、仮引数のindexとarrayは、(function()内で使ってないので)それぞれ省略しても動きます。

逆に敢えて宣言しておけば、こちらで中身を定義せずとも値を持っているので、function()の処理の中でそれぞれを使うこともできます。
ただし、indexはcurrentValue(↑のサンプルコードではname)の添え字なので、0ではなく1から出力されることに注意しましょう。

※混乱してきたら、return文の1行前に↓を追加して実行し、ログを確認してみましょう。

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

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

書き換えてみた。

処理の流れ

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

  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のリファレンスでは該当の部分を見つけられませんでした…
探し方が悪いのか、そもそもコンテンツが違うのかな…?

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

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

参照

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

書籍

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

リファレンス