Chrome に PWA for Desktop が来ていたので next-editor で試した

追記: Canary じゃなくてもいいらしいのでタイトル修正した。が…実装具合はよくわからない

今年中に来るとは聞いていたやつ。要はウェブアプリを デスクトップアプリ化する。Electron と違って Chrome の Sandbox と同じ権限で動いている

試した

Chrome Canary 落として有効化する

https://i.gyazo.com/82888deb2ee0fce09a23c245c70d0dbc.png

最近作ってる PWA のGit 組み込みエディタの next-editor に試してみた。特に理由がないが PWA 対応していた。オフラインでも Git が動くエディタ。

Next Editor を開いて 「Next Editor で開く」を選択

https://gyazo.com/f6654e1900831b8f83f429fa3b4d1e04.gif

https://i.gyazo.com/cf00e83c14101b2bd7fea2cd897d1981.gif

最高です。

感想

デスクトップPCという世界は業務以外でシュリンクすると思っているのだが、業務やオーサリング系はおそらくいまと同じPCか、それと同じ横長の画面幅のタブレットが長く生き残るだろう。next-editor はそこを狙って作っている。

Electronでアプリを作っていた経験としては、人はそれがどういう実体であろうとも、アプリを起動するメタファとしてのデスクトップアイコンに惹かれるところがある。その点でDock に入るのはすごく嬉しいし、それだけで価値がある。Electronのプロセスモデルの万能性(や、それが引き起こす脆弱性)なんかより、ただ開けるという事実のが大事なんだと思う。

最近まで PCでPWA化するメリットを具体的に説明できていなかったが、とくにツール類はデスクトップ化できるというのが売りになる時代が来ると思っている。

Slay the Spire: 3キャラ目

3キャラ目追加されてたので久しぶりにやった。 たぶん4回目ぐらいの挑戦でクリア。

Pyramid + Cursed Key と引けたので理想的な展開。最終的なビルドどんな感じになったかはこの動画の通り

youtu.be

だいぶ前に紹介記事を書いたけど、売れてるみたいでよかった。

デッキ構築型ローグライクRPG『Slay the Spire』100万プレイヤー突破。わかりやすく奥深いゲーム性が人気を呼び、Steamの定番タイトルに | AUTOMATON

漸進的型付け言語の時代に必要なもの

最近では、Gradual Typing、漸進的型付けと呼ばれる型システムを備えた言語(拡張)が増えてきています。

次のようなもの

flow/pyre-checker/hack と facebook 製が多いですね。

この記事は、それらを使う動機と運用について書きます。この記事の出発点として、 おそらく TypeScript/Flow で発生した問題が後発の言語で発生すると思っており、それらを使う方や、設計する人への提言でもあります。

自分は昔 https://github.com/mizchi/TypedCoffeeScript というAltJS作ろうとして、実装のツラミはなんとなく知ってるつもりです。ホビーレベルで作るものではなかった…。

アノテーションの再評価

一昔前の Web プログラミング言語のトレンドは動的型付け一辺倒でしたが、その時代も終わり、静的な型宣言を再評価するフェーズが来ているように思います。

この背景には、おそらく Web プログラミングの規模が年々肥大化しており、動的検査のコストが増してきたのが理由にあるでしょう。

WAFの考え方だと、型をつけづらい外部IOであるところのHTTPリクエストを受け、HTML文字列を返す、という世界観では、型宣言は単に「おまじない」を多くするだけの邪魔者だったかもしれません。外部IOに型をつけづらいのは、外部IOの本質的な問題ではあります。

しかし今では、どのWAFも内部では分厚いビジネスロジックを持ち、実質的に静的なフィールドを持つORMを読み書きし、何らかの型を暗に想定する JSON を返す、という風にトレンドが変わってきました。型やIDEの支援なしにコードを育て続けるには、逆に高度なモジュール分割のノウハウや、状況に応じたストラテジーが必要になっています。

それらの肥大化の対応として、ドキュメントを書く文化や、テスト駆動も普及してきましたが、一周回って「検査可能なドキュメント」としての型アノテーションの価値が再評価されたように思います。

誰のための型か

