経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

npmから@typesを使う場合はDefinitelyTypedを見に行かないといけないことがある

Link Preview JSという、「allows you to extract information from a HTTP url/link (or parse a HTML document) and retrieve meta information such as title, description, images, videos, etc」なレポジトリがあった。

github.com

そこでは、cheerioというライブラリを用いて、パースされたレスポンスから特定の要素を取得している。
https://github.com/ospfranco/link-preview-js/blob/d22888f26718674d7a32cfb935434f7357c80817/index.ts#L236

Link Preview JSはTypeScriptで書かれているのだが、cheerioに対する型が全くなく、anyが使用されていたので、@types/cheerioを導入した。

github.com

だが、一部だけ以下のようなエラーがでて、@types/cheerioを用いてもanyが残ってしまっていた。

Property 'attribs' does not exist on type 'Element'.
Property 'attribs' does not exist on type 'TextElement'. ts(2339)

https://github.com/ospfranco/link-preview-js/pull/83#discussion_r563268635


このまま放置するのも気持ちが悪かったので、実際に@types/cherrioの型が書かれているDefinitely Typedをみにいった。

github.com
DefinitelyTyped/types/cheerio at master · DefinitelyTyped/DefinitelyTyped · GitHub

すると、cheerio.ElementTagElementにのみattribsの型が定義されていることがわかった。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/68e48c5ad46f0e7733feace0a9120d7dc3108462/types/cheerio/index.d.ts#L33

テストを見ても、elementtypetextではない、つまり、TextElementではなくTagElementであることを確認している。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b41bbafcd298b4b2aaa851d43ffadb6ef3565e69/types/cheerio/cheerio-tests.ts#L347

なので、それを適用して、自分で作ったanyを回収しにいった。
github.com

src/@types/*d.tsに型定義があるのと比べて、npmでインストールした場合は、node_modules/@types/*/*d.tsに型定義が隠蔽されてしまうので、便利だけど難しいなと思う。
それを解消する何かがもうすでにあるのだろうか。



TwitterでこのOSSの作者から感謝してもらえた。
https://twitter.com/ospfranco/status/1353312260823855105

(フォローしたけどフォローは返ってこなかった)

自分のPRを見て思う、TableDrivenTestsの良さ

jdという、commandline utility and Go library for diffing and patching JSON valuesなライブラリにTableDrivenTestsを導入したので、その良さを主観的にまとめておこうと思う。

github.com

github.com


まずは、実際にdiffの一部を見てみる。
https://github.com/josephburnett/jd/pull/29/files#diff-a0a78ab7b3426f60e7b758b565129777bdcef35e95a490999a0ff377389576d1R83

before

ctx := newTestContext(t)
checkDiff(ctx, `[]`, `[]`)
checkDiff(ctx, `[[]]`, `[[1]]`,
	`@ [0,0]`,
	`+ 1`)
checkDiff(ctx, `[1,2,3]`, `[1,null,3]`,
	`@ [1]`,
	`- 2`,
	`+ null`)
checkDiff(ctx, `[]`, `[3,4,5]`,
	`@ [0]`,
	`+ 3`,
	`@ [1]`,
	`+ 4`,
	`@ [2]`,
	`+ 5`)

after

ctx := newTestContext(t)
tests := []struct {
	context   *testContext
	a         string
	b         string
	diffLines []string
}{
	{ctx, `[]`, `[]`, []string {}},
        {ctx, `[[]]`, `[[1]]`, []string {
	     `@ [0,0]`,
	     `+ 1`,
	  },
        },
	{ctx, `[1,2,3]`, `[1,null,3]`, []string {
	    `@ [1]`,
	     `- 2`,
            `+ null`,
           },
        },
        {ctx, `[]`, `[3,4,5]`, []string {
            `@ [0]`,
	    `+ 3`,
            `@ [1]`,
            `+ 4`,
            `@ [2]`,
            `+ 5`,
	   },
	},
}
for _, tt := range tests {
     checkDiff(tt.context, tt.a, tt.b, tt.diffLines...)
}

