この記事は Ableton Live を JavaScript でハックする方法を紹介したシリーズの、二番目記事です。このシリーズでは読者は Ableton Live 9 suite を持っていて、JavaScript にある程度慣れ親しんでいることを前提としています。

This is the 2nd of a series of articles about hacking on Ableton Live with JavaScript. These articles assume you own Ableton Live 9 Suite and are comfortable coding JavaScript.

この記事では、これから作っていくプロジェクトのデバグをするにあたって便利な、ユーティリティコードを作っていきます。Ableton Live 内で JavaScript をコーティングする良い練習になるはずです。

In this article we'll iteratively build some utility code for logging that will be useful for debugging purposes in all our projects. This will be a good practice session for coding JavaScript inside Ableton Live.

もしとにかく Live のハッキングを今すぐ始めたくて logging/debugging ツールがどのようになっているのかあまり気にならない場合には、この記事を飛ばして次の記事に移動してください。

If you're eager to start hacking on Live ASAP and don't care about how this logging/debugging stuff works, you can skip ahead to the next article.

この記事が理解できない場合には、まずはシリーズ最初の記事から読み始めてください。

If you aren't sure how to get started, try the 1st article in this series.

post() について知る

Getting to know post()

Max の JavaScript API は post() メソッドを提供しています。これを使うことで Max ウィンドウに文字を出力することができます。シンプルな "Hello World" プログラムを作ってみましょう。以下のコードを Max の JavaScript エディターに入力してから、保存し実行させます。

Max's JavaScript API provides a method post() for printing to the Max window. Let's make a simple "Hello World" program to try it out. Enter the following into Max's JavaScript editor, and save it to run it:

ハローワールド
post("Hello World!");

Max ウィンドウに「Hello World!」と表示されていれば上手くいっています。ですが post() メソッドは logging においてベストなものではありません。例えば何回も print をしてみましょう。次のコードをセーブして実行しましょう。

Over in the Max Window, you should see: Hello World! So far so good. But, as you'll see, the post() method isn't the best for logging. What if we print something multiple times? Enter this code and save again to run the updated script:

複数回 post() を実行する
post("Hello World!");
post("Goodbye World!");

実行すると全てのテキストが Max ウィンドウの「同じ行」に表示されていまいます。これはこれで便利ですが、本当はそれぞれのメッセージが別の行に表示されるようになった方がいいはずです。そうするためには ("\n") をそれぞれのメッセージに追加します。次のようになります。

All this text appears on one line in the Max window. This can be useful, but we'll usually want a logger to print each message on its own line. We can do this by appending a newline ("\n") to each message we post(), like this:

改行する
post("Hello World!\n");
post("Goodbye World!\n");

log() 関数の導入

Introducing log()

毎回 "\n" を入力をするのは面倒なので、logging 関数を作ることにしましょう。

We don't want to have to manually type "\n" all the time, so we can create a logging function that will do it for us:

logging 関数
function log(message) {
  post(message);
  post("\n");
}
 
log("Hello World!");
log("Goodbye World!");

簡単ですが十分便利ですね。ただ、文字列じゃない値を表示しようとするとどうなるでしょうか?

Easy enough. What about logging objects that aren't strings?

文字列以外の値を出力する
function log(message) {
  post(message);
  post("\n");
}
 
log( new Date() );

これは jsobject -1266632764615976 といった内容が主力されるはずです。これはあまり便利とは言い難いですね。これをあつかうためには toString() を入力された message に対して使用します。

That will print something like jsobject -1266632764615976, which is not particularly useful. To address this, we can call toString() on the message parameter:

toString を活用する
function log(message) {
  post(message.toString());
  post("\n");
}
 
log( new Date() );

こうすると Mon Apr 21 2014 19:29:07 GMT-0700 (PDT) といったような内容が出力されるようになります。このほうがいいですね!

Now this outputs something like Mon Apr 21 2014 19:29:07 GMT-0700 (PDT). Much better!

カスタムクラスを自分で作った場合に、toString() メソッドを実行することでその内容をロギングすることができます。実際の例はこの記事の最後の方で紹介します。

Note that if we design some custom classes in our scripts, we can give them a toString() method to make them show whatever we want when logging. We'll see an example near the end of this article.