注意してほしいのは、ここでいう型宣言の需要は、人間のために書く、ドキュメントとしての型アノテーションで、コンパイラに効率よくランタイムを生成してもうらための型ではありません。これを混同している人が多いですし、「高水準な、良く設計されたプログラミング言語」はそれらを区別せずにプログラマに書かせようとしてきます。(Rust などは低水準を目指しているので明示的に区別します)

言語としてその方針は間違ってないですが、使う側や、言語を選定する側は区別しないといけません。抽象メモリマシンである C や、型の表現が未発達だった時代の Java の冗長な宣言などに引っ張られて静的型付けを批判する人がいますが、それらは現代の静的型付け言語ではないです。最近の推論機は優秀で、見た目上は動的型付けと同じものを書けます。

また Language Server Protocol といったエディタのためのプロトコルによって、静的解析できるメタデータの重要性も上がっています。

何の機能をもってどう表現するかは、 私と型システムとポエム - The curse of λ という記事で、よく雰囲気が表現されています。

漸進的型付けの登場

What is Gradual Typing: 漸進的型付けとは何か - Qiita

基本的に、動的型付の言語に後付で型宣言を追加するものです。このとき、これらが新しい言語やコンパイラと言うのはちょっと微妙なところで、例えば TypeScript + Flow は拡張文法のパーサ + 拡張文法を取り除くだけのコンパイラ + 静的検査という組み合わせで、どちらというと Linter などに立ち位置が近いです。

自分が知る限り(主に TypeScript, Flow, Dart)、こういう特徴があります。

  • 未知の変数は any 型であると仮定する
  • あらゆる変数の型は any 型にアップキャストできる(Top)
  • any 型はあらゆる型にダウンキャストできる(Bottom)
  • あらゆる型は any 型と合わせて操作すると any にアップキャストされる
  • 自分で宣言した型を扱う限り、型エラーが発生する
  • アノテーションはランタイムに関与しない

(typescript の scrictAny や noImplicitAny オプションはこの挙動をより厳密にコントロールできます)

つまり、こういうコードが通ってしまいます。

const x: number = 1
const y: any = x
const z: string = x + y // pass

これが駄目だという話ではなく、型システムを後付している以上、柔軟性の方が重視されます。というかそうでないと、コンパイラを納得させられずに開発が「詰む」ことがあります。 any を書いた際に守るかどうかは自己責任です。

「型アノテーションはランタイムに関与しない」が特徴的で、型アノテーションを取り除くコンパイラとともに実装されるのも合わせて、なんというかそれ自身では控えめなコンパイラです。その結果として現れる挙動として、Flow/TypeScript は int や float の宣言はできず、必ず number として扱う必要があります。

また、既存のものに無理矢理型を当てはめるという特性上、Generics、Union Type, Subtyping といった高度な型表現を備えているのが事実上必須な特性となっています。

type X<T> = T | null | [T, T] | Array<T | 1> | 'yeah!' | 1 | 2

こんな風になったりしますね。

元は動的型付けからの発展だと思うと、この柔軟さや、any への妥協は必要なラインだと思います。

漸進型付けの利点

  • 既存の資産をそのまま利用できる
  • 「自分で書く範囲のコードは」厳密に運用することができる

一応言っておきますが、僕は動的型付の言語をすべて否定しているわけではなく、大規模なコードに適用するのはそぐわないという意見です。

一つのことをうまくやるUNIX哲学に照らし合わせても、文字が来て文字を返すぐらいに抽象化されたエコシステムでは、型宣言はそこまで重要視されないでしょう。たとえば npm のほとんどそういう世界だと思っています。

その上で、動的型付けのノウハウと資産を活かしつつ、その利用者になりつつ、巨大なアプリケーションを構築する手段として漸進型付けは有用だと思っています。

漸進型付けで発生した問題

ここからは自分が TypeScript と Flow の運用で発生した問題について。

TypeScript/Flow では、npm の資産をそのまま動かせる、というかランタイムに全く関与しないのですが、基本的に、扱うライブラリの型宣言はないです。

