@ledsun blog

無味の味は佳境に入らざればすなわち知れず

React on Ruby on Railsの素振り

Ruby on RailsとReactの連携の仕方がイマイチ理解できていないので、簡単なアプリケーションを作って勉強します。 4月生まれのホットな動画を見つけたのでこちらを参考にします。

www.youtube.com

動画でなくテキストがお好みの人は、GitHub - zayneio/open-flights: OpenFlights - A CRUD app example built with ruby on rails and react.js using webpacker で、手順が公開されているので御覧ください。

Typoに苦しみながらもSerializerの生成までは上手く行きました。 しかしRails consoleでSerializerの動作確認をしたいのに、Serializerの読み込みが上手く行きません。

次のようにuninitialized constant AirlineSerializerが表示されます。

~ bundle exec rails c
Running via Spring preloader in process 47499
Loading development environment (Rails 6.0.3.1)
irb(main):001:0> AirlineSerializer
Traceback (most recent call last):
        1: from (irb):1
NameError (uninitialized constant AirlineSerializer)

最初「Rails6でやっているので、コードローダーがzeitwerkに変わったからかな?」と思ったのですが、参考にしている動画もRails6です。関係ありません。 仕方がないので、雑にfast jsonapi uninitialized constantでググって次のissueを見つけました。

Uninitialized Constant on Namespaced Controller · Issue #98 · Netflix/fast_jsonapi · GitHub

Doing a spring stop and then restarting server/console should fix it in those cases (for anyone that finds this later on).

ああ、springお前だったのか!

Reactに辿り着く前に力尽きましたとさ。おしまい

プログラマにできるとよさそうなこと

十行程度のプログラムが読めること

  • プログラミング言語の文法を知っている
  • 分岐とループを追いかけることができる
  • 変数の状態変化を追いかけることができる
  • 関数呼び出しを追いかけることができる
  • 十行程度のプログラムを複数回書いたことがある

プログラムを読んでプログラムの動的な振る舞いを想像できる

  • プログラムの主な処理の結果を想像できる
  • 主な処理の終了条件がわかる
  • プログラムから主な処理を読み取れる
  • 似たようなプログラムを書いて、動かしたことがある
  • 既知のプログラムと読んでいるプログラムの違いがわかる
  • イディオムを知っている
  • イディオムを書いたことがある
  • プログラムがどう動くか知っている

重複したソースコードを関数に抽出できる

  • 重複したソースコードがわかる
  • 同じ入力と出力をもつコードブロックがわかる
  • コードブロック単位で入出力を比較できる

プログラムのある機能がソースコードのどの部分に依存しているか読み取れる

  • 機能の表示する情報が、プログラムのどこで管理されているかわかる
  • プログラムで管理している状態が、いつ初期化され、いつ変更されるのかわかる
  • 状態が、プログラム上のどこから変更されるのかわかる
  • ユーザ操作によって呼び出される関数が、ソースコードのどこにあるかわかる
  • 関数呼び出しが追える
  • 関数呼び出しが、複数の関数、モジュールを跨いでも見失わない
  • コールグラフの途中経過を抽象化できる
  • プログラム中のモジュールの大まかな役割分担を知っている
  • モジュール分割したプログラムを書いたことがある

バグの原因を特定できる

  • バグが起きた理由を説明できる
  • バグの原因を知っている
  • バグが起きない状態が作れる
  • バグが起きる状態が作れる
  • バグの再現方法を知っている
  • バグが起きる方法を試したことがある
  • バグが起きる方法が思いつく
  • プログラムのモジュール構成を知っている

ソースコードから書いた人の気持をエスパーできる

  • コメントや関数名、変数に惑わされずに、処理内容を追うことができる
  • 似たような失敗をしたソースコード書いた経験がある

作法にのっとって機能を実装できる

  • 特定のフレームワーク、ライブラリ、ツールをの使い方のどう変えれば、何が変わるかわかる
  • ツールに慣れ親しんでいる
  • ツールを使ったことがある
  • ツールを使ったプログラム書いて、その動きを試すことができる
  • ツールの使い方を調べることができる
  • ツールの名前を知っている

動くけど使いにくいプログラムを改善できる

  • UIを改善ができる
  • よいUIと悪いUIがわかる
  • よいUIを知っている
  • よいUIがどういうときによいUIか知っている
  • ユーザがプログラムを使う文脈を想像できる
  • ユーザがプログラムを使う文脈を知っている
  • ユーザになったつもりで、プログラムを動かすことができる

動くけど問題を解決していないプログラムを改善できる

  • 機能仕様のバグを検出できる
  • 問題を解決する機能を考案できる
  • ユーザがプログラムを使って解決したい問題を知っている
  • ユーザの抱えている問題を知っている
  • ユーザの立場を知っている

