Open141

.notes

Takanori IshikawaTakanori Ishikawa

JS/TS の Prettier のような単一のコーディングスタイルを強制する Formatter は書きやすさ、読みやすさともに優れている。何が楽って、コードのリファクタリングや編集をしているときに、雑に書いても、

constructor(data: MemberInitializer) {
  this.organizationRole = data.organizationRole;
  this.user = data.user;
    }
    static fromResponse(data: TrainingJobResponse): Member {
      this.organizationRole = data.organization_role;
  this.user = new User(data.user);
}

VSCode で保存すれば、瞬時にキレイにしてくれる。

constructor(data: MemberInitializer) {
  this.organizationRole = data.organizationRole;
  this.user = data.user;
}
static fromResponse(data: TrainingJobResponse): Member {
  this.organizationRole = data.organization_role;
  this.user = new User(data.user);
}

「普通、こんなふうに書かんやろ」と思うかもしれないが、別のテキストエディタ(検索/置換がやりやすい)でコード片の整形や変形をやってからコピペすることが多いので割とある。

しかし、Python のように、Statement のブロックを表現するときにインデントを用いる言語だと、この方法がうまくいかないことがある。コンパイルエラーならいいが間違った解釈をされると痛い。

Python の場合「インデントを強制させる = 読みやすい」という意味もあったと思うが、その需要は自動 Formatter で満たされているだろう。あと、現実的に長いコードになると、インデントによるブロックは、{...}do...end による明示的なブロックにくらべて特別読みやすいとも感じない。

Takanori IshikawaTakanori Ishikawa

Wren

System.print("Hello, world!")

class Wren {
  flyTo(city) {
    System.print("Flying to %(city)")
  }
}

var adjectives = Fiber.new {
  ["small", "clean", "fast"].each {|word| Fiber.yield(word) }
}

while (!adjectives.isDone) System.print(adjectives.call())
  • 所謂 C 族シンタックス
  • 動的スクリプティング言語
  • C で書かれている。VM は約 4,000 行でコードも読みやすい
  • 依存ライブラリがない
  • Erlang のプロセスっぽい Fiber がある。メッセージ・パッシング
  • 何故、Wren はこんなにも速いのか?
    • いくつかのベンチマークで Ruby や Python よりも高速 [1]
    • VM は Bytecode Interpreter であり、JIT は使っていない
    • すべての値は 8 bytes の倍精度浮動小数点として表現される
      • これは number type でもあるので数値計算に余計なオーバーヘッドがない
      • 数値以外は NaN の使われていないビットで表現. NaN boxing
      • 似た手法としてポインタの特定ビットにフラグを立てて、その場合は比較的小さい整数とみなす、などがある. Pointer tagging
    • Wren のオブジェクトはすべてクラスのインスタンスであり、クラスは実行時に変更できない。なので、コンパイル時にインスタンスのレイアウトが分かる。また、プロパティにアクセスするときもハッシュテーブルから検索する必要はなく、直接、プロパティのアドレスにアクセスするだけで済む。
    • 継承ツリーは固定で変更できない。そのため、すべての継承されたメソッドはそのクラスにコピーされている。メソッドを探すときに継承ツリーを辿る必要がない
    • Wren のメソッドは引数の数でオーバーロードされるので、文法的に間違った数の引数で呼び出せない。そのため、実行時にチェックは不要
    • VM の命令ディスパッチは Computed Goto で実装
    • 1 pass compiler なのでコンパイルも効率的
  • Wren の速さは言語デザインの選択によるところが大きい
    • パフォーマンスに影響するところの自由度(動的)は低い
    • C99, IEEE double precision floats が必要
脚注
  1. Performance – Wren ↩︎

Takanori IshikawaTakanori Ishikawa

V programming language

fn main() {
  areas := ['game', 'web', 'tools', 'science', 'systems',
            'embedded', 'drivers', 'GUI', 'mobile']
  for area in areas {
    println('Hello, $area developers!')
  }
}
Takanori IshikawaTakanori Ishikawa

型システム入門 プログラミング言語と型の理論の第1章(公式サイトから無料で読める)の脚注には次の記述がある。

配列の境界検査を静的になくしてしまうことは、型システム設計者の長年の目標である。原理的に必要な仕組み(依存型(30.5 節を見 よ)に基づくもの)はよく理解されているが、表現力、型検査の予測可能性と計算量的な扱いやすさ、およびプログラム注釈の複雑さ をうまく均衡させた形でその機能を言語に組み込むことは重要な課題として残されている。この分野に関する最近の進んだ研究成果は Xi and Pfenning [502; 503] に記載されている。

依存型は名前しか知らなかった。「そういえば、Idris って言語がサポートしてたな...」程度の認識。なんとなく気になって適当に検索した結果を読んでみた。

F* という言語も面白そう。C や WASM も出力できる。ソフトウェア検証に興味があったのでちょうどいいタイミング。

配列の境界検査という話だと、D 言語には配列の長さを型で指定できた覚えがある。[1]

import std.stdio;

void main()
{
    int[3] foo;
    writeln("Hello World!");

    int x = foo[3];
    writeln(foo);
}

これを Playground で実行してみると以下のエラー

onlineapp.d(8): Error: array index 3 is out of bounds foo[0 .. 3]
onlineapp.d(8): Error: array index 3 is out of bounds foo[0 .. 3]

この場合、配列は値型になり同じ長さのものしか代入できない。

import std.stdio;

void main()
{
    int[3] foo = [1, 2, 3];
    // int[4] bar = foo; /* Error: mismatched array lengths, 4 and 3 */
    int[3] bar = foo;

    foo = [4, 5, 6];
    writeln("foo = ", foo);
    writeln("bar = ", bar);
}

あと、馴染みが深いものとしては Tuple とか? まあ、本で書かれているのはそういうことではないんだろうけど、D 言語を思い出して懐かしかった。