https://github.com/DefinitelyTyped/DefinitelyTypedhttps://github.com/flowtype/flow-typed/ という型定義の集積リポジトリがあります。ただしこれらが使い物になるかというと、おおよそ使えつつも様々な問題が起きます。

  • そもそも型が付けられるインターフェースではない(redux.combineReducers など)
  • 作者が型定義の運用に興味がない(PRを受け付けてくれない)
  • ↑の問題の結果、作者以外によって書かれた型アノテーションが間違っている
  • Generics がある以上、型定義が一意に定まらず、定義者の主張が強く出てしまう

そもそも型が付けられるインターフェースではない(redux.cobineReducers など)

たとえば、Redux の combineReducers は (State, Action) => State の reducer と呼ばれる関数を複数合成して、新しい (State', Action') => State' の新しい関数を生成する関数です。

combineReducers({
  counter, user, auth
})

これで、合成される State' は次のような型になります。

{
  counter: {...},
  user: {...},
  auth: {...}
}

これらをTypeScript/Flowの推論器で一般化するのはちょっと難しく、flow では専用の $compose という型で実装されようとしていますが、いまいち使いづらいので、結局推論器に任せずに自分でその型を宣言しなおすことになります。

これにかぎらず、JS の昔ながらのイディオムだと options: boolean | { isXXX: boolean, ... } みたいな引数が多く、型を付けられなくはないが、ダイナミックすぎて、ユニオンタイプを酷使するみたいな状況が起こりがちです。

作者が興味がない | 作者以外が書いた型が間違っている | 型の解釈の意見が合わない

そもそも言語拡張なので誰もが使うというものではなく、立場が弱いです。作者が興味が無いから PR が無視されたり、メンテ出来ないからごめんね、といってリジェクトされます。

また、Generics によって型の一意性はないです。

type Foo = { a: number, b: string }

という型があったとして、その定義は

type A<T> = { a: number, b: T }
type Foo = A<string>

なのか

type B<T> = { a: T, b: string }
type Foo = B<number>

なのか、使う人によって解釈が異なります。作者以外が書いてる場合は尚更です。ユーザーが使いたい時の値が Foo だとして、ライブラリとして提供される型の表現が A なのか B なのか、解釈に依存します。またどれぐらいの厳密なのかも方針次第で異なって、単に any で型名がわかるだけだったり、逆にオプショナルな値が解釈違いで必須になっていたりといった手続きが頻出します。

また、厳密な型定義ができるように Generics を大量に使ってしまった結果、高難易度な型パズルが頻出するライブラリが疎まれたりします。DefinitelyTyped/index.d.ts at master · DefinitelyTyped/DefinitelyTyped · GitHub

気に食わないなら自分で書き治すか、諦めて any にキャストする必要があります。

実際の運用について

興味がないのものを握りつぶす

実際に型がないと困るのは自分の扱うコードと接する範囲です。これらを厳密に扱う必要がないときは、単に無視します。 これに型を書いてもいいですが、現実に扱うすべてのコードに型をつけるのは現実的ではありません。よほど暇な時でしょう。

// declaration.d.ts
declare module "a";
declare module "b";
// flow-typed/lib.js.flow
declare module "a" { };
declare module "b" { };

握りつぶした上で、段階的に自分が興味がある範囲の型を徐々に追記していく、ということが多いです。

外部IOの出口を押さえる

とにかく大事なことは、自分が扱う範囲の型を守ることです。

function parseXXX(input: {args: string[]}): {data: string} {
  // input から文字列を取り出して加工する
  return { data }
}

const {data} = parseXXX(input as any)

input にはもっといろんなプロパティがありますが、ここでは args にしか興味がないとして、 最終的に data: string が帰ればOK、というやつです。

これは 最終的に string にダウンキャストして扱っています。ここは型がない危険な領域ですが、まあ頑張るとする。

express のようなサーバーを扱ってると、うまく型がつかず、こういう風にラップしていることが多いです。

後発の言語へ言いたいこと

Python 3.5 や、もしかすると Ruby 3 にも型が入るかもしれません。

TypeScript/Flow はそれ自身の立場が弱いので、ライブラリへの型定義ファイルのPRが受け入れられず、外部の型集積は結構な地獄になってしまいましたが、静的型付けを受け入れられない作者がおそらく一定数いる以上、どの環境でもないよりはマシです。

