トップ 追記

2017-07-21 [長年日記]

_ flowで依存ライブラリがRequired module not foundになる際の最小の対策

背景

先日から flow でシコシコ type 付けをしていたが、新たに lodash を使おうと思ったら以下のようなエラーが出てしばらく悩んだ。

3: import _ from 'lodash/lodash.min'
                   ^^^^^^^^^^^^^^^^^^^ lodash/lodash.min. Required module not found

まとめ

  • 上のエラーは最小限の declaration を用意することでしのげる
  • declaration はデフォルトでは flow-typed/ 以下に置く(これなら .flowconfig で設定不要)
  • declaration は module_vx.x.x.js の名前で作れる
    • lodash/lodash.min なら lodash.min_v4.x.x.js
  • 内容は以下の通り
declare module 'lodash/lodash.min' {
  declare module.exports: any;
}

実はlodashには定義済みのtype情報がある

んだけど、lodash/lodash.min には対応していないのであった。

最小限の方法で行くという選択

flowtype/flow-typed: A central repository for Flow library definitions

今回試しに flow-typed を入れて scaffold までやってみたのだが、

flow-typed は独自に flow-typed コマンドを使うので完全自動化まで考えると使い勝手が悪い

ということが分かった。例えば babel の plugin を babel コマンドで管理するようなものと思えばよいだろうか。npm や yarn のコマンド体系の外にいるので、それを適切に呼び出せる環境をちゃんと準備しないとこの恩恵をみんなで共有することはできない。(postinstall とかでやることが増える)

真面目に flow-typed で type 情報を共有できるようにするのは一つの正しい方法ではあると思うけど、今のところ手元での再現や CI での実行には工夫が必要になってしまう。大掛かりな仕組みで依存ライブラリがたくさんある場合は正しい手法を選択した方がよいとは思うが、今回は最小限の dirty hack でしのぐ方法を紹介することとした。

どうせ全部の定義が欲しいわけではないしね。

Tags: JavaScript

2017-07-13 [長年日記]

_ Flowtypeを試してみた

背景

JavaScript はオブジェクトの構造とかコンパクトでプラットフォーム非依存な仕様など、面白いと思う反面、

'' < 1

が true になってしまうなど割とつらい仕様もあるので、大々的には入れたくないなぁと以前から感じていたのですが、ここ数年は JavaScript に型を入れるというのは AngularJS 2 の TypeScript 採用、Facebook の Flow など話題だし、ES2015 にはなんとかえっちらおっちら追いつけそうなので、いっちょ Flow でも試してみることにしました。

※ 私は Ruby を使っていますが、Ruby の場合は型が欲しいと思うことはありませんでした。Ruby にも静的型はないじゃないかと思うかもしれませんが、上のような謎挙動はないので、全然意味が違うということは添えておきたいと思います。

実際のところは直接書いたコードではなく簡易DSLのようなナニカで上の '' < 1 を踏み抜いてしまったことにより型の必要性を感じているので、これを入れればすぐ問題は解決、というわけにはいきませんが、興味が湧いちゃったのでまずはこれをやってみましょう。

判断基準

なぜ Flow なのか。以下を読むべし。

型なき世界のためのflowtype入門 - Qiita

このエントリでは条件が整えば TypeScipt でよい、という判断ですが、自分の場合は、ここ何年かの JavaScript の情勢や ES のリリース方針を見ると、本流である Ecmascript が常に漸進的に更新されていくことがもはや明確であり、正直言うと本家以外の ES の亜流には手を出したくないというのが本音であり、TypeScript も MS がいつまで本気でやるかも分からないという一抹の不安はやはり残っています。*1

その点 Flow は type 情報を strip すればただの Ecmascript というところが安心。いま欲しい何かを満たしつつ言語仕様そのものは標準を維持できるので長期的に見てもメリットが大きいと思います。*2

Flow: A Static Type Checker for JavaScript

ということで Flowtype 行ってみよー。

準備

すでに babel, eslint は使えているものとします。

$ yarn add --dev \
 babel-eslint \
 babel-preset-flow \
 babel-plugin-transform-class-properties \
 eslint-plugin-flowtype \
 flow-bin

以下、追加変更する設定ファイルの部分。

.eslintrc.json

{
  "parser": "babel-eslint"
}

type の書かれたコードは通常の Ecmascript とは異なるので、eslint の parser を babel を通したものにします。

package.json

