それまで面識がなかった人と一緒にLT会をさせていただいた。

『3年以上前に未経験からエンジニアになった人たちの「これまで」「今」「これから」について話すLT会』で司会をさせていただきました。
運営側の視点で記録に残しておこうと思いました。
(当記事は私の主観的な感想です)

【概要】

  • 12/15(金) 21:00〜23:00
  • オンライン
  • 登壇者10名含む定員30人(Xで募集)

【参加条件】

  • ビデオON
  • このLT会の告知がXでポストされた時点で主催者S氏(Sは主催者のS)をフォローしている
  • 未経験からエンジニアにキャリアチェンジした人

先に発表内容の感想を述べると、クローズドな会だったためか生々しいリアルな内容で、突っ込んだ質問も多くておもしろかったです。 十人十色、いろんな道があることを知れました。

参加の動機

S氏とは直接の面識はありませんでしたが、Xで積極的にご自身の考えを共有されたり、勉強会も開催されているのを拝見していたので、ぜひ参加してみたいと思いました。
私はまだエンジニアにキャリアチェンジはしていませんが、募集に対して参加表明のリプライを行ってみたところ、その点は問題ありませんでした。

司会をすることになった経緯

S氏の告知に以下のように書かれていたので「何かお手伝いできることはないか」という旨のDMを送りました。

参加いただく方の中で「イベントやりました!」という実績が欲しい人は共催しましょう。

私が学んでいるスクールの受講生たちが輪読会やイベントを企画しているのを目にしていたので、自分も何かやってみたいと思っていました。 その後「司会をしてみいないか」というご提案をいただいて、以下のような理由で迷いましたが、せっかくなのでさせていただくことにしました。 迷った理由は、

  • 司会経験なし。
  • 人前で臨機応変に対応できるのか。
  • S氏について、Xの投稿は拝見していたが直接関わったことがないので、相手が実際どのような人物なのかはお互いわからないと思った。

今思うと、「エンジニアの集まりに乗り込んでいって、やったこともない司会をする」という点で結構なチャレンジャーだったよな、と思いました(笑) 引き受けたからにはしっかりやりたいという気持ちでした。

当初の疑問

  • 共同主催ってどういう役割分担をするんだろう?
  • S氏のイメージしているLT会とは?

上記2つの疑問がありました。 連絡手段はXでのDM。 S氏主催なので、どうコミュニケーションを取っていけば良いのか、「あんまりしゃしゃり出てもな」と思って、初め様子見してしまってました。

その後、「不安に思っていることがあったら遠慮なく言ってください」と仰っていただき、不安に思っていること、決めておかなくてはいけないだろうことをメッセージして話が進んでいきました。ある程度ものごとが定まってきてから一度 zoom で20分程度顔合わせしていただきました。

本番当日に実際参加してみて、S氏がイメージしていたLT会は自分が思っていたものよりフランクでした。 自分のイメージしていたものは、スクールでよく参加しているものだったりconnpass で申し込んで参加するようなカッチリ?したようなものでした。 今回は、人数限定でクローズドな会だったので、司会で終始緊張はしていましたが、参加者のみなさんが開放的で楽しかったです。もちろん、普段参加するLT会が楽しくないというわけではありません。

決めておいたこと

主にスクールで参加したLT会を参考にイメージしながら考えました。

  • ツールは何を使うか
    zoom を使いました。普段は discord で輪読会などよく参加していますが、電波が悪い時や接続しているマイクの関係で音が相手に聞こえなかったりするときがあります。zoom の電波が悪い印象はありませんが、当日も安定していました。
  • ミーティングURLの発行はどうするか
    S氏の知り合いの方がやってくださりました。
  • タイムテーブル
    時間とカンペと出演順など必要事項を書いたものを一応用意しました。最初Googleスプレッドシートで作っていたのですが、勝手が悪くてExcelに変えました。
    事前に一人zoomにつないで画面に向かってカンペを見ながらしゃべってみたのですが、明らかにカンペ見てるよねって具合になりました。頭に入れてカンペを見ないか、画面上に用意しておくのが良いですね。結局フランクな会だったので、カンペは必要ありませんでした...
    LT会終了まででタイムテーブルとの誤差はそんなになく、質疑応答3分の余裕を持った設定が良かったのだと思いました。
    • 初めの挨拶 → 発表10名 → 終わりの挨拶
    • 発表の準備時間(質疑応答):3分
    • 発表時間:5分 発表はグループDMで5分 or 7分の多数決で決まりました。
  • 質疑応答をどうするか
    マイクオンで直接発言してもらうか、zoom のチャット欄に投稿してもらうことにし、当日はチャット欄の質問を拾っていく形が多かったです。
    業界経験があれば、もっと登壇者から話を引き出せたかもしれないです。
  • 発表順をどうするか
    特にこれというのはなかったそうです。
  • 懇親会を開くか
    発表者が10名だったので、ルームへ振り分ける時間(登壇者を均等に振り分けたかった)と終了時間を考慮して懇親会はしないことになりました。
  • 視聴者の発表に対するリアクションをどう行ってもらうか
    どんな人たちが参加されるのかわからなかったので、もし、お通夜みたいな明るくない空気になったらどうしよう・・・とか考えていました。しかし、エンジニアのみなさんは普段からLT会などのイベントは参加慣れしているようで杞憂だったのかもしれません。
    リアクションは、ハッシュタグを付けてXに投稿してもらうこと、zoom のチャット欄にコメントを書き込んでもらうことを事前にグループDMで案内しました。
  • 発表終了の合図をどうするか
    一応決めておきました。スマホで5分をセットしておき、時間になったらマイクオンしてベル(百均で仕入れたもの)を鳴らす。
    2人目ぐらいまでは鳴らしていましたが、そんなに長くなることもなく、話を遮るのもどうかと思ったので途中から鳴らすのをやめました。