よいと思うところを2つ書く。

関数呼び出しが冗長でなくなった

beforeafterで関数を呼び出す回数は変わらないが、afterはループの中で呼び出しているので、パッと見て冗長な表現にはなっていない。
func TestArrayDiff(t *testing.T) {のなかのテストなので、checkDiffを何回も呼び出している(ように見える)のは多少くどい。

関数の引数が宣言的になった

checkDiff関数は、func checkDiff(ctx *testContext, a, b string, diffLines ...string) {という形で引数を取る。
jd/common_test.go at d1d4edfee5375f88c1155ef55158597fb8488751 · josephburnett/jd · GitHub

beforeでは、どこからがdiffLinesが多少わかりにくくなっているが、afterでは、[]stringでまとめているため、checkDiffに何がどの引数で渡されるのかが分かりやすくなっている。

{
	context   *testContext
	a         string
	b         string
	diffLines []string
}

また、上のように、テストのはじめに宣言できることで、可読性が非常に高くなっている。
今回のようにbeforeがあの状態だったら、TableDrivenTestを導入しない手はないのではないかと思う。

割といい変更がだせたので自画自賛タイトルをつけてみた。

単純作業の効用は意外と大きい

owという、TypeScriptの型制限を拡張したOSSに何度かコミットして気づいたことは、単純作業は意外と効用が大きいということだ。
github.com

具体的に何をしたかというと、typescript-eslintの、explicit-function-return-typeが無効になっていたのを、有効にした。
github.com

まずはじめにpackage.jsonexplicit-function-return-typeの設定を"off"から"error"に変更し、テストを走らせた。
https://github.com/sindresorhus/ow/pull/206/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R89

めちゃくちゃ落ちた

あとは、そのテストが落ちた箇所にひたすら型をつけていった。
この型づけは自分的には単純作業のうちに入るのだが、自分のスキルはそれほど伸びないものの、得られる効用は大きい。
ほんの30分の型づけにより、これからこのOSSにコミットする全ての人は、function return type をつけなければならなくなった(つけないとテストが落ちる)。
これは、このOSSの保守性に直結することである。
関数が返す型が明確になると、初めてこのOSSに訪れる人のコードリーディングが捗るというオプションもついてくる。
このように、O(1)の単純作業でO(N)の効用が得られる。

ただし、個人的にはこのexplicit-function-return-typeを有効にするには然るべきタイミングがあると思っている。
まず、プロジェクトの初期からこれを有効にすると、テストが高確率で落ちることになり、開発スピードが落ちてしまう。
また、explicit-function-return-typeを有効にするときに、他のPRが溜まっている状態だったら、凄まじいコンフリクトが生じてしまう。
https://github.com/sindresorhus/ow/commits/main
なので、今回は、OSSとしてある程度枯れていること、他の人とPRが被らない時期であることを確認してから、このPRを出した。

ちなみにこういった単純作業をするときは、ミスチルの彩りを聴きながらすると捗る。

Cypress で `blitz new` をテストしたい(が、できていない)


ことの発端は、最近よくBlitzを聞くようになり、GitHubを訪れると、good first issueが転がっていて、テストに関するものがあったので、Blitzを触ったことも、GraphQLPrismaもあまり知らなかったけど、拾ってみた。

このissueの内容は、blitz newの挙動をCypressを使って確かめたい、というものだった。
Add e2e CLI test for `blitz new` · Issue #37 · blitz-js/legacy-framework · GitHub

We currently have a few blitz new tests, but not a complete e2e test for the new app.
...
Add Cypress to our CLI testing setup. See the Redwood CLI cypress test for a great example/starting point.

Cypressもはじめて聞く名前だったが、ドキュメントを読んでちょっと手元で動かして何となく理解し、Blitzに導入することにした。

github.com

(Cypress導入第1人者かと思っていたが、先人がいた)

マージされてコントリビューターにもなれたので、本腰をいれてテストを増やしていこうと思ったのだが、Blitzレポジトリの中でblitz newをe2eテストする、ということがよくわからなくなってしまった。
blitzを試すために手元でblitz newしたレポジトリも作って触っていて、どっちで何をしたいのかわからなくなり混乱が加速した。
なので、issueのなかで言及されていた、Redwoodのe2eテストがどうなっているのかを見にいくことにした。


まずは、RedwoodのCONTRIBUTING.mdを読み、e2eテストを動かすことにした。
が、うまく動かなかったのでissueを出した。

github.com

すると、2日足らずで問題は解決したようで、Cypressに慣れるためにも新たにテストを追加した。

github.com

Cypressは完全に理解できたので、Blitzにも入れていきたいが、Blitzレポジトリの中でblitz newをe2eテストするということがよくわからい問題が健在している。

Redwoodでは、起点がシェルスクリプトで、"We will copy './packages/create-redwood-app/template' and the packages/*"していることがわかっている。
redwood/test-tutorial at d61734296bf843d50933d36cabb2a9ea25ede919 · redwoodjs/redwood · GitHub


だが、Blitz'./packages/create-redwood-app/template'に当たるものがあるとは思えない。
また、これと同じようにシェルスクリプトで起動するとしても、Blitzの場合はデフォルトのフォームを選択する部分があり、そこをどうモックしたらよいのかわからない問題もある。
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub

Blitz既存のnew.test.tsを見にいったところ、mockImplementationでモックしていることがわかったが、これはあくまでcliのテストなのでやりたいこととは違う。

jest.mock("enquirer", () => {
  return jest.fn().mockImplementation(() => {
    return {
      prompt: jest.fn().mockImplementation(() => ({form: "React Final Form"})),
    }
  })
})

詰まってしまったので、そもそもblitz newコマンドを打ったら、どのようにしてファイル群を生成しているのかを見に行ったら、ヒントがあるのではないかと思い読み始めた。
すると、new.tsでは、generatorを生成してそれを走らせているっぽいことがわかった。
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub
blitz/new.ts at e2383fbb5f52a59042640dcf4cb67d2c8344de06 · blitz-js/blitz · GitHub

const generator = new (require("@blitzjs/generator").AppGenerator)({
...

await generator.run()

blitz newのテストは実質的にはgeneratorのテストなのか?、ということで"@blitzjs/generator"を読みにいこうとしたところで力尽きている。

exmapleにすでにテストがあるのでそれでいいのではないかと思い始めているけど、勉強になりそうなのでちょっとづつ読んでいきたいと思っている。
https://github.com/blitz-js/blitz/blob/canary/examples/auth/cypress/integration/index.test.ts


2/19追記

was this issue resolved in #1846?

という質問が来ていたので、疑問点をissueに書き込んでおいた。
https://github.com/blitz-js/blitz/issues/1767#issuecomment-781707012

NTT Performance Tuning Contest の対策と反省と抱負

nttcom.connpass.com


今までISUCONのようなパフォーマンスチューニングのイベントには参加したことがなかったのですが、興味はありました。
23卒があつまっているSlackで参加したいとつぶやくと、一瞬で反応が返ってきてチームを組めることになり、@PANORAMA_01さんと@marin_a___さんで参加しました。
大学ではHello Worldしないのでこういうコミュニティありがたいです。

対策

github.com

言語はRubyを選んでいたので、Sinatra でくるだろうと思い、チュートリアルを軽く眺めてget, postの挙動を確かめたりしました。
Rubyの文法を忘れている部分があったのでちょっと復習しました。
qiita.com


パフォーマンスチューニングの雰囲気を掴むために過去開催されていたブログを読みました。
www.ntt.com
isucon.net

N+1問題について調べました。
Nは発行したクエリの数であり、探索したレコードの数ではないことを知り理解がクリアになりました。
qiita.com
qiita.com

インデックスとEXPLAINコマンドについて復習しました。
dev.mysql.com
qiita.com
blog.utgw.net


Nginxを調べて、リバースプロキシやキャッシュの設定を試しました。
proxy · wafuwafu13/sinatra-tutarial@fc25c4e · GitHub
画像キャッシュの設定 · wafuwafu13/sinatra-tutarial@8218b11 · GitHub

画像のキャッシュがHITしている

qiita.com
qiita.com
rin-ka.net

alpについて調べて試しました。
logをマウント · wafuwafu13/sinatra-tutarial@f49103e · GitHub

alpでNginxのログを解析している

nishinatoshiharu.com
zenn.dev

MySQLのスロークエリの設定やpt-query-digestについて調べて試しました。
pt-query-digest · Issue #1 · wafuwafu13/sinatra-tutarial · GitHub

masayuki14.hatenablog.com
thinkit.co.jp
dtyler.io

ISUCONのススメが前日に発売開始されていたので眺めました。
nextpublishing.jp


本番に向けて3人で勉強の進捗確認とか作戦会議をしました。
3人ともフロント寄りというかアプリケーションのコードを書いているのでゆるくがんばろうというモチベーションで参加しました。

反省と抱負

本番は、pt-query-digest入れて実行してスロークエリ見つけてインデックスをはってN+1を見つけて解決策がわからず放置してalpつかおうと思ってNginx入れようとしてわからなくなって放置して画像カラムどうにかしようとして解決策がわからなくなって放置して終了しました。


ベストプラクティスを脳に詰め込んでいっても手は動かないことを実感した7時間でした。
個々の使い方やボトルネックは分かるけど本番のアプリケーションにうまく落とし込めないのが放置して終了の原因だと思われます。
個々のアルゴリズムはわかるけど問題が解けない競プロと同じ感じがしています。
自分たちで本番と同じような環境を構築して経験を積んでコンテストに出ないと足元にもおよばないので練習あるのみという感じです。
ただ、今回参加に向けて勉強したり本番でできたりできなかったりした知見は結構大きいなと思っています。
あとは言語ですね、今回はRubyを使用しましたがパフォチューにはGoという感じがしているのでつめていきたいです。
言語やその周辺ライブラリの習得には、文法を理解する、自分でアプリケーションを作る、現場で使う、OSSを読んでPRする、の4段階あると思っています。
JS/TSは、現場で使う、OSSで生かす、OSSで学ぶ、現場で生かすのブーストが(たぶん)かかっている状態なので半年〜1年間でGoやその周辺ライブラリもそこまで持っていきたいと思います。
NTTさん、チームのかた、ありがとうございました!!
リベンジしたい!!!

「関数型プログラミングの基礎」モナドを作るを理解する

wafuwafu13.hatenadiary.com

と同じ本の、p.268~p.274あたりでつまずいたので、メモを残しておく。
コードはここにおいてある。
akimichi.github.io

モナドの機能は、「値にコンテキストを付加すること」、「コンテキストを付加したまま処理を合成すること」である。
unit関数は、モナドインスタンスを生成するための関数で、unit:: T => M[T]という型情報を持つ。
Tモナドに渡される値の型であり、Mモナドの型である。
flatMap関数は、処理を合成するための関数で、flatMap:: M[T] => FUN[T => M[S]] => M[S]という型情報を持つ。
M[T]というモナドインスタンスを受け取ると、そのモナドから値を取り出してT => M[S]という関数を適用し、その結果としてM[S]を返す。

今回メモするモナドは、恒等モナドである。
恒等モナドは、値にコンテキストを付加することなく、そのまま値として扱う。
恒等モナドの関数を、以下のようにIDという名前空間に定義する。

const ID = {
  unit: (value) => {
    return value;
  },

  flatMap: (instanceM) => {
    return (transform) => {
      return transform(instanceM);
    };
  },
};

また、次のような関数を定義する。

const succ = (n) => {
  return n + 1;
};

const double = (m) => {
  return m * 2;
};

unit関数は、単に値を返しているだけである。

ID.unit(1) == 1;

flatMap関数を介して1succ関数を適用する処理は、succ(1)と同じ結果になる。

ID.flatMap(ID.unit(1))((one) => {
  return ID.unit(succ(one));
}) == succ(1)

これを順を追ってみていく。

まず、ID.flatMapは、引数ID.unit(1) = 1なので、以下を返す。

(transform) => {
   return transform(1);
};

そして、(one) => { return ID.unit(succ(one)); }が適用される。

(one) => {
   return ID.unit(succ(one));
}(1)

よって元のモナドは、succ(1)と同じ結果になる。

次に、flatMap関数を入れ子にしたものをみていく。
これは、次の関数を合成する関数をおなじ働きをする。

const compose = (f, g) => {
  return (arg) => {
    return f(g(arg));
  };
};

つまり、以下の等式が成り立つ。

ID.flatMap(ID.unit(1))((one) => {
  return ID.flatMap(ID.unit(succ(one)))((two) => {
    return ID.unit(double(two))
  })
}) == compose(double, succ)(1)

これを順を追ってみていく。

まず、以下の関数に着目する。

ID.flatMap(ID.unit(succ(one)))((two) => {
  return ID.unit(double(two))
})

ID.flatMapは以下を返す。

(transform) => {
   return transform(ID.unit(succ(one)));
};

そして、(two) => { return ID.unit(double(two)) }が適用される。

((two) => {
   return ID.unit(double(two))
})(ID.unit(succ(one)))

よって、元の以下の関数は、

ID.flatMap(ID.unit(1))((one) => {
  return ID.flatMap(ID.unit(succ(one)))((two) => {
    return ID.unit(double(two))
  })
})

以下のようになる。

ID.flatMap(ID.unit(1))((one) => {
  return  ((two) => {
      return ID.unit(double(two))
  })(ID.unit(succ(one)))
})

ID.flatMapは、引数ID.unit(1) = 1なので、以下を返す。

(transform) => {
   return transform(1);
};

よって、以下の関数が適用されると、

(one) => {
  return  ((two) => {
      return ID.unit(double(two))
  })(ID.unit(succ(one)))
}

最終的には以下のようになる。

(one) => {
   return  ((two) => {
      return ID.unit(double(two))
   })(ID.unit(succ(one)))
}(1)

よって元のモナドは、関数を合成する関数と同じ働きをする。

const compose = (f, g) => {
  return (arg) => {
    return f(g(arg))
  }
}

ID.flatMap(ID.unit(1))((one) => {
  return ID.flatMap(ID.unit(succ(one)))((two) => {
    return ID.unit(double(two))
  })
}) == compose(double, succ)(1)

モナドの初歩が理解できたので、この後出てくるMaybeモナドやIOモナドの理解もしやすくなるはず。

GoでPythonのrandom.shuffle()を実装し、カイ二乗検定を用いたユニットテストをする

背景

Goでコントリビュートできそうなレポジトリを探していると、goshというレポジトリがあった。
これは、JavaScriptPythonでの構文をGoで書く、というものだった。
面白そうだったので、Pythonshuffleを実装し、PRを出そうと思った。
Pythonのshuffleというのは、配列を受け取り、その配列の要素の順番をランダムに変換して返す、というのだった。
commitしようとしていたコードは、以下だった。

func Shuffle(s []interface{}) []interface{} {
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] })
	return s
}

しかし、commitしようとした時にはもう実装されていたのでissueを閉じた。
github.com
commitしようとした時のテストコードは、

  • shuffleする前とした後で配列の長さが等しい
  • shuffleする前とした後で配列の要素が等しい

ことを確認するものだった。
しかしよく考えると、上のコードでは破壊的な操作はしておらず、上記の2項目は自明であった。
肝心なのは、

  • shuffleによって、要素の順番が満遍なく変換されている

ことを確認することだった。
今回のような、ランダム性をテストする時にはどうしたらよいかを調べてみると、以下のスライドがあった。
speakerdeck.com
このスライドによると、カイ二乗検定によってランダム性をテストできる、ということだった。

カイ二乗検定とは

帰無仮説が正しければ検定統計量が漸近的にカイ二乗分布に従うような統計的検定法の総称である。
カイ二乗検定 - Wikipedia

ざっくりとした感じで例を出す。
サイコロを振って、目が均等に出ているかどうかを検定したいとする。
実際に振ってみて出た目の回数を観測度数、目が均等に出ている場合の回数を期待度数という。
それぞれの回数が以下の場合、

サイコロの目 1 2 3 4 5 6 合計
観測度数 17 22 21 14 22 24 120
期待度数 20 20 20 20 20 20 120

 x_0^2 = \dfrac{(17-20)^2}{20} + \dfrac{(22-20)^2}{20} +\dfrac{(21-20)^2}{20} +\dfrac{(14-20)^2}{20} +\dfrac{(22-20)^2}{20} + \dfrac{(24-20)^2}{20} = 3.5

という計算で x_0^2を求める。
この x_0^2が、棄却域より小さければ、目が均等に出ていることが保証される。
棄却域有意水準自由度で決まる。
有意水準は、たいてい5%か1%で、自由度は、(要素の数 - 1)で、今回だったら、(目の数6 - 1 )= 5となる。
棄却域の分布表は以下から取得できる。
http://www3.u-toyama.ac.jp/kkarato/2016/statistics/handout/chisqdist.pdf

コードの概要

今回作ったリポジトリが以下である。
github.com

shuffle.goには、先述したshuffleのコードを書いている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle.go

package chi_square

import (
	"math/rand"
	"time"
)

func Shuffle(s []interface{}) []interface{} {
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] })
	return s
}

