クラスター社内で今年開催されたLT会を一部紹介

この記事は クラスター Advent Calendar 2023 (2枚目) の1日目の記事です。

気が付けば昨年のアドベントカレンダー会社に雑LTという文化があったので開催してみたを公開してから1年が経過してしまったぽけばです、ごきげんよう。 今年も色んな事があったような気がするし何かはやっていた気がする……ということで、今回は2023に私が企画した社内LT会について紹介したいと思います。

※ここで紹介する社内LT会は業務時間外に有志が集まって開催しているものです

そもそもLTって?

ライトニングトークと呼ばれる短いプレゼンテーションのことを、英単語の頭文字をとってLTと主にIT技術界隈で呼んでいます。 一般的に知られているスタイルは持ち時間5分で終了のものですが、クラスター社内で行われるLT会ではあまりかっちりn分といったような形ではないパターンが多いです。 業務じゃないし緩くていいんじゃない、と私も思っているので「だいたいn分くらい」くらいの雑さになっていたりします。

clusterを知る会

クラスターでclusterの開発に携わっている身なので、当然のことながらclusterのことを知る機会はたくさんあります。 とはいえ単に仕事をしているだけではなかなか知る機会がない物事、特に人々の知見や工夫といったものあるのではないか、という考えが自分の中にあったので開催してみることにしました。 ちなみにLT会といいつつ、発表者が多いわけでもないので長くても良いですよとか言ってた気がします(LT会とは)。

お題の方向性がちょっと難しいこともあり喋ってくださる方がいるか不安ではあったものの、当日は7名の方に喋っていただけました(自分も含めると8です)。 いくつかの機能の話、考え方ややっていることから、Cluster CultureやHello Clusterの"歴史"についてなどこの会だからこそ喋ってもらえた内容もあり、個人的には改めてclusterについて考える良いきっかけになりました。

推しLT会

前述したclusterを知る会が終了した後にとある方*1が推しているあるものについて話を振ったら話が盛り上がり、そういう自分が今推している物事を紹介する機会があってもいいんじゃね、という話になったので「推しLT会」を開催しました。 まぁとにかく好きなことを好きなように喋りましょうという感じですね。

レギュレーションはこんなかんじ:

  • あなたが推したいものについて喋ってください
    • あなたの推しであるなら技術・機材・etcのようなネタもOK
  • 押し付けは良くないので、語彙に気を付けましょう

この会で発表を希望した方々は、やはり自分の推しのことを喋るだけあってとても熱量の高い発表ばかりでした。 ネタとしてはアニメやアイドル、VTuberのような鉄板ネタから推しの企業紹介という方向性のもの、ブラウザのようなツール系、そして(喋った人に許可を取ってないので)ここで書いて良いかわからない剛速球や変化球まで多種多様で面白かったです。

ちなみにこの会で喋った方のひとりが喋った内容を記事にしていたりします。 どんな発表が行われたのか垣間見れると思うのでぜひ読んでみてください。

反省としては3時間以上の会になってしまったことでしょうか。 推しの語りを止めるのはなー、という想いからタイムキープを緩めにしていたのですが……塩梅が難しいですね。 この会は折を見てまたやりたいと思いつつ、時間という制約とのバトルになりそう。

年末だよ雑LT

年末だし雑さが欲しいな……ということでなんと12/1、今日このアドベントカレンダー担当日と同じ日に雑LTを開催することにしていました! わざわざこのネタでアドベントカレンダー1日目枠を取りに来たのはこういう裏があったわけです。

雑LTは今回で9回目の開催だそうです。 社内有志で行われているイベントとしてはけっこうかなり息の長い部類に入るのではないでしょうか。 主催者を固定せずやりたいと思った人が雑に開催するというスタイルやジャンルを固定せずに隙に喋って良いという方向性が、会社文化として根付いている理由なのかもしれません。

ちなみに今回の雑LTの裏ではとある別のイベントも開催されます。 特に示し合わせたわけではなくいくつかの事情が重なった結果として同日開催になり「こういうこともあるんだな~面白い」という気分です。 それだけ社員同士で何かできる環境だということかもしれません。