リファクタリングができる

  • モジュールの影響範囲のいびつさがわかる
  • モジュールの影響範囲のあるべき姿がイメージできる
  • モジュールの影響範囲のあるべきパターンを知っている
  • モジュールを実装したことがある

たくさんリファクタリングできる

大きなリファクタリングができる

自分の書いていないソースコードの重複を発見できる

  • 他人の修正を見ることができる
  • 他人の作業と自分の作業の関わりがあるか判断できる
  • 他人の作業目的から自分がやっていることと似たことをしていると想像できる
  • 機能からソースコードやモジュール構成をイメージできる

レールから外れたプログラムの設計ができる

チーム開発で気をつけていること

この日記は同僚が読んでいないことを前提に書いています。もし読んでしまった同僚の方は絶対に感想を伝えないでください。

日に二度のオンラインミーティング

4月に在宅勤務が始まって以降、一日2回zoomを使って開発チームでオンラインミーティングをしています。 そこではチームメンバーが担当するタスクを話しています。

いわゆる朝会に近いです。 ただし、時間を区切った朝会と違い、目指す仕様や実装方法に疑問があるときはその場で相談しています。 ですので、一回のミーティング時間は15分〜1時間半までバラツキがあります。

その間、相談してないメンバーもミーティングに参加しています。 これが良いのか悪いのかは判断がついていません。 よくある朝会の手引では、「会は一定時間で打ち切り、個別の相談事はあとで行う」を勧めていると思います。 反しています。 3人チームなので、無駄にする時間がx1でレバレッジが効いていないので、そのままにしています。 人数が増えたら、考えるかもしれません。

オンラインミーティングは、退席すると「どんなことを話しているのか」わからなくなることを懸念しています。 オンサイトであれば、会議に参加していなくても、「ずいぶん長いこと話しているな」レベルのフィードバックが得られます。 オンラインでは何もわかりません。結論をチャットで共有してもいいのですが、それはそれで手間です。

「チームでお仕事をしている感」の演出になればと思っています。 チームメンバー間で、勝手にオンラインミーティングで相談し合うなら気にしなくてもいいのかもしれません。 それはそれで、問題が速く解決する方法なのか、という問題があるかもしれません。

日に二回のオンラインミーティングで、詰まっている点を確認して都度解決しています。 質問で割り込まれることはほとんどありません。 在宅勤務作法の教育上は、よくないかもしれません。 いまのところは、そこまで先は考えていません。

オンラインミーティングで決めていること

オンラインミーティング中に、次のことを決めています。

  1. おおまかな実装方針
  2. MergeRequest(いまGitlabを使っています)を作成するまでの目標タイム
  3. 画面の見た目

目標タイム

タスクのざっくりした実装方針を決めています。 が、大抵は仕様を見落としています。

最近、大きすぎるタスクを振って、大きなMergeRequestをレビューするはめになりました。 自業自得です。 一番の要因は、仕様を見落としていました。 大きなタスクであることに気がつかなかったのです。

目標タイムを設定するようにしました。 原則、当日中にMergeRequestを出せるようにタスクを調整します。 ちょっと無理そうな場合は、なるべくタスクを分割します。 ストーリーの単位より小さくわけます。 例えば、編集画面が大きすぎれば「編集画面に現在値を表示するまで」と「値を更新する処理」にわけます。

見た目

新規の画面では、見た目を相談することもあります。 オンラインミーティングで画面共有して確認しています。 あるいはスクリーンショットをとって、加工してコミュニケーションを取ることもあります。

話していないこと

逆に話していないのは、ソースコードの細かい実装方針です。 既存コードの追い方がわからない場合は、一緒に見ています。 既存コードの真似して書ける部分は、コピペでもよく、動作が機能を満たしていれば良いです。

ソースコードの細かい指摘は、MergeRequestのレビューで行っています。 共通関数の抽出やスタイルの修正などは、全部MergeRequestで行います。 なるべく理由は説明して指摘するようにしています。

言葉で伝わらなさそうなときは、コメントにソースコードを書きます。 この辺はスラスラ、プログラミングできるスキルがあってよかったと思います。

最初に「動作する」に取り組み、その後で「きれいな」に取り組む

動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+α - t-wadaのブログ

動作確認

機能を動かしてからわかる、妙な動作は、マージ後に確認しています。 1つの開発ブランチに対してチームで開発しています。 開発ブランチですので、ストーリー単位までいく前にどんどんマージします。 マージした開発ブランチを、手元のPCで動かして、意地悪なテストをします。

明確な不具合を見つけたらIssueを作成します。 動かしながら、仕様の理解を深めて、仕様の矛盾点がないか探り出します。 疑問点を洗い出したり、仕様を確認する作業までは一人でやっています。

最終的な決定は、オンラインミーティングで誰かと喋りながらのほうが決めやすい気がします。