テストは、[]interface{}{"a", "b", "c", "d", "e"}というスライスをShuffleに渡したとき、うまくシャッフルできているかを確認する。
カイ二乗検定を行うChiSquareは、第1引数に有意水準、第2引数に自由度、第3引数に観測度数、第4引数に期待度数をとり、 x_0^2棄却域より小さい場合にtrue、大きい場合にfalseを返している。

Shuffleを100回実行したとき、スライスの先頭、つまり"a"の位置が、0,1,2,3,4番目にくる回数がそれぞれ等しかったら、うまくシャッフルできているといえる。
よって、期待度数は、[]float64{20, 20, 20, 20, 20}となる。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L28
観測度数は、実際にShuffleを100回実行し、"a"のインデックスを累計して求めている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L27
有意水準は5%に設定し、自由度は(配列の長さ-1)で求められている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L29

if !ChiSquare(5, len(expected_frequency)-1, observation_frequency, expected_frequency) {
	 t.Error("not shuffled")
}

 x_0^2の計算および、棄却域との比較は、chi_square.goでやっている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/chi_square.go

...
x += (math.Pow(observation_frequency[i] - expected_frequency[i], 2)) / expected_frequency[i]
...
	if x > rejection_area {
		return false
	} else {
		return true
	}
...

テスト実行と結果