ただし、それらは握りつぶせる必要があります。原則的に型宣言や型推論ありきで運用するのは、別の言語として再スタートしない限りは不可能です。

また、既存のコードが型を想定していない以上、推論器の限界で表現できないものがたくさんあります。また、逆説的ですが、表現力がありすぎる型とその型推論は、使う側も結構辛いです。

たとえば、 flow の Redux connect の型定義はこうなっています

declare export function connect<Com: ComponentType<*>,
    A,
    S: Object,
    DP: Object,
    SP: Object,
    RSP: Object,
    RDP: Object,
    MDP: Object,
    MP: Object,
    RMP: Object,
    ST: $Subtype<{[_: $Keys<Com>]: any}>
    >(
    mapStateToProps: ?MapStateToProps<S, SP, RSP>,
    mapDispatchToProps: ?MapDispatchToProps<A, DP, RDP>,
    mergeProps: MDP,
    options: ConnectOptions<S, SP & DP & MP, RSP, RMP>
  ): (component: Com) => ComponentType<$Diff<ElementConfig<Com>, RMP> & SP & DP & MP> & $Shape<ST>;

パッと見、よくわかりません。推論機の癖を読み切ってジェネリクスを一つずつ解いて型パズルを完成させる必要があります。単に無視されてアップキャストされることのほうが多いです。そして割れ窓になります。

matz は Ruby に型宣言を入れずに推論機でどうにかしたいと言っていましたが、おそらく後方互換性を保ったままだと不可能だと思います。それはそれとしてドキュメントとしての型宣言は選択肢として提供するべきではないでしょうか。

Flow や mypy みてると思ったのは、むしろ言語として実装するのは型アノテーションのフィールドの構文予約だけでよくて、その実装はコミュニティに丸投げでいいのかもしれません。

最後に

ネガティブな点を多々挙げましたが、これらは 0 を 1 にする際の、型宣言がなかったものをあるようにするための一歩のための苦しみで、これ自体は間違いなく進歩だと思います。

最初から型がある言語でやればいいのでは?というツッコミはもっともなんですが、サーバーサイド以外はプラットフォームによって言語を選べないことが多く、JavaScriptはその最たるもので、ある種苦肉の策であります。

個人的には単一の推論ルールを持った処理系を、環境によってその厳しさを調整できると嬉しいような気がしていますが、そういう柔軟性がある言語はいまのところ無いですね。

新技術の紹介する際の「魂が震える」テキストのパターン

これは自己観察の結果で、自分が新しい技術の採用を行う際にアジる記事のパターン、個人的に「魂が震えるシリーズ」と呼んでるんですが、それがどういう文章構造を持つことが多いかメタ的に解釈したものです。

単に誰もがこうすればいいという話ではないではないです。功罪あると思ってます。


導入

  • 新技術の既存の文脈での解釈
  • +αの示唆
  • 仮想敵の宣言

概要

  • 説明
  • ポテンシャルの例示
  • 極端な例の例示
  • 現実的な制約の存在で現実に引き戻す
  • ユーザーが知るべきことを要約

実例

  • 既存の技術とのアナロジー
  • 古い手法から進化している点を指摘
  • 今の手法の問題点をいかに解決してどんな未来が来るか

応用

  • 既存の考え方を、あたらしい技術で再解釈
  • 本来は無関係だった他の技術との親和性を指摘

課題

  • 新しい技術ゆえのエコシステムのなさを指摘
  • 構造上の欠陥を指摘
  • まず取り掛かれる現実的なエントリポイントを例示

未来

  • ここまで読んできたなら良い面悪い面わかってるはずだ、といって再読を促す
  • 俺はこの技術にベットすると宣言し、お前はどうだ、と煽る

いつ ReactNative を使っても大丈夫か

AirBnb がReactNativeをやめることが話題になってますね。

medium.com

以下私見です。

RN採用可否のフローチャート

自分がRN使いたいといって相談された際にはこういう感じで返してます。基本的にはExpo 採用可能か否かで判断してます。 Expo ではじめる ReactNative 開発環境 - Qiita

  • プラットフォームごとにUXを突き詰める必要がある => RN やめとけ
  • Q: 社内にモバイルのエンジニアがいない
    • YES:
    • NO:
      • Q: Web開発者多数 + モバイルエンジニア少数の環境である
        • YES: => RN採用可能
        • NO: 別の案を検討
  • とにかくエッジなのを採用してモチベーション上げたい => Flutter
  • C# 派 => Xamarin
  • Web標準派 => PWA