スケジュール管理

単純な線形予測では、微妙に遅れています。 見積もりの不確実性コーンをぶっちぎる程は遅れていないので、気にしないようにしています。 内心ビビってますが「教育の投資効果で取り戻せるはず」と、自分に言い聞かせています。 焦ってした雑な仕事が、あとで自分の首をしめるのは目に見えています。

一度moveしたオブジェクトは移動先のRactorが死んでも帰ってこない

moveしたオブジェクトを参照するとRactor::MovedError

次のRubyスクリプトがあります。

dead = Ractor.new {}
dead.send STDIN, move: true
STDIN.gets

RactorにSTDINをmoveしてから、STDINを使います。 するとRactor::MovedErrorが起きます。

~ ruby dead_and_move.rb
Traceback (most recent call last):
        1: from dead_and_move.rb:5:in `<main>'
dead_and_move.rb:5:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)

move先Ractorが死んでもRactor::MovedError

次のようにmove先のRactorが終了するのを待ちます。

dead = Ractor.new {}
dead.send STDIN, move: true
dead.take # Ractorの終了を待つ
STDIN.gets

結果は変わらず、Ractor::MovedErrorです。

~ ruby dead_and_move.rb
Traceback (most recent call last):
        1: from dead_and_move.rb:4:in `<main>'
dead_and_move.rb:4:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)

moveしたオブジェクトを再び使うには再びmoveする

次のように、Ractorで受け取ったオブジェクトを再度moveすると、メインスレッドで使えます。

dead = Ractor.new do
  Ractor.yield Ractor.recv, move: true # 受け取ったオブジェクトを送り返す
end

dead.send STDIN, move: true
dead.take.gets

一度moveしたオブジェクトは自動的には帰ってきません。 再び使いたければ、明示的に戻します。

国家公務員法改正案に関するメモ

国家公務員法改正案が廃案になるそうなので、どういう話だったのかメモを集めておきます。

法案

衆法 第196回国会 30 国家公務員法等の一部を改正する法律案

議案というのがパッチで、要項がdescriptionみたいです。

第201回 通常国会|内閣官房ホームページ

こっちのほうが解説PDFと現時点の法案が載っていてわかりやすいです。 法案そのものは、現時点の僕の知識で読み解くのは難しいです。

平均寿命の伸長や少子高齢化の進展を踏まえ、知識、技術、経験等が豊富な高 齢期の職員を最大限に活用するため、定年の65歳引上げについての国会及び内閣 に対する人事院の「意見の申出」(平成30年8月)に鑑み、国家公務員の定年を 引き上げる。

意見

橋下徹氏が“束ね法案”国家公務員法改正案の問題点を指摘 「60歳以上の給与7割保障は疑問です」:芸能・社会:中日スポーツ(CHUNICHI Web)

国家公務員法の改正によって60歳以上の給与7割保障は疑問です。それをやるなら民間にも7割保障を義務化すべきだと思います」と言及。さらに「民間に義務化ができないなら、公務員もそのときの民間水準に合わせるという仕組みが筋なのではないでしょうか?」

https://twitter.com/hashimoto_lo/status/1262546929986138112

連合|国家公務員法等の一部を改正する法律案において措置されてい...(事務局長談話)

連合は、公務員の65歳への定年の引き上げは諸環境の変化等を踏まえた、不可欠な勤務条件であり、必要とされる社会的政策と認識してきた。従って、本改正案については、「検察官の勤務延長規定を削除すれば賛同できる」と表明している野党(共同会派)が主張しているとおり、本来であれば国家公務員法改正法案から切り離した上で、検察官人事における内閣の関与の範囲など丁寧な検討が必要である。その上で、国民の疑念に対し真摯に対応すべきである。

高橋洋一の霞ヶ関ウォッチ 「定年延長」国家公務員法改正案は、黒川氏人事とは関係ない: J-CAST ニュース【全文表示】

国家公務員定年延長には長い経緯がある。2008年国家公務員制度改革基本法中に65歳まで定年延長は盛り込まれている。その法律は福田康夫政権のときだが、その企画立案の一人として筆者も関わり、当時の民主党の協力で成立した。その後2回(2011年9月、2018年8月)の人事院から政府への意見申出、3回(2013年3月、2017年6月、2018年2月)の閣議決定を経て現在にいたる。

新展開…自民・世耕氏が検察官含む公務員の定年延長自体に異論 新型コロナ受け…検察庁法改正案の行方は?

この景気状況、雇用状況の中で本当に(公務員の)定年延長していいのかどうかも含めて、立ち止まってしっかり議論することが重要だ

いったい検察庁法改正案の何に抗議しているのか|徐東輝(とんふぃ)|note