実際にテストを実行してみると、以下のような出力を得る。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/chi_square.go#L14

$ go test
x: 1.0999999999999999, rejection_area: 9.49
PASS
ok      go-chi-square-test      0.474s

 x_0^2の値、つまり出力xの値は実行するたびに異なる。
このことからもシャッフルがうまく行われていることがわかる。

< go test
x: 4.1, rejection_area: 9.49
PASS
ok      go-chi-square-test      0.148s

また、shuffle.gorand.Seed(time.Now().UnixNano())の処理を消すと、出力xの値は実行するたびに同じになり、(テストの結果をログに残しておけば?)シャッフルの処理がうまく行われていないこともわかる。

ちなみに、テスト対象のスライスを[]interface{}{"a", "a", "a", "a", "a"}にすると、毎回"a"が0番目にあると判定されてしまうので、テストは落ちる。(そもそも同じ要素だけのスライスをshuffleする動機が謎なので落ちて正解という捉え方もできる)
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L9

$ go test
x: 400, rejection_area: 9.49
--- FAIL: TestShuffle (0.00s)
    shuffle_test.go:30: not shuffled
FAIL
exit status 1
FAIL    go-chi-square-test      0.414s

テスト対象のスライスによってexpected_frequencyの値を変えてTableDrivenTestsすればよいと思うけど、もう満足したので終わる。