より堅牢な log() 関数

A more robust log()

バグを引き起こしてみましょう。次のコードを実行するとどうなるでしょうか。

We've introduced a bug. What happens if we run this?

エラーになる
function log(message) {
  post(message.toString());
  post("\n");
}
 
log( null );

エラーが出ますね。"Javascript TypeError: message is null, line 2" この部分はもう少し注意深く処理する必要があります。バグを避けるために、次のように単純なチェック機構を入れることにしましょう。

We get an error: Javascript TypeError: message is null, line 2. We have to be more careful. A simple check avoids the bug:

simple check
function log(message) {
  if(message && message.toString) {
    post(message.toString());
  }
  else {
    post(message);
  }
  post("\n");
}
 
log( new Date() );
log( null );

つまり message が null/undefined でもなく、かつ toString() メソッドを持っている場合には、toString() を実行します。これでエラーは出なくなりましたが、log( null ) を実行すると jsobject -1266632764615976 といったものが表示されます。これだとまたわかりにくいので、直していきましょう。

In other words, if the message isn't null/undefined, and it has a toString() method, then call its toString(). Now there's no error, but the log( null ) is printing jsobject -1266632764615976. Let's fix that:

fix
function log(message) {
  if(message && message.toString) {
    post(message.toString());
  }
  else if(message === null) {
    post("<null>");
  }
  else {
    post(message);
  }
  post("\n");
}
 
log( new Date() );
log( null );
log( {}['non-existent-property'] );