さてはて、今日はいったいどのようなLTが行われるのでしょうか。 喋りたい人が名前とひとこと書く欄の多くが開始直前まで「なにか」としか書かれないのでまったく予想ができないんですよね。 それが楽しかったりもするのですけど。

来年やってみたいこと

ここまでは今年やってみたことでしたが、ついでに来年やってみたいことも書いておきます。

clusterの機能を使い倒すLT会をやれないかなーと考えています。 画面共有やファイル機能はあるわけだし、なんならスライドなんて用意せずクラフトアイテムやアクセサリーでLTしたって良いじゃないとかなんとか。 写真フィードもできたことですし。

おしまい

そんなわけでクラスター社のLTイベントの一部紹介でした。 皆さんが所属する会社、コミュニティでも緩いLT会をやってみませんか?

明日のアドベントカレンダー担当はLT会で何度か喋ってくださっている @ryu3taki さんです!

*1:もうひとりのクラスタアドベントカレンダー2023 1日目担当

会社に雑LTという文化があったので開催してみた

この記事は クラスター Advent Calendar 2022 (2枚目) の13日目の記事です。

昨日は monac さんの アバターCM動画をYouTubeで公開したよ! でした。 動画づくりの一端がみえて勉強になりますね。

というわけこんばんは、クラスター社でUnityとserverの間を行ったり来たりしているpocketberserkerです。 今年5月に入社してから既に7か月少々……月日が経つのは早いものですね。

技術的な話は先月の会社tech blogの記事を書いて満足したので、今回は技術ではない話……会社のLT会について紹介したいと思います。

雑LT

どういった話の流れか忘れてしまいましたが、ある月1出社日に同僚と雑談していた際に「半年前くらいに雑LTというのあって~」という話になりました。 そのときにふと思ったことがひとつ。

「LTはLightning Talkだとして……雑って何?」

前々職ではたまに社内エンジニアLT会の司会を担当していた身、気にならないはずがありません。 というわけで同僚に教えてもらった社内ドキュメントを見てみると、こんなことが書かれていました(一部抜粋+要約)。

  • 雑にLTする会
  • 開催したくなったら適当にページをつくって募集して雑に開催する
    • 誰かが主催者になって定期開催、とかはしない
  • 全社員だれでも参加自由
  • テーマは自由
    • 技術に寄りすぎるとおいてけぼりにしてしまうこともあるよ
    • でも裁量は喋る人にお任せ
  • 雑に好きなことしゃべったり聴いたりしたい
    • LTならどんなにくだらない話をしても5分で終わる
  • 業務ではない

なるほど不定期に情報発信する機会いいですね、参加してみたい……などと考えていたところ、 やりたいとおもった人が手をあげる 方式だと風のうわさが聞こえてきたので「やります!」と声をあげました。 こうして私がやると言い始めた 雑LT #7 は雑に開催が決定したのです。

ちなみに #7 と書いたところからもわかる通り、雑LTは今回で7回目の開催だったそうです。 数年にわたってこういった催しが自主的に開催されているのは地味にすごいですね。

準備

開催日時を決めて、ページを作って告知して、たまに再告知して終わりです!雑ですね!

……これだけだとあれなので捕捉しておくと、この手のイベントは主催が頑張りすぎると今後のハードルがあがってしまいますし、準備に疲弊してしまうのもなというのがあったので場を設ける程度の準備に留めた感じです。 開催ハードルを下げておくことが継続への道、とかなんとか。

発表者募集に関しては、ありがたいことに自主的に手を挙げてくださった人々のおかげで枠に困ることはありませんでした。 やはりこの手の会社に勤めている人は語りたいことのひとつやふたつはあるのだな、と安心した記憶があります。

そんな感じで前日まではほぼ何もしてなかったわけですが、当日準備のいくつかは人々に手伝ったいただきました。 声をかけたら笑顔の二つ返事でやってくださってとても助かりました。

発表内容

