Islands in the byte stream

Technical notes by a software engineer

Rails.application.credentials.foo は fetch(:foo) にすべき

表題のとおりで、fetch(...) はエントリが存在しないと例外を投げるため、エントリが存在しないときに nil を返すアクセサよりもtypoに対して安全です。

こういうのは rubocop-rails でカバーできるといいなと思って起票して、良さそうというコメントはもらったんですが:

https://github.com/rubocop-hq/rubocop-rails/issues/42

いかんせん rubocop-hq/rubocop-rails 自体がまだ開発中なのでいつ使えるかわかりません。

こういうときにquerlyを使うといいですね。

(cf. プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream)

というわけでruleはこんな感じです。

rules:
  - id: kibela.prefer_fetch_for_rails_secrets
    pattern:
      subject: "Rails.application.'credentials.'key()"
      where:
        key: '/.+/'
        credentials: '/(?:secrets|credentials)/'
    message: "`Rails.application.secrets.fetch(...)` を使いましょう。"
    before:
      - "Rails.application.secrets.foo"
      - "Rails.application.credentials.foo"
    after:
      - "Rails.application.secrets.fetch(:foo)"
      - "Rails.application.credentials.fetch(:foo)"

SafariにはMediaQueryList.prototype.addEventListenerがない

ないと困るので、 polyfillをつくりました。MediaQueryList すら実体がないので、やむをえず mathMedia() を上書きしています。

// MediaQueryList.prototype.addEventListener.ts 

if (typeof matchMedia !== "undefined" && !matchMedia("all").addEventListener) {
  console.log('installing polyfill: MediaQueryList.prototype.addEventListener');

  const originalMatchMedia = matchMedia;
  self.matchMedia = function matchMedia(mediaQuery: string): MediaQueryList {
    const mql = originalMatchMedia(mediaQuery);
    mql.addEventListener = function (eventName: "change", listener: (event: MediaQueryListEvent) => void) {
      // tslint:disable-next-line
      this.addListener(listener);
    };
    return mql;
  };
}

なぜないかというと、このAPIを定めたCSSOMで、最初は addListener というメソッドでイベントリスナを登録していたのが、途中からDOM標準の EventTarget interface を実装するものとして addEventListener を利用可能にしたから、ということのようです。

https://drafts.csswg.org/cssom-view/#mediaquerylist

Note: This specification initially had a custom callback mechanism with addListener() and removeListener(), and the callback was invoked with the associated media query list as argument. Now the normal event mechanism is used instead. For backwards compatibility, the addListener() and removeListener() methods are basically aliases for addEventListener() and removeEventListener(), respectively, and the change event masquerades as a MediaQueryList.

こういう経緯で TypeScript の lib.dom.d.ts では addListener がdeprecated扱いではあるものの、素直に addEventListener を使えない、と。こういうのはpolyfillするのがいいですね。npmにもありそうですが、実装するのは簡単なので自前実装ということにしました。

なぜTypeScriptに失望してしまうのか

TypeScriptに対する失望は2パターンあって、その理由は理解できるのですが、いずれにせよそこでTypeScriptを捨てる判断をするのはもったいないと思っています。この2つの失望を感じたとしてもなお、TypeScriptには導入する価値があると思っています。

パターン1: 実はJavaScriptに対する失望である

そこらのブログやTwitterで観測していると、理由の7割くらいこれです。これは、TypeScriptが独立した言語ではなくJavaScriptへのトランスパイラ(言語変換ツール)であり、独立したランタイムを持たないことに由来します。

TypeScriptはJavaScriptの上位互換(=すべてのJavaScriptは、構文的にはTypeScriptとして正しく、コンパイルが通ればほぼJavaScript同じ振る舞いをする)なので、JavaScriptのいまいちイケてない言語機能をそのまま継承してしています。

なので、TypeScriptを単体の言語として評価するとありえないレベルのイケてない機能がたくさんありますが、それはTypeScriptではなくJavaScriptの欠点であることがほとんどです。

これについては仕組み上どうしようもないので、JavaScriptを学んでJavaScriptの罠にはまらないようにするしかありません。

パターン2: "noImplicitAny": true がつらくて失望する

追記: この件に関してはまた別の記事を書きました: エンジニアHubにTypeScriptの記事を書きました - Islands in the byte stream

"noImplicitAny": true というのは、暗黙の any を禁止するオプションで、tsconfig.json に設定します。

これをtrueにすると、型推論の効かない箇所(関数の引数など)で型アノテーションが必要になります。また、ライブラリのimport時にそのライブラリが型情報(.d.ts)を提供していることを要求します。

この「ライブラリに型情報を要求する」というのが制約として非常に強いため、チームの開発者全員がTypeScriptのエキスパートでないかぎりは、型付けに際して消耗を強いられます。そして消耗を強いられたメンバーは「TypeScriptを導入したらDXが下がった」と感じる可能性が高いでしょう。

