TypeScriptのanyみたいな「なんでも代入できる型」を分類する

安全でないもの - void * (C)

いろいろあるんだろうけど、Cから始めよう。void*にはなんでもキャストできるし、元の型に戻すこともできる。まあany。問題は変なことをやった場合。(Cはよくわからないから誰かが例を書いてくれ。)

キャストによって変な型が得られた場合の挙動は未定義なんだと思う。(知らないから、誰か仕様を調べてくれ。)未定義というのは、コンパイラとか計算機は何をしても良いという意味で、

  1. プロセスの実行を止める
  2. 最適化によってコードが消える
  3. プロセスのメモリを破壊しながら動き続ける
  4. 鼻から悪魔が出る

などのパターンがありうる。実際には大体SEGVで落ちると思うんだけど、それは仕様には含まれていない。

ここで「安全である」というのは「プログラムの実行中に未定義の状態にならない」という意味である。つまり、Cは安全ではない。

キャストの検査で安全になったもの - dynamic (C#)

動的型付けの言語が流行ったり、本質的に型を書くのが困難なパターンがあることが判明したりして、いくつかのプログラミング言語には「コンパイル時に型検査されない型」というのが導入された。C#で言うところの dynamic である。

string x = "";
dynamic y = x;
float z = y;

こんなプログラムはコンパイルできる(つまり静的な型検査が通る)が、実行してみるとz = yでエラーが起きる。このことから、float型の変数zstringを代入して良いかどうかを実行時に検査していることがわかる。つまりz = yのところでdynamicからfloatのキャストが挿入されていると考えて、キャストを実行時に検査していると解釈する。検査に失敗したらRuntimeBinderExceptionが発生する。

キャストに注目すれば、JavaObjectとかもこのグループに入る(プログラマによる明示的なキャストは必要なので、dynamicとはかなり書き味が違うけど、それはおいておくとして)。つまり、キャストは必ず実行時に検査されて、失敗したときになにが起きるかは言語によって定義されている。あるいは、C++dynamic_castも、失敗を判定できて失敗したときの挙動が定義されているという観点でこちらに入る。

さて、キャストの失敗でなにが起きるのかは定義されるようになったのだから、未定義ではない。つまり(少なくともこの面において)C#Javaは「安全」になった。

安全なんだけど検査が省略されるもの - any (TypeScript)

dynamicまでだと話が綺麗なんだけど、TSはまた厳しいところをついていて、ちょっと難しい。

TSではanyから他の型へのキャストは実行時に検査されない。じゃあ安全じゃないのかっていうと、安全である。つまり、JavaScriptとして実行されるので、キャストの失敗を見過ごすことによって発生する問題は、どちらにしてもJSのランタイムによって保護されているのだ。どうせ安全なんだから、キャストの際の型検査を省略しても良いという発想。これは、かなり強烈な性質で、Java以降のプログラミング言語にかなり広く共有されている「キャストのような静的な型検査では問題を検査できない操作には、実行時のチェックで補完しよう」という戦略から離れていることを意味する。すごい。

逆に言うと、この「キャストの検査」を諦めたことによって、TypeScriptではかなり柔軟な型が書けるようになっている。仮に、キャストの検査をやる方向でデザインするとすると、JSのランタイムで表現できる型しか使えないことになるので、けっこう厳しい。最近はクラスが入ったので良いんだけど、少し前まではobjectnumberstringarrayと……くらいしかなかった。「この式の型はobjectです(numberとかstringではないけど、どういうメソッドがあるのかはわかりません)」という型付けは、あまり実用的ではない。TSでは、オブジェクトの型もかなり詳細に書けるようになっていて、まあ現実的ではない感じのランタイム型検査になる。(もちろんやればできる。 )

そういうわけで、anyからのキャストで検査を省略するというのはまあ現実的な判断だと思うんだけど、意外性はあるのだった。

ちなみに、anyがなくてもTSの配列はcovariantなので、その点で型検査は健全ではない。

const xs: string[] = []
const ys: (string | number)[] = xs    // 本当はこの代入ができてはいけない

ys.push(1)

const s: string = xs[0]   // 本当はnumber

(どうせ一回anyを介せば代入できてしまうんだから、まあ気にせずにcovariantにするのが正しいのだろう。)

宣伝

特にここで書いた中ではTypeScriptに関係が深いのですが、「動的型付けの言語に型を付けようとする人類の試み」について一回話がしたいなーと思っていて、せっかくなので富山Ruby会議でやることにしました。

toyamarb.github.io

いろいろ人類が試行錯誤した歴史を私が理解している限りで解説して、「じゃあRuby (Steep) でどうすんの?」という話をします。みなさまふるってご参加ください。

GrillRB 2019にいってきた

こちらです。

grillrb.com

speakerdeck.com

話は大体いつものやつですが、ちょっと型を付けてプログラミングしていくことに踏み込んだ内容でした。*1

このGrillRBというのは、ポーランドのグロツワフという都市で開催されるカンファレンスで、大きな特徴として「会場が屋外」「提供される食事がバーベキュー」という2点があります。

f:id:soutaro:20190908000041j:plain f:id:soutaro:20190901000256j:plain

トークをしているとだんだん肉が焼ける匂いが漂ってくるという、楽しい体験ができました。屋外で話す、ということでスクリーンがかなり厳しいのではないかと想像しましたが、テントの中にテレビが二台あってまあ余裕でした。

小さい街で、いかにもヨーロッパという見た目で、楽しかったのですが、まあ建物を眺めているとすぐに飽きます。美術館に行ってみましたが、「昔の絵は大体キリストかマリア」「時代が少し下がると偉い人のポートレートになる」ということを学びました。

f:id:soutaro:20190830173010j:plain f:id:soutaro:20190830221702j:plain

上の写真は小人で、なんか好き勝手にその辺の建物を作る人が増やしていくらしいです。これはPC屋さんの前なのでキーボードを持っています。

楽しいので、来年以降、皆さんも参加されると良いと思います。(なお、スピーカー含めて大体の参加者はポーランドかヨーロッパからで「バスできた」みたいな感じで、「飛行機で14時間くらいかかって移動してるんだけど……」という難しい気持ちになれます。)

*1:あと例によって日本人がいなかったので、その辺を意識した導入になっている。

Sorbetについて

昨日のAsakusa.rbに行ったらSorbetについて聞かれたので、少し話したんだけど、ちょっときちんと確認しないまま話したらぐだぐだだったので、もう少しまとめておく。

全体的な話として、Sorbetはとても良くできているので、これで良いと思う人はそれで良いと思います。

$ srb --version
Sorbet typechecker 0.4.4306 git 31ef906fe5eb572865f2b5d3ea5805793554c676 built on 2019-06-25 06:45:18 GMT with debug symbols

Rubyと少し合わないところはあるので、その辺を挙げる。

1. サブタイピング

いわゆるDuck typingというやつが、まあ良くない。

sig { params(arg: Eachable).void }
def push_from(arg)
  arg.each do |x|
    @array << x
  end
end

みたいなの。この引数のEachableが特定のクラスなら良いんだけど、each が定義された自作のクラスを渡したりArrayを渡したりするとちょっと難しいことになる。

なるんだけど、workaroundは用意されているので、それで良い人はそれで良いと思う。

# .rbiに欲しいメソッドが定義されたモジュールを定義する
module Eachable
  sig { params(blk: T.proc.params(x: String).void).void }
  def each; end
end

# .rbにモジュールを定義する(このファイルは型検査しない)
# typed: false
module Eachable
  # これはincludeできればいいので空で良い
end

# 別に.rbにクラスなどを定義する

class A
  # このincludeによってAのインスタンスはEachableになった
  include Eachable

  def each(&blk)
    yield "foo"
    yield "bar"
    yield "baz"
  end
end

push_from A.new   # これがokになる

動かしてないんだけど、これでいけるはず。

「既存のクラスについても後からincludeすることでEachableにできる」というのがポイントなんだけど、Arrayでやろうとしたら型が難しかったので諦めた……

2. 型の構文

好みの問題なので、これが良いと思う人も世の中にはいるのかもしれない。

結構強烈なのは

def initialize(x:)
  @x = T.let(x, String)
end

のような「T.letメソッドでインスタンス変数の型を書く」だと思う。

3. Overloading

できないみたい。Sorbetの.rbiには複数のsigを一つのdefに対して付けることができないみたいだ。

Array#map とかはどうなっているのかというと、標準ライブラリは特別扱いになっているようにみ見える。 # typed: __STDLIB_INTERNAL などと書いてある。

で、これだと困るような気がするのは

def fetch_people
  if block_given?
    yield people.get(id)
  else
    people.get(id)
  end
end

みたいな「ブロックがあったらyieldして返り値を返す」パターンで、これ僕は結構書くので、ちょっと苦しいと思う。

4. Runtime

ライブラリに使おうとすると、Rubyコードの実行に言語内DSLのためのランタイムが必要なので、まあそれどうなのという。

ランタイムがあることの利点はあって、ランタイムの型検査ができるようになるし、Typed Structsみたいな便利ライブラリもあるので、なかなか難しいところだと思う。

感想

僕は、

  • 絶対にRuntimeの挙動を変えたくないし、gemにはRuntimeの依存関係を増やしたくない
  • 型の構文が苦しい(何日かすれば慣れるかもしれないけど)
  • Overloadingが全然書けないのはさすがに問題では
  • 実はStructural subtypingにこだわるつもりはないんだけど、今のworkaroundに必要な構文はさすがに苦しいと思う。

くらいの意見があります。

JSONやYAMLのデータ構造をチェックするライブラリStrongJSONのご紹介

JSONYAMLを使うと、かなり複雑なデータが作成できますが、これが意図通りの形式になっているかを確認するのは自明ではありません。XMLにはXML Schemaがありますが、そんな感じのものがJSONにも欲しかったので、作りました。

github.com

こんな感じで使います。

Schema = StrongJSON.new do
  let :phone, object(phone: string)
  let :email, object(email: string)
  let :contact, enum(phone, email)
  let :person, object(name: string, contacts: array(contact))
end

json = Schema.person.coerce(JSON.parse(input, symbolize_names: true))  # symbolize_namesが必要です

最新バージョンは1.1とかで、最近「人間にも読めるエラーメッセージ」を実現しましたので、みなさまご活用ください。例えばテストにある例なんですが、

TypeError at $.items[0].price: expected=numeric, value=[]
 "price" expected to be numeric
  0 expected to be item
   "items" expected to be items
    $ expected to be checkout

Where:
  item = { "name": string, "count": numeric, "price": numeric }
  items = array(item)
  checkout = {
    "items": items,
    "change": optional(number),
    "type": enum(1, symbol),
    "customer": optional(
      {
        "name": string,
        "id": string,
        "birthday": string,
        "gender": enum("man", "woman", "other"),
        "phone": string
      }
    )
  }

とか出せるので、少し頑張れば人間にも読めるんじゃないかなあ。そのままアプリケーションに組み込んで使っている例としては、 sider/goodcheckがあります。そのままアプリケーションに組み込めば良いということで、YAMLファイルとかで設定を書かせるときに、validationできて便利ではないかと思います。

JSON Schema

さて、ところで世の中にはJSON Schemaというものがあることをご存じの人がいるでしょう。JSON Schemaではダメなのかというと、まあ別に良いんですけど、

  • SchemaもJSONになっていて欲しい気持ちが別にない → Ruby DSLでSchemaを書くようにした
  • 「数字」とか「文字列」とかそういう感じがテストしたいのであって、「電話番号」みたいなデータ型はいらない

という感じです。

YAMLでは

RubyYAMLライブラリだと、まあ大体JSONみたいなデータ構造になれるので、それでいけます。JSON以外のデータ構造もできたと思いますが、そういうのはダメです。

どういう名前なんこれ??

RailsのStrong parametersからの借用です。Strong parametersは、クエリパラメータみたいな構造は上手く処理できるのですが、JSONみたいなのを扱おうとすると幸せな感じではないので作りました。

が、実はこのライブラリは5年くらい前から作っていて、当時のStrong parametersと今のstrong parametersの違いとかは知らないので、もしかしたらRailsので良いのかもしれません。

型はつくの?

つきます。JSONは、まあ色々あるんで any にしちゃうのが良いと思いますが、そいつからレコード型のデータ、例えば { name: String, contacts: Array[contact] } に変換すると思ってください。変換できないものはエラーになる。

一方で、この型付けが一番便利かというとそんなことはなくて「頑張れば型がつくと強弁できる」くらいの程度で、まあSteepを直さないといけないですねえ……

Balkan Rubyに行ってきた

balkanruby.com

Genadiが誘ってくれたので、Balkan Rubyに行ってきたのだった。話はいつものやつ。

speakerdeck.com

RubyKaigiでMatzとか id:ku-ma-me とかが話してたんだけど、大体そんな感じになっているはずです。私の言葉になっているけど、内容的には新しいことはありません。

トークの話はこのくらいにして、Balkan Rubyが良かった話をしようと思う。

オーガナイザが手厚い

ホテルを取ってくれるし、空港からホテルも手配してくれているし、ホテルについたらSIMもあるし、ご飯もあるしで、特になにも考えずにトークの内容だけ準備してSofiaに行けば良いので楽だった*1。2日前の午後にSofiaについたんだけど、そのまま既にSofia入りしているスピーカと晩ご飯になっていた。その流れで、だいたい毎食スピーカ同士でご飯を食べていた。

f:id:soutaro:20190516031004j:plain

これはRakiaという酒でブルガリア人はこれを飲ませてくる。強い(40〜50%)ので注意すること。

なんでこんなにスピーカの世話が手厚いんだ、と聞いてみたら「スピーカの数が少ないから」などと言われたので、まあ確かにね、という感じ。

Sofiaが楽しい

キリル文字とか建物とかにロシアとかソビエトみたいな感じがあって、ヨーロッパとも違って楽しい。と言いつつ、私にとって初のヨーロッパの国だったので他のヨーロッパ地域のことは良くわからん。町並みは綺麗で、晴れていると遠くに山が見えて大変に楽しい。公園には人々がいて、小さい子どもとかが遊んでたりするので、平和で良い気持ちになれる。特に夜遅くまで明るいので(緯度のせいか経度のせいかはわからない)、カンファレンスが終わって外に出たときに「まだ4時くらいかな??」と思ったら実は7時過ぎだったりするので危ない。

f:id:soutaro:20190518220010j:plain f:id:soutaro:20190516203049j:plain

Balkan Rubyの参加者は大体ブルガリア人なんだけど、スピーカはヨーロッパ各国からで、それに加えてアメリカ・アジアが数人という顔ぶれだった。こんなにヨーロッパ人が周りにいることはあんまりなかったので、ちょっと不思議な感じだった。

f:id:soutaro:20190519172231j:plain

Balkan Rubyが終わった日曜日は英語で無料の市内ツアー(2時間)があって、そういうのに行ってた人もいる。

f:id:soutaro:20190519183321j:plain

ラーメン屋があったのでうっかり入ってしまったが、会計のタイミングでカード支払いができないことが発覚し、ATMまで現金を下ろしに走った。それ以外は全部カード支払いでいけたから、油断していた。

*1:というと、まるで私がトークの内容を準備してからSofiaに行ったように見えるが、実際はそんなことはなくて、Sofiaでずっと支度をしていた。

Siderで使っているTSLintの設定

今のところこんなんです。

{
  "extends": [
    "tslint:latest",
    "tslint-immutable"
  ],
  "rules": {
    "array-type": false,
    "arrow-parens": false,
    "interface-name": false,
    "interface-over-type-literal": false,
    "max-classes-per-file": false,
    "max-line-length": false,
    "member-access": false,
    "object-literal-key-quotes": [true, "as-needed"],
    "object-literal-shorthand": false,
    "object-literal-sort-keys": false,
    "ordered-imports": false,
    "quotemark": false,
    "semicolon": false,
    "trailing-comma": false,
    "variable-name": [
      true,
      "ban-keywords",
      "check-format",
      "allow-leading-underscore",
      "allow-pascal-case"
    ],
    "whitespace": false,
    "no-angle-bracket-type-assertion": false,
    "no-shadowed-variable": false,
    "no-namespace": false,
    "readonly-keyword": [true, "ignore-class"],
    "no-trailing-whitespace": false,
    "no-implicit-dependencies": [true, ["i18n-js"]],
    "no-submodule-imports": false
  }
}

特に解説はないのですが、

  • スタイル関連は基本的に無効
  • なんか怒られる度に無効にしていく
  • == って書くと怒るやつは欲しい

みたいな雑な設定でやっています。

一つだけ有効にしているプラグインがあって、tslint-immutableです。これは我々がreduxを使っているからで、stateのためのinterfaceのattributeをimmutableにするのを忘れないようにしています。このプラグインはなかなか便利なのですが、一つ問題があって、ある種の型適用に immutable と書きたくないという話があります。

こんなん。

const routes = {
  assignSeat: new ApiRoute<{orgId: number, memberId: number}>(
    "/api/c/gh/orgs/:orgId/members/:memberId/seat_assignments"
  )
}

const url = routes.assignSeat.stringify({ orgId: 123, memberId: 456 })

このApiRouteというのは、APIリクエストとかで使うURL生成のためのクラスで、 orgId に文字列を渡したり null が入ったりすることを防げる優れものですが、ここでいちいち readonly と書きたくないという問題があります。良い感じのオプションがないし、どんなときに readonly と書かなくて良いかしばらく考えたけど判定方法を思いつかないので、もう諦めてしまって誤爆が出ると無視するようにしています。(Siderを使います。)

Rubyでnilとfalseを区別する方法が他にもある話

soutaro.hatenablog.com

他にもいろいろあるという話をTwitterで聞きました。下のツイートから大体見られるんじゃないかな。

TracePointで私のアイディアを騙すとか、引数の評価に入れるとTracePointを回避できるとか、 $@false を代入すると例外が上がるとか、 false..nil にして確実に例外が上がるようにするとか、いろいろな方法があることがわかりました。Rubyの勉強になりますね。

今の時点で決定版かなーと思っているのは

ですかねぇ。(わかりませんが。)

一つだけわかったこととしては、「Ruby 2.3より前では nilfalse が区別できなかった」というのは間違いかも、というところで、一筋縄ではいきませんでしたが2.3以降でやるときと同程度の仮定で nilfalse を区別できていたような気がします。

ちなみに、「 is_nil? を呼び出す方法がない」というのがオチのつもりでした。(誰かに上書きされないことが保証できないので。)