個人的には、人生100年時代において、民間と同じく国家公務員も定年を延長することに異論はありません。さらに、人件費削減のために役職定年制を設けることにも賛成ですし、もっというと、特殊なケースで役職定年制に特例を設けることにも賛成です。  しかし、どうもまだこの「特殊なケース」の判断軸が見えてこない。

検察庁法・国家公務員法改定案/塩川氏の質問 衆院本会議 (日本共産党塩川鉄也議員)

国家公務員法等改正案の最大の問題は、憲法の基本原理である権力分立を破壊する検察庁法改正案を入れ込んでいることです。

東京高検黒川弘務検事長の定年延長を行った閣議決定の撤回を改めて求めるとともに、国家公務員法等の一部を改正する法律案のうち検察庁法改正案に反対する会長声明|決議・声明|東北弁護士会連合会

この改正案によれば、内閣及び法務大臣の裁量によって検察官の人事に介入をすることが可能となり、検察に対する国民の信頼を失い、さらには、準司法官として職務と責任の特殊性を有する検察官の政治的中立性や独立性が脅かされる危険があまりにも大きく、憲法の基本原理である権力分立に反する。

徳島弁護士会 検察庁法に違反する定年延長の閣議決定の撤回を求め、国家公務員法等の 一部を改正する法律案に反対する会長声明

特例措置が受けられるという法律に改正されてしまうと、時の政府の意向次第で、検察庁法の規定に基づいて上記の東京高検検事長の定年延長のような人事が可能になってしまう。これは、汚職事件など政界が絡む犯罪に対し、強い職務権限を持ち、準司法的役割を担う検察官の政治からの独立性・中立性の確保という検察庁法の趣旨を根底から揺るがすものであり、到底、容認することはできない。

国家公務員の定年を段階的に65歳に引き上げ 役職定年制も導入(国家公務員法等の改正法案を国会に提出) | 社会保険労務士PSRネットワーク

予定どおりに国家公務員の定年が65歳となれば、民間企業においても、定年自体の引き上げが本格的に検討されることになりそうですね。 なお、民間企業については、今のところ、定年の引き上げの予定はありませんが、高年齢者雇用安定法の改正案(現国会で審議中)により、令和3年4月から70歳までの就業機会の確保が努力義務とされる予定です。

検察庁法改正案はクロかシロか | いさ進一 活動の記録 (公明党のいさ進一議員)

公務員の共済年金と厚生年金が一元化され、公務員の年金の支給開始年齢も、会社員と同様に徐々に引き上げられています。最終的には支給開始は「65歳」からとなりますが、その途上にある現在では、支給開始年齢は「64歳」です。今回の法案が成立して施行され、検察官の定年も「65歳」となれば問題ありません。しかし、現在の検察官の定年は「63歳」です。つまり、64歳までの一年間、仕事もなく年金もないという状況が発生することとなります。しかも、一般の国家公務員では認められている「勤務延長制度」も「適用されない」こととなっています。今回、定年を「65歳」に合わせるのであれば、「勤務延長制度」も他の国家公務員同様、適用させることにしたらよいではないか、法務省はそう考えました。

検察庁法の一部改正法案の撤回及び違法な定年延長の閣議決定の撤回を求める会長声明 | 長崎県弁護士会 - NAGASAKI BAR ASSOCIATION

「全ての検察官の定年を一律に65歳まで延長すること」や「63歳の役職定年を設定すること」自体について反対するものではないが、「内閣又は法務大臣の裁量による役職延長や勤務延長」を認める部分については強く反対する。

日キ靖国神社問題特別委員会 「検察庁法改正案」に反対声明 2020年5月12日 | キリスト新聞社ホームページ

現在、国会に提出されている当該改正案は、政府や行政を監視すべき検察庁の機能を骨抜きにし、三権分立と民主主義とを崩壊させる危険な改悪であり、わたしたちはこれに強く抗議するとともに速やかな撤回を要求します。

検察庁法改正案の中身がやっと理解できたよ | ジャーナリスト神保哲生 official blog

内閣に気に入られれば2年から最長で5年もの長きにわたり今のポストにとどまれるのはもちろんのこと、場合によってはもう一つ上のポストも狙える一方で、どんなに優秀な検察官でも内閣に煙たがれれば63歳でお払い箱ということになり、内閣が検察幹部の人事に対する絶大な裁量を手にすることになります。

前川喜平(右傾化を深く憂慮する一市民) on Twitter: "要するに、2022年4月の改正法の施行までは現行国家公務員法により、施行後は改正検察庁法により、黒川氏の定年は自在に延長できるわけだ。2025年2月の68歳誕生日まで黒川氏を検事総長に据え置くこともできる。検察庁法再改正で定年を70歳にすれば2027年まで据え置ける。 #検察庁法改正案に抗議します"

2025年2月の68歳誕生日まで黒川氏を検事総長に据え置くこともできる。検察庁法再改正で定年を70歳にすれば2027年まで据え置ける。