私は "noImplicitAny": false を奨励しています。型付けに振り回されないように、がんばらないTypeScriptでいいと思っています。それでもJavaScriptに比べれば静的にチェックできる範囲がずっと広いので、DXの向上やアプリケーションの安定した運用には十分役にたちます。

アプリケーションコードではではなるべく型をつけたほうがいいので、ライブラリの型情報を要求しない弱い "noImplicitAny": true があればいいんですけどね。

最近は tsc --init で生成する tsconfig.json が "noImplicitAny": true をデフォルトにしていますが、強い気持ちでこれをfalseにして「がんばらないTypeScript」を実践することをおすすめします。

TypeScriptはこの先どうなるか

ほかに代替がないし、考えられもしないので、生き残るでしょうね。

単体の言語としてはもっとまともなaltJSはすでにいくつかあります。しかし、JSがブラウザで動作する唯一の高級言語(低級言語としてはWASMがあるので)である限りは、それの限りなく薄いラッパーであるTypeScriptは価値を持ちしつづけるでしょう。

もっとも、発展的解消として別の名前を与えられて規格化する未来はあると思います。たとえば、SwaggerがOpenAPIとして規格化されたように。

エンジニアHubに『「GraphQL」徹底入門』という記事を寄稿しました

書きました*1

employment.en-japan.com

GraphQLという規格そのものについての解説が前半で、後半はRailsアプリのWeb APIをRESTful APIからGraphQL APIに書き換えるというハンズオンです。

GraphQLの特徴やら Swagger, gRPC, etc との比較はわりとよく見かけるので、そのあたりについては最低限触れるにとどめて、GraphQLという規格について知るための入門記事として書きました。特にRelay (Relay Server Specification) についてはあまり日本語の説明を見ないので、この記事できちんと紹介できてよかったなと思っています。

サンプルコードは https://github.com/gfx/graphql-blog にあります*2

この記事は結構長いんですが、それでも発展的な使い方に触れる余裕はあまりありませんでした。このあたりはまたどこかでフォローできたらと思います。

この記事で触れていないトピックについては、私を含めた数名が書いている次のscrapboxに少しあったりはします。 ただしこのscrapboxは graphql-ruby 専用です。

Notes on GraphQL for Ruby

*1:執筆期間は1ヶ月くらいでした。

*2:余談ですが、このサンプルコードのwebpackの設定(webpack.config.js)は、GraphQLとは関係ないので該当記事では一切ふれていませんが、それなりに真面目に書いてます。

potatotips #56 で 「OSS開発のリテラシー / Android編」という発表をしました

追記: 加筆修正してGitHub projectにしました: https://github.com/gfx/android-oss-best-practices


paper.dropbox.com

(potatotips #56 (iOS/Android開発Tips共有会) - connpass)

久しぶりのpotatotipsでのLTでした。

「OSS開発のリテラシー」は他の言語版もほしいですね。誰か書いてくれないかな〜|д゚)チラッ

apollo-boost はサンプルコード専用と思ったほうがよい

追記: Apollo Boost は Apollo Client に統合される見込みのようです。Apollo Client 3.0 Roadmap · Issue #33 · apollographql/apollo-feature-requests · GitHub


TypeScript用のGraphQL clientであるところの Apollo ですが、これのGet Startedなどのドキュメントでは apollo-boost というパッケージを使っています。

www.apollographql.com

しかし、 apollo-boost はサンプルコードをシンプルにするための apollo-client などのラッパーと考えたほうがよく、プロダクションコードでは apollo-boost ではなく apollo-client および関連モジュールを直接使うほうがいいです。

以下理由:

  • apollo-boost は "zero-config" な apollo-client のラッパーだが、カスタマイズ性に乏しい
    • カスタマイズ不要なときに apollo-boost を使い、カスタマイズが必要になったら apollo-client を使え、という位置づけのよう
    • apollo-boost から apollo-client への移行ガイドもある:
    • Apollo Boost migration | Apollo Client
    • Apollo Boost is a great way to get started with Apollo Client quickly, but there are some advanced features it doesn’t support out of the box.

  • 特に apollo-client の強力な機能である "link" (Rack middleware のようなもの) をカスタマイズできないので、バッチリクエストや GraphQL subscriptions を利用する方法がない
  • エラーハンドリングも細かく制御したいときは apollo-client を生で使うほうがよい
  • 公式ドキュメントで import ApolloClient from "apollo-boost"import { ApolloClient } from "apollo-client" の2パターンがあるが、この2つの ApolloClient は(継承関係にあるものの)コンストラクタのオプションが全く違うので別物と考えたほうがよく、非常に混乱する

特に、 apollo を使うとすぐ batch request をしたくなるでしょうから、プロダクションコードでは最初から apollo-boost を使わないで apollo-client を使うほうが移行する手間が省けるというものです。