いざ本番

そうして本番を迎え、何とか無事終えることができました。 やはり人前でスムーズに話すのは難しいです。みなさん盛り上がっていて発表もおもしろかったですし、準備してなるようになったので良しとします!準備大事。
このような場に司会として参加する機会を与えてくださったS氏に感謝です。

Image from Gyazo

テストとは。最初のイシューでエラーに遭遇した話。

これは「フィヨルドブートキャンプ Advent Calendar 2023」の14日目の記事です。

こんにちは。現在、フィヨルドブートキャンプ(以下 FBC)でチーム開発の課題に取り組んでいます。今回はチーム開発のプラクティスに入って最初のイシューでエラーに遭遇したので、そのとき学んだことについて書きたいと思います。

目次

FBCチーム開発の特徴

最初にFBCではどんな形でチーム開発が進められるのか、特徴について簡単に触れたいと思います。

Image from Gyazo

Bootcampというシステムを使って開発を行う

Bootcamp は受講生たちが毎日書く日報、質問の投稿・閲覧を行うQ&A、メンターさんが書かれる Docs やブログ記事、各種イベント、受講生の入会・退会なども管理されており、FBCの主要なシステムとして実際に使われているものです。 オープンソースソフトウェア(OSS)としてソースコードも公開されています。また discord のチャンネルでは、発見したバグや欲しい機能などを discord 内のメンバーが誰でも投稿したり、イシューとして提案することもできます。

開発手法はアジャイル開発のひとつであるスクラムを採用

チーム開発プラクティスの前準備として、アジャイルスクラムについて学び、それらがどういうものなのか、どのような目的でスクラムを利用するのか、ある程度理解したうえでチーム開発に入るようになっています。
以下の書籍で学びました。

FBCのスプリント期間は一週間で、作業完了したものが毎週リリースされます。
ふりかえり・計画ミーティングも毎週行われ、そこでは一週間の進捗報告や実装したもののデモを行なったり、詰まっていることなどを相談することができます。

PRは、まず受講生にレビューを依頼してOKをいただいてから、スクラムマスターに依頼する

受講生同士レビューを行うことで、レビュー側の視点も持つことができ、自分のイシューだけでなく自分以外の受講生が取り組んでいるイシューについても調べることになるので、より多くのことを学べる仕組みになっています。

最初のイシューでエラーに遭遇した

修正作業

最初は簡単な作業からイシューを振り分けていただけるのですが、私が担当したのはこちらのイシューの一部でした。

rubocop-fjord とは、FBC用の RuboCop の設定で課題提出する前にこちらを通します。 バージョンアップにより slim-lint で警告が出るようになったので、ハッシュを省略形に修正して警告を出なくするというものでした。 Image from Gyazo 修正自体は他の受講生が既に別のファイルを担当されていたので、PRの書き方などそちらも参考にしながら取り組みました。

テストを通す

修正が終わったらテストを通します。

正しい順番
修正する
↓
ローカルで、修正部分に関係すると思われるテストを通す
↓
ローカルテストが異常なければ、プッシュする
↓
CIが通っているか確認する
↓
CIに異常がなければ、レビューを依頼する

継続的インテグレーション (CI) とは? - CircleCI

私は順番を間違えてしまい、先にGitHubの自分の作ったブランチにプッシュして受講生にレビューを依頼してしまいました。
その後、間違いに気づいてローカルテストを通さなくてはと思い、そこで事件は起きました。

CI は通っていました。
Image from Gyazo
最初に使っていた実行コマンドがこちらで、

$ bin/rails test:all

なかなか安定せず、通らないものと通るものがバラバラでした。それでも何回かやったら通るようになっていくのかと思い、次のコマンドを実行しました。