経緯

2018年 「定年を段階的に65歳に引き上げるための国家公務員法等の改正についての意見の申出」人事院

人事院の意見の申出 より https://www.jinji.go.jp/iken/30mousidekossi.pdf

ここから今回の法案が出てきて、廃案になったようです。 意見の申出は次の閣議決定に基づいているそうです。

2017年 「経済財政運営と改革の基本方針2017」閣議決定

経済財政運営と改革の基本方針2017 - 内閣府 より https://www5.cao.go.jp/keizai-shimon/kaigi/cabinet/2017/2017_basicpolicies_ja.pdf

⑦ 若者が活躍しやすい環境整備、高齢者の就業促進 就職氷河期世代や若者の活躍に向けて、職務経歴、職業能力等に応じた集中的な正社 員化支援等を行う。また、高校中退者等の高卒資格取得に向けた学習相談・支援を行う。 65 歳以降の定年延長、継続雇用延長等を行う企業への支援を充実し、継続雇用年齢等 の引上げを進めていくための環境整備を行う。2020 年度(平成 32 年度)までを集中取 組期間と位置付け、助成措置の強化等を行い、集中取組期間の終了時点で、継続雇用年 齢等の引上げに係る制度の在り方を再検討する。公務員の定年の引上げについて、具体 的な検討を進める。また、多様な技術・経験を有するシニア層が、幅広く社会に貢献で きるよう、ハローワークにおける求人開拓を強化する。

2020年までに高齢者の就業促進をすることを決めていて、公務員の定年延長も含まれていたようです。

2013年 「国家公務員の雇用と年金の接続に関する基本方針」国家公務員制度改革推進本部行政改革実行本部決定

今後の公務員制度改革の在り方に関する意見交換会(第3回)議事次第 よりhttps://www.gyoukaku.go.jp/koumuin/arikata-ikenkoukan/dai3/sankou3.pdf

退職共済年金の支給開始年齢の引上げに伴い、再任用により雇用と年金の接続を図る 方針を決定

2013年から年金支給年齢の引き上げが始まるので、再任用で年金支給までの期間を埋めました。 このときは定年延長は案としてあっても、確定しなかったようです。

2011年「定年を段階的に65歳に引き上げるための国家公務員法等の改正についての意見の申出」人事院

人事院の意見の申出 より https://www.jinji.go.jp/iken/23mousikossi.pdf

2013年から年金支給年齢の引き上げが始まるので、2013年から2025年までの期間で、国家公務員の定年を65歳まで引き上げる案が出たようです。

2009 ~ 2011年 内閣人事局国家公務員法等改正法案」など 廃案

この時期の国家公務員法改正は、人事院内閣人事局に変更することが焦点だったようです。国家公務員の定年との関係は薄そうです。

2006 ~ 2008年 天下り対策

天下り問題が盛り上がって対策として、「国家公務員制度改革基本法成立」成立しました。

国家公務員制度改革基本法 - Wikipedia

公務員の再就職を一元管理する「官民人材交流センター」を内閣府に設け、公務員にも能力・実績主義を導入し、設置後3年以内に各省庁による天下りのあっせんも全面禁止する

https://www.gyoukaku.go.jp/siryou/koumuin/080613kihonhou_gaiyou.pdf

③ 雇用と年金の接続の重要性に留意して、次の措置を講ずる。 a 定年まで勤務できる環境を整備するとともに、再任用制度の活用の拡大を図る。 b 定年を段階的に六十五歳に引上げることについて検討する。 c aの環境の整備及びbの定年の引上げの検討に際し、高年齢である職員の給与の 抑制を可能とする制度その他のこれらに対応した給与制度の在り方並びに役職定年 制度及び職種別定年制度の導入について検討する。

その中に再任用や定年の引き上げも含まれていました。

RailsConf 2020のKent Beckのトークを見た

koicさん*1を真似ました。

Software design is an exercise in human relationships

我々、プログラマは次のような段階を経て、コミットの粒度を工夫していきます。

  1. 巨大な1つの塊
  2. 構造の変更(リファクタリング)と機能の変更に分ける
  3. 構造の変更(リファクタリング)と機能の変更のいくつもの小さなコミットに分ける
  4. 3を1つのPRから、小さなPRへ分ける

な話を解説していました。 僕個人の体験とも一致していて納得感があります。

Kent Beckらしいと思ったのは、この工夫を「relationshipのメンテナンスだ」と表現しているところです。

Waiter(プロダクトオーナー)とChanger(プログラマ)の人間関係、Changer(プログラマ)とChanger(プログラマ)の人間関係のメンテナンスだと。

なるほど、"XP is about social change."*2のKent Beckでした。

参考:RailsConf 2020