テーマは自由 ということもあってか、技術的な話もあれば趣味全開の話もありつつ、発表者それぞれの個性が現れていてとても面白い発表ばかりでした。 また、圧倒的な内容で視聴者一同がぽかーんとするものから常に笑いの嵐を起こす発表、そんな歴史があったんかみたいな内容、資料を用意せずVRの実演のみで視聴者に興味をひくスタイルもあったりと、なかなかバラエティに富んでいて勉強にもなりました。

ところで皆さんは、 クラスター Advent Calendar 2022 (2枚目) の 9日目の記事は読まれましたか? 趣味開発アプリのVRM 1.0対応という興味深い内容なので、未読の方はぜひ読んでみてください!

この記事の 本題 部分にはスライドが埋め込まれているのですが、実はこれは、雑LTで発表してくださったときの資料だそうです! テーマの縛りがないからこそこういった趣味開発の紹介ができることも、雑LTの良い一面ではないかと考えています。

心掛けたこと

今回の雑LT開催にあたって個人的に心掛けていたことは以下の通り。

  • 発表タイトルは発表時まで決まってなくてもOK
  • 飛び入りあり
  • タイムキープはするが強制終了はしない
  • できる限りひとことコメントを返す
  • Slackをちゃんとみる

まず発表タイトルについてですが、この手のLT大会は開催日当日が近づくにつれてテンションが変動し内容を変えたくなることがあるんですよね(という勝手な自己経験則)。 個人的にはそういうのもLTイベントの醍醐味かなと思っているので、特にタイトルを埋めてもらうようなことはしませんでした。 (実は明記してなかったですが)当日飛び入りありにしたのもこのあたりが絡んでいます。

タイムキープについては物理的な時間の誓約(深夜に突入しないようにしたい)があったので悩ましいところだったのですが、まー雑なLT会だしということで残り時間や超過時間は共有しつつも強権は発動しないスタイルに落ち着きました。 せっかくの喋る機会を奪いたくないという個人的なわがままもあります。 あとなるべく発表者を遮りたくなかったので、将棋のタイムキーパーみたいな発声を心掛けてみました(役に立ったかはわからない)。

続いては司会に関する話です。 世に広まっているLT会の多くは質問時間を設けないことが多く、時間的制約を鑑みて今回の雑LTもそのフォーマットに沿って行いましたが、ここで問題になるのが発表者へのフィードバックの少なさです。 特にオンライン発表の場合は拍手といった物理的フィードバックも得られないことが多いので、いまいち発表した気になれない問題が付きまといます。 こういった状況を払しょくしたかったこともあり、常にマイクを持っている司会の私は"どんなに拙くてもひとことは反応を返す"べきだと考え実行に移したのでした。 必ずしも適切にコメントできたとは思っていないですが、参加者から「良いコメント」みたいな反応をいただけたのでここは司会として意識しておいて良かったと感じた部分です。

最後に Slackをちゃんとみる ですが、これは参加者や社内の状況をちゃんと見ておきましょうという話です。 発表者もそうですが、もちろん単に参加している人たちにもできる限り楽しんでもらいたいので、人々の反応をみながら休憩を挟んだり発表順を入れ替えたりしました。 とはいえ、これとタイムキープとコメントを考えるのを同時に行うのはかなり大変だったので、分担したほうがよかったなという反省があります。

おわりに

というわけで、雑LTという社内のお祭り的なイベントの紹介でした。 こういったイベントが半年~1年に一度あると程よく好奇心が知識欲がくすぐられたり、業務的にはあまり関わりのない方とも会話できる良い機会になるのでおすすめです。

普段から技術LT会をやっているよーという会社さんは、逆に 某社で行われていたらしい闇LTのようにエンジニアリングの話題を話してはならない縛りを入れてみるのも面白いと思います。 たぶんきっと楽しいイベントになると思うので、皆さんの会社でもぜひやってみてはいかがでしょうか?

雑が皆さんを待っています。

2022/12/19 4コマ漫画で紹介されました

Poker Handle ( inspired by Wordle and Worldle ) を作ってみた

流行っているものは自分でも実装チャレンジしたくなるタイプなので、Wordle インスパイアの Poker Handle というものを作ってみた。 別作業の息抜き、プログラミング能力の低下を防ぐためという目的もそこそこある。

