obniz

obnizのパーツライブラリの舞台裏を覗いてみた

この記事はobniz アドベントカレンダー2021の15日目の記事です。

obnizはパーツライブラリが充実しているのでIoTプログラムをサクッと書けます。しかし、私のようなズボラな人は、与えるべきパラメータを間違えたり、ひどい時はパーツ名のスペルを間違えて動かないプログラムを書いてしまいます。

2年ほど前に「obnizのJavaScriptライブラリがTypeScrip対応した」というニュースが流れました。TypeScriptは型チェックを行なってくれるので、私が書くいい加減なプログラムの間違いを指摘してくれるんじゃないかという期待が膨らみます。そこで、パーツライブラリがどれぐらい型チェックしてくれそうか調べてみました。

obnizのパーツライブラリとは

obnizには豊富なパーツライブラリが用意されているので、LED、温度計、サーボモータといった部品を使うプログラムを簡単に書くことができます。例えばLEDを使うプログラムはこんな感じになります。(引用元: https://obniz.com/ja/doc/reference/common/parts/)

obniz.wired(“LED”, {anode:0, cathode:1}) という部分でLEDオブジェクトを生成しています。一般的には obniz.wired(部品名, パラメータ) というように記述します。

この書き方の面白い点は、

  • どの部品を使うかは文字列で指定する
  • パラメータは キー:バリュー の列で指定する

というところです。上記の例では、

  • 部品名は “LED”
  • パラメータは {anode:0, cathode:1}

です。

部品名が何で、どんなパラメータを渡せばいいかはパーツライブラリのドキュメントに記載されています。例えばLEDのドキュメントの冒頭はこんな感じです。

パーツライブラリの利用でつまずく点

私がよくつまずくのは次のような点です。

  1. パーツ名のスペルを間違えます。例えばうっかり小文字で”led”と書いたりします。文字列なのでwired関数が実行されてエラーが出るまで間違いに気づきません。
  2. パラメータで渡すデータの型がわからなかったり、間違えたりします。

①は、文字列という自由度満点なデータ型が使われているために起こる問題です。文字列を直接記述するのではなく、const値としてパーツ名が定義されていてそれを使うようにすればスペルミスを防げるかもしれません。

②は、なかなか厄介です。例えば上述のLEDの場合、anodeとcathodeを渡すことはわかるのですが、その値は数値なのか”io0″のような文字列なのかそれ以外の何かなのかはわかりません。ドキュメントの例題プログラムを読んで推測するか、ライブラリのソースコードを読んでみるかしか方法がありません。

②はパーツ固有の関数についても同様です。例えばLEDには output(value) という関数がありますが、valueとして何を渡すべきなのかは例題を見ないとわかりません。

JavaScriptは型宣言がない言語なので、これらの問題を回避するのは難しいです。TypeScriptであれば強力な型チェック機能があるので問題を解決してくれる可能性があります。

こんな使い方がしたい

雰囲気としこんな風に書けると型チェックしてくれそうです。LEDというクラスがあって、コンストラクタでanodeとcathodeを指定します。

let redLED: LED = new LED(0, 1);

constructorに渡す引数を

LED(“io0”, “io1”)

と書いたり、作成したredLEDのoutput()関数を呼び出そうとして

redLED.output(“ON”);

とか書いたら、TypeScriptコンパイラが「引数の型が違いますよ」と指摘してくれるわけです。

注:TypeScriptの勉強を始めてからまだ1日しか経っていないのでコード例が間違っている可能性大です。雰囲気だけ感じてください。

パーツのTypeScriptコードを眺めてみる

上述のような使い方ができるのか、パーツのTypeScriptコードを見てみます。例題としてLEDを見てみましょう。LEDのドキュメントページにはTypeScriptというタブがあります。

TypeScriptタブをクリックすると、LEDのTypeScriptコードが表示されます。開くページはgithubのページです。

17行目をみると、LEDクラスはObnizPartsInterfaceをimplementsしていますね。Parts Classというのがあってそれをextendしているのかと思っていましたが、そうではないようです。でもここは本題ではないので深追いするのはやめて、気にしないことにします。何せTypeScript経験1日なので。

重要なのはconstructorがどう定義されているかです。class定義の先を見ていくと34行目にconstructorが書いてあります。

あれ? constructorに引数がありません。wired関数で渡している引数はどこで使われるのでしょうか。謎が深まります。

class定義の先を読んでみると、constructorの直後にwired関数が定義されていました。

でも、引数はobnizだけで、anodecathodeはありません。どうやらこのwired関数は

obniz.wired(“LED”, {anode:0, cathod:1})

というコードの下請けとして呼び出されるもののようです。

ドキュメントをよく読んでみる

LEDクラスの造りがよくわからなくなったので、パーツライブラリのドキュメントに立ち返って読んでみることにします。

おお、パーツを作り公開するというドキュメントがありました。

このドキュメントの終わりの方をみるとこんなことが書いてあります。

パーツクラスのwired関数はobniz.wired()から呼び出されることが明記されています。obniz.wired()を使うとクラス名による型チェックや引数の型チェックが難しくなるので、この方法は使いたくありません。パーツクラスのインスタンスを直接作りたです。

そう思っていたら、ちゃんと説明がありました。

インスタンスを作る手順はこういうステップを踏むようです。

  1. 引数なしのconstructorでインスタンスを作る。
  2. パラメータのリストをセットする。
  3. wired関数を呼び出す。

インスタンスを作るだけなのにちょっと手順が多いですね。またこの方式だとパラメータの型チェックはちょっと難しそうですね。obniz.wired()から呼び出されることを前提にパーツクラスが設計されているのでこうなっているのでしょう。

ここまで調べてみた感想

obnizのJavaScriptライブラリがTypeScript化されて、

let redLED: LED = new LED(0, 1);

というように書けるのかなと想像していましたが、そうはなっていないようです。よく考えてみると、ほとんどの人はJavaScriptでプログラムを書くので、TypeScriptでプログラムを書く人が便利になる機能の実装は後回しになるのは当然ですね。

でも、型チェック機能はプログラムの生産性に大きく寄与するので、そこが充実してくれると嬉しいです。