$ CI=1 PARALLEL_WORKERS=1 bin/rails test:all
  • CI: CI環境で有効になる変数。これがあると失敗したテストをretryしてくれる。
    失敗するテストが多い時にこのフラグを付けると時間が余計にかかる。
  • PARALLEL_WORKERS: 並列実行する数を指定する変数。1の場合は並列実行しない。

何回かやったら通るのか?という考えを持っていたのは、「ローカルテスト」や「Flakyなテスト」というものに理解がなかったからでした。調べてはいましたが...
(その部分については後半のテストの考え方をご参照ください。)

flaky testとは何かというと、コードにまったく手を触れていないにもかかわらず、成功したり失敗したりするテストのことです。
自動テストに対して信頼がだんだん失われていく時とは - 和田卓人 - ログミーTech

最終的に、通らないテストが3つぐらいになったので、もう何回かやったら全部通るのではないかと思いましたが、そこからエラーがバンバン出るようになりました。

エラー多発前のテスト結果

あと3つ通れば...

$ CI=1 PARALLEL_WORKERS=1 bin/rails test:all
~ 省略 ~
Finished in 3457.581514s, 0.4659 runs/s, 1.2682 assertions/s.
1611 runs, 4385 assertions, 3 failures, 0 errors, 2 skips
エラー多発後のテスト結果

コマンド実行直後からエラーがバンバン出る状態。

$ CI=1 PARALLEL_WORKERS=1 bin/rails test:all
~ 省略 ~
Finished in 3516.373300s, 0.4581 runs/s, 0.8261 assertions/s.
1611 runs, 2905 assertions, 38 failures, 752 errors, 2 skips

発生したエラーの全てにNil location provided. Can't build URI. (ActionView::Template::Error)の記載があり、何回やっても752個のエラーが検出されるようになりました。 「これもFlakyなテストなのだろうか?もうわけわかんねぇ...」と思ったので、FBCのQ&Aで状況報告の投稿をして助けを求めました。

調査

Q&Aで何回かやりとりをさせていただき、伝わらない部分があったのでメンターさんとビデオ通話で一緒に原因調査をさせていただけることになりました。

エラー内容
Nil location provided. Can't build URI. (ActionView::Template::Error)
確認したこと

デバッグしたり、関係ありそうなことを一つ一つ確認して潰していきました。

yarn installで npm パッケージがきちんとインストールできているか。
  • Node.jsの代表的なパッケージの管理ツールには、npm と yarn があるが、プロジェクトによってどちらを使うかが決まっている。
  • 確認方法:bin/setup ファイルにsystem! 'bin/yarn'という記載があり yarn を使うということがわかる。この場合、npm install の結果は関係がない。

yarn install の実行結果

$ yarn install
yarn install v1.22.17
warning ../../package.json: No license field
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.22s.

異常なし。

CI のテストは通っているか。

CI のテスト結果 Image from Gyazo
Success!!

RAILS_ENV=test rails assets:precompile を実行して問題なくコンパイルが成功するか?

RAILS_ENV=test rails assets:precompile の実行結果

public/assets 下に209個のファイルが作成された。Git の変更は210個。 正常に実行されたことが確認できればこの変更は削除して良いし、public/assets が .gitignore ファイルに記載されていればこの変更は出ない。
(後日 .gitignore ファイルに記載するためのPRが作成されました。)