Reactをv16にアップグレードしたらuglifyjsの秘孔をついてハマった件

追記: Terser v3.10.11 でこの問題が修正されていることを確認しました。現在は collapse_vars: false というワークアラウンドは不要になりました。


webpackのproduction buildの話です。

Reactが悪いわけではないんですが、たまたま秘孔をつくコードがReactないし関連するReact v16に依存するライブラリにあったんでしょうね。

というわけで webpack.config.js の optimization の設定はこんな感じにしました。

if (production)
  config.optimization = {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          sourceMap: true,
          parallel: true,
          compress: {
            collapse_vars: false, // workaround for a minifier's bug: https://github.com/terser-js/terser/issues/120
            drop_console: true,
          },
        },
      }),
    ],
    noEmitOnErrors: true,
  };
  // ...
}

Code Review Meetup #4 で「歴史的経緯の説明 as code」という発表をしました

Sider社の開発する Querly を使うと「歴史的経緯の説明」をコード化できるよという話です。

Querlyの話は以前も書いたことがあります: プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream

Querlyは便利で可愛いやつなんですが「価値がわかる」ところに到達するのが難しいツールなので(ぼくなんて2年かかってようやく理解できましたからね!)、もうちょい公式ドキュメントがわかりやすくなるといいな〜と思いますね。あと querly init がほしい。

特定のファイルに対して一定のテストカバレッジを保証する

ある特定のファイルのテストカバレッジが、ある特定のテストファイル(群)を実行したときに一定のカバレッジ率であることを保証したいと思うことはありますか?私はあります。

そこで、 次のような spec/coverage_helper.rb を用意して、 spec/rails_helper.rb などから require すると、 COVERAGE_ASSERTION=app/models/ability.rb rspec spec/models/ability_spec.rb:99 のようなコマンドをCIで実行したときに一定のテストカバレッジ以下のときにCIがコケるようにしました。

# frozen_string_literal: true

class CoverageAssertion
  class CoverageAssertionFailure < StandardError
  end

  attr_reader :file_map
  attr_reader :highline

  def initialize
    require "highline"

    coverage_assertion = ENV['COVERAGE_ASSERTION']
    @file_map = Hash[coverage_assertion.split(',').map { |pair| pair.split(':') }]
    @file_map.transform_keys! { |key| Pathname.new(key) }
    @file_map.transform_values! { |value| value.to_f }

    @highline = HighLine.new
  end

  def format(result)
    failure_message = +""
    result.groups.each do |_name, files|
      files.each do |f|
        filename = Pathname.new(f.filename).relative_path_from(Rails.root)
        threshold = file_map[filename]
        if threshold && f.covered_percent < threshold
          failure_message << highline.color("#{filename}: expected at least #{threshold}% but got #{f.covered_percent.round(2)}%\n", :red)
        end
      end
    end

    if failure_message.present?
      raise CoverageAssertionFailure, "Coverage assertion failed!\n#{failure_message}"
    end
  end
end

if ENV['CI'] || ENV['COVERAGE'] || ENV['COVERAGE_ASSERTION']
  require 'simplecov'
  require 'simplecov-lcov'

  # COVERAGE_ASSERTION="$file1:$least_coverage1,$file2:$least_coverage2,..."
  # See also .circleci/config.yml
  if ENV['COVERAGE_ASSERTION']
    SimpleCov.formatter = CoverageAssertion
  else
    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
      [
        SimpleCov::Formatter::HTMLFormatter,
        SimpleCov::Formatter::LcovFormatter,
      ],
    )
  end

  SimpleCov.start 'rails' do
    # add_filter [...]
  end
end

Rails + rspec + simplecov という条件で動くはず。

cmake-js があれば node-gyp に依存せず NodeJS native addon を作れそう

github.com

cmake-js という cmake のラッパーを使うと、 node-gyp を使わずに NodeJS native addon を作れるみたいです。 node-gyp は Python 2.x に依存しているのが嫌なので、 node-gyp 依存をなくせるというだけで個人的にはけっこう嬉しかったりします。

ついでに NodeJS native addon の新しい C++ API であるところの N-API もちょっと試してみました (src/hello.cc)。これは、 NodeJS のJSエンジンである V8 API を抽象化して、V8 API のバージョンごとの差異を吸収する API layer です。N-API 登場以前の NodeJS native addon は NodeJS / V8 のアップデートですぐビルドできなくなっていたものですが、 N-API を使うようにすることで複数の NodeJS バージョンに対応することが容易になりそうです。

cmake-js + N-API で NodeJS native addon がかなり開発しやすくなりましたね。NodeJS はスクリプト言語としては十分に高速なので速度のために native addon を開発する必要はほとんどないのですが、既存の資産 の活用はもっと気軽にできていいので、native addon を開発しやすくなるのはありがたいです。