"scripts": {
  "flow": "flow check",
  "flow-stop": "flow stop"
}

.babelrc

{
  "plugins": ["transform-class-properties"],
  "presets": ["flow"]
}

で、babel を通すと flow な preset が適用され、余計な情報はカットされます。

yarn flow -- init

として .flowconfig を以下のような感じにしてみました。

[ignore]
.*/doc/.*
.*/dist/.*
.*/node_modules/.*

[include]

[libs]

[options]
log.file=flow.log

[lints]

ほとんど何も設定してません!

気づかなかったけど気をつけた方がよいこと

flow は daemon として動く。そしてそのまま居座る。

0.49.1 で試し直したら終了した時点でプロセスが消えたので、これが正しい動作なのかもしれません!!

やってみた

@flow

コメントに @flow を書くと有効というか検査対象になります。どこでもよいわけではないようで、できるだけ1行目に書いた方がよさそうです。

// @flow

でも

/* @flow */

でも

/**
 * @flow
 */

でもいいけど、とにかくいちばん上に置くのが肝要なようです。

ちなみに、いちいち @flow を書くのが面倒な場合は

flow check --all

とすればよいようです。ただし、テストコードでこれが有効になると割と面倒なことになりそうです。

型を書かなくても error になる
console.log('' < 1)
            ^^ string. This type cannot be compared to
console.log('' < 1)
                 ^ number

リテラルを解釈して型が違うものを比較している場合にはそりゃーダメだよと教えてくれます。いきなり便利!

引数に型を書く

基本的には変数名の後ろに :<type> を付ける形。イマドキこの形が多いと聞いたような? でもよく分かっていない。

function foo(abc :string) {}

と type 情報を付けておくと、

foo(1)

呼び出しが書かれている場合、flow を実行すると以下のように怒られます。

: foo(1)
      ^ number. This type is incompatible with the expected param type of
: function foo(abc :string) {}
                    ^^^^^^ string

おおおお。これが自分的にはいちばん嬉しいかも。

戻り値に型を書く
function foo() :string {}

となっていたら number を返そうとしてると怒られるわけですな。

ローカル変数に型を書く
var foo :string = 1

var foo :string = 1
                  ^ number. This type is incompatible with
var foo :string = 1
         ^^^^^^ string

みたいに怒られますが、この例のように書いてしまうことはまずないだろうし、何かの設計をミスって長寿命な変数を使ってしまった場合に有効なのかな?

プロパティに型を書く

これがドキュメントを見渡しても分からず、

this.foo :string = 1

と書いてみたところ

this.foo :string = 1
         ^ Unexpected token :

型のチェックではなく文法的にアウトという表示になってしまう。これの正解は上でしなっとインストールしてあった

Class properties transform &#183; Babel

を使って

class Foo {
  bar :string = 1
}

のような形で書くことで解決です。

フィールド定義は 2017-07 時点では Stage 2 ( Draft )

今回利用した機能は 2017-07 時点で Stage 2 ( Draft ) の内容らしく、ES2015 の範囲外の機能となる

tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals

の Field declarations に書かれている内容になります。要は