$ RAILS_ENV=test rails assets:precompile
yarn install v1.22.17
warning ../../package.json: No license field
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.18s.
I, [2023-11-24T07:15:50.502974 #10379]  INFO -- : Writing /Users/kw/bin/bootcamp/public/assets/manifest-1abee1fd73fc0752f6b1b9ff7228ddd5a78e08474f73e235db5047c62b437427.js
~省略~
Everything's up-to-date. Nothing to do

異常なし。

$ git status
On branch chore/fix-slim-lint-issues-6994
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    public/assets/

デバッグしてログを出力

デバッグの内容

元のコード
  def avatar_url
    default_image_path = '/images/users/avatars/default.png'

    if avatar.attached?
      avatar.variant(resize: AVATAR_SIZE).processed.url # urlがnil
    else
      image_url default_image_path
    end
  rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError
    image_url default_image_path
  end
デバッグ
def avatar_url
  default_image_path = '/images/users/avatars/default.png'
  
  if avatar.attached?
    my_variant = avatar.variant(resize: AVATAR_SIZE)
    if my_variant.processed?
      # puts "existing-image: #{my_variant.send(:record)}/#{my_variant.image.url}"
      my_variant.process unless  my_variant.image.url
      # puts "existing-image2: #{my_variant.send(:record)}/#{my_variant.image.url}"
    else
      my_variant.process
      # puts "processed?: #{my_variant.processed?}"
      # puts "created-image: #{my_variant.image.url}"
    end
    puts "existing-image: #{my_variant.image.inspect}/#{my_variant.image.url}"
    my_variant.image.url
    # my_variant
    # avatar.variant(resize: AVATAR_SIZE).processed.url
  else
    image_url default_image_path
  end
rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError
  image_url default_image_path
end
  • prosessメソッド:画像をリサイズしたファイルが保存され、DBにも variation_digest が保存される。
  • variation_digest:リサイズしたファイルの情報が含まれている。
デバッグして出力したログの一部
existing-image: #<ActiveStorage::Attached::One:0x00000001170d58a0 @name="image", @record=#<ActiveStorage::VariantRecord id: 15341, blob_id: 50006297, variation_digest: "JO2GMsBpNz8fyJD0VI9jyWmfQoA=">>/http://127.0.0.1:53601/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDVG9JYTJWNVNTSWhiM2h6T1hveU1UaGhaSEJxY21vME5UQnhaVzgyWjNreU56SnRiZ1k2QmtWVU9oQmthWE53YjNOcGRHbHZia2tpUTJsdWJHbHVaVHNnWm1sc1pXNWhiV1U5SW10dmJXRm5ZWFJoTG5CdVp5STdJR1pwYkdWdVlXMWxLajFWVkVZdE9DY25hMjl0WVdkaGRHRXVjRzVuQmpzR1ZEb1JZMjl1ZEdWdWRGOTBlWEJsU1NJT2FXMWhaMlV2Y0c1bkJqc0dWRG9SYzJWeWRtbGpaVjl1WVcxbE9nbDBaWE4wIiwiZXhwIjoiMjAyMy0xMS0yM1QxMTo0NjoxNS42MTRaIiwicHVyIjoiYmxvYl9rZXkifX0=--1bf0bbbbc9e5799e4585a96eb1df16572545e961/yamada.png
existing-image: #<ActiveStorage::Attached::One:0x0000000100981920 @name="image", @record=#<ActiveStorage::VariantRecord id: 15342, blob_id: 300959870, variation_digest: "IkByHq+XI5DNYzeXPjUKpqesijM=">>/http://127.0.0.1:53601/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDVG9JYTJWNVNTSWhZV3Q1YzNBeE9YRmpkREZ2WVRsd05HUjBkbkp3ZFhCM05uVnhlQVk2QmtWVU9oQmthWE53YjNOcGRHbHZia2tpUVdsdWJHbHVaVHNnWm1sc1pXNWhiV1U5SW0xaFkyaHBaR0V1YW5Cbklqc2dabWxzWlc1aGJXVXFQVlZVUmkwNEp5ZHRZV05vYVdSaExtcHdad1k3QmxRNkVXTnZiblJsYm5SZmRIbHdaVWtpRDJsdFlXZGxMMnB3WldjR093WlVPaEZ6WlhKMmFXTmxYMjVoYldVNkNYUmxjM1E9IiwiZXhwIjoiMjAyMy0xMS0yM1QxMTo0NjoxNS42NzhaIiwicHVyIjoiYmxvYl9rZXkifX0=--405eac42fc47992f3a1b222f9090d625eb4b7e7f/sato.jpg
existing-image: #<ActiveStorage::Attached::One:0x000000011119f9d0 @name="image", @record=#<ActiveStorage::VariantRecord id: 15342, blob_id: 300959870, variation_digest: "IkByHq+XI5DNYzeXPjUKpqesijM=">>/http://127.0.0.1:53601/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDVG9JYTJWNVNTSWhZV3Q1YzNBeE9YRmpkREZ2WVRsd05HUjBkbkp3ZFhCM05uVnhlQVk2QmtWVU9oQmthWE53YjNOcGRHbHZia2tpUVdsdWJHbHVaVHNnWm1sc1pXNWhiV1U5SW0xaFkyaHBaR0V1YW5Cbklqc2dabWxzWlc1aGJXVXFQVlZVUmkwNEp5ZHRZV05vYVdSaExtcHdad1k3QmxRNkVXTnZiblJsYm5SZmRIbHdaVWtpRDJsdFlXZGxMMnB3WldjR093WlVPaEZ6WlhKMmFXTmxYMjVoYldVNkNYUmxjM1E9IiwiZXhwIjoiMjAyMy0xMS0yM1QxMTo0NjoxNS42OTVaIiwicHVyIjoiYmxvYl9rZXkifX0=--bca229fe940c5489f021d6393cfe7db7944096af/sato.jpg


解決

puts で 上記のようにmy_variant.image.inspect を出力してログを確認すると、DB の id で極端に古いものが含まれている、、怪しい、というところから DB でテーブルを確認すると古いレコードが残っていました!!

$ RAILS_ENV=test rails db -p
psql (14.9 (Homebrew), server 14.0)
Type "help" for help.

bootcamp_test=# SELECT * from active_storage_variant_records;
  id   |  blob_id   |       variation_digest
-------+------------+------------------------------
 13681 |  801588450 | IkByHq+XI5DNYzeXPjUKpqesijM=
 13682 |   50006297 | JO2GMsBpNz8fyJD0VI9jyWmfQoA=
 13683 | 1005505682 | JO2GMsBpNz8fyJD0VI9jyWmfQoA=
(3 rows)

よってレコードを削除。(後日 PRが作成され、こちらの作業は不要になっています。)

bootcamp_test=# delete from active_storage_variant_records;

その後、テストを実行するとエラーは出なくなりました。

$ PARALLEL_WORKERS=1 bin/rails test:all

原因

Activestorageが古いレコードを認識すると、(variation_digestが同じであれば)既にリサイズしたものと誤認して、実ファイルが存在しないので url が nil になってエラーが起きていました。同じファイルを同じようにリサイズすると variation_digest が同じになるのでは、という見解です。

fixturesには active_storage_attachments と active_storage_blobs のymlファイルしかなく、

active_storage_attachments → 古いデータは削除される
active_storage_blobs → 古いデータは削除される
active_storage_variant_records → 古いデータは残ったまま

という状態になり、DB上のデータに不整合が発生するという状況になっていました。
fixtures に空の yml ファイルを用意しておくことで、テスト毎にレコードが残っていない状態でテストを開始することができるようになります。
PR: テスト実行時の"ActionView::Template::Error: Nil location provided. Can't build URI."エラーを回避する #7074

Image from Gyazo

active_storage_variant_records について
2 セットアップ - Active Storage - Rails ガイド

テストの考え方

ビデオ通話時、絶好の機会だと思い、疑問に思っていたことをたくさん質問させていただきました。
(以下は考え方の一つであって、現場によって変わるのかもしれません。)

bin/rails test:all を実行する必要があるのか。

行うべきではあるが、、以下の理由から現実的ではない。

  • テストには時間がかかるので、開発のテンポが遅くなる。
  • マシン(PC)の性能に依存し、個人のマシンはそれぞれ違う。人によってうまくいくときもあればうまくいかないときもあり、何回やっても失敗する可能性もある。テストが追加されて増えるほど影響は大きくなる。

ではどのように実施するのが良いのか

  • 新規追加の機能、修正した部分に関係していると思われるテストを行う。 ただ、システムは複雑なので全然関係ないところでテストが転ける場合もある。 ダメなのは、関係するテストがあるのに行わないこと。

テスト実行時は、

  • マシンがテストだけに集中できるよう、使わないブラウザやアプリは全て閉じ、CPUとメモリを開放する。
  • Macにデフォルトで入っているアクティビティモニタでメモリとCPUの動きを把握できる。

Flaky なテスト

ローカルで Flaky なケース

  • 落ちたり通ったりして安定しないテスト。
  • 個々人のマシンに依存するので、ローカルテストでは基準が作れない。

CI で Flaky なケース

  • 例えば、3回に1回ぐらい落ちたり、何回かやれば通るというような場合。

基準にするのは

ローカルテストではなく CI を基準にする。

  • 修正後、思いがけないところが壊れることはよくある。当たりをつけても違うテストが転けることもある。
  • CIで落ちたときにジャッジする。自分の修正に関係あるのか Flaky であるのか。 自分のした修正に関係ありそうなら修正する。関係なさそうなら落ちたテストをローカルで実行して(何回かやって)みて、ローカルがパスしたら CI は Flkay。 ローカルもCIも同じ落ち方なら修正。

    今回のエラーでは CI は通っていましたが、途中からローカルで何回テストを実行しても752個のエラーが発生するようになりました。ということは Flaky ではなく、修正する必要があったのだと思います。 bin/rails test:allではなく範囲を絞ると通ったり通らなかったりするものもあったとは思いますが、Nil location provided. Can't build URI. (ActionView::Template::Error)の数は多かったです。

    後日談として、受講生の方の簡単なイシューのレビューで、関連するファイルを一つテストしたのですが綺麗に通りました。
MacBook-Pro: ~/bin/bootcamp (feature/change-QA-title-format =)
$ bin/rails test test/system/questions_test.rb
Running via Spring preloader in process 7112
Run options: --seed 55144

# Running:

..........................................
[Minitest::CI] Generating test report in JUnit XML format...


Finished in 89.827101s, 0.4676 runs/s, 1.0465 assertions/s.
42 runs, 94 assertions, 0 failures, 0 errors, 0 skips

テキスト形式で質問するとき

  • 一部分のスクショだけ切り取ったものを載せても、どこのファイルで起きているかがわからない。git の変更が210個出ましたというのであれば、git status の結果も一緒に記載するとわかりやすいかも。
  • エラーの一部分をテキストで貼り付ける場合も、その前後関係がわからないので安易に端折らない。GitHub Gist などを使って広範囲を載せると良いかも。
  • 端折る場合はその旨を記載する。記載がないと、そのように表示されたエラーに見えてしまう。
  • 一貫したリポジトリで説明する。あるときは clone A で、あるときは clone B で説明するのは同じブランチであったとしても一貫性がない。
  • エラーの再現手順も記載する。PRにも変更確認方法が記載されている。

最後に

わからないことが複数あると、それをテキストでどのように伝えれば良いのか難しく感じました。
今回はチーム開発に入りたてということもあったのか、メンターさんに全面的にフォローしていただき感謝しかありません。確かその日は祝日で、にも関わらず半日近くお付き合いいただき、エラーが消えたときは感動でした。 解決したときに、「このようなエラーはよく出るものですか?」というような感じの質問をしたのですが、エラーを解決しないといけない場面はよくあって、「名探偵コナンのように謎を解決に導いていくことが、プログラマーの醍醐味でもある」というようなお言葉をいただきました。今回であれば、Active Storage のソースコードを読んだり、調べ直すことで、自分の中に知識として蓄えていくのだそうです。考えて、調べて、試して、考えて、調べて、試して。
自分で解決できるようになりたいです!
最後までお読みいただきありがとうございました。

npm パッケージを公開しました!memory-speed-numbers

npmパッケージを公開したので、その内容について書きます。

この記事の流れ

  • パッケージの説明
  • 開発に用いた要素
  • 開発の背景
  • アイディア出し
  • 詰まった点
  • 工夫した点
  • 反省点
  • 最後に

パッケージの説明

ランダムに表示された5桁掛ける5行の数字を暗記して答えるというものです。
memory-speed-numbers

開発に用いた要素

  • enquirer
  • エスケープシーケンス
  • 非同期処理 Promise
  • クラス構文

開発の背景

現在、プログラミングスクール フィヨルドブートキャンプ(以下 FBC)でプログラミングを学んでおり、プラクティスの一環として作りました。

ラクティスの修了条件

作った npm パッケージの npm パッケージサイト( https://www.npmjs.com/ )上のURLを提出してOKをもらう。

注意

ここで作った npm は自分のポートフォリオの一つになります。すでにあるものや、同じような内容の npm では技術力を高める意味では作る意味はありますが、世にリリースをする意味を持ちません。なので、被らない内容のものを考えましょう。

アイディア出し

「アイディア、思いつかないなぁ」というところから始まりました。 アイディアを生むためには、日頃から頭の片隅に置いておき、考える必要があると思います。ですので、少し早めに考えることにしました。
まずやったことは、他の受講生さんの制作物を拝見し、インストールして使ってみました。 感想としては「え、こんなものが作れるの、すごい!」でした。「自分に作れるんだろうか・・・」 その中で以下のパッケージが印象に残り、自分も何か動くものを作ってみたいなという気持ちになりました。

  • flash-anzan フラッシュ暗算ができる。
    足し算を表示したり消したり、動いておもしろい!と思いました。

  • dummy_email_address_maker ユーザーテスト用のダミーメールアドレスを生成。
    ランダムな文字列でアドレスを生成している点が参考になりました。

上に挙げたパッケージと被る部分もありますが、、「数字を暗記して答える」ということを思い付きました。 調べるとスピードナンバー(英語名:Speed Numbers)という競技もあるようで、このアイディアで行こうと決めました。
ジャパンオープン記憶力選手権〜Japan Open Memory Championship〜

詰まったところ

ターミナルに表示したものを消すためにエスケープシーケンスを使っているのですが、画面の範囲外のものは消せないという仕様を知りませんでした。
エスケープシーケンスによる画面制御

当初は表示する数字の桁数と暗記時間の上限を決めていませんでした(そこが問題)。最初の入力値で500桁を入力すれば、ランダムな数字500桁を40桁ずつ改行して出力していました。 ギネス記録保持者は5分で500桁を記憶するらしく、桁数は多い方が良いと思っていました。
1分で数字100桁が記憶できる古代ギリシャ式4ステップ記憶術

利用者の入力桁数に合わせた出力行数を消すコードが完成し、「これでどんな桁数にも対応できる!やったぜ(ルンルン🎵)」と思ったのも束の間、100桁、200桁、...400桁と確認したところで、おかしいことに気づきました。

50
51
52
53
54
55
56 ← 桁数が多くなると、上にスクロールしたときに画面より上にあった部分が消えていないことがわかる。
MacBook-Pro: ~/bin/js_sample
$
簡略化したコード
function clearTerminalAfterDelay(numbers, milliseconds) {
  console.log(numbers); // 100行を出力する

  let deleteLine = 100; // 消したい行数
  setTimeout(() => {
    process.stdout.write(`\x1b[${deleteLine}A\x1b[0J`); // 出力した行数を消す
  }, milliseconds * 1000);
}

const digits = 100; // 桁
// 1から100までの配列を作り、改行文字で連結して文字列を作る。
// (実際には0から9までの数字で利用者が入力した桁数のランダムな数字の文字列を表示します。)
const numbers = Array.from({ length: digits }, (_, index) => index + 1).join(
  "\n"
);
const milliseconds = 3; // 3秒で消す
clearTerminalAfterDelay(numbers, milliseconds);


「NO !! これではアプリとして破綻しているではないか...」
3時間ほど絶望して考えた結果、出力桁数を25桁、暗記時間は選択式にして制限することにしました。

ここで得た教訓:壮大なものを作ろうとしない。

上限がないというのは良くないと思っていて、何かしらの制限が必要だとは思っていました。

工夫したところ

  1. 答え合わせの際、自分が間違えた箇所が確認できる
  2. 暗記した数字を入力する際、改行、スペースを使い放題
  3. 暗記した数字を入力する際、1文字以上25文字以内で、数字のみ入力可のバリデーションを設定

以下、順番に説明させていただきます。

答え合わせの際、自分が間違えた箇所が確認できる

FBCには Linux の cal コマンド(カレンダー)を実装しようというプラクティスがあります。そのプラクティスの歓迎要件で(必須条件ではない)、「今日の日付の部分の色が反転する(背景色と文字色が入れ替わる)」というのがあり、そのプラクティスでは取り組みませんでしたが、そういえばと思ってヒントを得ました。

Image from Gyazo

話が逸れますが、FBCでは Linux の cal、ls などのコマンドを Ruby で一から実装するプラクティスがあります。そしてその後のプラクィスでは、それをJavaScriptで再び作ったり、オブジェクト思考を学んで、それを適用して新しく作ったりします。同じ題材で別の技術や要素を利用して作るという点には一貫性があり、FBCのカリキュラムがよく考えられていて優れている点だと感じています。

暗記した数字を入力する際、改行、スペースを使い放題

実際のスピードナンバーという競技では専用の用紙が渡され、それに記入していくという形のようですが、それをCLIで実現するにはどうすれば良いのか考えました。 時間も限られており、あまり難しいこともできない、複雑になると見づらくなりそうだと思ったので、入力しやすくするために改行とスペースは使い放題可という結論に至りました。
入力値で複数行を受け付けるには enquirer で multiline: trueを書けば良く、大変便利だと思いました。

function inputUsersAnswerPrompt() {
  return new Input({
    multiline: true,
  });
}
暗記した数字を入力する際、1文字以上25文字以内で、数字のみ入力可のバリデーションを設定

以下サンプルコード(option-validate.js)を参考にしました。

'use strict';

const { Password } = require('enquirer');

const prompt = new Password({
  name: 'password',
  message: 'What is your password?',
  validate(value) {
    return (!value || value.length < 7) ? 'Password must be 7 or more chars' : true;
  }
});

prompt.run()
  .then(answer => console.log('Answer:', answer))
  .catch(console.error);

実際のコードはこちら。

import enquirer from "enquirer";
const { Input } = enquirer;

function inputUsersAnswerPrompt() {
  return new Input({
    message: "Input your answer",
    hint: " (Enter numbers.)",
    multiline: true,
    validate(value) {
      value = value.replace(/\s/g, "");
      return value.length === 0 || value.length > 25 || /[^\d]/.test(value)
        ? "Input must be between 1 and 25 characters and consist of numbers only."
        : true;
    }
  });
}

以下URLにサンプルコードがたくさんあります。
enquirer / examples /

反省点

反省する点としては、作る手順とリファクタリングです。見通しが立っていなかったというのもありますが、

  1. 全体として動くものを作る
  2. main.jsファイルに async / await を適用
  3. クラス構文を使ってオブジェクト思考っぽくクラス分けしてリファクタリング

の順番になりました。 全体を作って全体を作り変えていくというのは大変です。オブジェクト思考の部品化するという利点を活かすと、最初からオブジェクト指向でトライしていくべきだったのではないでしょうか。

最後に

実際に作ってみると、作るまで不都合なことに気づかなかったり、こういう機能を付けたらどうだろうというアイディアもいくつか浮かんできました。考えたことが実現できたときや、パッケージとして一つのものを作って公開できたときには達成感も感じました。新しいすごいことに挑戦するというよりは、カリキュラムでやってきた要素を少しずつ使うという形になりました。作ってみて学べることがたくさんあったと思います。
カリキュラムはまだまだ続く。
最後までお読みいただき、ありがとうございました。

Macでよく使うショートカットキー

はじめに

今回は、私がショートカットキーでよく使っているものをご紹介します。
Mac歴は1年3ヶ月くらいです。
マウスを使わないので、ショートカットキーを調べて使っています。 こういうショートカットキーないかなと思ったら調べてメモアプリに記しておき、忘れたものはメモを見て使ったりしています。 場面ごとに分けてみました。

ショートカット

ターミナルで使うもの

プログラミングの学習を初めた頃、馴染みがなかったのがターミナル操作でした。最初期は文字を消すのにDeleteキーを押しまくっていました。

ショートカット 説明
control + A カーソルを先頭に移動
control + E カーソルを末尾に移動
control + U 行を全削除
control + K カーソル行の右側を全削除
option + ← または → 一単語ごとに右または左に移動

Mac全般で使うもの

13インチの小さな画面で、やりくりしています。

ショートカット 説明
command + tab アプリの切り替え
option + tab 同一アプリでウィンドウ切り替え
command + C →
option + command + V
ファイルの移動
control + A 全選択
command + control + F ウィンドウを小さくしたり、全画面との切り替え
command + M ウィンドウを最小化してDocにしまう
command + R リロード
shift + command + R スーパーリロード
command + W ウィンドウを閉じる

Chromeで使うもの

ショートカット 説明
option + command + I chrome検証画面の起動
option + command + B ブックマークマネージャを開く
command + shift + B ブックマークバー開閉
command + ↑または↓ ページ最下部 / 最上部へスクロール
control + tab chromeタブを右に移動
control + shift + tab chromeタブを左に移動

その他

ショートカット 説明
スクリーン
全体のスクショ
command + shift + 3
スクショ/録画
選択
command + shift + 5
→ ↑ ↓ ← zl , zk , zj , zh | 矢印の書き方

ターミナル操作

ターミナル操作の上達には、何を学べば良いのだろうと思っていた時期がありました。検索するとLinuxという言葉が引っかかる。 そんなあなたに「新しいLinuxの教科書」をおすすめします。 こちらで体系的に学ぶことができました。説明も平易に書かれています。

Macの「ターミナル」とは、キーボードからコマンドと呼ばれる命令文を打ち込んでPCに命令をする、Macに最初から搭載されているデフォルトアプリです。

あとがき

低スペックな中古のWindowsノートPCでプログラミング学習をスタートしました。それから数ヶ月経ち環境構築でのエラーに対応できなくなったのでMacの購入に踏み切りました。 購入当初はMacの使い方に戸惑いを感じた部分もありましたが、慣れてくると使いやすいものだなぁと今では思います。

【ruby】getoptsと条件分岐

はじめに

初ブログです。
みなさん、「getopts」これなんて読んでますか?
私は「ゲットップツ」?と思っていますが定かではありません。

ということで今回は、「getopts」と「キーワード引数を使ったメソッド」を組み合わせた条件分岐の書き方を知ったので、記事に残しておこうと思いました。

getopts と 条件分岐

require 'optparse'

option = ARGV.getopts('abc') # getopts : 引数をパース(分析)した結果を、Hash として返す。
p option # sample.rb -a => {"a"=>true, "b"=>false, "c"=>false}
p option # sample.rb -ab => {"a"=>true, "b"=>true, "c"=>false}
p option['a'] # sample.rb -a => true
p option['a'] # sample.rb -b => false
p option['b'] # sample.rb -ab => true

def xxx_or_yyy(option: false) # デフォルト値はfalse('yyy')を返す。
  option ? 'xxx' : 'yyy' # ・・①
end
p option['a'] # sample.rb -a => true # ・・②
p xxx_or_yyy(option: option['a']) # オプションaが選択されるとtrue(②)、①により'xxx'を返す。aを含まないオプションbやcが選択されたときはデフォルト値であるfalse('yyy')を返す。

その他

getoptsではない書き方
def parse_option(argv) # メソッド定義とのきは仮引数
  argv_option = {}
  OptionParser.new do |opt|
    opt.on('-a') { |v| argv_option[:a] = v }
    opt.on('-b') { |v| argv_option[:b] = v }
    opt.on('-c') { |v| argv_option[:c] = v }
    opt.parse!(argv)
  end
  argv_option
end
parse_option(ARGV) # メソッド呼び出しでの引数は実引数
キーワード引数に = を使う書き方もある
def xxx_or_yyy(option = false)
  option ? 'xxx' : 'yyy'
end
p xxx_or_yyy(option = option['a'])
三項(条件)演算子(?:)を使った条件分岐
option ? 'xxx' : 'yyy'

# 以下のif文を書き換えたもの。
if option
  'xxx'
else
  'yyy'
end

このようにシンプルなif文は1行で書くことができる。読みにくくならない範囲で使う。

elseやelsifがないパターンのif文は、後置ifを使って1行で書くことが多い。
xxx if 条件 # 条件がtrueのときxxxが実行される。
参考