sjisの文章をShift_JISとして読むとEncoding::UndefinedConversionError

Rubyの話です。

‘Ⅸ’(ローマ数字の9) を含むsjisの文章をShift_JISとして読むとエラーが起きます。sjisとして読むとエラーが起きません。

たとえば、

''.encode('sjis', 'utf-8').encode('utf-8', 'Shift_JIS')

はエラーが起きて

''.encode('sjis', 'utf-8').encode('utf-8', 'sjis')

はエラーが起きません。 発生するエラーはEncoding::UndefinedConversionErrorです。

最初、Encoding::UndefinedConversionError は変換先が無いときに出るのだろう、と考えました。 ローマ数字がutf-8に含まれていないのかな?、と考えました。 今どきそんな事ある?と疑いつつ

色々試しているうちに、変換元の文字コード指定の問題(sjisShift_JISか)ということに気が付きました。 厳密なShift JISにはローマ数字が含まれていなくて、CP392のようなWindows拡張のShift JISにはローマ数字が含まれているからです。

そこで「Rubyのどこかに文字コードの変換テーブルがあって、その変換元がないときもEncoding::UndefinedConversionErrorがでる」と仮説を立てました。 どこでしょう?

このへんかなあ?C言語なんもわからん・・・

イベントハンドラーAの処理があるときはイベントハンドラーBの処理を止めたいです

ブラウザのJavaScriptの話です。 イベントハンドラーAの処理があるときはイベントハンドラーBの処理を止めたいです。

イベントハンドラーAは新しいNodeを作って選択します。 イベントハンドラーBは選択を解除します。

期待する動作は、新しいNodeができて選択されてほしいです。今はイベントハンドラーBが発火して、新しいNodeの選択が解除されます。

最初、stopPropagationを使おうと思いましたが、どちらのイベントハンドラーもリッスンしているElementは同じのため、伝播タイミングが同じなので、ダメでした。 とりあえず、イベントハンドラーAでイベントオブジェクトにフラグをたてて、イベントハンドラーBでフラグチェックすれば、期待通りに動作します。 もう少しかっこいい実装方法はないものでしょうか?

  1. イベントオブジェクトではない、何かで状態を持つ
  2. イベントハンドラーAのリッスンするElementを、子Elementにする(で、stopPropagationを使う)
  3. イベントハンドラーBをイベントハンドラーAより先に発火させる

結局2にしました。

rbenvでのバージョン指定が効かなくなりました

~ rbenv version
2.5.5 (set by /Users/shigerunakajima/pubannotation/.ruby-version)
~ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]

ruby のバージョンが上がっている。パスが上書きされたのでしょうか?

~ echo $PATH
/usr/local/opt/libxml2/bin /usr/local/opt/ruby/bin /Users/shigerunakajima/.yarn/bin /Users/shigerunakajima/.rbenv/shims /usr/local/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

/usr/local/opt/ruby/bin/ruby/Users/shigerunakajima/.rbenv/shims より前にいるのがよくないのでしょうか? /usr/local/opt/ruby/bin/ruby を消してみましょう。

あれ?historyset -U fish_user_paths (string match -v /usr/local/opt/ruby/bin $fish_user_paths) が残っているなあ・・・前にも同じことをしたのかな? 実行してみます。

~ set -U fish_user_paths (string match -v /usr/local/opt/ruby/bin $fish_user_paths)
set: Universal variable 'fish_user_paths' is shadowed by the global variable of the same name.

なぜか怒られます。グローバル変数の fish_user_paths がある?

~/.config/fish/config.fishset -g fish_user_paths "/usr/local/opt/ruby/bin" $fish_user_paths って書いてありました! 消したら

~ rbenv local 2.5.5
~ ruby --version
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-darwin18]

わーい

ただしHomebrewで入れているRubyは使えなくなりました。PATHから /usr/local/opt/ruby/bin 消したんだから、そりゃそうか・・・ set -U fish_user_paths $fish_user_paths /usr/local/opt/ruby/bin したら

~ echo $PATH
/Users/shigerunakajima/.yarn/bin /usr/local/opt/ruby/bin /Users/shigerunakajima/.rbenv/shims /usr/local/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

rbenvの前に来ちゃった・・・

再起動したら理想的な並び順に変わりました。

~ echo $PATH
/Users/shigerunakajima/.rbenv/shims /usr/local/bin /Users/shigerunakajima/.yarn/bin /usr/local/opt/ruby/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

なんじゃそれ?

gem install libxml-ruby -v '3.1.0' に失敗します

Building native extensions. This could take a while...
ERROR:  Error installing libxml-ruby:
    ERROR: Failed to build gem native extension.
    current directory: /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/libxml-ruby-3.1.0/ext/libxml