本家にある機能は一通り揃えたつもりである。 特にprivateにしておく理由もない気がしたのでリポジトリも公開した(てきとーに書き散らしているのであまり参考にしないほうが良い)。

https://pocketberserker.github.io/poker-handle/

https://github.com/pocketberserker/poker-handle

テストプレーに協力してくれた同僚、元同僚、知人の皆様にこの場で感謝を。

なんでポーカー?

Wordleのパネルは5枚……ポーカー(正確にはテキサスホールデム)の共通カードも5枚……よしこれを使おう、くらいのノリ。 今はちょっと反省している。

論外から無理ゲーになるまでの軌跡

Poker Handleははじめ、わりとWordleに近いルールで実装していた。

  • ハンドは自分と相手1人の4枚が公開
  • 自分は相手よりも強い(役は同じでも数字のスコア的に上になる場合もある)ハンドになる
  • 場のカード5枚を完全一致で当てる

が、テストプレーしたりしてもらった段階でそうそうに「これはゲームにならないね」という結論に至った。 48枚の中から5枚を選択しようにも、組み合わせ爆発して試行回数が全然足りずに終わってしまう。 1枚正解が当てられれば良いほうになっていた。

Wordle(やほかのまだプレーされているクローン)は入力の組み合わせが比較的に少なく、こういった事故が基本的に起きない。 題材選びで差がでてしまったといえる。

いろいろ検討や調整の末、Poker Handleにはいくつかの独自要素を組み込むことになった。

  • 公開ハンドは自分と相手3人の8枚
  • 相手の役はオープン
  • flop, turn, river概念の導入
    • flip3枚は順不同で正解
  • 青パネル概念の導入
    • その数字の別スートがどこかに含まれている、というものを青パネルで表現することにした

これくらい仕様を追加しても4手が最速手になりがちなのは、トランプのカード枚数が多いのだからもうしょうがない気がしている。

inspired by Wordle and "Worldle"

Poker HandleはWordleだけでなくWorldleも参考にしている。 どのあたりを参考にしたかというと……i18n対応されているところだ。 Worldleはフランス語対応しているので、Poker Handleも真似して日本語対応してみたところがある。

(Worldleの挙動を確認するまでは、面倒なので英語オンリーでいいかなと考えていた)

派生物を作って思ったこと

Wordleは非常によくできている。 Simple is best を久々に思い知らされたし、それ以外にもいろいろと考えられている(と私は感じた)。

  • ルールがシンプルなので人々が挑戦しやすい
  • 回答候補が組み合わせ爆発しない
    • ゲームとして成立する
    • プレイヤーに考えることを要求する
  • モバイルでもPCでも同じ画面
    • UIによって個人差がでるということがない
  • アニメーション
    • ちょうどよいくらいの量
    • ないとチープに見えてしまうし、過剰だと邪魔
  • 統計表示
    • わかりやすい
  • シェア方法
    • 一部コンテキストを落としても結果を共有できる仕組みを考えたのがすごい
  • ヘルプが整備されている
    • 改めてそのシンプルさを思い知らされる

Worldleも、Wordle感を損なわずに大胆に題材や入出力を変えていて素晴らしい出来だと思う。

前述の2つに共通するものを考えてみたが、入力候補が少ない点(400個くらい?ちゃんと調べてないけど)と、題材がそこそこ身近な内容である点だろうか。 英単語は人によるだろうがそこそこ馴染みがあるだろうし、国というものも知らないわけではない。 ゲームプレーへの敷居が低いこともあって、一発ネタにとどまらずプレーされ続けているのではないかと考えている。

あえて問題点をあげるとするなら、2つのゲーム共に知識がクリアを左右しがちということだろうか。 Wordleは5文字の単語を連想できなければ始まらないし、Worldleは国の形や英語での国名を知っている必要がある。 やってみた人の何割かはこれが原因で1回しか挑戦しなかった、とかはありえるのではないか。

Wordleクローン実装は良い教材かもしれない