class Foo {
  x = 0;

に対して type 情報を追加すると

class Foo {
  x :number = 0;

になるということなのですが、そもそもこの書き方が ES2015 の範囲外なので、Flow を利用してプロパティ / フィールドに type 情報を追加したい場合、Babel に

babel-plugin-transform-class-properties

を追加することが必要になります。

実際にはこんなアホな初期化ミスはないと思いますが、ローカル変数と違ってプロパティの場合はどこからアクセスされるか分からない長寿命な変数の一種と言ってよいので、type の恩恵はおおいにありそうです。

ちなみに

なんでこの書き方に思い至ったかというと、

TypeScript in 5 minutes &#183; TypeScript

を見て気づいたのでした。

結局 TypeScript の情報もチェックしてるよ!

あとはIDE的なものの対応がよく分からない

自分の場合は Emacs で読み書きして別途 flow や lint を動かすので構わないんだけど、IDE にこの辺の機能が組み込みの場合にどう扱ったらいいかは分からないです。実際 VS Code で開くと *.ts の場合は自動的に TypeScript と判定されて問題ないのですが、*.js の場合は type が書いてあると怒られます。

自動化の部分ではクリアできてるとは言え、いちいちコマンドを呼ばなくてよい、表示も統合されてて見やすいといった IDE のメリットがこのままではスポイルされてしまいます。

…と思ったら

Flow Language Support - Visual Studio Marketplace

を見ると

"javascript.validate.enable": false

するのが Known Issue になってた。うーーーーーーん。イマイチだなぁ。

ESLint - Visual Studio Marketplace

も、こまごま設定する必要があるんですな。この辺もバージョン管理にぶちこめるといいのかもしれないけど、それはそれでアレ。

まぁ今後の課題ということで。

Tags: JavaScript

*1 開発環境を Visual Studio に、動作環境を MS プラットフォームに限定できるなら迷わず TypeScript でもよいと思います。

*2 ※ ちなみに、今でも「Rubyのように全部式」を実現していた CoffeeScript の方が好きな部分はあります


2017-07-10 [長年日記]

_ sinonでmockしてみた

やってみた

内容は以下のような感じで、Node.js ネイティブ環境では動かないオブジェクトに対して、それでも期待通りに呼び出されたかどうかを確認するためのテスト。

describe('SpreadsheetApp return ActiveSheet', ()=> {
  it('called getActiveSheet() once', ()=> {
    // まず Node.js 環境で動かないメソッド呼び出しを stub out
    // SpreadsheetApp -> Spreadsheet -> Sheet の構造があって、
    // getActiveSheet() は中間のオブジェクトの持つメソッドで、どうやっても動かせない
    // これを spreadsheet.book().sheet() の構造にマップしようとしている
    // getActiveSheet() メソッドを持つダミーのオブジェクトを返すメソッドを stub out
    sinon.stub(spreadsheet, 'book').returns({getActiveSheet: function() { return {} }})

    // stub out したメソッドを mock にして期待する動作を設定
    let book = sinon.mock(spreadsheet.book())
    book.expects('getActiveSheet').once()

    // メソッドを実行したのち、結果の verify
    spreadsheet.sheet()
    book.verify()
  })
})

※ もちろんこれをコピペしても動きません

ポイント

  • 恐らくイマドキ JavaScript のテストのかなりの割合は Node.js 上で実行されている
  • Node.js 環境には存在しないオブジェクト、メソッドに対して期待する振る舞いを記述するために、まず stub out する(でないと実際に呼び出されてしまったらエラーになる)
  • そのうえで stub out したメソッドが期待通り呼び出されたかどうかを mock で verify する

で、たぶん合ってると思います。間違えてたら教えてくだしあ。

参考

Sinon.JS - Standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework.

この中の mock の記述、分かったような分からないような感じなんですよね。というか RSpec の時もそうだったんだけど、mock の説明の記述って汎用的に書かれているとほんとによく分からないので、個人的には RR の記述が分かりやすくてよかったのでオススメです。

RR - a test double framework for Ruby

RR は RR の記述そのものがすごくシンプルで分かりやすく、意味の方に集中しやすくてよいです。(が、RSpec 3+ とは完全には統合できないので注意が必要)

Tags: JavaScript

2017-07-07 [長年日記]

_ JavaScriptのデフォルト引数とundefinedの考え方がよく分かっていなかった話

結論

関数の引数がデフォルト値を持つ場合、 undefined を渡すことはできないので、そういう設計をやめよう

サンプルコード

以下は Node.js 6 と Chrome 59 で確認した。

デフォルト引数がない場合
> function bar(abc) { console.log(abc) }
> bar(undefined)
 undefined
デフォルト引数がある場合
> function foo(abc = null) { console.log(abc) }
> foo()
 null
>  foo(1)
 1
> foo(undefined)
 null

なんでこれに気づいたか

こんなコードを書いた。

bar(foo() || undefined)

function bar(name = null) {}

この場合、foo() の戻り値が false 相当の場合、bar() の中の name が undefined になることを期待していたが、実際にはそうはならなかった。

foo() || undefined

だけを評価した場合は foo() の戻り値が false 相当の際に期待通り undefined となる。

したがって、関数に渡ってくる部分での評価で undefined を受け取らないことになっているようだ。

ちなみに Babel で変換すると

var arg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : <default>;

になるので、あーそりゃ undefined 渡せませんよね、というのは分かります。

まとめ

  • 本来 JavaScript の関数の引数に undefined を渡すことはできる
  • ただし Node.js や ES2015 対応していてデフォルト引数を使える場合は渡せないような挙動になる
  • そもそも undefined を渡すとはナニゴトだという設計の方が正しそうなので、そうしよう
Tags: JavaScript