Expo を推してる理由は、RNの根本的に不安定な部分を、比較的安定している Expoに押し付けることができるからです。Expoを使わない場合、現時点ではRNのバージョンアップの追従で3プラットフォーム全てに習熟する必要があり、技術的な要求レベルも割くべきリソースも必要です。

正直なところ、 RN も PWA も、現時点では貧者のツールなのは否定できません。また、RN 開発で必要なノウハウはWebのスキルと言うより、もはや GUI 開発のスキルなので、JSが使える以上のスキル転用は望めません。僕のようなSPA開発者が漲ってコード書ける環境、という方が近いでしょう。

UXを突き詰めたい、というのも温度感次第で、突き詰めたくないと思ってる人はいないので、程度問題です。簡単な答えはなく、結局チーム構成に依存します。AirBnbのような会社なら重いパラメーターでしょうが、とりあえずAppStoreのディスカバリーに載せたい程度の気持ちで作ってるならRNでも十分だと思います。色々いいましたが、いわゆるウェブサービスのモバイルアプリは、ほとんどはこれに該当すると思います。

以下ツイートから引用

ブラウザ上で完結するGit組み込みエディタ作っている

PWA-Editor(仮)

https://i.gyazo.com/e95abffcddcb0f21f4b004ae076e34ef.gif

デザインとかは適当なんだけど、コンセプト的にどこまで実装可能かの検証を一通り終えた。頑張れば本格的なものが作れそう、という手応えがある。 IndexedDB バックエンドに fs 動かして ismorphic-git を動かしている。 UIは全然足りないが、 ポテンシャル的には GitHub に push できることも検証済み。ServiceWorker でオフラインで動くようになっている。

デプロイ先は https://nervous-kilby-73c9b0.netlify.com/

開発中のものなので、予告なく互換が壊れることがある。

動機

Chromebook 買ったんだけど、やはり開発機として使うには厳しい気持ちがあった。主にまっとうなエディタがないのが辛い。cloud9 とか試したけど、辛かった。

フロントエンドのツール周りはJSで完結して PWA でオフライン化できるのは検証済みで、 isomorphic-git が使い物になる品質だったので、一旦は GitHub を実質的なバックエンドとして編集したファイルの push/pull/clone できるもの、というゴールを設定した。

うまいことやれば金になりそうな気がするので、まだソースの公開はしない。これを利用したビジネスプランがあって金を出せる!みたいな人がいたら twitter @mizchi まで教えてほしい。

前作ってたエディタは、コンセプト実装終わった段階のセルフレビューで、使い心地が微妙、という感じで、お蔵入りしてしまった。前回の反省を生かしていい感じに進めたい。

ゴール

非エンジニアを開発ワークフローに巻き込むのに足りないのは Git 周りのツールだと思う。Git がブラウザで動いて push できれば、いい感じにブラウザ上のUIから特定ドメインの作業を手軽に使えるようにして、markdown みたいなドキュメントとか、フロントエンド系のプレビューを組み込めばいい感じにできそうな気がしている。

React Component 視点でのアトミックデザインの解釈といくつかの疑問

フロントエンドの中でも、JS書くプログラマと、CSSを書くマークアップと、デザインカンプを作るデザイナで、コンポーネントという概念がズレる。だいたいこれらが一人だったり兼任だったりで1~2レイヤーの開発ステップになるが、完全分業だったり人が多くなると混乱の元になる。

誰かが決定的に間違ってるというつもりはない。正直、どっちかというと本来のデザイナ側の用語定義に倒した方がいい気がしているが、プログラム上の都合もいろいろ混ざってきて、話が簡単ではない。

自分の理解が間違ってる可能性もある。この記事はレビューをもらうために書いている側面もあり、指摘されたら追記していく。

読んだもの。

Atomic Design の大雑把な理解