Webフロントエンド技術を用いた練習題材としてちょうどよいかもしれない、と思った。

  • serverを必要としない
    • データの保存はlocalstorage
  • ゲームの仕様自体はシンプル
  • そこそこコンポーネントを作る必要がある
  • PCとモバイル表示の調整
  • 入力データやアニメーションに関する状態をどう伝播させるか考える必要がある
  • イラスト等のデータが不要
  • 世の中に参考実装が公開されている
  • 1週間くらいで作れなくもない
    • 技量依存だけど

手軽さはもちろんTODO listとかだけど、選択肢としてはこっちもありなのではないだろうか。

まとめ

流行るものは流行る理由があるんだなぁ。

コンピュテーション式で"はちみーのうた"

コンピュテーション式でキーワード引数 - ぐるぐる~カスタムオペレーションの呼び出し順序を制御する - Qiita をみていたら久々にコンピュテーション式で遊びたくなったのでひとネタ供養。

今回のネタは状態遷移を書けると噂の”はちみーのうた”です。 歌詞通りにキーワードを呼び出さないとコンパイルエラーになるはず、きっと。 まぁ多少の妥協があります。

type GotoHachimi = interface end
type GotoAshi = interface end

type Hachimi = Hachimi
  with
    override _.ToString() = "はちみー"
    interface GotoHachimi

type Hachimi<'T when 'T :> GotoHachimi> = Hachimi of 'T
  with
    override this.ToString() =
      match this with
      | Hachimi t -> sprintf $"{t}はちみー"
    interface GotoHachimi

type Nameru<'T when 'T :> GotoHachimi> = Nameru of 'T
  with
    override this.ToString() =
      match this with
      | Nameru t -> sprintf $"{t}をなめると"
    interface GotoAshi
    interface GotoHachimi

type Ashi<'T when 'T :> GotoAshi> = Ashi of 'T
  with
    override this.ToString() =
      match this with
      | Ashi t -> sprintf $"{t}あしがー"
    interface GotoAshi

type Hayakunaru<'T when 'T :> GotoAshi> = Hayakunaru of 'T
  with
    override this.ToString() =
      match this with
      | Hayakunaru t -> sprintf $"{t}はやくーなる"
    interface GotoHachimi

type Lyrics = Nameru<Hachimi<Hachimi<Hachimi<Hachimi<
  Nameru<Hachimi<Hachimi<Hachimi<Hachimi<
    Hayakunaru<Ashi<Ashi<Ashi<Nameru<Hachimi<Hachimi<Hachimi<Hachimi>>>>>>>>
  >>>>>
>>>>>