/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/ruby -r ./siteconf20200414-17043-wpqllw.rb extconf.rb
/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/ruby: warning: shebang line ending with \r may cause problems
checking for libxml/xmlversion.h in /opt/include/libxml2,/opt/local/include/libxml2,/usr/local/include/libxml2,/usr/include/libxml2... no
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.
Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/$(RUBY_BASE_NAME)
    --with-xml2-config
    --without-xml2-config
    --with-xml2-dir
    --without-xml2-dir
    --with-xml2-include
    --without-xml2-include=${xml2-dir}/include
    --with-xml2-lib
    --without-xml2-lib=${xml2-dir}/lib
 extconf failure: need libxml2.
    Install the library or try one of the following options to extconf.rb:
      --with-xml2-config=/path/to/xml2-config
      --with-xml2-dir=/path/to/libxml2
      --with-xml2-lib=/path/to/libxml2/lib
      --with-xml2-include=/path/to/libxml2/include
To see why this extension failed to compile, please check the mkmf.log which can be found here:
  /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-18/2.5.0/libxml-ruby-3.1.0/mkmf.log
extconf failed, exit code 1
Gem files will remain installed in /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/libxml-ruby-3.1.0 for inspection.
Results logged to /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-18/2.5.0/libxml-ruby-3.1.0/gem_make.out

libxml2が見つからないみたいです。

~ brew install libxml2
Warning: libxml2 2.9.10_1 is already installed and up-to-date
To reinstall 2.9.10_1, run `brew reinstall libxml2`

Homebrew的にはインストール済みです。

checking for libxml/xmlversion.h in /opt/include/libxml2,/opt/local/include/libxml2,/usr/local/include/libxml2,/usr/include/libxml2... no

とエラーメッセージが出ています。ヘッダファイル自体はあります。

~ ls /usr/local/opt/libxml2/include/libxml2/libxml/xmlversion.h
/usr/local/opt/libxml2/include/libxml2/libxml/xmlversion.h

ヘッダファイルを探すパスに /usr/local/opt/libxml2/include/libxml2 が入っていないのが問題のようです。

f:id:ledsun:20200508061818p:plain

~ bundle config build.libxml-ruby --with-xml2-config=/usr/local/opt/libxml2/bin/xml2-config
~ bundle config
Settings are listed in order of priority. The top value will be used.
build.libxml-ruby
Set for the current user (/Users/shigerunakajima/.bundle/config): "--with-xml2-config=/usr/local/opt/libxml2/bin/xml2-config"

gem installは失敗します。だってbundle configだもん。 でもbundle installは成功するので、大丈夫でした!

切符を通したときだけ通れる改札口Ractor

前回、Ractor.selectでtakeとrecvを両方待てることがわかりました。 これを使って切符を通したときだけ通れる改札口を作ります。

完成形

キーボード入力を待つRactor = Ractor.new { loop { Ractor.yield gets.chomp } }

改札口Ractor = Ractor.new(キーボード入力を待つRactor) do |キーボード入力|
  ゲート = :閉

  loop do
    # キーボード入力と自身へのメッセージを両方待つ
    メッセージの種類, 受信したメッセージ = Ractor.select キーボード入力, self

    if メッセージの種類 == :recv
      ゲート = :開
    else
      Ractor.yield 受信したメッセージ if ゲート == :開
      ゲート = :閉
    end
  end
end


loop do
  改札口Ractor.send 'に切符を入れる'
  入場者 = 改札口Ractor.take

  p "#{入場者} が入場しました"
  sleep 1
end

実行すると、たくさんキー入力をしても、1秒に一回だけ受け付けます。

感想

Ractor.selectの戻り値の第一引数、自身へのイベントだと:recv帰ってくるのですね。selfが帰ってくるのかと思っていました。

なんだろう、この・・・ES5以前のJavaScript感。懐かしい。クラスを作らないで...

  1. Ractorでスコープを切る
  2. ブロック内のローカル変数で状態を持つ
  3. Ractor.newで定義と同時に、外部への参照を受け取る(実行する)

モジュールパターンやんけ!

var module1 = function(initState){
  var state = initState

  return {
    getState: function(){
      return state
    },
    setState: function(newVal){
      state = newVal
    }  
  }
}('hoge')

みたいな、関数スコープしかなかった時代にプライベートな状態を持つオブジェクトを作るためにやっていた工夫と似ている点を感じました。

Ractor.recvの代わりにRactor.selectする

RactorはsendされたメッセージをRactor.recvで受け取ります。 Ractor.selectでも受け取れます。 例えば、次の例です。

r = Ractor.new do
  # message = Ractor.recv と同じ
  _, message = Ractor.select self
  p "Hello #{message}!"
end

r.send 'World'
r.take

実行するとsendされた'World'を結合して、Hello World!を出力します。

Ractor.selectでsendされるメッセージを待てると何がうれしいのでしょうか?