なぜ null を比較するために === を使ったのでしょうか。もし == を使ってしまうと、暗黙的な型変換が行われてしまい、問題が起きるからです。 値は(例えば ({key:'value'}['non-existent-key'] など)== null だと true に、=== null だと false になります。null と undefined 値を比較するために ==== を使う必要があるのです。デバグする際には非常に有効な手法です。

Why the triple === in the comparison with null? If we just used double ==, it allows for automatic type conversions, which can cause some confusion. values (such as {key:'value'}['non-existent-key']) are == null, but are not === null. So we can distinguish between null and undefined values by using ===, which can be helpful when debugging.

Logging objects as JSON

まだ全てのケース全てを扱えるようにはなっていません。オブジェクトに関してはうまくいきません。

We haven't covered every edge case yet. Most objects don't log in a reasonable way:

オブジェクトは上手く表示されない
function log(message) {
  if(message && message.toString) {
    post(message.toString());
  }
  else if(message === null) {
    post("<null>");
  }
  else {
    post(message);
  }
  post("\n");
}
 
log( {myObject:123} );

これを実行すると [object Object] と表示されます。ありがたいことにビルトインメソッドである JSON 関数を使うことで簡単に表示できるようになります。

This prints [object Object]. Thankfully, we can use built-in JSON functions to easily log something useful:

JSON.stringify を使う
function log(message) {
  if(message && message.toString) {
    var s = message.toString();
    if(s == "[object Object]") {
      s = JSON.stringify(message);
    }
    post(s);
  }
  else if(message === null) {
    post("<null>");
  }
  else {
    post(message);
  }
  post("\n");
}
 
log( {myObject:123} );

これで {"myObject":123} と出力されるようになります。いい感じですね。ただし残念なことにデータがネストされた構造になっている場合にはまだうまくいきません。次のように実行してみてください。

This prints {"myObject":123}. Cool, now we log data structures. Unfortunately, it's more complicated with nested data structures. Try this:

ネストされたデータ
log( [1,{key:'value'},3] );

これを実行すると 1,[object Object],3 と出力されます。これをもっと上手く表示することにしましょう。JavaScript の説明書にはこう書かれています。

That logs 1,[object Object],3. We need a better solution. JavaScript documentation indicates:

全てのオブジェクトは toString メソッドを持ち、オブジェクトが文字列値として表されるべきときや、文字列が期待される構文で参照されたときに自動的に呼び出されます。デフォルトで、toString メソッドは Object の子孫にあたるあらゆるオブジェクトに継承されています。このメソッドがカスタムオブジェクト中で上書きされていない場合、toString は "[object type]" という文字列を返します(type は そのオブジェクトの型)。

the toString() method is inherited by every object descended from Object. If this method is not overridden in a custom object, toString() returns "[object type]", where type is the object type.

なるほど…[object Object] だけではなくて [object SomeType] というパターンもあるわけですね。 "[object " が toString() を適応させる値の内部にある場合、 JSON.stringify を使ってオブジェクトを文字列に変換することにしましょう。

Ok... Besides [object Object], we might see things like [object SomeType]. Let's convert an object to JSON when we see "[object " anywhere in its toString() value:

JSON.stringify
function log(message) {
  if(message && message.toString) {
    var s = message.toString();
    if(s.indexOf("[object ") >= 0) {
      s = JSON.stringify(message);
    }
    post(s);
  }
  else if(message === null) {
    post("<null>");
  }
  else {
    post(message);
  }
  post("\n");
}
 
log( {myObject:123} );
log( [1,{key:'value'},3] );
log( [1,2,3] );

すると以下のように出力されます。

This outputs:

出力
{"myObject":123}
[1,{"key":"value"},3]
1,2,3

ただ実はこれには問題もあります。例えば log( [1,2,3] ) は 1,2,3 と出力されてしまいます。この問題も上手く扱うことはできます。(例えば if(message instanceof Array) などを使って。)ただし今までみてきたことでわかっていただけると思いますが、ロギング関数で全てのケースを上手く扱うのは非常に難しく、また完全なものにするには時間がかかります。ですので現時点では、大体うまくいっているのでこれくらいにして、Live のハッキングの方に移ることにしましょう。

It's inconsistent that log( [1,2,3] ); outputs 1,2,3 instead of [1,2,3]. We could handle that case too (such as with an if(message instanceof Array) check). As you can see, it's difficult to make a comprehensive logging function, and we could waste a lot of time trying to make this "perfect". At this point, I say "good enough" and move on, so we can start hacking on Live soon.

もっとシンプルにできないの?

Can't we simplify?

log() 関数をもっとシンプルに実装することはできます。toString() も JSON.stringify(message) も使わずにです。もちろんこれも有効な方法ですし、私たちが実装しきたものよりもかなりシンプルです。以前は私もこのような log 関数を使ってきました…

A simpler implementation of log() wouldn't even bother with toString() and would blindly call JSON.stringify(message) on everything. That's certainly a viable approach and a lot simpler than what we've built here. I have tried writing log() that way...

しかしこのアプローチにはいくつか問題があります。

There's a few problems with that approach.

Date オブジェクトなどには toString() を使って Sat Apr 26 2014 11:49:42 GMT-0700 (PDT) と表示させてきました。しかし JSON.stringify() を Date 使ってしまうと "2014-04-26T18:49:42.694Z" のように多少可読性の低い表示になってしまいます。こういった問題はかなりあって、このような特殊なケースをうまく扱うために、私たちが実装してきた log() 関数が必要なのです。

Some objects, like Date, have a nice toString() representation like Sat Apr 26 2014 11:49:42 GMT-0700 (PDT). If we call JSON.stringify() on Date, we get a less readable format "2014-04-26T18:49:42.694Z". You'll find many oddities like that, and to address them you'd need to handle various special cases like in our current log() implementation.

同様にこれから作っていくカスタムクラスをうまく表示させるためには、toString() のほうが toJSON() よりいいのです。(わからん?かっこ)

Also, when we start making custom classes and we want to print them out, it conceptually makes more sense to provide a toString() method than it does to provide a toJSON() method (which might not even convert to valid JSON because we're just trying to print some debugging info).

このような理由で私は log() 関数において toString() を toJSON() よりも好んで使用しています。JSON.stringify(message) を使用する特殊なケースはなるべく最小にしたいのです。(実際カスタムクラスは toJSON ではうまくログ出力できないはずです。)

For these reason, I prefer toString() over toJSON() in our log() function. I've tried to keep the special cases to a minimum.

複数の引数を受け取る

Multiple parameters

もう一つ改修を加えて、log() 関数が任意の数の引数を受け取ることができるようにしましょう。こうすることで log(x, y, z) とすることができるので便利です。log("" + x + y + z) としなくてもよくなります。

One more tweak and then we're done building our log() function. Let's enhance log() to support a variable number of arguments. This is a small convenience that will allow us to do things like log(x, y, z) instead of log("" + x + y + z)

JavaScript においては、関数を呼び出した際に、関数に渡された全ての引数を、配列として受け取ることができます。この配列に対して loop を適用させて、今までやってきたことと同じロジックを適応させましょう。以下のコードが完成形です。

In javascript, every function call has access to an arguments array that contains the values of all the parameters passed to the function. So we can loop over that and apply the same logic as before. Here is the final version:

完成形
function log() {
  for(var i=0,len=arguments.length; i<len; i++) {
    var message = arguments[i];
    if(message && message.toString) {
      var s = message.toString();
      if(s.indexOf("[object ") >= 0) {
        s = JSON.stringify(message);
      }
      post(s);
    }
    else if(message === null) {
      post("<null>");
    }
    else {
      post(message);
    }
  }
  post("\n");
}
 
log("___________________________________________________");
log("Reload:", new Date);

最後の二行はなんのためにあるのでしょうか?私は再実行された際に、分割線を最後に引いて、さらに再実行された時間を表示するほうが便利だなと思っています。こうすることで、コードを書いてそれから保存して再実行するということを何度も繰り返していく上で、今どの部分なのかということが追いやすくなるからです。プロからの助言:Max ウィンドウのログはいつでも消すことができます。

What's with the last 2 lines? I like to show a divider line and the current time every time I re-run the script. It helps keep track of different runs of the JavaScript program while you are working on the code and saving/re-running repeatedly. Pro-tip: you can also clear the Max window at any time.

最後の "\n" はループの外側にあります。これによって受け取った全ての引数を、同じ行の中にスペース区切りで表示することができます。

Note the final "\n" is outside the loop. This has the effect of joining all the arguments with a space and printing them together on one line.

ではこれを実際に使ってみましょう。以下のコードを最後の部分に足します。

Let's test it out. Add the following lines to the end of the script:

追加するコード
log( 123, 1.23, 'some text' );
log( null, {}['nothing here'] );
 
log( 1,2,3 ); 
log( [1,2,3] );
log( [1,{A:2},3] );
log( {key:{nestedKey:[1,2,3]}, anotherKey:'value'} );
  
// Example of a custom class with a toString() method
MyClass = function(value) {
  this.value = value;
  this.toString = function() {
    return 'MyClass(value=' + this.value + ')';
  }
}
log( new MyClass(123) );

すると以下のように Max ウィンドウに表示されるはずです。

Which prints this in the Max window:

表示結果
___________________________________________________
Reload:  Sat Apr 26 2014 12:05:55 GMT-0700 (PDT)
123  1.23  some text
<null>  <undefined>
1  2  3
1,2,3
[1,{"A":2},3]
{"key":{"nestedKey":[1,2,3]},"anotherKey":"value"}
MyClass(value=123)

まとめ

Wrapping up

どんなプロジェクトにおいても活用できる便利なコードを書いてきました。私は全ての Live JS プロジェクトを立ち上げる際に、一番冒頭にこれをコピペすることにしています。

We just wrote some general-purpose utility code that's useful in any project. I tend to paste this into the top of all my Live JS projects when I'm setting them up.

最後のティップスです。私はウェブ開発をかなりやってきたので、console.log() と入力してウェブブラウザや Node.js で表示させようとする癖がついています。なのでつい Max のプロジェクトでも入力してしまい、イライラするので、以下のコードも追加することにしています。

One last tip. I've done a lot of web development, and I've been conditioned to type console.log() to log messages in the web browser and on Node.js. I got tired of accidentally typing that in my Max projects, so I added this line of code:

console.log にさしこむ
console = {log: log}

こうしておくことで log() でも console.log() でも動作するようになります。ウェブ開発の経験がある人はこのテクニックの恩恵を受けることでしょう。

This way, either log() or console.log() will work. Those of you with a web development background may appreciate this trick.

次のステップ

Next Steps

これで Live API を JavaScript から操作して Live の機能を拡張する準備ができました。次は Live API の基礎を扱っていきます。

Now you're prepared to start using the Live API to extend Live's functionality with your own JavaScript programs. The next article in the series explores the basics of using the Live API.