基本的にはあるコンポーネント所属するドメインではなく、独立した稼働単位(Atoms)でコンポーネントを分割して、それを積んで(Molecules)、それらを稼働単位(Organisims)でまとめ、ページ(Page)は複数の稼働単位を持つ、という考え方、と理解している。

自分は Reactの src/components はこういう構成を取っていることが多い。

atoms/
  Text.js
  Button.js
  Comment.js
molecules/
  CommentList.js
organisms/
  Header.js
  UserProfile.js
pages/
  User.js

コンポーネント名は仮のもの。

ゴールの一致

プログラマ視点だと、コンテキストを作るのが難しいコンポーネントを storybook で作れたり、 storyshots でスナップショットテストできたり、 puppeteer でスクショとったりできて嬉しい。あと地味に嬉しい点で、粒度別にアルファベット順になってエディタで読みやすい。

コンポーネント境界と分割指針がズレる

ここから問題。

デザインとプログラムで、「コンポーネントが独立稼働できる」の単位が違う。

プログラマ的にはコンポーネント定義の再利用性で決めることができる。例えば React のプログラム的に独立した atom を発見するのは簡単で、他の Component を import していない、ということになる。

しかし、デザイン上の出現単位とってプログラム的な依存ステップは必ずしも一致しない。そういうときは、自分はさらにディレクトリを掘って分解している。

atoms/
  Button/
    ButtonIcon.js
    ButtonLabel.js
    index.js

1ファイルに押し込めないのは、コードが長くなって可読性が悪くなったりテストしづらい、というプログラム上の問題で、後は import path を深くしたくないので、 ./atoms/Button で import できるというのは変えない。index.js から import するだけ。index.js 以外は、「外から見てプライベートなモジュール」としている。

ただ正直 Atoms と Molecules はプログラム上の境界が曖昧でいつも困っている。上の例だと atoms/Icon.js になるのかもしれないが、そう出来ないときもある。

こうして、プログラム上では Atoms が Atoms に依存したり、Molecules が Molucules に依存しそうになって、(さすがに Atom が Molecules に依存することはないが)都度解釈をこねくり回している。

プログラム上にしか存在しないデータパス(props)

プログラマ的には、React Component は基本的に props のデータパスを設計し、末端でデザインが表出する、というマインドセットになる。

逆に、デザイナ的にはデザインモックから起こしていくので、その過程にデータパスは存在しない。プログラマはそのデザインモックを見て、props 渡す経路を考えたり、 connect する単位を考えることになる。

コンポーネント A - B はデザイン上独立しているように見えているのに、A - B のデータパス上の関係においては依存があって、実質的に紐付いている場合、これは Atom か Molecule かの解釈が立場によってズレる。

自分は、基本的には Organisms から redux へ connect にするように心がけるが(これは React のパフォーマンス的な理由もある)、デザイン的な稼働単位に忠実に分割すると、 Atoms に直接 connect せざるを得ないケースも出現する。

Pages はデザインモックなのか

Pages は 「ページ単位のデザインモック」単位と定義されていることが多いが、 実データを入力に持てる JS ではページ単位の入力という単位に pages を当ててることのほうが多いような気がする。自分はとりあえず Router でマウントする単位としている。

この辺や Templates、いろんな実装を見たが人によって解釈が違う。 templetes/DefaultLayout.js みたいな 「ヘッダーとその中身」みたいなコンポーネントがあったのもみた。(流石にこれは語感から拾っただけだと思うが)

React や storybook を使うとき、storybook が実質的にデザインモックの実装になったりするので、 Pages の実装では、モックであるというというのは忘れたほうが良いのではないか。

また、例示した DefaultLayout のような、Layout 情報だけを持っているユーティリティ的なコンポーネントがどこに所属するか難しい。Row や Column などは Atom でいいのか? それに相当する言葉を自分が知らないだけかも。

おわり

とりあえず自分が決めているのは、デザインガイドから作るのではなく、大きな Organisms を作りながら、ユースケース単位で小さなコンポーネントに分割していく、というフローを取っている。最初にデザインガイドをトップダウンでみっちり決めてもあんまり使われない。

正直一人で作るときは完全にプログラマ都合で作るのだが、大規模だとそうもいってはいられなくなる。

間違っていたり、こうしたらいいい、というのを指摘いただけると嬉しいです。