別のRactorをtakeしているときに、自身にsendされるメッセージと別のRactorからのtakeを同時に待てます。 例えば、次の例です。

r1 = Ractor.new do
  Ractor.yield 'r1がyieldしたメッセージ'
  'r1がreturnしたメッセージ'
end

r2 = Ractor.new(r1) do |r1|
  loop do
    # takeとrecvを同時に待つ
    _, message = Ractor.select self, r1
    p message
  end
end

r2.send 'r2にsendしたメッセージ1'
r2.send 'r2にsendしたメッセージ2'

r2.take

実行すると次のように表示されます。

~ ruby select_take_and_recv.rb
"r2にsendしたメッセージ1"
"r2にsendしたメッセージ2"
"r1がyieldしたメッセージ"
"r1がreturnしたメッセージ"

歌うRactor

Ractorで歌ってみましょう。

歌う部分はRactorは関係ありません。出落ちです。 Ractor内で、spawnしたプロセスをメインスレッドでkillできるか確認します。

非同期なspawn関数をRactor内で実行する必要があるのかは知りません。

完成形

LYRICS =<<FIN
ちょうちょう ちょうちょう 菜の葉にとまれ
菜の葉にあいたら 桜にとまれ
桜の花の 花から花へ
とまれよ遊べ 遊べよとまれ
FIN
.gsub("\n", ' ')
.freeze

pid = Ractor.new { spawn "say #{LYRICS}" }.take

sleep 10

Process.kill 9, pid

pidは数値なので、Ractorと共有可能です。

共有可能なオブジェクト

文字列は共有可能なオブジェクトではありません。 Ractorから参照するとNameErrorが起きます。 freezeして共有可能なオブジェクトにします。

Ractor間のメッセージ送受信 Pull型とPush型

やることは前回と同じです。Ractorの構成を組み替えてみました。

Pull型とPush型

Ractorのメッセージ送受信にはPull型とPush型があります。 前回、メインスレッド(?)でRactorインスタンスをselectしてイベントを待ちました。Pull型です。

f:id:ledsun:20200422213039p:plain
Pull型

今回、描画用のRactorインスタンスrendererを用意して、他のRactorインスタンスからイベントをsendします。Push型です。

f:id:ledsun:20200422213126p:plain
Push型

完成形

require 'io/console'

# 結果を描画するRactor。inputとclockの存在を知りません。
renderer = Ractor.new do
  val = 0
  loop do
    # メッセージを待ちます
    msg = Ractor.recv

    # メッセージがユーザー入力だったら値をリセットします。
    val = 0 if msg == :reset

    # カウントアップします。
    val += 1
    # 行をクリア
    print "\e[2K"
    # 行頭へ移動
    print "\e[0G"
    # 出力
    print val
  end
end

# ユーザー入力を待つRactor。引数でrendererを受け取ります。
input = Ractor.new renderer do |renderer|
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    # メッセージを送ります
    renderer << :reset
  end
end

# クロックイベントを発生するRactor。引数でrendererを受け取ります。
Ractor.new renderer do |renderer|
  loop do
    # メッセージを送ります
    renderer << nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# Ractorの終了を待つ?
Ractor.recv

はまったところ

Ractor.recvを書かずに実行したら、何も出力されずにプログラムが終了しました。 Ractorインスタンスが動き出す前に、メインスレッド(?)が終了したようです。

感想

Pull型とPush型では、Ractor間の依存関係が逆転しました。 この規模では、ふーんて感じです。

Ractorを使ったTUIプログラムの一歩

前回のユーザー入力とクロックを両方待つプログラムを少し改造します。

  • Enter以外の入力を受け取る
  • カーソルを移動せずに書き換える

後者はRactorは関係ないです。

Enter以外の入力を受け取る

任意の1文字を受け取るためにSTDIN.getchを使いたいです。 STDINは共有不可能オブジェクトです。 次のようなRactorを書くとcan not access non-sharable objects in constant STDIN by non-main Ractors (NameError)が発生します。

# ユーザー入力を待つRactor
input = Ractor.new do
  while  "\C-c" != STDIN.getch
    Ractor.yield :reset
  end
end

共有不可能オブジェクトをRactorに渡すときはmoveします。

# ユーザー入力を待つRactor
input = Ractor.new do
  io = Ractor.recv
  while  "\C-c" != io.getch
    Ractor.yield :reset
  end
end

input.send STDIN, move: true

カーソルを移動せずに書き換える

エスケープシーケンスを使ってカーソルを操作します。

  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val

完成形

require 'io/console'

# ユーザー入力を待つRactor
input = Ractor.new do
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val
end

次のように動きます。一定のペースでカウントアップし、何か入力すると1に戻ります。

f:id:ledsun:20200421225620g:plain
プログラム実行時のスクリーンショット

参考