脚注
  1. [Arrays - D Programming Language]( ↩︎

Takanori IshikawaTakanori Ishikawa

Elixir のコンパイルと実行について理解してないと混乱する

これはコンパイルできなくて、

defmodule User do
  defstruct name: nil, age: nil
  def drive?(%User{} = user), do: user.age >= 18
end

IO.inspect(User.drive?(%User{age: 10}))

これはコンパイルも実行もできる問題

defmodule User do
  defstruct name: nil, age: nil

  def new(fields), do: struct(__MODULE__, fields)
  def drive?(%User{} = user), do: user.age >= 18
end

IO.inspect(User.drive?(User.new(%{age: 10})))

理由が分かれば納得はできるものの、初心者には難しい…。

Takanori IshikawaTakanori Ishikawa

ReScript (ReasonML) の React 実装である ReasonReact では Hooks も使える。ポリモルフィックな関数がないから useEffect1, useEffect2... みたいな感じになるのか https://github.com/alexfedoseev/reason-react/blob/90033ea292239b07f6f71ddea9b354c11fd24392/src/React.re#L152

  • ポリモルフィックな関数がないから、とかの表現にしたい
  • Bind to JS Function | ReScript Language Manual ここを見る限り、bs.unwrap で出来そうなきがする
    • あー、引数の数が違うから?? と思ったけど違うな
Takanori IshikawaTakanori Ishikawa

大域脱出を伴う「例外」という仕組みはなくすか、型で「例外を投げる可能性があるかどうか」だけはチェックしてほしい気がする。例外の種類まで宣言させるのは不要。

Takanori IshikawaTakanori Ishikawa

英訳した記事を dev.to に公開するときの手順

自身のブログ記事を dev.to にも公開するときの手順メモ。なお、クロスポストは許可されている。[1]

Can I cross-post something I've already written on my own blog or Medium?

Absolutely, as long as you have the rights you need to do so! And if it's of high quality, we'll feature it.

カバー写真を探す

流行りに乗ってそれっぽいカバー写真を探す。いつも使っているのは Unsplash

  • クレジット表記を忘れないように追記
  • 画像サイズも幅 4096 以下にする

Photo by Quinton Coetzee on Unsplash

FrontMatter を修正

Editor Guide - DEV Community 👩‍💻👨‍💻

  1. date をその時点の日時に直す。
    • このコマンドで出力 date '+%Y-%m-%dT%H:%M:%S%z'
  2. canonical_url 元記事へのリンク。Google bot が見てくれる
  3. cover_image これはちょっと分かりづらいが、記事編集画面でアップロードしてからその URL をコピペする 最初の投稿時にアップロードすれば不要っぽい?
  4. series シリーズにしたい場合はシリーズタイトルをつけておくといい
  5. published 最後に true に設定して公開
脚注
  1. Frequently Asked Questions 🤔 - DEV Community 👩‍💻👨‍💻 ↩︎

Takanori IshikawaTakanori Ishikawa

補完の効きやすい文法

テキストでプログラムを記述する場合、その文法はエディタで補完が効きやすいと助かる。

たとえば、JS や TypeScript の import はインポート元を最後に書くので補完が効かない。

import { myFunction } from './MyModule';

一方、Python の import はインポート元を先に書くので補完が効く。

from mymodule import myfunc
Takanori IshikawaTakanori Ishikawa

昔は、プロトタイプや書き捨てのプログラムを書くときは静的型付け言語は面倒くさいなーと思ってたけど、IDE やエディタが高性能になった結果、補完も効くし静的型付け言語の方が楽になった。

Takanori IshikawaTakanori Ishikawa

cargo-generate を入れようとして、

error[E0283]: type annotations needed
   --> /Users/takanori.ishikawa/.cargo/registry/src/github.com-1ecc6299db9ec823/cargo-0.46.1/src/cargo/util/config/de.rs:471:63
    |
471 |                 seed.deserialize(Tuple2Deserializer(1i32, env.as_ref()))
    |                                                           ----^^^^^^--
    |                                                           |   |
    |                                                           |   cannot infer type for type parameter `T` declared on the trait `AsRef`
    |                                                           this method call resolves to `&T`
    |
    = note: cannot satisfy `std::string::String: AsRef<_>`

で怒られる件、Issue で報告されている。serde のバージョンアップが起因みたい。

$ cargo install cargo-generate --locked cargo

がワークアラウンドとして言及されていた。

Takanori IshikawaTakanori Ishikawa

WebAssemby のテキスト表現には、いくつかのシンタックス・シュガーがある。

(module
  (func (export "addTwo") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

(module
  (func (export "addTwo") (param i32 i32) (result i32)
    (local.get 0)
    (local.get 1)
    (i32.add)))

(module
  (func (export "addTwo") (param i32 i32) (result i32)
    (i32.add (local.get 0) (local.get 1))))

これらはすべて同じ

Takanori IshikawaTakanori Ishikawa

package.jsonfiles に指定したファイルが npm にアップロードしたパッケージに含まれずに悩んでいたが解決した。files の記述の仕方が悪かった。

変更前の files

package.json
"files": [
  "./dist",
  "./jest-setup.js"
],

これだと、dist ディレクトリは含まれるが、jest-setup.js が含まれない。

変更後の files

package.json
"files": [
  "dist",
  "jest-setup.js"
],

先頭の ./ を外すと含まれる。

  • 公開するときに使っているコマンドは yarn workspace <package> publish
    • npm publish でも同じ結果
  • npm にアップロードされた tarball の URL は npm view node-expo-random dist.tarball で取得可能
Takanori IshikawaTakanori Ishikawa

Rust で Enum をテストしたいとき、Enum に PartialEq を実装して、

assert_eq!(node, Node::Integer(1))

する方法があるけど、Node::Integer の構造が変わったら修正が大変。std::matches を使って、

assert(matches!(node, Node::Integer(1)))

こう書く方法が一般的みたいだけど、これだと、エラーメッセージに node の内容が出なくなってしまう。

assert_matches を使えばエラーメッセージに node の内容も出ていい感じだけど、あまり認知度高くないのかな?

こんな感じで書けて、失敗したときはテスト対象の debug メッセージが出力されるので便利。

#[test]
fn operator_associative() {
    let node = parse("1 + 2 + 3").unwrap();

    assert_matches!(*node, Node::Add(lhs, rhs) => {
        assert_matches!(*lhs, Node::Add(x, y) => {
            assert_matches!(*x, Node::Integer(1));
            assert_matches!(*y, Node::Integer(2));
        });
        assert_matches!(*rhs, Node::Integer(3));
    });
}
Takanori IshikawaTakanori Ishikawa

Rust の match が多段になってネストが深くなる問題、if let 構文だったり、Option, Result の便利メソッドだったりである程度の救済はあるんだけど、自分で雑に定義した Enum だと結局は match にするしかなくなる。ネストが深くなることを除けば、match は読みやすいし一番いいとは思うけど。

Takanori IshikawaTakanori Ishikawa

Gatsby の Markdown plugin で mermaid.js を適用した。Route が変わったときも mermaid.init() を呼ぶ必要があるんだけど、DOM が描画されるタイミングが取れないっぽいので、Route が変わったときに setTimeout() で 10 回まで呼んでる。。

Takanori IshikawaTakanori Ishikawa

型システムをプログラミング言語におけるダイアグラム(図)の表現として考えるとスッキリした。

古典的な型システムは UML のように、オブジェクトのプロパティと関係性を記述できることを目的にしていた。今はそこにフローチャートやシーケンス図を導入しようとしている。そうなるとオブジェクトはアクターになるし、型にはコミュニケーションと副作用を記述できる表現力が求められる。

つまり、ダイアグラムは宣言的ってことだと思う。逆にダイアグラムに付与されているメモや Excel などの自由記述形式の文字列は手続き的。これらは実装の一部であり、実装言語の採用するパラダイムが何であれ手続き的に書いて実行するしかない。ただ、そうやって手続き的に実装していくときも、ダイアグラムという仕様から導出された型が制約をつけてくれる。

Takanori IshikawaTakanori Ishikawa

Rust の Lifetime を毎回書くのは面倒くさいし、単純なルールで省略できるようになっている。[1] これは分かっている人には便利な反面、初学者には「Lifetime がなぜ必要なのか」を分かりづらくしていると思う。また、省略ルールの前提から外れた使い方をされる構造体や関数を実装したときにハマる。そのときはたまたま上手くいっていても、構造体/関数の使い方を変えたときに前提が崩れて謎のエラーに悩まされることも多い。

脚注
  1. Lifetime Elision - The Rustonomicon ↩︎

Takanori IshikawaTakanori Ishikawa

Rust で文字列を受け取る関数を書くときに型をどうするか?
Idiomatic string parmeter types: &str vs AsRef<str> vs Into<String> - The Rust Programming Language Forum

  • &str が一般的かつ分かりやすいが、それよりも効率的な方法もあり一概にどれがベストとは言えない
  • &strString として保持したい場合は callee がコピーする必要がある
  • Into<String>>AsRef<str> であれば、callee が own したいのか ref したいのか caller に分かる
    • ただし、型変数を導入する必要があり、それはそれで分かりづらい
  • こんなことを考えながら型を書きたくない、というのが正直な感想...
Takanori IshikawaTakanori Ishikawa

[Rust] よくやる Option 型の扱い

なんだかんだ match が一番分かりやすいと思うが、以下のケースではそれ以外を使う。

if let ...

値があるときだけ何かをしたい場合は if let ... による取り出しを使う。

if let Some() = fun_node.body {
    ...
}

else が必要な場合は match を使う。

unwrap(), unwrap_or_else()

今のところエラーハンドリングは手抜きなので使ってる。

map(), and_then()

中身を別の値に変換したいときは Option の map() や and_then() メソッドで変換する。

let maybe_some_len = maybe_some_string.map(|s| s.len());

replace(), take()

変更可能な Option の中身を置き換えたり、取り出したり。前者は構文解析しているときに徐々にメタデータを追加していくときによく使う。後者は Builder パターンで最終結果に値を移したいときに。

? 演算子

使ってない。return する演算子というのは馴染みがないせいかもしれない。
最近使うようになった。?; この並びに違和感がないわけじゃないけど。ただ、やはり、ここで return されることを見逃しそうな不安はある。

Takanori IshikawaTakanori Ishikawa

rustc か rust analyzer どちらの挙動かちゃんと見てないけど、VS Code でエラーを直していると、

  1. 型エラーを全部直す
  2. borrow checker が動いて、今度はそちらのエラーを直す

という順番になり、プロジェクト内のファイルを 2 周しないといけない。

Takanori IshikawaTakanori Ishikawa

Lifetime を使おうとして最初はうまくいってもすぐにダメなパターンが見つかるのは、再帰的なデータ構造を相手にしているからかもしれない。フラットな構造を処理するときは問題なくても、ネストすると temporary value を返すことになってダメになってしまう気がする。

そもそも、Rc を使っているのだから、Lifetime で頑張る必要性もあまりない。

Takanori IshikawaTakanori Ishikawa

HTTPoison (hackney) がタイムアウト時にどういうエラーを返すのか知りたかったので、久しぶりに httpbin を使った。

iex(3)> HTTPoison.get("https://httpbin.org/delay/10", [], recv_timeout: 12_000)
{:ok,
 %HTTPoison.Response{
   body: "{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {}, \n  \"form\": {}, \n  \"headers\": {\n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"hackney/1.17.0\", \n    \"X-Amzn-Trace-Id\": \"Root=1-603702a7-234329b5589db7a01fed959c\"\n  }, \n  \"origin\": \"14.3.63.179\", \n  \"url\": \"https://httpbin.org/delay/10\"\n}\n",
   headers: [
     {"Date", "Thu, 25 Feb 2021 01:51:45 GMT"},
     {"Content-Type", "application/json"},
     {"Content-Length", "285"},
     {"Connection", "keep-alive"},
     {"Server", "gunicorn/19.9.0"},
     {"Access-Control-Allow-Origin", "*"},
     {"Access-Control-Allow-Credentials", "true"}
   ],
   request: %HTTPoison.Request{
     body: "",
     headers: [],
     method: :get,
     options: [recv_timeout: 12000],
     params: %{},
     url: "https://httpbin.org/delay/10"
   },
   request_url: "https://httpbin.org/delay/10",
   status_code: 200
 }}
iex(4)> HTTPoison.get("https://httpbin.org/delay/10", [], recv_timeout: 5_000) 
{:error, %HTTPoison.Error{id: nil, reason: :timeout}}
Takanori IshikawaTakanori Ishikawa

Rust のライフタイムを明示的に書く必要が生じたら危険信号、と思うようになった。一度立ち止まって、関数から返すとき、他の構造体に渡すときでも動くか考える必要がある。できれば、それらを網羅したユニットテストを書きたいところだが、たいていの場合はさっさと BoxRc にすることが多い。

Takanori IshikawaTakanori Ishikawa

ビルディングブロックをひとつひとつ積み上げて、徐々に便利になっていくのが実感できると楽しい。

Takanori IshikawaTakanori Ishikawa

VSCode の Semantic Hightlight に対応していないテーマを対応させる

VSCode の Semantic Hightlight に対応していないテーマを対応させるには、以下の記述を settings.json に追加する。

"editor.semanticTokenColorCustomizations": {
  "[Monokai Pro]": {
    "enabled": true
  },
  "[Monokai Pro (Filter Octagon)]": {
    "enabled": true
  },
  "[Monokai Pro (Filter Spectrum)]": {
    "enabled": true
  },
  "[Monokai Pro (Filter Machine)]": {
    "enabled": true
  },
  "[Monokai Pro (Filter Ristretto)]": {
    "enabled": true
  }
},
Takanori IshikawaTakanori Ishikawa

Rust で関数から Iterator を返す方法

よくわかっていなかったところ。

単純なときは impl Iterator, 条件が入るときは Box<dyn Iterator> かな? また、Box<dyn> を使うときは、RFC599 により、Lifetime が static になるので注意。

+ で適切な Lifetime を与えてやる必要がある。

fn iter(&self) -> Box<dyn Iterator<Item = &'_ MyItem> + '_> {
  ...
}
Takanori IshikawaTakanori Ishikawa

React Native で SVG を扱うためには react-native-svg/react-native-svg: SVG library for React Native, React Native Web, and plain React web projects. というライブラリがあり、割と積極的にサポートされているのだが、

無理せず PNG 使うのがいいかな...。

Takanori IshikawaTakanori Ishikawa

Expo SDK を 41 にアップグレードした

手順通り、expo upgrade で問題なく完了。すごい。

Takanori IshikawaTakanori Ishikawa

[Rust] RefCell 内の参照を返す関数を書きたい時にどうするか?

Takanori IshikawaTakanori Ishikawa

[Rust] _ in Rust

Scala では _ が多様な意味で使われている、という記事を読んだ記憶があるが、Rust の場合、

  • let _ = ...
  • パターンマッチでの _
  • 匿名ライフタイム '_
  • 型引数の省略 Vec<_>
Takanori IshikawaTakanori Ishikawa

結局「mutable な参照はひとつだけ」と「Rc は循環参照つらい」という現実があるので、

  • Tree や Graph を構築するだけなら、循環参照を気にしなくていい Arena が便利
  • ただし、多様な型を扱うのが難しい
  • ノードのデータを変更したい場合は Cell/RefCell 使う
    • Arena は mutable な参照を取得できるが、それを Tree/Graph struct に保持しておくのは難しい
  • ノードからの参照を変更したい場合は Cell<Option<'a T>>

ということなのかな?

Takanori IshikawaTakanori Ishikawa

"strawman syntax" というのをたまに見かけて「藁人形の形ににた演算子とか?」と思ってたけど、strawman が「たたき台」ってことなのか

Takanori IshikawaTakanori Ishikawa

React Navigation (v5) でひとつ前の画面の Route name が知りたい

// Get the name of previous name (UNSTABLE?)
let previousRoute: string | null = null;
const state = navigation.dangerouslyGetState();

if (state != null) {
  const index = state.index - 1;

  if (index >= 0 && index < state.routes.length) {
    previousRoute = state.routes[index].name;
  }
}
Takanori IshikawaTakanori Ishikawa

起動中の iOS シミュレータの UUID を知る

 $ xcrun simctl list | egrep '(Booted)'
    iPhone 12 Pro Max (1FEAF45D-B3F1-XXXX-81B4-XXXX) (Booted) 
    Phone: iPhone 12 Pro Max (1FEAF45D-XXXX-4A8B-81B4-XXXX) (Booted)

シミュレータのデータ

$ ls -la ~/Library/Developer/CoreSimulator/Devices
total 16
drwxr-xr-x  43 takanori_is  staff  1376  4  2 09:48 .
drwxr-xr-x   5 takanori_is  staff   160  1 13 19:53 ..
drwxr-xr-x@  4 takanori_is  staff   128  1 28 08:28 014F278D-F9C7-4B10-A97B-6926D478FC05
drwxr-xr-x@  4 takanori_is  staff   128  1 28 08:33 01CF44F0-BDE2-4A50-8464-E789597C1BC3
...

キーチェインを探す

$ ls -la ~/Library/Developer/CoreSimulator/Devices/1FEAF45D-XXXX-4A8B-81B4-XXXX/data/Library/Keychains 
total 14568
drwx------  15 takanori_is  staff      480  5 26 13:03 .
drwxrwxr-x  78 takanori_is  staff     2496  4 30 16:52 ..
drwxrwxr-x  14 takanori_is  staff      448  3 29 10:37 Analytics
-rw-------   1 takanori_is  staff      182  5 26 13:03 PriorMitmRoots.plist
drwx--x--x   3 takanori_is  staff       96  5 26 13:03 SupplementalsAssets
-rw-------   1 takanori_is  staff    16384  3 26 13:42 TrustStore.sqlite3
-rw-------   1 takanori_is  staff        0  3 26 13:43 com.apple.security.exception_reset_counter.plist
drwx--x--x   5 takanori_is  staff      160  5 26 15:52 crls
-rw-------   1 takanori_is  staff   831488  5 26 13:03 keychain-2-debug.db
-rw-------   1 takanori_is  staff    32768  5 26 13:03 keychain-2-debug.db-shm
-rw-------   1 takanori_is  staff  3052952  5 26 16:03 keychain-2-debug.db-wal
-rw-------   1 takanori_is  staff    53248  5 18 09:56 ocspcache.sqlite3
-rw-------   1 takanori_is  staff    32768  5 26 13:03 ocspcache.sqlite3-shm
-rw-------   1 takanori_is  staff  3077672  5 26 12:34 ocspcache.sqlite3-wal
-rw-------   1 takanori_is  staff   163840  3 26 13:42 pinningrules.sqlite3
Takanori IshikawaTakanori Ishikawa

初めて Rust のマクロを書いた。Option から値を取り出して、None なら return する。

/// Unwraps an optional value or early return.
#[macro_export]
macro_rules! pick {
    ($expr:expr $(,)?) => {
        match $expr {
            Some(val) => val,
            None => {
                return;
            }
        }
    };
}
Takanori IshikawaTakanori Ishikawa

[Rust] 文字列のリストを作る

Rust で「文字列」の「リスト」を作るとき、できるだけ読みやすいコードで書きたい。もちろん、一番 idiomatic なコードは

vec!["a".to_string(), "b".to_string(), "c".to_string()]

なのだが、それぞれの要素が離れすぎていて、個人的には読みづらい。Iterator と .map を使えば、要素を凝集できる。

vec!["a", "b", "c"].iter().map(|s| s.to_string()).collect()

もう少し改良すると、最初に Vec を作る必要はない

["a", "b", "c"].iter().map(|s| s.to_string()).collect()

更に、要素の型 String は自明なので、.to_string() よりもう少し短い .into() が使える

["a", "b", "c"].iter().map(|&s| s.into()).collect()

ただ、これは参照を外すための & が必要になるので、少し noisy かもしれない。

本当は、クロージャを使わずに書きたいのだが無理な気がしている...。

Takanori IshikawaTakanori Ishikawa

KeyboardAvoidingView と Android

サンプルには

behavior={Platform.OS === 'ios' ? 'padding' : 'height'}

となっているが、Android ではそもそも behavior は指定しない方がいい。そうしないとキーボードの高さが変化したときに追従しなくなる。また、これによってレイアウトにも問題が出てくる。結局、両 OS を判定しながらレイアウトのロジックを書くことになる。

Keyboard の Hooks

Takanori IshikawaTakanori Ishikawa

PostgreSQL で外部制約キーの名前を CREATE TABLE 時に指定するときは `CONSTRAINTP 句で指定できる。

CREATE TABLE IF NOT EXISTS my_table (
  ...
  CONSTRAINT "my_fkey" FOREIGN KEY (id, name) REFERENCES other_table(id, name)
);
Takanori IshikawaTakanori Ishikawa

React Native の zIndex, 要素の構造を工夫しないと思うように効かない理解だったけど、少なくともマイナスの値は iOS/Android ともに効くようになっている気がする?

同じ「階層」に存在している View 同士で zIndex が比較されてる感じ。それ以外は要素の出現順と親子関係に準じる。

Takanori IshikawaTakanori Ishikawa

[Go] rand を Read するとランダムなバイト列を生成できるの、なるほど、という感じだ。Go を触ってると、こういう「自分の感覚とは違う設計」が多くて勉強になる。

package main

import (
    "math/rand"
    "fmt"
)

func main() {
    token := make([]byte, 4)
    rand.Read(token)
    fmt.Println(token)
}

「自分の感覚とは違う設計」

Python の "".join(...) もそう

Takanori IshikawaTakanori Ishikawa

Expo で NetInfo を試した。コネクション変更は簡単に取得できる。

export default function App() {
  const [netInfoState, setNetInfoState] = useState<NetInfoState | null>(null);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      setNetInfoState(state);
    });

    return unsubscribe;
  }, []);
  ...


ドキュメントにも書かれている通り、SSID/BSSID はそのままでは取得できない。

Takanori IshikawaTakanori Ishikawa

Expo で Base64 を扱うときは base64-js を使ってる。

import * as Base64 from 'base64-js';
...
const byteLength = Base64.byteLength(base64Image);
const u8Image = Base64.toByteArray(base64Image);

コードも十分短いし、環境依存もなさそう。ちゃんと Uint8Array で返してくれる。

Takanori IshikawaTakanori Ishikawa

これを読んで、React Hooks が「コンポーネントのトップレベルでしか呼んではいけない(render のたびに毎回同じ順序で呼ばれなくてはいけない)」理由が分かった。

あと、Dan Abramov の

But Aren't Hooks Magic?

以降の文章は必読。

Spread Love, Not Hype

このフレーズ、めちゃいい。

Takanori IshikawaTakanori Ishikawa

作図のためのアプリがほしいなーと思ったけど、Keynote が優秀だった。オブジェクト同士を接続して、移動してもいい感じに表示してくれる。

Takanori IshikawaTakanori Ishikawa

TypeScript で URLSearchParams の .entries() とかを認識させるには tsconfig の lib に dom.iterable を追加する

Takanori IshikawaTakanori Ishikawa

Go: 長い文字列リテラルを複数行に分割して書きたいとき、Raw string literal が使えるが、これは改行も含まれてしまう。

`First
Second`

こういう場合は単純に + で連結すればいい。

"First"+
"Second"

これは Constant expression なので、コンパイラによって一つの文字列に最適化される。

Takanori IshikawaTakanori Ishikawa

useSWR() のキャッシュ戦略

stale-while-revalidate の仕様に従ってます」という説明があるけれど、別に HTTP レスポンスヘッダーを見てくれるわけではない(というか API の仕様上読めない)

マニュアルで更新するか、更新イベントが発生するまではキャッシュが使われる。

Takanori IshikawaTakanori Ishikawa

Expo ファイルを消したり名前を変更したら、そのあとずっとエラーが出る、みたいなときは以下のコマンドでキャッシュを消す

$ expo r -c
Takanori IshikawaTakanori Ishikawa

React Hooks の dependency array の各要素の比較は Object.is

なんらかの配列に依存していて、しかもその配列のサイズが変わる場合はどうしたらいいのだろう?

const objectIds = items.map((x) => x.id);

return useMemo(
  () => ({
    items
  }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [...objectIds]
);

こんな風にすると、以下の警告が出てしまう。

Warning: The final argument passed to useMemo changed size between renders. The order and size of this array must remain constant.

変更を検知できるプロパティを連結して文字列化するのが手軽かな...?

Takanori IshikawaTakanori Ishikawa

TypeScript

ある値が unknown 型の場合、null チェックをしても Object is possibly 'null'. と怒られてしまう。

function foo(v: unknown): boolean {
  return v != null && typeof v === 'object' && v.a === 1;
  //                                           ^^^
  //           	                               Object is possibly 'null'
}

これは、

  1. v != null のチェックの後も unknown 型のまま
  2. typeof v === 'object' のチェックの後に Object | null になる
  3. Object is possibly 'null'

ということらしい(typescript - TS2531: Object is possibly 'null' even if I check that it is not - Stack Overflow)。

Takanori IshikawaTakanori Ishikawa

Expo 以下のエラーが出て expo run build:ios できなくなった。

Building optimized bundles and generating sourcemaps...
Starting Metro Bundler
src/App.tsx: Unexpected token name «_000», expected punc «,» in file src/App.tsx at 191:18
Error: Unexpected token name «_000», expected punc «,» in file src/App.tsx at 191:18
    at /home/myapp/node_modules/metro/src/JSTransformer/worker.js:437:17
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (/home/myapp/node_modules/metro/src/JSTransformer/worker.js:75:24)
    at _next (/home/myapp/node_modules/metro/src/JSTransformer/worker.js:95:9)
    at /home/myapp/node_modules/metro/src/JSTransformer/worker.js:100:7
    at new Promise (<anonymous>)
    at /home/myapp/node_modules/metro/src/JSTransformer/worker.js:92:12
    at JsTransformer._minifyCode (/home/myapp/node_modules/metro/src/JSTransformer/worker.js:444:7)
    at /home/myapp/node_modules/metro/src/JSTransformer/worker.js:374:33

minify するライブラリを変えることで解決できそうだったので、metro.config.js で指定してみる。

const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer.minifierPath = 'metro-minify-terser';
config.transformer.minifierConfig = {
  // Terser options...
};

module.exports = config;

これで解決した。

Takanori IshikawaTakanori Ishikawa

TypeScript enum のように反復可能な union 型

なるほどー。

export const DiagnosticSeverity = {
  /**
   * Reports an error.
   */
  Error: 1,
  /**
   * Reports a warning.
   */
  Warning: 2,
  /**
   * Reports an information.
   */
  Information: 3,
  /**
   * Reports a hint.
   */
  Hint: 4,
} as const;

export type DiagnosticSeverity = typeof DiagnosticSeverity[keyof typeof DiagnosticSeverity];
Takanori IshikawaTakanori Ishikawa

SWR を React Testing Library で使うときにキャッシュを回避したい

SWR >= 1.0.0

Custom Cache Provider で、テストごとに新規のインメモリキャッシュを設定する。

import React from 'react';
import { SWRConfig } from 'swr';

export const SWRWithoutCache: React.VFC<{ children: React.ReactNode }> = ({ children }) => (
  <SWRConfig
    value={{
      provider: () => new Map(),
      dedupingInterval: 0,
    }}>
    {children}
  </SWRConfig>
);

SWR < 1.0.0

とりあえず、SWR 0.5 で動くやつ。

Jest の setup に以下を追加

import { cache } from 'swr';
...
// clear SWR cache globally.
// NOTE: SWR 1.0 will introduce custom cache mechanism.
// https://swr.vercel.app/docs/advanced/cache
afterEach(async () => {
  await waitFor(() => cache.clear());
});

dedupingInterval を 0 にする

import React from 'react';
import { SWRConfig } from 'swr';

export const SWRWithoutCache: React.VFC<{ children: React.ReactNode }> = ({ children }) => (
  <SWRConfig value={{ dedupingInterval: 0 }}>{children}</SWRConfig>
);

wrapper に指定

const { result, waitForNextUpdate } = renderHook(() => useFoo(undefined), {
  wrapper: SWRWithoutCache,
});
Takanori IshikawaTakanori Ishikawa

TeX の花文字。論文とかでよく見る「ひげ」が特徴的な文字。

  • T は花文字だと \mathscr{T}

これは以下のように書く

$\mathscr{T}$
Takanori IshikawaTakanori Ishikawa

[Rust] 以下のようなコードを書いていて、Option#map が値を move する (= レシーバが self ) であることに初めて気づいた。

if r.order > result.map_or(0, |r| r.order) {
    result.replace(r);
}

as_ref() を使うことで、Option<T> -> &Option<T> -> Option<&T> にできる。

if r.order > result.as_ref().map_or(0, |r| r.order) {
    result.replace(r);
}

リファレンスは Copy できるので、これで問題なくなる。今まで若干雰囲気で使ってたなー。

Takanori IshikawaTakanori Ishikawa

React Navigation v6

Upgrading Guide に従って、v5.x からアップグレードしている。

params が自動でマージされなくなったので、この挙動を期待しているところは、オブジェクトによる指定に変更し、merge: true を追加する。

navigation.navigate({
  name: 'MyStackScreen',
  params: { name: newName },
  merge: true,
});

モーダル周り

mode="modal" はなくなり(というか mode プロパティ自体がなくなり)、options で指定するようになった。

<RootStack.Navigator screenOptions={{ presentation: 'modal' }}>
  ...

デフォルトでヘッダが表示されるようになっている箇所があるので、適宜、headerShown: false 追加

<RootStack.Screen
  name="MyScreen"
  component={MyStackScreen}
  options={{ headerShown: false }}
/>

また、モーダルのスタイルも iOS 13 以降のものがデフォルトになったので、以前の全体を覆うものにしたい場合は TransitionPresets.ModalSlideFromBottomIOS でアニメーションを変更する。

<RootStack.Screen
  name="MyScreen"
  component={MyStackScreen}
  options={{ ...TransitionPresets.ModalSlideFromBottomIOS }}
/>

ただ、これのおかげで、今まで cardOverlayEnabledcardStyleInterpolator を指定していたものは不要になった。

<RootStack.Screen
  name="MyScreen"
  component={MyStackScreen}
  options={{
    cardOverlayEnabled: true,
    cardStyleInterpolator: CardStyleInterpolators.forModalPresentationIOS,
  }}
/>

ヘッダーの高さも微調整されているので、ここも修正が必要。

Takanori IshikawaTakanori Ishikawa

Go の GCS SDK で SignedURL を作成するためには、SignedURLOptions

  • GoogleAccessID
  • PrivateKey or SignBytes

を指定する必要があるが、これを Cloud Run や GCE ではデフォルトのものを使いたい。

そのための PR がレビュー中なので、差分を読み解いてみる。以下の手順で上記オプションを自動設定できそう。

  1. transport.Cred() で google.Credential を取得する
    1. これは内部で google.FindDefaultCredentials を呼ぶ
    2. GOOGLE_APPLICATION_CREDENTIALS 環境変数または $HOME/.config/gcloud/application_default_credentials.json あるいは環境依存の方法 (GCE, AppEngine の場合は JWT アクセストークンから) で読む
      • ただし、最後の方法は JSON ファイルがないので、最終的に defaultSignBytesFunc(GoogleAccessID) に行く
    3. PR では scope に https://www.googleapis.com/auth/cloud-platform を追加しているが不要??
  2. GoogleAccessID
    1. JSON を unmarshal
    2. client_email プロパティがあれば使用する
    3. metadata.OnGCE() なら
      1. metadata.Email("default") で取得できれば使用する
  3. PrivateKey or SignBytes
    1. JSON を unmarshal
    2. private_key プロパティがあれば使用する
    3. なければ、defaultSignBytesFunc(GoogleAccessID) を SignBytes に設定
      1. defaultSignBytesFunc はプライベートで保持している HTTP Client を再利用しているが、これは別途作成しておけばいい?
      2. IAM Service Account Credentials API を有効にする必要あり
      • signBlob の permission iam.serviceAccounts.signBlob も必要
Takanori IshikawaTakanori Ishikawa

[React Native] 環境を切り替える方法

環境ごとに異なるビルドを TestFlight を配布したい。

  • React Native の __DEV__ グローバル変数
    • ローカルビルドの場合は true

Expo の app.config.js で環境変数を設定する

  • app.json ではなく、app.config.js を用意することで、動的に設定を切り替えられる
    • 環境変数に応じて、bundleIdentifier を切り替えたりできそう
    • 環境変数の設定は npm script に入れてしまうので良いかも。ちゃんとやるなら dotenv
  • .extra フィールドで自由に値を設定し、Constants.manifest で読み出せる
  • Environment variables in Expo - Expo Documentation
  • アプリ名を ios.infoPlistlocales で設定している場合、アプリ名も切り替えるためには言語ファイル JSON を複数用意することになる(JSON は実行時に処理できないので)
    • これは管理が大変なので、開発やステージング用のビルドでは、locales を設定せず、config で name のみ設定するのが良さそう
Takanori IshikawaTakanori Ishikawa

jest-expo で app.config.js を読めない

  ● Test suite failed to run

    ConfigError: Failed to read config at: /home/app.config.js

      at getDynamicConfig (node_modules/@expo/config/build/getConfig.js:70:9)
      at getConfig (node_modules/@expo/config/build/Config.js:339:51)
      at readExpoConfig (node_modules/jest-expo/src/preset/createMockConstants.js:50:10)
      at createMockConstants (node_modules/jest-expo/src/preset/createMockConstants.js:19:22)
      at Object.<anonymous> (node_modules/jest-expo/src/preset/setup.js:112:8)

これは Jest の設定で moduleFileExtensionsjson を追加すれば直る。

  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
Takanori IshikawaTakanori Ishikawa

[ElasticSearch] text フィールドは単語分割されたり小文字で格納されたりしているので、term クエリーで検索するときは「分割された結果」に対してクエリーを書く必要がある。

keyword フィールドは分割などされないが、match クエリーが使えない。

あるフィールドを textkeyword 両方で格納することもできる。

Takanori IshikawaTakanori Ishikawa

React Native の FlatList で要素の追加/削除をするときにアニメーションさせたい。

LayoutAnimation を使うのが公式の方法だが、Android でうまくいかない問題もあるようだ。

Reanimated で置き換えた、という記事

From my experience working on various react native projects, animations based on LayoutAnimation tend to be pretty reliable on iOS. However, when it comes to using it on Android, many times I’ve encountered inconsistencies, visual glitches, or crashes caused by using it.

Takanori IshikawaTakanori Ishikawa

React Native Testing Library の act warning

Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

https://github.com/callstack/react-native-testing-library/issues/379#issuecomment-672932435

I’ve noticed that this error/warning happens when I use more than 1 await within a it statement

単に、react-test-renderer の判定方法が RN と合ってない気がする。

Takanori IshikawaTakanori Ishikawa

iPad で、デバイスの向きを

  1. app.jsonlandscape に設定。
  2. 起動時に ScreenOrientation.lockAsync(orientationLock) で Landscape Left に固定

しているのだが、何らかのタイミングで Landscape Right になってしまう。原因不明なので ScreenOrientation も app.json による指定もせず、デバイスの向きに任せることにした...。

Takanori IshikawaTakanori Ishikawa

npm 8.3 以降を強制して、package.json で overrides オプションを使いたい。

EAS build で npm 8.3 以降を指定する方法

  • eas.json で node を指定しても、指定したバージョンがインストールされなかった🤔
  • package.json の hooks で npm をインストールするようにした。
  "scripts": {
    "eas-build-pre-install": "npm install -g npm@8.5.3",
Takanori IshikawaTakanori Ishikawa

[Rust] 異なる trait 同士で同名のメソッドを実装している場合、Universal Function Call Syntax を使うことで、それぞれのメソッドを明示的に呼び出すことができる。

Foo::f(&b);
Bar::f(&b);

メソッドではない Associated function も呼び出すことができる。

impl clap::ArgEnum for MyArg {
  ...
  fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
        if ignore_case {
            FromStr::from_str(input.to_lowercase().as_str())
        } else {
            FromStr::from_str(input)
        }
    }
}

impl FromStr for MyArg {
  ...
}
Takanori IshikawaTakanori Ishikawa

Phone as Security key について

Google ブログの One step closer to a passwordless future を読んで気になったのでメモ

FIDOアライアンスが言う「パスワードレス認証の普及を加速させる取り組み」とは?

これは caBLEっていう仕組みで以前から知られているもので、特徴としては

  • スマートフォンの認証器として利用する
  • Bluetoothを使って認証器とFIDOを利用する端末をつなげる

と言うあたりです。

パスワードの不要な世界はいかにして実現されるのか - FIDO2 と WebAuthn の基本を知る

Google は caBLE (cloud assisted BLE、「ケーブル」) という FIDO2 の拡張を提案しています。これはスマートフォンを BLE の cross-platform な認証器として使えるようにする、というもので、これが実現すれば、わざわざ専用の認証器を用意しなくても、多くの人がすでに持ち歩いているスマートフォンをそのまま認証器として使えるようになり、デバイスを跨いでパスワードなしのログインが可能になる、と期待されています。

公開情報から読むCloud-assisted BLE(caBLE)をつかったWebAuthn - Speaker Deck

  • 公開鍵暗号方式による認証
  • サービスへの登録時に鍵ペアを生成、公開鍵を RP に登録
  • Authenticator = 認証器
    • 内部認証器 = ローカルに実装されている組み込み認証器
      • 指紋、虹彩、顔
    • 外部認証器 = 移動可能な認証器
      • この文脈ではスマートフォンが該当
  • FIDO2
    • WebAuthn
    • CTAP2
  • caBLE
Takanori IshikawaTakanori Ishikawa

Expo 45 が来た。

ただし、Dev Client を使っている場合、Android で Hermes がクラッシュする問題がある、とのこと。

expo-dev-client for Android currently does not work with Hermes —the app will crash in development. This is being tracked in expo/expo#17339, and we hope to have a solution soon. In the meantime, we recommend holding off on upgrading to SDK 45 if you use Hermes together with expo-dev-client on Android.

React Native 0.68

Takanori IshikawaTakanori Ishikawa

しかし、PKCS #1 v1.5は200万ペアほどの選択暗号文を取ることで攻撃可能なことが知られており、IND-CCA2ではない。

なお、OpenSSLのrsautlコマンドでは、パディング方式としてPKCS #1 v1.5がデフォルトとなっている。

RSAに対する適応的選択暗号文攻撃とパディング方式 - ももいろテクノロジー

主旨を要約すると次のようになります。

相互運用性の観点からアルゴリズムの種類を制限するのがよい。PKCS1-v1.5 は安全ではないので、RS256 は排除している(JWS 仕様策定時もこの問題は認識していたがライブラリ群のアルゴリズムサポート状況を勘案して PKCS1-v1.5 を選択)。言うまでもなくアルゴリズム none も排除している。

Financial API 実装の技術課題 - Qiita

Takanori IshikawaTakanori Ishikawa

iOS/Mac の SecKeyCopyExternalRepresentation で生成される PEM は PKCS#1 フォーマット。これを X.509 フォーマットに変換しないと他プラットフォームで使えない場合がある。これを実現する方法を探していたが、以下の方法が簡単そう。

つまり、以下のような公開鍵がある場合、

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwyNIPR4pavGx7NUMcSnvbesw3ITCyoN8Scxe/ynV/gE3cDKkeN86
1L41A5Uk0cj58FxeT1f8Vo3vQDKPaLUnEkmsZQvUagkjJRnGit3Q1Ngai7DAmjhk
+P7zwotxdUiQAYmOMnvQFjlORU606VKhXaojue2BcuBDAAxbq7MJAOP8KGbUIm9v
0hb3+eZ45UKMvdL/XTpGV5t1UuXX9g6zU7qR1h5VgQinVlaSFz1NFe1SztFOLnNR
cN8zkZe0oqQo6h6yie3fTpwxppdRcRpobcdk6UjGhsl5jeu1y48iIBKj97N2olEq
5bf86kCPEVyirDkUzYTWjefao+VoGDfQdQIDAQAB
-----END RSA PUBLIC KEY-----

まずは、BEGIN RSA PUBLIC KEYEND RSA PUBLIC KEY を変更する。

-----BEGIN PUBLIC KEY-----
MIIBCgKCAQEAwyNIPR4pavGx7NUMcSnvbesw3ITCyoN8Scxe/ynV/gE3cDKkeN86
1L41A5Uk0cj58FxeT1f8Vo3vQDKPaLUnEkmsZQvUagkjJRnGit3Q1Ngai7DAmjhk
+P7zwotxdUiQAYmOMnvQFjlORU606VKhXaojue2BcuBDAAxbq7MJAOP8KGbUIm9v
0hb3+eZ45UKMvdL/XTpGV5t1UuXX9g6zU7qR1h5VgQinVlaSFz1NFe1SztFOLnNR
cN8zkZe0oqQo6h6yie3fTpwxppdRcRpobcdk6UjGhsl5jeu1y48iIBKj97N2olEq
5bf86kCPEVyirDkUzYTWjefao+VoGDfQdQIDAQAB
-----END PUBLIC KEY-----

先頭に MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A を追加

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAwyNIPR4pavGx7NUMcSnvbesw3ITCyoN8Scxe/ynV/gE3cDKkeN86
1L41A5Uk0cj58FxeT1f8Vo3vQDKPaLUnEkmsZQvUagkjJRnGit3Q1Ngai7DAmjhk
+P7zwotxdUiQAYmOMnvQFjlORU606VKhXaojue2BcuBDAAxbq7MJAOP8KGbUIm9v
0hb3+eZ45UKMvdL/XTpGV5t1UuXX9g6zU7qR1h5VgQinVlaSFz1NFe1SztFOLnNR
cN8zkZe0oqQo6h6yie3fTpwxppdRcRpobcdk6UjGhsl5jeu1y48iIBKj97N2olEq
5bf86kCPEVyirDkUzYTWjefao+VoGDfQdQIDAQAB
-----END PUBLIC KEY-----

Base64 を 1 行 64 文字に揃える。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwyNIPR4pavGx7NUMcSnv
besw3ITCyoN8Scxe/ynV/gE3cDKkeN861L41A5Uk0cj58FxeT1f8Vo3vQDKPaLUn
EkmsZQvUagkjJRnGit3Q1Ngai7DAmjhk+P7zwotxdUiQAYmOMnvQFjlORU606VKh
Xaojue2BcuBDAAxbq7MJAOP8KGbUIm9v0hb3+eZ45UKMvdL/XTpGV5t1UuXX9g6z
U7qR1h5VgQinVlaSFz1NFe1SztFOLnNRcN8zkZe0oqQo6h6yie3fTpwxppdRcRpo
bcdk6UjGhsl5jeu1y48iIBKj97N2olEq5bf86kCPEVyirDkUzYTWjefao+VoGDfQ
dQIDAQAB
-----END PUBLIC KEY-----
Takanori IshikawaTakanori Ishikawa

Expo の Camera を使った FaceDetector は気軽に使えるのだが、実際に使ってみると問題があった。

  • iPad で使うと顔検出できないケースが多い
    • 顔を動かすと検知される?

そこで、VisionCameravision-camera-face-detector で置き換えてみた。結果、

  • 検出頻度は格段に上がった。ほぼ 100% 検出できる
    • Reanimated の Worklet で実装されているため、パフォーマンスも十分
      • Worklet は個別の JS VM で別スレッドで実行される

ただし、

npm 7 以降であれば、以下のようにしてインストールできる。EAS build でも問題なく動く。

npm install github:rodgomesc/vision-camera-face-detector#acbc0b866c05e8739ce58087bc66179adb225322
  • ただ、bounding box が正確ではない気がする...
  • bounding box は、Camera.format の videoWidth/videoHeight の領域での値なので、カメラ上に表示するときは変換が必要
  • contours (Landmark) が機能していない
  • EyeOpenProbability: 目が画面外にある場合は不正確な値

vision-camera-face-detector を有効にするには、babel.config.js に以下を記述。必ず plugins 配列の最後に記述する

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      ...
      [
        'react-native-reanimated/plugin',
        {
          globals: ['__scanFaces'],
        },
      ],
    ],
  };
};

使う側では、以下のような感じ

import 'react-native-reanimated'; // これは先頭に書く
import { Camera, PhotoFile, useCameraDevices, useFrameProcessor } from 'react-native-vision-camera';
import { Face, scanFaces } from 'vision-camera-face-detector';

  const frameProcessor = useFrameProcessor(
    (frame) => {
      'worklet';
      const scannedFaces: Face[] = scanFaces(frame);
      console.log('scannedFaces', scannedFaces);
    },
    []
  );
Takanori IshikawaTakanori Ishikawa

VisionCamera では、Camera コンポーネントに format を指定できる。

フォーマットによっては Frame Processor が動かなかったり、画像サイズが大きすぎて ML の推論が重くなってしまうので気をつける。

Selecting a Format for a Frame Processor

  • If you are running heavy AI/ML calculations in your frame processor, make sure to select a format that has a lower resolution to optimize it's performance.
  • Sometimes a frame processor plugin only works with specific pixel formats. Some plugins (like MLKit) don't work with x420.

カメラがサポートしているフォーマットは CameraDevice の formats から取得できるので、適切なものを選ぶ。フォーマットは例えばこんな感じ

{
  "photoWidth": 1280,
  "isHighestPhotoQualitySupported": false,
  "pixelFormat": "420f",
  "videoHeight": 768,
  "supportsPhotoHDR": false,
  "colorSpaces": ["srgb"],
  "autoFocusSystem": "none",
  "videoStabilizationModes": ["auto", "off"],
  "maxZoom": 60,
  "maxISO": 1504,
  "minISO": 47,
  "photoHeight": 960,
  "supportsVideoHDR": false,
  "frameRateRanges": [
    {
      "maxFrameRate": 30,
      "minFrameRate": 1
    }
  ],
  "videoWidth": 1024,
  "fieldOfView": 58.205101013183594
}

Pixel format について

VisionCamera: CameraDevice の format には pixel format が含まれているが、

  • 420f, 420v がある
  • x420 もある
    • compressed 10-bit HDR format, whereas 420f and 420v are 8-bit non-compressed formats.
    • MLKit とかはこのフォーマットだと動かない
Takanori IshikawaTakanori Ishikawa

VisionCamera のパーミッション確認が微妙に面倒臭いので、以下のようなカスタムフックを書いた。

import { useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';

export type UseCameraPermissionReturn = {
  hasPermission: boolean | undefined;
  permission: CameraPermissionStatus | undefined;
};

export function useCameraPermission() {
  const [cameraPermissionReturn, setCameraPermissionReturn] = useState<UseCameraPermissionReturn>({
    hasPermission: undefined,
    permission: undefined,
  });

  // Request permission
  useAsync(async () => {
    const cameraPermission = await Camera.getCameraPermissionStatus();

    switch (cameraPermission) {
      case 'denied':
      case 'restricted':
        setCameraPermissionReturn({
          hasPermission: false,
          permission: cameraPermission,
        });
        return;
      case 'authorized':
        setCameraPermissionReturn({
          hasPermission: true,
          permission: cameraPermission,
        });
        return;
      case 'not-determined':
        break;
    }

    const newCameraPermission = await Camera.requestCameraPermission();

    switch (newCameraPermission) {
      case 'denied':
        setCameraPermissionReturn({
          hasPermission: false,
          permission: newCameraPermission,
        });
        return;
      case 'authorized':
        setCameraPermissionReturn({
          hasPermission: true,
          permission: newCameraPermission,
        });
        return;
    }
  }, []);

  return cameraPermissionReturn;
}

パーミッションが取得できてないときはアラートを出したいので以下のように使う。

// Request permission
const { hasPermission: hasCameraPermission } = useCameraPermission();

useEffect(() => {
  if (hasCameraPermission === false) {
    alert('No camera access permission.');
  }
}, [hasCameraPermission]);
Takanori IshikawaTakanori Ishikawa

vision-camera-code-scanner は、QR コードの読み取りに Google の MLKit を採用しているが、どうやら、分割された QR コード(シンボルの連結機能)は未サポートのようだ。

.rawData というフィールドは取得できるが、試してみたところ、テキスト部分のデータが手に入るだけだった。

AVFoundation の QR コード読み取り機能を使えば取得できるらしいが、AVCaptureSession は VisionCamera が握っているので、実装できなさそう。

zxingify/zxingify-objc: An Objective-C Port of ZXing でもサポートされている、とのこと。

Frame processor の中で CMSampleBufferRef を変換して zxingify で処理すればいけそう?

CMSampleBufferRef -> CMSampleBufferGetImageBuffer() -> CVImageBufferRef -> [ZXCGImageLuminanceSource createImageFromBuffer:videoFrame]
Takanori IshikawaTakanori Ishikawa

NativeBase でボタンに使われてる色とかを取得したい。React Navigation のヘッダー色を合わせたい、というのが元々の動機。

<Button mt="2" colorScheme="indigo">
  ボタン
</Button>

テーマで設定された各カラーは useTheme で取得できる。

import { useTheme } from 'native-base';

const { colors } = useTheme();

const backgroundColor = colors.indigo[800];

colorScheme から色を取得する方法は分からない...。

Takanori IshikawaTakanori Ishikawa

カメラからの入力 CMSampleBuffer を画像に変換するには、Video Toolbox の VTCreateCGImageFromCVPixelBuffer() を使う。

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(frame.buffer);
CGImageRef videoFrameImage = NULL;

if (VTCreateCGImageFromCVPixelBuffer(pixelBuffer, NULL, &videoFrameImage) !=
    errSecSuccess) {
  // error
}

この関数が回転もしてくれる(っぽい)ので、自前で画像の回転をする必要はない。

Takanori IshikawaTakanori Ishikawa

Zxingify-ObjC で QR コードの矩形を表示するためには、そもそも QR コードの仕様を理解しておく必要があった。

QR コードの 3 隅に配置されている位置検出用パターンを finder pattern という。そして、バーコードスキャナで検出される位置情報は、この検出パターンの位置

points related to the barcode in the image. These are typically points identifying finder patterns or the corners of the barcode. The exact meaning is specific to the type of barcode that was decoded.

コードを読んだ感じ、位置は

  • 左上
  • 右上
  • 左下

の順で、最後が

  • アライメントパターン (モデル2より採用)
Takanori IshikawaTakanori Ishikawa

TypeScript 4.7 以上 (4.7.3 で確認) にしていると、@types/react (17.0.2) + react-native (0.68.2) で、React Native のコンポーネントを使うと、以下のようなエラーが出る。

  Its instance type 'SafeAreaView' is not a valid JSX element.
    The types returned by 'render()' are incompatible between these types.
      Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react-native/node_modules/@types/react/index").ReactNode'.
        Type '{}' is not assignable to type 'ReactNode'.

128     <SafeAreaView style={styles.container}>
         ~~~~~~~~~~~~

TypeScript 4.5 に戻すことで解決した。

Takanori IshikawaTakanori Ishikawa

zxingify-objc の kResultMetadataTypeStructuredAppendSequence と kResultMetadataTypeStructuredAppendParity を組み立てているソースコードの箇所

// sequence number and parity is added later to the result metadata
// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
symbolSequence = [bits readBits:8];
parityData = [bits readBits:8];

その直前に 4bits をモード判定のために読み込んでいる

mode = [ZXQRCodeMode forBits:[bits readBits:4]]; // mode is encoded by 4 bits

QR コードの structured append は以下のような構成になっているはず

+--------------------+-------------------+-------------------+
| mode (4bits = 0x3) | seq index (4bits) | seq total (4bits) |
+--------------------+-------------------+-------------------+

インデックスも総数も 0 始まりなので、

seq = metadata[kResultMetadataTypeStructuredAppendSequence]
index = seq >> 4
total = (seq & 0x0f) + 1

で計算できる。

Takanori IshikawaTakanori Ishikawa

ZXingify-Objc: 複数のバーコードを読み込むためのクラス

ZXMultipleBarcodeReader (protocol)

このインターフェースの実装は、1枚の画像から複数のバーコードを読み取ろうとするものである。

ZXGenericMultipleBarcodeReader

画像の一部を繰り返しデコードすることで、画像内の複数のバーコードの位置を特定しようとするものです。1つのバーコードが見つかると、バーコードのZXResultPointsの左、上、右、下の領域が再帰的にスキャンされる。

呼び出し側は、QRコードのような複数の2次元バーコードを画像から検出しようとしたときに、ZXByQuadrantReaderを使用したい場合があります。

つまり、ZXReaderを渡す代わりに、[[ZXByQuadrantReader alloc] initWithDelegate:reader] を渡せばよい。

ZXByQuadrantReader

このクラスは、画像全体ではなく、画像の部分集合をスキャンして、画像からバーコードをデコードしようとするものである。これは、画像中に複数のバーコードがある場合に重要で、 バーコードを検出すると複数のバーコードの一部を見つけてしまい、 デコードに失敗することがある(例えばQRコード)。また、バーコードが中央にある場合に備えて、中央の「四分円」もスキャンします。

Takanori IshikawaTakanori Ishikawa

Zxingify-Objc: ZXMultiFormatReader

複数フォーマットをサポートするための ZXMultiFormatReader は ZXReader プロトコルを実装しているので、以下のふたつのメソッドを実装している。

- (ZXResult *)decode:(ZXBinaryBitmap *)image error:(NSError **)error;
- (ZXResult *)decode:(ZXBinaryBitmap *)image hints:(ZXDecodeHints *)hints error:(NSError **)error;

しかし、これらのメソッドを使うと ZXMultiFormatReader の hints がリセットされてしまうので非効率。それよりは予め hints を指定して、独自に実装している

- (ZXResult *)decodeWithState:(ZXBinaryBitmap *)image error:(NSError **)error;

を使った方がパフォーマンスは良い。

ただ、ZXGenericMultipleBarcodeReader や ZXByQuadrantReader のように ZXReader をデリゲートにとるオブジェクトもあり注意が必要。解決策としては、

  • ZXMultiFormatReader をラップし、常に decodeWithState:error を呼び出す ZXReader を実装する

のが良さそう。

Takanori IshikawaTakanori Ishikawa

npm の依存に git が含まれていると、GitHub Action で Permission error

npm ERR! Warning: Permanently added the ECDSA host key for IP address '192.30.255.113' to the list of known hosts.
npm ERR! git@github.com: Permission denied (publickey).
npm ERR! fatal: Could not read from remote repository.

node 16.x 未満を対象から外すことで解決する。

Takanori IshikawaTakanori Ishikawa

Sentry SDK で capture error するときにメッセージを追加したい。しかし、あまりいい方法がない。Transaction name は設定できるが、あまり目立たないし、使い所も微妙

Sentry.Native.captureException(syncError, (scope) =>
  scope.setTransactionName('MyContext:syncKey')
);
Takanori IshikawaTakanori Ishikawa

Expo app.config.jsupdates.url を定義(し、更にそれが https://u.expo.dev で始まっている場合)、manifest のプロトコル (?) が変わる。これが何に影響するか、というと、Constants.manifest が null になり、Constants.manifest2 になる...。

Takanori IshikawaTakanori Ishikawa

EAS Update では、EAS Build で利用できる環境変数 (Secrets / eas.json で指定した環境変数) は利用できない。そのため、これらの変数に依存した処理を実行時に書いていると EAS Update 時にバグる。

  • 環境変数はビルド環境 (dev/staging/prod) を区別するひとつのみとする
  • 残りの変数は上記の環境変数で切り替える
  • コマンド実行時は常に上記変数を指定する
  • 秘匿情報はビルド時にしか使わない
Takanori IshikawaTakanori Ishikawa

[iOS] アプリに Bluetooth を許可していない状態で、Core Bluetooth の startAdvertising: を呼ぶと、delegate の peripheralManagerDidStartAdvertising:error: が呼ばれるわけではなく、以下のようなログが出力されるだけ。

2022-07-16 09:28:41.043052+0900 example[56292:7309870] [CoreBluetooth] API MISUSE: <CBPeripheralManager: 0x283a94aa0> can only accept this command while in the powered on state
Takanori IshikawaTakanori Ishikawa

[React] 今までこんなふうに書いていた

export const MyComponent: React.FC<{
  propA: number;
  children?: React.ReactNode;
}> = ({ propA, children }) => {...

こう書けることを知った。

export const MyComponent: React.FC<
  React.PropsWithChildren<{
    propA: number;
  }> = ({ propA, children }) => {...
Takanori IshikawaTakanori Ishikawa

[React] act について

  • コンポーネントのレンダーが発生するコードは act で囲む
  • 問題は act で囲んだコードの副作用で非同期処理が走る場合
    • コールバック関数 -> await (処理) -> setState とか
    • setState まで呼ばれることを保証するわけではない
      • それはそう
    • 気軽に async/await をしているとテストがしづらくなる
      • act の後に waitFor を書かないと動かないテスト
  • ただ、await を使わないといけないケース、使わないと無駄に複雑になるケースもある
    • 設計次第。コンポーネントの外側から見て、非同期的振る舞いを許容できるかどうか

どうしても、非同期処理が必要な場合は、act の中で React Testing Library の waitForXXX などを使って待つ

act {
  doSomethingToUpdateState();
  waitFor(() => ...);
}

ESLint に怒られる場合は wait を外に出す(または、testing-library/no-unnecessary-act ルールの isStrict オプションを外す)。

Takanori IshikawaTakanori Ishikawa

SQL で IN に複数書けるの知らなかった。subquery で検索したい時に便利

select
  id
from my_table
where
  (foo, bar) in
(
  select
    foo,
    min(bar) as bar
  from
    ...

Takanori IshikawaTakanori Ishikawa

useSWRInfinite() を使って自動で最後のページまで読み込む方法。全件取得だと遅すぎたり、件数制限があるときに。今は以下のようにしている。

const {
  data,
  size,
  setSize,
  isValidating,
} = useSWRInfinite(
  getKey,
  fetcher,
  {
    // Disable unnecessary updates while loading.
    revalidateFirstPage: false,
    initialSize: 1,
  }
);

// Read pages in order until it reaches the last end.
useEffect(() => {
  if (data?.length === size) {
    setSize(size + 1);
  }
}, [data?.length, setSize, size]);

また、最初のページはできるだけ早く表示したい。getKey() でページ番号に応じて件数を調整することもできるかもしれない(未検証)。今は Context を用意して、最初のページ(件数少なめ)を事前ローディングしておき、useSWRInfinite のデータが取得できるまではそちらを使うようにしている。

Takanori IshikawaTakanori Ishikawa

LSP 実装の E2E テストを TypeScript で書いている。

LSP クライアントを実装するときは、vscode-languageserver-node

  • vscode-jsonrpc
  • vscode-languageserver-protocol

のふたつを使って実装するのが良さそう(最初は、vscode-languageclient を使おうとしたが、deprecated な vscode パッケージに依存していて面倒くさそうだった)。

import path from 'node:path';
import process from 'node:process';
import { spawn } from 'node:child_process';
import * as rpc from 'vscode-jsonrpc/node';
import { InitializeRequest, InitializeParams } from 'vscode-languageserver-protocol/node';

const serverPath = path.join('path', 'to', 'lsp-server');
const ls = spawn(serverPath, { cwd: undefined, env: {} });

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

const connection = rpc.createMessageConnection(
  new rpc.StreamMessageReader(ls.stdout),
  new rpc.StreamMessageWriter(ls.stdin)
);

connection.listen();

const params: InitializeParams = {
  processId: process.pid,
  rootUri: null,
  capabilities: {},
};
connection.sendRequest(InitializeRequest.type, params);
Takanori IshikawaTakanori Ishikawa

Expo dev-client をビルドするときに Android.manifest を確認する方法。expo prebuild でコードを生成できる。ただし、このとき、eas.json で指定した環境変数を使うことができないので、マニュアルで指定する。

$ ENV=... expo prebuild --platform android

android/app/src/main/AndroidManifest.xml にファイルが生成されている。ここで手動で修正することもできる。その後、いつも通り EAS Build でビルドする。

$ eas build --platform android --profile development-client
Takanori IshikawaTakanori Ishikawa

[TypeScript] 定数文字列の配列から Union 型を作る

const TAGS = ["h1", "h2", "p"] as const;
type Tags = typeof TAGS[number]; // type Tags = "h1" | "h2" | "p"
Takanori IshikawaTakanori Ishikawa

Expo SDK 46 で React Native Reanimated 2.10.0 を動かす。

@babel/plugin-proposal-export-namespace-from を依存に入れる。

$ npm i --save-dev @babel/plugin-proposal-export-namespace-from

babel.config.js に以下を追加

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
...
      '@babel/plugin-proposal-export-namespace-from',
      'react-native-reanimated/plugin',
    ],
  };
};
Takanori IshikawaTakanori Ishikawa

Union type を Swift で表現するには、Associated value を持つ Enumeration を使う。

enum SpecialToken {
    case token(AddedToken)
    case string(String)
}

しかし、使う側は毎回 Variant を指定しなければならない。

let specialTokens: [SpecialToken] = [.string("A"), .string("B")]

文字列で指定することが多いケースでは、以下のように書きたい。

let specialTokens: [SpecialToken] = ["A", "B"]

こういう暗黙的な変換が実現できないか調べたところ、ExpressibleByExtendedGraphemeClusterLiteral プロトコルを実装すればいいらしい。

enum SpecialToken: ExpressibleByStringLiteral {
    case token(AddedToken)
    case string(String)

    init(stringLiteral value: String) {
        self = .string(value)
    }
}

これで実現したいことができるようになった。

Takanori IshikawaTakanori Ishikawa

Rust の Arc<RwLock<T>> から mutable reference を得る方法。

let mut m = model.model.write().unwrap();

if let ModelWrapper::BPE(b) = &mut *m {
    self.trainer.read().unwrap().train(b)
} else {
    panic!()
}
  1. .write() で WriteGuard を取得。mutable な変数で束縛
  2. *m で Guard を外し、
  3. &mut で参照を得る
Takanori IshikawaTakanori Ishikawa

SwiftPM では Package.swift を宣言的に書くことができる。

dependencies: [
    .product(name: "Tokenizers", package: "tokenizers-swift")
]),

ここで .product と書いてあるのは Enum かと思ったが、ドキュメントを確認すると static method だった。

/// Creates a target dependency on a product from a package dependency.
///
/// - parameters:
///   - name: The name of the product.
///   - package: The name of the package.
///   - condition: A condition that limits the application of the target dependency. For example, only apply a
///       dependency for a specific platform.
static func product(
    name: String,
    package: String,
    condition: TargetDependencyCondition? = nil
) -> Target.Dependency

このような型の省略を Type Omission というらしい。

宣言的な DSL を実装するための仕組みとして面白い。

Takanori IshikawaTakanori Ishikawa

Swift のコレクションの要素数やインデックスの型がなぜ、UInt ではなく Int なのか?

Takanori IshikawaTakanori Ishikawa

久々に Swift をちゃんとやり直したけど、非同期周りがめちゃ進化してた。async/await もあるし、特に Actor がとても良い

  • [Swift] actor reentrancy のありがたみと注意点 - Qiita
  • Actor はクラスのように定義でき、プロパティやメソッドを持てる
    class …actor ... にするだけ
  • Actor へのアクセスはすべて非同期になり、固有の実行キューで実行。ブロッキング/デッドロックしない
    • 不適切な呼出はコンパイラがチェックしてくれる
  • 「メインスレッドで実行する Actor」も作れる
    • 普通「UI の処理はメインスレッドからしか呼んではいけない」のだけど、これまではここの制御が大変だった(特に非同期処理を多段で呼び出す場合)
Takanori IshikawaTakanori Ishikawa

angular - day.js timezone plugin method produce an invalid Date - Stack Overflow

  • Unicode を扱う ICU のバージョンによって、JS エンジンが Date.prototype.toLocaleString で出力される文字列のスペースを U+202F (narrow no-break space) にしてしまう
  • その結果、new Date() で変換できる文字列にならない
  • しかし、dayjs の timezone プラグインがこれらを使って実装されている
  • そのため、dayjs.tz を通すと Invalid Date になってしまう

この問題を解決しようとしているけど、そもそも、

  • toLocaleString() の吐き出す文字列が 1/1/2022, 00:00:00 という形式
  • new Date() で保証されている形式は ISO-8601 形式のみ
Takanori IshikawaTakanori Ishikawa

nb で、毎回 nb sync するのが面倒だし忘れるので、自動化したい。特定のフォルダ以下のファイルが更新されたコマンドを実行できるようにする。

まずは fswatch を brew でインストールする

brew install fswatch

変更対象のパスなどは不要なので、以下のコマンドで ok (.git などは無視しないと無限ループする)

fswatch -o ~/Documents/nb/home --exclude '/\.' | xargs -n1 -I{} nb sync
Takanori IshikawaTakanori Ishikawa

改行と空白でレイアウトを調整しているテキストを React (JSX) にするのが意外と面倒

  • remark-markdown で Markdown を JSX に変換 (一部、強調とかもサポートしたかった)
  • そのままだと改行を反映するには Markdown の hard break syntax を使う必要があるので、remark-break プラグインを導入する
import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';

<ReactMarkdown remarkPlugins={[remarkBreaks]}>{text}</ReactMarkdown>
  • ただ、これでも空白類による横揃えができない (行頭の空白類は削除される)。また、&nbsp; も削除されてしまう(なんで?)
  • なので、Markdown の引用やリストをうまく使いつつ、レイアウトの調整はスタイルシートで行う