インク吸入器コンバーターを洗って干しました。 組み立てやすい順番に並べて干していたつもりが、順番が入れ替わって良くわからなくなりました。 セーラーのコンバーターを分解して洗浄することに - 暮らしと手帳と万年筆 の写真の並び順を参考に組み立てました。
再発防止のために手順を記録します。
インク吸入器コンバーターを洗って干しました。 組み立てやすい順番に並べて干していたつもりが、順番が入れ替わって良くわからなくなりました。 セーラーのコンバーターを分解して洗浄することに - 暮らしと手帳と万年筆 の写真の並び順を参考に組み立てました。
再発防止のために手順を記録します。
マネジメント入門 その2 - @ledsun blog につづいて 管理職必読 順番に読むと理解が深まる「マネジメントの名著」11冊 | 日経BOOKプラス で紹介されていた本の三冊名です。
前回から間が空きました。 読むのを途中でやめたからです。 最後まで読まないことにして、ここに日記だけ書いておきます。
この本は講演会での話し方の本でした。 大勢の前で話して、注目を集めて、高揚させて、伝えたいことを伝える感じの方法です。 もしかするとRubyKaigiで話すときの参考になるのかもしれません。 まあ、でも僕がしたいのはそういうことじゃないんですよね。 という感じで迷いながら6割位読みました。 続きを読むエネルギーは出てこなかったので、本棚にしまいます。
管理職向けの本て、組織のリーダー向けの本と中間管理職向けのの本があります。 今のところ興味があるのは、中間管理職向けの本です。 この本は組織のリーダー向けかな?と思いました。 「100人に話して30人にやる気にさせる話し方」をイメージしました。 中間管理職は沢山の人に語るより、個人とちょっとずつこまめに話す方が良いんじゃないかなあ?と予測しています。
ruby.wasmように値がJavaScriptのFalsyな値か判定する関数を実装しました。
def self.falsy?(value) value == JS::Null || value == JS::Undefined || # Use the strictly_eql? method to compare values using JavaScript's `===` operator. # This is because JavaScript's `==` operator is loose in its judgment. # For example, in JavaScript, `[] == ""` is true, but `[] === ""` is false. value.strictly_eql?(JS::EmptyString) || value.strictly_eql?(JS::False) || value.strictly_eql?(JS::Zero) || value.strictly_eql?(JS::NinusZero) || value.strictly_eql?(JS::BingIntZero) || # `Number.isNaN`` is used to compare whether a value is `NaN`` or not. # This is because `NaN == NaN` is false in JavaScript, but `Number.isNaN(NaN)` is true. JS.global[:Number].isNaN?(value) end
もっとシンプルになるかと思ってました。 思ったより複雑でした。
というのは、たとえば次の比較は真になります。
JS.eval('return []') == JS.eval("return ''")
なぜかというとJavaScriptの==
がなかなかマイルドだからです。
> []=='' true > !([]) false > !('') true > []==='' false
そういえば、昔「JavaScript: The Good Parts」を読んだときにこの説明がありました。
その教えを守り、JavaScriptでプログラム書くときは===
しか使いません。
それもESLintでAuto Fixしています。
そして、今ではこの仕様のことをすっかり忘れていました。
実装してから気がついたのですが、これは僕が欲しいものでは無かったです。 名前も悪くないし、実装自体は、なかなか気にいっています。 ただ、使い所が思いつかないので、プルリクエストにするのは、一旦やめておきます
いつか使い所を思いついたときのために、自分のリポジトリのブランチとしては残しておきます。
ruby.wasmを使ってRubyでフロントエンドフレームワークをつくったらどうなるかを考えてみましょう。
テンプレートエンジンはERBを使うと良さそうです。
イベントハンドラーのバインドはRailsのルーティングっぽくなる良さそうです。 次のイメージです。
OrbitalRing.routes.draw do click ".ok_button", to: "task.done" ... end
アプリケーションのレンダリング先のHTMLElementのidはangular風にapp_root
固定にしましょう。
たぶんこんな感じで初期化します。
OrbitarRing.initialize
を呼ぶOrbitarRing.initialize
はLayout
を継承したクラスのnew("#app_root")
を呼ぶLayout#new
はクラス内に定義されたテンプレートを呼び出すdef initialize(selector) JS.global.querySelector(selector)[:innerHTML] = template.result end def template ERB.new(<<~'END_HTML') <%= Task.reder_all %> END_HTML end
class Task def self.render_all template_all.result_with_hash elements: all end def self.template_all ERB.new(<<~'END_HTML') <ul class="tasks"> <% elements.each do %> <%= _1.render %> <% end %> </ul> END_HTML end def reder template.result_with_hash element: self end def template ERB.new(<<~'END_HTML') <li> <%= element.name %> <button class="ok_button" data-id="<%=id%>"> </li> END_HTML end def self.done(event) task = find_by(event[:target]{:dataset][:id]) task.done end def done is_done = true end end
ERBを良い感じに埋め込む方法ほしいですね。
Taskインスタンスを自動的に見つけて欲しいです。 何か良い感じのIDの埋め込み方が要りそうです。
OrbitarRing.initialize
はなんか良い感じでイベントハンドラーをバインドする。マネジメント入門 その1 - @ledsun blog につづいて 管理職必読 順番に読むと理解が深まる「マネジメントの名著」11冊 | 日経BOOKプラス で紹介されていた本の二冊名です。
1冊目同様、部長、課長くらいの日本企業の中間管理職向けの本です。 1冊目と比べると具体的な話が少なめでした。 1冊目で大枠をつかんで、2冊目で引き出しを増やす感じでしょうか。 「これしかやらない」というタイトルとは、あまり一致していないのかもしれません。
印象深かった内容
「自分がどうしたい」を考えないとか、「仕事を面白くする方法を知らない」とか、驚きです。 そういう人が居るのは知っていました。 そういう人を導いていくことがマネジメントに含まれるとは思っていませんでした。
エンジニアコミュニティにはそういう人少ないのです。 創立20年くらいの小企業にも少ないです。 いや、そうではなく、そういう人達は生き延びることができなくて退場していきます。
言われてみれば「そういう人達をモチベートして、退場しないで戦力にできる。戦力にできたら強い組織になる」のは、そらそうだなっと思いました。 選ばれた特質を既に備えている人を選別するのではなくて、備えられるように育てる。 そら、まっとうな話ですね。
2冊読んで2冊とも似たようなことが書いてあるので、まあ、そうなんだろうなって思います。 まじかあ・・・
管理職必読 順番に読むと理解が深まる「マネジメントの名著」11冊 | 日経BOOKプラス で紹介されていた本です。 とりあえず一冊目を読んでみました。
マネジメントの本は経営者からチームリーダー向けまでターゲットがわかりにくいことがあります。 この本は、部長、課長くらいの日本企業の中間管理職向けの本です。
1 on 1の話は3章からはじまります。 1~2章をつかってマネージャーの仕事を説明しています。 100ページくらいにコンパクトにまとまっていてわかりやすかったです。
1 on 1 の具体的なハウツーは3章に書いてあります。 僕の所属組織は筆者の方の所属組織とは形が違うのでハウツーをそのままでは使いにくいです。
4章以降も1 on 1と絡めてはいますが、マネージャーの仕事全般の話で興味深かったです。
印象深かった内容
この中でも特に自分に足りていない点は、次の2点かなと思います。
本当に、サーバントリーダーシップというか、部活のマネージャー的なマネージャーなんだなと、思いました。 新人マネージャーが一冊目に読む本として、内容の網羅具合もページのコンパクトさも良さそうです。
お仕事でソフトウェアを開発するプログラマーチームに対して、僕が持っているイメージを整理しました。
人数 | |||
---|---|---|---|
2 | リードとサブ | コンビ | |
3 | リードとサブ x 2 | コンビとサポート | チーム |
4 | リードとサブ x 3 | チームとサポート | チーム |
5 | マネージャーとメンバー x 4 | チームとサポート | コミュニティ |
この表は、右に行くほどチームが自律性高くふるまうことをあらわします。
自律性が高い方が良いとは限りません。 自律性が低いと5人以上のチームでは、チームマネジメントを専任する人が必要になると思います。 それをマネージャーとメンバーと表現しています。 プログラマーの中にはチームマネジメントを専任したくない方が一定数います。 もしも、プログラマーからチームリーダーになり、チームマネジメント専任になりたくない場合、チーム人数が少ないうちから自律性を高めておくのがオススメです。
チームの人数が増えてから自律性を高めるには時間が掛かります。 チームメンバー同士が対話する機会を設け練習を繰り返す必要があります。 週1回程度の話し合いの機会を半年~一年くらい続けると、自律性が高まるように思います。
チームの自律性を重視するときに注意事項があります。 2人チームがリードとサブになるかコンビになるかは、性格の相性に強く依存します。 2チームでは、自律性をあまり重視しない方が無難です。
一方、3~4人では自律性を重視すると良いと思います。 リードとサブ x n の状態で人数が増えると、リーダとチームメンバーのコミュニケーションが増え、チームメンバー同士のコミュニケーションが増えません。 リーダーがコミュニケーションにつかうコストが増え、チームメンバー同士のコミュニケーションを増やす施策が打てなくなります。 チームの人数が増えるとチームのパフォーマンスは一度下がります。 チームのパフォーマンスを上げるために、リーダとチームメンバーのコミュニケーションに時間を使いたくなると思います。 一度パフォーマンスが上がってからチームの自律性を高めようとしても、チームの自律性を高めるには再びパフォーマンスを一時的に下げる必要があります。 怖くてできません。 「パフォーマンスが上がっているし、自分が我慢すれば良い」と思いがちです。 人間は「自分が我慢すればよい」誘惑に抵抗するのに、ものすごいパワーが要ります。
上の表をみると、5人チームにコミュニティという謎の状態があります。 チームとコミュニティの違い、会社・組織をどう捉えるか | Social Change! に出てきているコミュニティをイメージしています。 なぜ、チームではなくコミュニティなのかというと、5人以上になると、ミッションに対して100%動いていないメンバーがでてきます。 4人のチームはめちゃくちゃ上手く行っていると4人分の成果がでます。 5人のチームはめちゃくちゃ上手く行っても4.5人分の成果です。
スキルの差からではなく、タスクの投入量が上手くコントロールできなくなるために起きます。 手の空くメンバーは特定の人物ではありません。時々によって手が空くメンバーは変わります。 タスクを上手く用意すれば5人分の成果がでるようになるかというと、僕の経験則では、なりません。
チームにミッション以外のことに取り組む余裕が生まれた状態と捉えるとよいです。 ミッションとは別の緊急ではないが重要なタスクを用意しておいて、チームメンバーに取り組ませるとよいでしょう。 これはチームの余裕です。 4人までのチームでは緊急ではないが重要なタスクは正式なタスクにしたり、リーダーが空き時間でこっそりやったりという工夫が必要です。 5人以上のチームがコミュニティになると、チームメンバーが自主的に緊急ではないが重要なタスクに取り組めるようになります。
また、もともと4.5人分の成果しか出ないので、メンバーの離脱や加入が容易になります。 一人離脱しても12.5パーセントしか下がらないのでインパクトが弱いです。 一人加入にしても1人分の成果は期待されないので、新メンバーの負担が低いです。 このメンバーの出入りが容易な状態が、チームではなく場でありコミュニティです。
チームの自律性を高めるコツは、チームメンバーに縄張りを作らせないことです。 人間は縄張り意識を持つと、その作業を他人にされるのが苦痛に感じます。 また、縄張り外の仕事するのがおっくうになります。
縄張りをつくらせないためには、リーダーはチームメンバーの扱いを変えてはいけません。 チームメンバーのスキル、年齢、性別、得意なことが違っても扱いを変えません。 経歴20年のベテランと新卒半年の新人の扱いを同じくします。 スキルが足りない部分のサポートは必要です。 スキルが足りている作業は平等に割り振ります。
チームメンバーによって実行速度の遅い速いの差はありますが、クリティカルパスに乗っていない作業は早さを気にする必要はありません。 また時間が掛かっても成果を出せた体験は、個人の成長に繋がります。 さらにクリティカルパスに乗っていない作業は早さを気にする必要が無いは頭で理解しても、なかなかその通りにふるまえません。 何度も実際に体験する必要があります。
平等に割り振らずに、ベテランにばかり難しい作業を振ると「この領域はおれの仕事」と縄張りを作りはじめます。 ベテランほど陥りやすいので、意図的にコントロールする必要があります。 コントロールせずに放置しておくと、よほどの人格者でないとブリリアントジャーク化します。 すでにブリリアントジャーク化していてコントロールできず、 短期的な成果よりチームの成長が重要な場面では、チームから外れてもらうことを考えてもいいかもしれません。
継続して開発していると、前回機能Aを担当した人に今回も機能Aをお願いしたくなります。 よほど急ぎでない限り担当はグルグル変えるようにしましょう。 繰り返しですが、クリティカルパスに乗っていない作業は早さを気にする必要はありません。 プロジェクトの進行が早くならないのに、縄張り作りを強化するのは、なにも得しませんよね。
MySQLのロックに関する調査メモ その3 - @ledsun blog の続きです。
SHOW ENGINE INNODB STATUS\G
で出力されるロック情報を眺めていたら意味がわかってきました。
まずテーブルに格納されているレコードの情報です。
select * from mysqlcasual; +----+------+------+ | id | col1 | col2 | +----+------+------+ | 1 | 2 | 0 | | 2 | 4 | 0 | | 3 | 6 | 0 | | 4 | 8 | 0 | | 5 | 10 | 0 | | 6 | 12 | 0 | | 7 | 14 | 0 | | 8 | 16 | 0 | +----+------+------+
begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 2 FOR UPDATE;
三つのロック情報が出てきます。 一つ目から見ていきます。
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7731 lock_mode X Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000002; asc ;; 1: len 4; hex 80000001; asc ;;
index idx_col1 of table
はインデックス idx_col1 に対するロックであることを表しています。
lock_mode X
はネクストキーロックであることを表しています。無印がネクストキーロックです。
0: len 4; hex 80000002; asc ;; 1: len 4; hex 80000001; asc ;;
はロックしているレコードを表してます。
80000002
はcol1カラムの値が2、80000001
はidカラムの値です。
選択するレコードを変えてみます。
begin;SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000004; asc ;; 1: len 4; hex 80000002; asc ;;
80000004
と80000002
になりました。
次のロック情報を見てみましょう。
RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks rec but not gap Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000002; asc ;; 1: len 6; hex 00000000162a; asc *;; 2: len 7; hex aa0000011e011d; asc ;; 3: len 4; hex 80000004; asc ;; 4: len 4; hex 80000000; asc ;;
index PRIMARY of table
とあるので、プライマリーインデックスへのロックです。セカンダリーインデックスをロックするとプライマリーインデックスもロックされるやつですね。
lock_mode X locks rec but not gap
は、レコードロックを表してます。ギャップロックでないものがレコードロックです。
0: len 4; hex 80000002; asc ;; 1: len 6; hex 00000000162a; asc *;; 2: len 7; hex aa0000011e011d; asc ;; 3: len 4; hex 80000004; asc ;; 4: len 4; hex 80000000; asc ;;
80000002
と80000004
と80000000
はレコードの値を表していそうです。
00000000162a
とaa0000011e011d
は謎です。プライマリーインデックスとidex_col1を表しているのかもしれません。
上は2つ目のレコードです。1つ目のレコードは下になります。
0: len 4; hex 80000001; asc ;; 1: len 6; hex 00000000162a; asc *;; 2: len 7; hex aa0000011e0110; asc ;; 3: len 4; hex 80000002; asc ;; 4: len 4; hex 80000000; asc ;;
00000000162a
は変わりません。
aa0000011e011d
はaa0000011e0110
になりました。
うーん、これはちょっとよくわかりません。
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7733 lock_mode X locks gap before rec Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000006; asc ;; 1: len 4; hex 80000003; asc ;;
index idx_col1 of table
なのでidx_col1へのロックです。
lock_mode X locks gap before rec
なので、ギャップロックです。
0: len 4; hex 80000006; asc ;; 1: len 4; hex 80000003; asc ;;
は、3つ目のレコードを表しています。 ギャップロックなので、3つ目のレコードの手前までをロックしているようです。
もしかして、1つ目のネクストキーロックを、2つ目のレコードロックと3つ目のギャップロックで実現していることを表現しているのでしょうか?
MySQLのロックに関する調査メモ その2 - @ledsun blog の続きです。 概念だけ勉強しても理解に限度があります。 実際にロックを起こして観察してみました。
InnoDBのロックの範囲とネクストキーロックの話 - かみぽわーる を参考にしました。 MySQL Shellを起動しMySQLに接続します。
\connect --mysql --user root \use sandbox \sql
sandboxスキーマは練習用に作ってあったものを使い回しています。 テーブルを作成します。
CREATE TABLE `mysqlcasual` ( `id` int(11) NOT NULL AUTO_INCREMENT, `col1` int(11) NOT NULL, `col2` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_col1` (`col1`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `mysqlcasual`(`col1`) VALUES (2),(4),(6),(8),(10),(12),(14),(16);
MySQL 5.7 以降では、ここからの手順が変わっています*1。
CREATE TABLE innodb_lock_monitor(a int) ENGINE=InnoDB;
の代わりに
SET GLOBAL innodb_status_output=ON; SET GLOBAL innodb_status_output_locks=ON;
します。 また、共有ロックではロックが表示されませんでした。
BEGIN; SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 LOCK IN SHARE MODE;
の代わりに
BEGIN; SELECT id, col1 FROM mysqlcasual WHERE col1 = 4 FOR UPDATE;
を試しました。
SHOW ENGINE INNODB STATUS\G
を実行するとTRANSACTIONSセクションにロック情報が表示されました。
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000004; asc ;; 1: len 4; hex 80000002; asc ;;
RECORD LOCKS space id 173 page no 3 n bits 80 index PRIMARY of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks rec but not gap Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000002; asc ;; 1: len 6; hex 00000000162a; asc *;; 2: len 7; hex aa0000011e011d; asc ;; 3: len 4; hex 80000004; asc ;; 4: len 4; hex 80000000; asc ;;
インデックスレコードロック?
RECORD LOCKS space id 173 page no 4 n bits 80 index idx_col1 of table `sandbox`.`mysqlcasual` trx id 7717 lock_mode X locks gap before rec Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 4; hex 80000006; asc ;; 1: len 4; hex 80000003; asc ;;
ギャップロック?
出力された情報の読み方が謎です。
万年筆のインクです。
赤っぽいのかと思っていました。 茶色とオレンジの間ぐらいの色でした。 好きな感じの色です。
写真で見ると、赤ですね。
このメモは別の資料を見ながら書いたメモです。 メモだけ見ても意味が通じないと思います。
MySQLのロックに関する調査メモ その1 - @ledsun blog の続きです。
MySQLのロックについて - SH2の日記 のPDFを読みました。 面白かったです。
MySQL(というかInnoDB)の最初のスタート地点が「絶対にファントムリードさせたくない」なようです。 つまり一度トランザクションをはじめたら、他のトランザクションでの操作を見たくありません。 例えば、自トランザクションで見ている行と行の間に、他トランザクションからInsertされたくありません。 ということは行単位のロックでは足りなくて、自トランザクションで見ている範囲をロックしたいです。 これが、ギャップロックとネクストキーロックのようです。
今の段階ではギャップロックとネクストキーロックの違いはわかっていません。 特に、なぜネクストキーロックをしなければいけないのかよくわかりません。
どうもこの辺の思想は「トランザクション処理 概念と技法」のでしょうか?
もう一つ、MySQLの面白い特徴は、プライマリーインデックスのリーフに行があることです。
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.6.2.1 クラスタインデックスとセカンダリインデックス
すべての InnoDB テーブルは、行のデータが格納されているクラスタ化されたインデックスと呼ばれる特別なインデックスを持っています。
MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に次のように書いてあります。
レコードロックは、インデックスレコードのロックです。
どうもこういうことを説明しているみたいです。
また、セカンダリーインデックスをロックするときは、セカンダリーインデックスとプライマリーインデックスのインデックスレコードを両方ロックするそうです。
MySQLのロックのついては MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.1 InnoDB ロック に書いてあるはずです。 読んでもいまいちよくわかりません。
MySQL/MariaDBとTransactdのInnoDBロック制御詳細 その1 - BizStationブログを合わせて読むとわかりやすいです。 どうやら公式ドキュメントは内部実装の立場で説明しているようです。 たとえば、後者には次のような記述があります。
2つのトランザクションが同じレコードのロックを取得しようとしたときのロックの可否を表したのがInnoDBソースコードの以下の部分です。
このソースコーコメントをマトリックスにしたものが公式ドキュメントに載っています。 ですので、公式ドキュメントは実装者視点で読む必要があります。 利用者視点で読むと理解が難しいようです。
後者の文章には、実装者視点の解説があるので理解の手引きになります。
await と Task.Result によるデッドロックによるとWindows FormでTaskを使ったときにデッドロックするケースがあるようです。 検証してみます。
試しに次のようなコードを書いてみました。
namespace AwaitDeadLockEight { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { label1.Text = ""; string str = TimeCosumingMethod().Result; label1.Text = str; } private async Task<string> TimeCosumingMethod() { await Task.Delay(3000); return "TimeCosumingMethod の戻り値"; } } }
ボタンを押すと確かに固まります。
await Task.Delay(3000);
をコメントアウトすると動きます。
なので、非同期関数を呼ぶだけでは問題ないです。 非同期関数のなかで非同期関数を呼ぶと固まります。
Task.Delay(3000).Wait();
でも固まります。
await
を使うかどうかは関係ないようです。
次のように呼び出すイベントハンドラーを非同期関数にすると期待通りに動きます。
private async void button1_Click(object sender, EventArgs e) { label1.Text = ""; string str = await TimeCosumingMethod(); label1.Text = str; } private async Task<string> TimeCosumingMethod() { await Task.Delay(3000); return "TimeCosumingMethod の戻り値"; }
不思議な挙動です。何が起きているのでしょうか?
ここからさきは、今のところの仮説です。 イマイチ上手く説明できていませんが、現在の理解を書いておきます。
でも、この説明だとイベントハンドラーを非同期関数にしたときに上手く動く理由が説明できません。
そもそも 「Task.Delayの結果を処理する」タスクAがUIスレッドのタスクキューに積まれる
は本当でしょうか?
ワーかスレッドのキューに積まれるないのでしょうか?
謎です。
ruby.wasmのKernel#sleepをどう実装したものか? - @ledsun blog の続きです。
次のようにruby.wasmで動くKernel#sleep
を実装できます。
<html> <head> <title>Kernel#sleep</title> <script src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script> <script type="text/ruby" data-eval="async"> require 'js' module Kernel def sleep(time) JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await end end start = JS.global[:Date].now.to_i sleep(1) puts "#{JS.global[:Date].now.to_i - start}ms passed" </script> </head> </html>
Promiseの初期化を次のように書いています。
JS.eval("return new Promise((resolve) => setTimeout(resolve, time * 1000))").await
次のようにnew
メソッドにコールバック関数をブロックで渡したいです。
JS.global[:Promise].new do |resolve| JS.global.setTimeout(resolve, time * 1000) end.await
これは次のエラーがでます。
TypeError: Promise resolver undefined is not a function
しかしnew
メソッドの定義はブロックを考慮していません。
def new(*args) JS.global[:Reflect].construct(self, args.to_js) end
つまり次のJavaScriptを実行しているのと同じです。
new Promise(undefined)
ですので、resolver
引数がundefined
であると怒られます。
では、次のようにしてはどうでしょうか?
class JS::Object def new(*args, &block) JS.global[:Reflect].construct(self, block) end end module Kernel def sleep(time) JS.global[:Promise].new do |resolve| JS.global.setTimeout(resolve, time * 1000) end.await end end
new
メソッドでブロックを引数として渡します。
上手く行きそうですが、やはり同様のエラーが起きます。
TypeError: Promise resolver undefined is not a function
これはJavaScriptでかくと、次のようになります。
Reflect.construct(Promise, ()=>{})
Reflect.constructの第二引数には、配列風オブジェクトを渡す必要があります。 次のようにすると良さそうです。
def new(*args, &block) JS.global[:Reflect].construct(self, [block]) end
ひとまず動きます。
つぎのようにすると、new
メソッドにブロックを渡したときも、引数を渡したときもJavaScriptのコンストラクターを呼び出せるようになります。
<html> <head> <title>Kernel#sleep</title> <script src="https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.4.1-2024-01-26-a/dist/browser.script.iife.js"></script> <script type="text/ruby" data-eval="async"> require 'js' class JS::Object def new(*args, &block) if block JS.global[:Reflect].construct(self, [block]) else JS.global[:Reflect].construct(self, args) end end end module Kernel def sleep(time) JS.global[:Promise].new do |resolve| JS.global.setTimeout(resolve, time * 1000) end.await end end start = JS.global[:Date].now.to_i sleep(1) puts "#{JS.global[:Date].now.to_i - start}ms passed" p JS.global[:Date].new("2024-01-27T00:00:00Z") </script> </head> </html>
ruby.wasmでオートロードする - @ledsun blog でModule#const_missingをつかってオートローダーを書いてみました。
Module#const_missing
のAPIで、汎用的なオートローダーを書くのは少し難しそうなことがわかりました。
難しそうですが、どれくらい難しいのかよくわかっていません。
そこで、先人の知恵に頼ります。
具体的には、Rails 5.2 から6.0でオートローダーをclassicからzeitwerkに変更した出来事を調べます。
Rails 5.2までのオートローダーはModule#const_missing
を使って実装されています。
Rails 6.0以降のオートローダーzeitwerkはKernel.#autoloadを使って実装されています。
なぜでしょうか?
Module#const_missing
を使ったオートローダーの問題点classicローダーのModule#const_missing
を使った実装は、おおむね動いていましたが細かい問題がありました。
わかりやすい例は Understanding Zeitwerk in Rails 6 | by Marcelo Casiraghi | Cedarcode | Medium で、説明されています。
次のような例が挙げられています。
# app/models/user.rb class User < ApplicationRecord end # app/models/admin/user.rb module Admin class User < ApplicationRecord end end # app/models/admin/user_manager.rb module Admin class UserManager def self.all User.all # Want to load all admin users end end end
UserManager内でUserを呼び出したときに、Module#const_missing
では、呼び出された物がUserかAdmin::Userかわかりません。
Module#const_missing
の引数には、どちらの場合であってもシンボル:User
が渡されます。
その他の多くの問題がRails 5.2のRailsガイドで列挙されています。
Module#const_missing
でオートローダーを実装するには、とても多くの問題があるようです。
これらの問題を解決するためにzeitwerkはKernel.#autoload
を使って再実装されました。
Kernel.#autoload
はモジュールを読み込むパスを指定できます。
モジュールが必要になったときに指定したパスをつかってKernel.#autoload
します。
つまり、オートロードです。
正確にはオートロードに使うヒントをRuby VMに渡します。
zeitwerkでは、起動時にsetupメソッドを使ってルートディレクトリからモジュールのファイルパスを収集し、Kernel.#autoload
を呼び出します。
Kernel.#autoload
を使うには、モジュール呼び出しより前に、モジュールの実体ファイルのパスを調べます。
ruby.wasmでは、この方式は厳しいです。 サーバーに対してパス探索をかけたくありません。 自動的にモジュールのパスを集めるのは難しいです。
さらに前の段階でモジュールパスを調査しておいて、import mapsのようにブラウザに渡せばできるでしょう。
ですが、オートロードがうれしいのは設定が要らない点です。設定より規約(Convention over Configuration、CoC)です。
マップを作ってブラウザに渡すのは設定です。
Kernel#require_relative
を各Rubyスクリプトファイルに書くのと同じです。