type HachimiSongBuilder () =
  member _.Yield(()) = ()

  // 右辺が `Hachimi` だけだと `'T -> Hachimi<'T>` を返してしまうので、 `Hachimi.Hachimi` で固定できるようにしておく
  [<CustomOperation("はちみー")>]
  member _.Hachimi(()) = Hachimi.Hachimi

  [<CustomOperation("はちみー")>]
  member _.Hachimi(x: #GotoHachimi) = Hachimi(x)

  [<CustomOperation("をなめると")>]
  member _.Nameru(x: Hachimi<Hachimi<Hachimi<#GotoHachimi>>>) = Nameru(x)

  [<CustomOperation("あしがー")>]
  member _.Ashi(x: #GotoAshi) = Ashi(x)

  [<CustomOperation("はやくーなる")>]
  member _.Hayakunaru(x: Ashi<Ashi<Ashi<Nameru<_>>>>) = Hayakunaru(x)

  member _.Run(song: Lyrics) = printfn $"{song}"

let はちみーのうた = HachimiSongBuilder()

はちみーのうた {
  はちみー; はちみー; はちみー
  はちみー; をなめると
  あしがー; あしがー; あしがー
  はやくーなる

  はちみー; はちみー; はちみー
  はちみー; をなめると
  
  はちみー; はちみー; はちみー
  はちみー; をなめると
}

出力:

はちみーはちみーはちみーはちみーをなめるとあしがーあしがーあしがーはやくーなるはちみーはちみーはちみーはちみーをなめるとはちみーはちみーはちみーはちみーをなめると
  • 初回はかならず”はちみー”が来てほしいので単発の Hachimi が来るように YieldHachimi メソッドを用意
  • カスタムパラメータはoverloadが可能
  • 繰り返しを雑に省略定義するために interface を使用
  • はちみー を 4回繰り返さないと をなめると に移動できないように引数の型を調整
    • あしがー 3回も同じ方法
  • Run メソッドの引数を Lyrics 型に限定することで、歌詞通りなことを保証する
  • 型パラメータに制約をつけているけど、なくてもたぶん問題ない

久しぶりの記事がこんなのでいいのだろうか :thinking_face:

ZStringを使ったPrintfモジュールを作りたい人生だった

ZStringに組み込まれているものを使ってF#のPrintfモジュールに手が咥えられないか試してみたものの、成果は芳しくなかったので残骸だけ置いておきます。

https://github.com/pocketberserker/FSharp.ZPrintf

一番の問題はUtf16ValueStringBuilder向けのbprintfがうまく動かなかったこと。 https://github.com/pocketberserker/FSharp.ZPrintf/blob/a847d48e26912be2cff5ce516c5f65a7244c9349/src/FSharp.ZPrintf/ZPrintf.fs#L1532k を呼び出す前は値が書き込まれているのに、k 内でBuilderの中身を見ると空っぽになっていてどうして…。 眠くて何か見落としているだけかもしれないので気が向いたら精査するかも。

一応ベンチマーク:

https://github.com/pocketberserker/FSharp.ZPrintf/blob/a847d48e26912be2cff5ce516c5f65a7244c9349/examples/PerfBenchmark/Program.fs

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
  [Host]     : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT DEBUG
  DefaultJob : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
StringPlus 148.3 ns 1.67 ns 1.56 ns 0.0706 - - 296 B
StringConcat 167.3 ns 2.29 ns 2.14 ns 0.0880 - - 368 B
ZStringConcat 103.3 ns 1.35 ns 1.19 ns 0.0134 - - 56 B
StringFormat 172.9 ns 2.96 ns 2.77 ns 0.0305 - - 128 B
ZStringFormat 201.7 ns 1.50 ns 1.40 ns 0.0134 - - 56 B
Printf 792.6 ns 8.73 ns 7.29 ns 0.1221 - - 512 B
ZPrintf 826.8 ns 6.61 ns 5.52 ns 0.1049 - - 440 B
PrintfList 347,051.2 ns 6,814.98 ns 6,374.74 ns 15.1367 - - 63477 B
ZPrintfList 341,261.4 ns 3,644.64 ns 3,409.20 ns 15.1367 - - 63587 B

結果的にZStringのソースコード読み漁ったりPrintfモジュールの復習になったので :yoshi:

MessagePack.FSharpExtensions 2.0.0をリリースしました

www.nuget.org

全国約…何人かわからない F#erの皆様、お待たせしました(お待たせしすぎたかもしれません)。

MessagePack C# v2が正式リリースされてから6か月…さすがにまずいと思ったので重い腰をあげました。 実際は眠れない怒りをぶつけただけなのですけれども。 おそらくこの記事が予約投稿されたときには眠りについているはず。

こういう話はあるものの、自分は特に更新を急いでないからなぁ…と別件にずっとかまけててごめんなさい。 今回の作業は半日かかっていないので、v2がリリースされたときにやっておくべきでした…。

何が変わったのか

作業diff: https://github.com/pocketberserker/MessagePack.FSharpExtensions/pull/4

古い環境を窓から投げ捨てる勢いで書き換えました。

動作やAPIは特に変えてません。 とはいえ、大元のMessagePack C# v2が結構変わっているので、MessagePack.FSharpExtensions v1を使っている人(いるのか!?)は書き換えが必要です。 inref&がでてくるあたり、最近の F# っぽい(?)

他のライブラリと異なり、内部APIを引っ張ってきたりしつつILを直書きしている関係で、メンテナンスに気力が必要なんですよね。 判別共用体用のFormatterを生成するDiscriminatedUnionResolverDynamicObjectResolverDynamicUnionResolver悪魔合体させたような存在なので、元ネタがある分楽…ということもなく、同じResolverでStructも捌けるようにしないといけないので注意していないとTypeInitializationErrorの刑に処されます。つらい。

そのかわり、本家と違ってUnityを気にしなくていいのでその分は楽です(その環境が良いかどうかは別の話)。

追加機能をいれるか悩みましたが、何をやるにしても頑張りが必要そうだったので見送ってます。

.NET シリアライズ最前線に追い付けたので一安心。 置いていかれないように引き続きがんば…がん…ががが…

NaNとMap

先日、JavaScriptで以下の挙動になるのはなんでだろうねという話になった。

> m=new Map();
Map {}
> m.set(Number.NaN, 0)
Map { NaN => 0 }
> m.set(Number.NaN, 1)
Map { NaN => 1 }
> m.set(Number.NaN, 2)
Map { NaN => 2 }
> Number.NaN === Number.NaN
false
> Number.NaN !== Number.NaN
true

そして理由はMDNに書かれていた。

厳格な等価性では NaN を他のどの値 (自分自身も含む) とも等しくないものとして扱います

https://developer.mozilla.org/ja/docs/Web/JavaScript/Equality_comparisons_and_sameness

NaN は NaN と同じとみなされ (NaN !== NaN であっても)、他の値はすべて === 演算子の意味に従って等価性が考慮されます

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

めでたしめでたし…いや、もうちょっとだけ続くんじゃよ。

.NETのDictionaryとF#のMap

ふと、ほかの…例えば.NETのDictionaryやF#のMapはどうなるのか気になったので、Try F#上で試してみた。

open System.Collections.Generic

// nanの定義
// https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/prim-types.fs#L3758

Map.empty
|> Map.add nan 1
|> Map.add nan 2
|> printfn "%A"

let d = Dictionary<float, int>()
d.Add(nan, 1)
d.Add(nan, 2)
d |> Seq.iter (fun kv -> printfn "%A" kv)

nan = nan |> printfn "%b"
nan <> nan |> printfn "%b"
map [(NaN, 1); (NaN, 2)]
NaN,2
false
true

もうちょっと深堀りする必要がありそうだ。

Dictionary

DictionarySystem.IEquatable<T>が実装されていればそれを使うことになっている。

F#におけるfloatは.NETでいうところのdoubleだ。 DoubleはIEquatable<double>が実装されており、NaN == NaNはtrueとなるよう実装されている。よってDictionaryでは値が上書きされる。

https://github.com/microsoft/referencesource/blob/a7bd3242bd7732dec4aebb21fbc0f6de61c2545e/mscorlib/system/double.cs#L147

https://github.com/dotnet/runtime/blob/221f869e9bac3cafcfe6bd35d062e2fbfe8accba/src/libraries/System.Runtime/tests/System/DoubleTests.cs#L111

F# Map

F#のMapのkeyはIComparer<'T>を使って比較される。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L123

今回はMap.emptyに要素を追加していったので、Map.emptyが用意したLanguagePrimitives.FastGenericComparer<'T>がcomparerとして使われる。

https://github.com/dotnet/fsharp/blob/6a8885ff1152db81ab37b94f048a01e88b8847d6/src/fsharp/FSharp.Core/map.fs#L466

もっと追ってみる。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2177

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2088

GenericComparisonを使っている関数は他にcompareがあるので、こいつで何を返しているか見てみよう。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L3293

compare nan nan |> printfn "%d"
1

ということで、別のkey扱いである。

もっと実装を追いたいなら以下を読み進めていけばよいはず。

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L2159

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1954

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1097

https://github.com/dotnet/fsharp/blob/78bb83aca6cba3d85dee111211d4c0c99a37595d/src/fsharp/FSharp.Core/prim-types.fs#L1080

ちなみに、NaNだとkeyが一致しないので値は取得できない。

let d =
  Map.empty
  |> Map.add nan 1
  |> Map.add nan 2

// Noneになる
d
|> Map.tryFind nan
|> printfn "%A"