プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

写真で管理する!技術書を読み返すための読書管理Webサービスをリリースしました

はじめに

こんにちは。フィヨルドブートキャンプで学習中のはるぐちです。 フィヨルドブートキャンプでは、最終課題として自作のWebサービスを作って公開するというプラクティスがあります。

今回、僕は技術書を読み返すための読書管理Webサービス「re:Read」をリリースしました。

reread-book.herokuapp.com

github.com

このエントリでは開発・自作サービスを作るにあたってよかったことや苦労したことなどを書いていきたいと思います。

自己紹介

改めて自己紹介をします。はるぐちといいます。フィヨルドブートキャンプには2019年の10月末から参加しています。

途中までは公立中学校の数学の教師をしながら参加していましたが、あまりにも進むのが遅いので今年度から仕事を辞めて無職で1年間学習を進めてきました。

現在はWebエンジニアになるべく転職活動と学習を並行して行っています。

re:Readの紹介

簡単にいうと「読書管理+再読管理」をするWebサービスになります。

もう少し詳しく説明しますと、何度も読み返したいと思っている本(エンジニアにとっての技術書など)に対して写真やメモを溜め込んでいきます。

気になりポイント(写真)一覧

このアプリでは写真やメモを「気になりポイント」と称して呼んでいます。 気になりポイントとして溜め込むのは

  • 本を読んでいて単純に気になったこと
  • 難しくて意味がわからなかったこと
  • 感動したこと
  • 図解

などなど。写真やメモを見ることで、読み返したいなーと思えるものを溜め込んでいくのがおすすめです。

また、アプリ内では「次に読み返す日」を設定する入力フォームがあります。

この入力フォームには書籍名がデフォルトで入っており、任意でメモを残すことができます。日付を選択してクリックするとGoogleカレンダーに自動的に登録されることになっているので読み返したい日を忘れにくくなっています。

読み返し日の設定(Googleカレンダーに登録)

使い方

簡単にこのWebサービスの使い方を説明します。

ログインして書籍を登録

Googleアカウントでログインします。

ログインしたらまずは読み返したい本を登録します。

読み返したい本リスト

読み返したい本を登録すると、その本に関する情報が表示されます。

具体的には「本のタイトル」「溜め込んだ気になりポイント(写真)の数」「最終更新日」「読み返す日の設定日」です。

気になったポイントを写真に撮り、投稿する

「読み返したい本リスト」からタイトルをクリックすると、溜め込んだ気になりポイント(写真)の一覧画面に遷移します。 初めは下の画面のように何も写真が登録されていないので「写真を投稿する」ボタンを押して写真を投稿してください。

気になりポイント

気になりポイント詳細&メモの編集

写真の一覧画面で写真をクリックすると、実際の大きさで表示されます。

また、メモを入力していた場合編集できるようになります。

気になりポイント詳細&メモの編集

Googleカレンダーに読み返す日を設定する

写真一覧画面で「Googleカレンダーに読み返す日を設定する」というボタンをクリックするとGoogleカレンダーに登録するための入力フォームに遷移します。 「サマリー」「読み返す日」「メモ(任意)」があり、タイトルに関しては自動で設定されます(もちろん変更もできます)。

そして、設定ボタンを押すとお使いのGoogleカレンダーに予定が挿入されます。

Googleカレンダー

解決したい問題

解決したい問題は大きく2つあります。

  1. 読んだ書籍の内容を忘れてしまう → 読書管理アプリで解決

  2. 読み返すタイミングを忘れてしまう → リマインダーで解決

別々のもので解決することを1つのアプリケーションで解決したら便利ではないか?そんな思いがあります。 また、読み返すハードルを少しでも下げられるうに前回読んでいた内容を視覚的に伝えられるように写真を投稿するというスタイルを取ってます。

  • 技術書や学習書籍を読んでいる中で難しくて読むのを諦めた本はありませんか?

  • 感銘を受けた本で、何度も読みたいと思って忘れてしまっている本はありませんか?

  • 読み返そうと思っても、どこまで読んだかわからずまた一から読み返す。それが面倒で書籍が進まないなんてことはありませんか?

僕は、あります。本棚に本をしまった段階で忘れ去られている本がたくさんあるので、そういった本を効率的に読み返したいという思いからこのアプリを作成することにしました。

なぜこのサービスを作ったのか?

もちろん、上記の問題を解決するためなのですが、ここでは少し個人的な話をします。

僕は数学が好きでよく学習書で勉強するのですが、如何せん書いてある内容が難しくて途中で挫折することも多々あります。「半年後にまた挑戦しようかな〜」なんて思って本棚にしまったが最後。すっかり本の存在を忘れてしまって、次読むのはかなり先の話。そういった事が多々ありました。

現状、今でも僕に読まれるのを待っている本がたくさんある状態です。また、どんな本がどんな状態で読みかけなのかは正直一切記録が残っておらず自分の記憶を探るほかありません。

リマインダー機能を使って忘れないようにしようとしたこともあるのですが、やっぱりどんな状態で読み終わっていないのかが不明瞭なことによって読み返すハードルが高く、難しかった記憶だけが蘇ることにより結局読まずじまいになっている本がたくさんあります。

また、ここ2年ほどプログラミング学習を始めてからも、何か新しい技術をキャッチアップするときは体系的に学べる書籍を利用するのですが、やはり同じ問題に直面しました。

そこで、「読み返すタイミングの通知」と「読んでいた状態を記録」できたら、読み返すハードルはぐんと下がるのではないか?そんな仮説をもとに作成することにしました。

こんな人に使ってほしい

書籍でなんらかの学習をしている人は少なからず何回も読み返したい本があると思うのでぜひ使ってもらいたいです。

僕は、趣味の数学書やプログラミングの学習で利用しています。

技術スタック(2022年4月現在)

リリース後はVue.js+TypeScriptを導入予定です。

今後の予定

サービスはリリースしてからが始まりだと思っているので今後もどんどん開発を続けていこうと思っています。ファーストリリースの期限を自分の中で設定していたため実装できていない機能や技術があるので、キャッチアップしながら進めていきたいと思います。

機能面

  • 画像トリミング機能
  • 写真の削除機能
  • 読み返しの履歴一覧ページの実装
  • サイト内で読み返し日の通知、しばらく読んでいない書籍の通知

技術面

  • フロントエンドをVue.js + TypeScriptに置き換えていく
  • RSpecのテストのガバレッジを上げていく

苦労したポイント

イデアだし、エレベーターピッチ

エレベーターピッチとは、エレベーターで投資家と乗り合わせたときに、エレベーターの中にいる間に自分が作ろうとしているサービスを投資家に売り込んで興味を持ってもらうという説明のテンプレートのことで。30秒くらいで説明しないといけないのでどうしてもそのアプリの本質を考える必要があります。

また、週1回あるwebサービス進捗報告会で自分の考えたアイデア(エレベーターピッチ)を説明するのですが、ここではいろいなツッコミが入ります。 それは本当に問題解決になるのか?既存のアプリもしくはアナログな手法で解決できるのではないか?と問答する中で自分の思考を整理していきます。

僕の場合は自分で納得できるサービスの形に落とし込むまでかなりの時間がかかりました。

フィヨルドの受講生にアドバイスするとしたら、次の3つです

  1. 早い段階から身の回りの困りごとがWebサービスで解決できないかアンテナを張っておく
  2. 矛盾しますが、アイデアだしの段階では1つのアイデア固執しない
  3. 愛着のあるアイデアでサービスを作る

僕は、自作サービスのプラクティスの1つ前のプラクティス(スクラム開発)が終わったタイミングでアイデア出しを始めたのですが、どうやらこれは遅すぎたみたいです。 コードを書かない時間ができてしまい、焦りから良いアイデアも出ないという悪循環になってしまいました。JSプラクティスあたりで考え始めてもいいかも知れません。

3つ目に関してはモチベーションの問題です。2つ目と矛盾しているんですが、作ると決まったものに関しては愛着がないとしんどいです。2〜3ヶ月程度の長距離走になるので疑問を持ったまま、納得していないまま自作サービスを作り始めると頓挫してしまう可能性が高いです。

3つ目は2つ目と矛盾しているんですが、アイデア出しの段階では質より量で勝負した方がいいかも知れません。1つのアイデアに変に固執してしまうとそのアイデアでは問題解決できないとわかった時に、ひきづってしまったり切り替えられなくなって次のアイデアが出づらいです。また、強行的にそのアプリを作ることにした場合、問題解決できないアプリを作るだけのモチベーションの維持が難しいです。

ダメだったら次!くらいの気持ちで量を出して、メンターの方と問答する中でしっくりくるアイデアを深掘りするのがいいと思います。

画像アップロード Shrine

自作サービスで初めて技術選定を行いました。

  • そのgemを使うと問題解決されるのか
  • そのgemは定期的にメンテナンスされているのか
  • GitHubのスターが多いか

などの観点で選定しました。 Railsの場合画像アップロードはActive Storage Carrierwave PaperClipなど色々なgemがありますが、Shrineというgemを使うことにしました。

shrinerb.com

理由は、上記の選定項目を満たしていたのとプラグイン形式になっており、必要な機能をプラグインとして導入するという拡張性の高さが決め手でした。 エレベーターピッチの段階では必要最低限の機能について検討していましたが、リリース後の機能の追加にも柔軟に対応できると思ったからです。

苦労した点としては、日本語ドキュメントが少なくQiitaなどの記事も少し古くて信頼できるか自分では判断できず、基本的な使い方をマスターするまで時間がかかったことです。

そもそも画像アップロードの仕組みというか、概念みたいなものが理解できておらず英語の公式ドキュメントを読んでもなんのことを言っているのかわからない迷子状態が続きました。

ただ、デモアプリを作りながら少しずつ実験していく中で少しずつ腑に落ちる点が増えていき理解できるようになりました。

全く初見のライブラリを公式ドキュメントを読みながら理解するという実務でも役立ちそうなので良い経験になりました。

Shrineの基本的な使い方に関しては情報が少ないので後日ブログにしようと思っています。

Googleアカウントでの認証 + GoogleカレンダーAPI

今回はアプリ内にある入力フォームからGoogleカレンダーAPIを叩いてGoogleカレンダーに登録するということを実現するため、ログイン部分もGoogleアカウントを使ってログインする方式にしました。

読み返し日設定の入力フォーム

処理の流れとしては以下のようになります。

  1. OmniAuthを使ってGoogleアカウントで認証する
  2. ログイン時にユーザに権限の確認をしアクセストークンを取得
  3. アクセストークンをもとにGoogleカレンダーAPIを叩いてGoogleカレンダーに次読み返す日の設定をする

この処理の流れ以前に、そもそも認証・認可とはなんなのか? GoogleカレンダーAPIをどうやって叩くのか?

といった概念的なところが全く理解できておらず、技術検証に時間がかかりました。 また、途中でOmniAuthとGoogle Authを至る所で混在させたり、アクセストークンも2回取得したりして、手戻りが発生したりしたのでかなり辛かったことを覚えています😅

APIを叩くところに関してもドキュメントにはRuby CLIでの実装方法しか載っておらず、Railsにどう反映させるのかに頭を悩ませました。

解決方法としては、小さなデモアプリをたくさん作り、「仮説→実験→ドキュメント」というのを繰り返していき、ドキュメントに書いてあるコードがどのように動くのか泥臭く実験していくことで理解しました。 また、実装したあとはメンターの方に方向性が間違っていないかペアプロしてもらうことで手戻りが最小限に抑えられたと思います。

こちらに関しても情報が少ないのでなんらかの形でアウトプットできたらと思います。

自作サービスを作っての感想

現時点では自分の実力通りのものができた

イデア出しの段階からリリースに至るまで大小含めて多くの「ああすればよかった、もっとこうすれば」みたいな小さな後悔があります。しかし、現時点で自分の力で調べ、実装し、Webサービスとしてリリースできたことが何よりも嬉しいです。現時点では間違いなく実力通りのものができました。

今後はどんどん実力を上げて改善していきます。

人に頼ることの大切さに気づいた

フィヨルドブートキャンプでは現場で戦力になるために自走力を求められます。 自走力というと自分で調べ解決する力だと思っていたのですが、どうやらそうではないことに気付かされました。

自分が感じた自走力は 自分で調べ、解決しようと仮説をたてやってみる。できなければ誰かに頼る

この、「誰かに頼る」部分も含めてきっちり問題解決していくのが自走力なんじゃないのかなと気付かされました。

自作サービスは正直1人では解決できない問題が多々ありました。しかし、その度にメンターの方にアドバイスをいただいたり、ペアプロしてもらったりして少しずつ解決していきました。

 最後に

イデアだしやペーパープロトタイプの段階からレビューをいただいた@ken_c_loさんエレベータピッチの段階からアドバイスを下さった@machidaさん、コードレビュー全般で適切なアドバイスを下さった@komagataさん、実装に関する疑問からペアプロまでわからないときは何度も頼らせていただいた@cafedomancerさん、Shrineを使った写真の登録でストロングパラメーターのバグで困っていた時にアドバイスいただいた@maedanaさん、エレベーターピッチの相談に乗ってもらった輪読会メンバーに感謝します。ありがとうございました。🙏

お使いいただいた方でお気づきの点があれば、@haruguchiまでフィードバックを頂けたら幸いです。とても喜びます。

UV study: Ruby LT会 Vol.3に登壇しました。

にゃんにゃんにゃん。

いわゆるネコの日にユニークビジョンさんのRubyのLT会に参加しました。

uniquevision.connpass.com

フィヨルドブートキャンプでのLT会は一度経験したことがあるんですが、外部でのLT会ということですごく緊張しました。以下感想を垂れ流します。

haruguchi-yuma.hatenablog.com

LT会で登壇した理由

純粋な気持ちから邪な気持ちまで大小様々な理由のうえ登壇することにしました。これが大人ということか。

  • 普段から輪読会で掘り下げて学習しているので技術ネタをアウトプットする場が欲しかった
  • 技術ネタを発表する責任感からちゃんと1次情報を調べて学習する癖がつくという期待(邪な気持ち)
  • フィヨルドブートキャンプの中でしか活動してないのでRubyコミュニティの方に名前を覚えてもらいたい(邪な気持ち)
  • 就職活動の足がかり、、、(邪な気持ち)

これでは筋斗雲には乗れないですね🌩

それに加えてこのLT会はVol.1-2でフィヨルド生が既に発表していたというのも大かった。やっぱり知っている人が過去に発表しているというのは安心感があります。 先人たちの勇気の上に成り立っている!感謝 🙏

発表内容

で、お前は何を発表したん?という声が聞こえてきそうなのでスライドを投下しておきます。

['a', 'b', 'c'].map(&:upcase) is 何? - Slidev

['a', 'b', 'c'].map(&:upcase)という初心者には摩訶不思議の呪文に見えるこのコードがどういう仕組みで動いているのか?というのを自分なりに調べて発表してみました。

ただ、このコードの動く仕組みというのはQiitaだったりZennだったりとネットの海に五万とあるので少し工夫したところもあります。

それは、自分で作ったコードを紹介したという点です。解説がメインなので自分の作ったコードはおまけなのですが、ずいぶん自分らしくなったと思います。

また、10分という時間制約の中でやむなく削った部分もあります。

  • Proc#callとyieldの違い
  • symbol.to_proc.callの第2引数以降について
  • Procオブジェクトの生成方法について
  • ブロックの中の処理を実行する方法(call以外)

と説明したいことは山ほどあったんですが、主題から逸れる部分は断腸の思いでカットしました。

procオブジェクトやブロックについてはまたわかったことをブログに書ければと思います。

その他

今回登壇以外にもツールで挑戦した部分があります。 それはslidevというマークダウンでスライドを記述するツールを使ったことです。 sli.dev

ドキュメントはid:ikmbearさんにより日本語にも翻訳されています。この日本語ドキュメントには大変お世話になりました。自身のスライドもかなりパクりました参考にさせてもらったのでここで感謝しておきたいと思います🙇

マークダウンで書けるのでスライド製作者はスライドの中身に注力できるというのが売りみたいですが、僕みたいな弱々マークダウナー(造語)には難しすぎた。 とはいえ慣れの部分もあるので何回かこれで書いて登壇を重ねていけばありがたさが実感できるような気がします。

【Ruby】selfキーワードとメソッド呼び出し

selfキーワードとメソッド呼び出しについて学習したのでまとめておきたいと思います。最初に言っておくとめちゃめちゃ長いです。

self

selfとは

るりまの記述を引用

るりま、擬似変数の項

self

現在のメソッドの実行主体。

じ、実行主体?🤔

どういうことでしょうか?インスタンスメソッドを作って試してみます。

class User
  def greeting
    p self
    puts 'hello'
  end
end

taro = User.new

taro.greeting
=> #<User:0x00000001070a48f0> selfの中身
=> hello

ここでの実行主体はtaroになります。

taroはUserクラスでnewされたインスタンスなので、正確には、Userクラスのインスタンス(上の例で言うとコイツ#<User:0x00000001070a48f0>)が実行主体ということになります。

メソッドを呼び出すときはレシーバ.メソッドという形式で呼び出しますが、このレシーバに当たるのが実行主体のようです。

「レシーバ」 =「 実行主体」

インスタンスメソッドとクラスメソッドのself

インスタンスメソッドの実行主体は、当然そのクラスのインスタンスなので、selfの中身はインスタンス自身となります。

では、クラスメソッドはどうでしょうか。確かめてみます。

class User
  # クラスメソッド
  def self.greeting
    p self
    puts 'hello'
  end
end

User.greeting
=> User
=> hello

クラスメソッドの実行主体はUserというクラス自身です。なのでselfの中身もUserということになります。 もちろんOrderというクラスに定義されているクラスメソッドの場合は、Orderとなりますし、Bookというクラスに定義されているクラスメソッドの場合はBookとなりますし、Hogeというクラ...(略)

いろんな場所でselfの中身を確かめてみる

1つ1つ確かめるのは面倒なので一気に確かめてみます。

# トップレベルでのself
p self

class Klass
  # クラス直下でのself
  p self 

  def instance_method # インスタンスメソッド
    p self
  end

  def self.class_method # クラスメソッド
    p self
  end
end

以下を表にまとめるとこんな感じです。

selfの場所 中身
トップレベ main
クラス定義式直下 Klass(クラス)
クラスメソッド定義内 Klass(クラス)
インスタンスメソッド定義内 #\<Klass:0x00007fb063a7ff60>(インスタンス)

mainというのはトップレベルでselfを表すものでp mainなどと参照はできません。

また、トップレベルでのself(main)のクラスを確認すると

self.class
=> Object

となるのでObjectクラスのインスタンスであることはわかります。ここでは難しく考えずに、華麗にスルーしておきます。

この関係性をもとにメソッド呼び出しについて考えてみます。

メソッド呼び出し

前提:レシーバの有無によるメソッド呼び出し

メソッドを呼び出すときってレシーバがある場合とない場合がありますよね?こんな感じです。

# レシーバなしでメソッドputsを呼び出してる
puts 'hello'

# レシーバありでメソッドを呼び出してる
'hello'.upcase

メソッド呼び出しについて「るりま」で調べると以下のような記述がありました。(強調は筆者によるもの)

メソッド呼び出し(super・ブロック付き・yield) (Ruby 3.1 リファレンスマニュアル)

メソッド呼び出し式はレシーバ(`.' の左側の式の値)のメソッドを呼び出します。レシーバが指定されない時は self のメソッドを呼び出します。

つまりレシーバなしで呼び出しているメソッドもそう見えるだけで暗黙的にレシーバ(self)のメソッドが呼ばれているということになります。

以下のコードはどちらでも同じです。

# selfに対してputsを呼び出す(明示的)
self.puts 'hello'

# レシーバ省略してるけど、selfに対してメソッドを呼び出している
puts 'hello'
「レシーバ省略されているときはselfがレシーバ」

以上のことを踏まえながらインスタンスメソッドやクラスメソッドないからメソッドを呼び出すことについて考えていきます。

インスタンスメソッドからインスタンスメソッドを呼び出す

特に意味のあるコードではないですが、問題を単純化するために以下のコードで考えます。インスタンス変数使えよ!とかそういう難しいことは考えません。

class User
  def greeting
    name + 'さん、ごきげんよう!'
  end

  def name
    '太郎'
  end
end

taro = User.new
taro.greeting
=> '太郎さん、ごきげんよう!'

greetingというインスタンスメソッドの中でnameというインスタンスメソッドを呼び出しているコードです。

インスタンスメソッドの中からインスタンスメソッドを呼び出すときはレシーバを省略できます。と習いましたが、ずっとなぜなんだろう?と思っていました。

ここまでのことが理解できると謎が解けます。

まず、さっきの話からレシーバが省略されている場合はselfが隠れています。明示的に記述していきましょう。

class User
  def greeting
    self.name + 'さん、ごきげんよう!'
  end
  
  def name
    '太郎'
  end
end

インスタンスメソッドはそのクラスのインスタンスがレシーバでないといけませんが、*1 ここでのselfはそのクラスのインスタンス自身だからインスタンスメソッドを呼び出せているということになります。

なので、クラスの外でself.nameまたは単にnameとすると(selfはmainなので)メソッドは呼び出せません。

class User
・・・略・・・
  def name
    '太郎'
  end
end

self.name => エラー # selfはObjectクラスのインスタンス
name => エラー

インスタンスメソッド内でインスタンスメソッドを呼び出すときにレシーバが必要ないのはインスタンスメソッド内のselfがインスタンス自身だからということになります。(インスタンス言い過ぎ)

インスタンスメソッドからクラスメソッドを呼び出す

以下のコードはダメな例です。

class User
  # これではクラスメソッドを呼び出せない
  def greeting
    name + 'さん、ごきげんよう!' ・・・(☆)
  end

  # クラスメソッド
  def self.name
    '太郎'
  end
end

クラスメソッドを呼び出す場合はクラス.メソッドという形になっていないとダメですがここでは(self.)name(☆のところ)の省略されているレシーバselfはインスタンス自身なのでクラスメソッドは呼び出せません。

呼び出す方法としては以下のような方法があります。

class User
  def greeting
    User.name # クラスを明示する
    self.class.name # self.class はUserとなる
    class.name # selfは省略できる
  end

  def self.name
    '太郎'
  end
end

どんどんいきましょう。

クラスメソッドからインスタンスメソッドを呼び出す

またもやダメなコード例から見てみましょう。

class User
  # これはではインスタンスメソッドを呼び出せない
  def self.greeting
    name + 'さん、ごきげんよう!'
  end

  def name
    '太郎'
  end
end

nameメソッドをレシーバなしで呼び出した場合、省略されているレシーバであるselfはクラス自身(User)になるのでクラスメソッドのnameが呼ばれることになります。

インスタンスメソッドのnameを呼び出すにはレシーバをインスタンスにする必要があるので以下のようになります。

class User

  def self.greeting
    User.new.name # ユーザークラスのインスタンスを生成してからnameメソッドを呼び出す(明示的)
    self.new.name # Userをselfにした
    new.name # selfは省略できる
  end

  def name
    '太郎'
  end
end

次!

クラスメソッドからクラスメソッドを呼び出す。

クラスメソッド内のselfはクラス自身なのでレシーバを省略して普通にメソッドを呼び出せます。

class User
  def self.greeting
     name + 'さん、ごきげんよう!'
  end

  def self.name
    '太郎'
  end
end

まとめ

  • selfとはメソッドの実行主体のこと
  • 実行主体とはレシーバ.メソッドのレシーバのこと
  • レシーバが省略されているメソッドはselfに対してメソッドを呼び出す。
  • 逆に言えばselfに対して呼び出せるメソッドはselfを両略できる
  • インスタンスメソッドのレシーバはそのクラスのインスタンスでないといけない(継承関係除く)
  • クラスメソッドのレシーバはそのクラスのクラス自身でないといけない

こんなに長い文章を読んでくださってありがとうございます。 読み飛ばして最後だけ読んでくれた方もありがとうございます。

*1:継承関係などは難しいので考慮してません

【Ruby】ミュータブルとイミュータブル

フィヨルドブートキャンプでチェリー本の第1版の輪読会をやっているんですが、その時出てきた「ミュータブル」と「イミュータブル」について誤解していたのでまとめたいと思います。

ミュータブル・イミュータブルとは

Rubyのオブジェクトは「ミュータブルなオブジェクト」か「イミュータブルなオブジェクト」の2種類に分けられる。

じゃあミュータブル・イミュータブルってなんやねん、という話。

  • ミュータブル:破壊的な変更が可能なオブジェクト

  • イミュータブル:破壊的な変更ができない(そもそも破壊的メソッドが定義されていない)オブジェクト

ミュータブルなオブジェクトの例

# 文字列(String)
str = 'hello'
str.upcase! #=> 'HELLO'
str #=> 'HELLO'

# 配列(Array)
ary = []
ary.push(1, 2, 3) # => [1, 2, 3]
ary # => [1, 2, 3]

# 配列(Hash)
hash = {a: 1, b: 2}
hash.merge!({c: 3}) #=> {a: 1, b: 2, c: 3}
hash#=> {a: 1, b: 2, c: 3}

ミュータブルなオブジェクトは生成するたびに異なるオブジェクトになる

ミュータブルなオブジェクトは、例え同じ文字列でも生成するたびに異なるオブジェクトとなります。 同一オブジェクトかどうかはオブジェクトIDを確かめるとわかります。

なおオブジェクトIDは実行環境で変わります。

# 同じ文字列でも生成するたびに異なるオブジェクト
'aaa'.object_id
=> 63180

'aaa'.object_id
=> 65580

'aaa'.object_id
=> 67980

異なるオブジェクトなのでそれぞれを別々の変数に代入し破壊的な変更を加えても、お互いに影響がありません。

str1 = 'aaa'
str2 = 'aaa'

str1.upcase! #=> 'AAA'
str2 #=> 'aaa'

str1.object_id == str2.object_id
#=> false

ミュータブルなオブジェクトの注意点

次のようにするとどちらの変数も同じオブジェクトを参照しているため破壊的変更を加えるとどちらも変更されてしまいます。

str1 = 'aaa'

str2 = str1
str2 # => 'aaa'

str2.upcase! #=> 'AAA'
str1 => 'AAA'

# str1もstr2も同じ'aaa'というオブジェクトを参照している
str1.oject_id == str2.object_id
#=> true

イミュータブルなオブジェクトの例

Integer, Floatなどの数値やSymbolなどがイミュータブルなオブジェクトに当たります。 一意であることを担保されているため、同じ値であればオブジェクトIDは常に同じです。

# 数値(Integer, Float)
1.object_id
=> 3
1.object_id
=> 3
1.object_id
=> 3

(1.4).object_id
=> -21617278211378382
(1.4).object_id
=> -21617278211378382
(1.4).object_id
=> -21617278211378382


# シンボル(Symbol)
:a.object_id
=> 808668
:a.object_id
=> 808668
:a.object_id
=> 808668

イミュータブルなオブジェクトはデフォルトでfreezeされている

ミュータブルなオブジェクトに対してfreezeメソッドを使うと破壊的変更ができなくなります。

一方、イミュータブルなオブジェクトはデフォルトでfreezeされています。(当たり前か、、、)

# ミュータブル
str = 'aaa'
str.frozen
#=> false

str.freeze
str.frozen?
#=> true

str.upcase!
#=> `upcase!': can't modify frozen String: "aaa" (FrozenError)

# イミュータブル
num = 1
num.frozen?
#=> true

ちなみに

「破壊的変更ができないこと」と、「変数に再代入できないこと」は別物です。ここを混同して勘違いしていました。 freezeしても「破壊的変更ができない」だけで再代入して参照するオブジェクトが変わってしまったら破壊的変更が加えられます。

僕は「再代入」を破壊的変更だと思い込んでいたので、そもそも再代入できないのでは?と誤解していました。

str = 'aaa'
str.freeze

# freezeしたので破壊的変更は加えられない
str.upcase! 
#=> `upcase!': can't modify frozen String: "aaa" (FrozenError)

str = 'aaa' # 異なるオブジェクトを再代入する
str.upcase!
#=> 'AAA'

参考

るりま:

2021年っていつだっけ?

2021年ももうすぐ終わるということで、そろそろ今年の振り返りブログを書いていきたいと思います!!!!

みたいな感じで2021年やり残した「今年の振り返りブログ」をあたかも現在が2021年の年の瀬であるかのように振る舞いながら振り返っていきます。

プログラミングのTipsみたいな話は一切出てこないし、全体的にプライベートな(プログラミング学習の)話しか書かないので悪しからず。

ではスタート!

1月

今年の1月はまだ学校で働いていました。

3年生担任ということもあり受験のことで頭がいっぱい。コロナ禍で受験対応が日に日に変わっていき右往左往していた気がする。

退職の話は去年から進めていたんですが、特殊な手続きを踏んで退職する都合上、膨大な書類の山を処理する羽目に。しんどかったな〜。

プライベートでは「めかぶ」にハマって晩御飯は一生めかぶ食べていた気がする。本当に食生活は最悪レベル。

2月

教室の雰囲気が混沌と化す時期。

スポーツ推薦や特色選抜で合格者が出始め、どこか気が抜けた生徒とピリピリする生徒が混ざり起きる現象。まぁ、毎年の風物詩。 幸い、生徒指導事案などはあまり起こらず、毎日誰かと教育相談をするくらいで乗り切れた。

部活動に関しては自分が主催する最後の大会がありました。2、3年前から中体連の専門委員になって土日も大体働いていたのでひと段落ついたことになる。

また、仕事が忙しくてストップしていたプログラミング学習は4月くらいになるだろうな〜と目星を付け出したのはこの頃。

3月

受験後の進路指導。引き継ぎ。卒業。退職。目の回る毎日。

離任式は中止だったにも関わらず、たくさんの生徒や保護者が駆けつけてくれたのはいい思い出。 8年間の教員生活は終わり、無職生活の突入。

果たしてなけなしの貯金で生活していけるのか!この頃は不安でいっぱいでした。今も不安だけど。

4月

あれ?仕事辞めたはずなのに、電話がじゃんじゃんかかってくる。

「あれってどうやっけ?」

「〇〇に書いときました!(怒)」

みたいなやりとりも多く、ちょっと怒ってた。

フィヨルドブートキャンプ再開。 前回は「Railsラクティス」まで進んでたけど、記憶喪失状態だったので大人しくHTMLのプラクティから始めることに。

最初は遠回りかなと思ったけど、結果的には一からやり直して知識が固まった気がするので大正解!!

久しぶりに再開したフィヨルドブートキャンプは結構変わっていて、受講者同士の交流が多くなってた。

この頃は散歩にハマっていて毎日神社の🐢を見るのが唯一の生きがいでした。

5月

ラクティスはSQL,DB設計,shinatraあたり。

DB設計では書籍『楽々ERDレッスン』を4回くらい通しで読んだことになるので、1回目と比べて理解度が上がったことに感動。

プライベートでは引越しの準備をしていたため、土日は家探しという感じ。 そして、引っ越すために断捨離を始めたのもこの頃から。本多すぎ!!

6月

ラクティスはRailsに突入。 まさか2ヶ月ちょっとで戻ってこれるとは思ってなかったのでびっくりした。反復こそ学習。

6月は新居が決定。引っ越しをして、同居生活が始まる。

同居生活1日目。前の家から持ってきてた冷蔵庫が壊れる。

同居生活2日目、オーブントースターが壊れる。

極めて順調な滑り出し。

もともと一人っ子で(生き別れた兄がいるけど笑)、10年以上1人暮らしをしていたので、人と住むなんてできるのか?と思ってたけど、大学生の時は寮生活をしていて、プライベートなんてないところで4年も生活していたことを思い出した。基本的にどこでも寝られるし、どこでも生きていける体質です。

この頃は昼ごはんにブロッコリーを食べることにハマっていて、ひたすらブロッコリーを食べていたらブロッコリー禁止令が出ました。

7月

ラクティスはRails,自動テスト,Rubyオブジェクト指向

ここら辺からRubyに対しての理解が1段階上がってかなり見通し良くなった。 忘れないうちにRubyのアウトプットしたいなーとDiscordの分報チャンネルで呟いていたらPaizaやAtCoder、をお薦めしてもらったり。

あと、伊藤さんからアウトプットの記事を紹介してもらったので、まずはそれからやることに。

blog.jnito.com

で、アウトプットしたことを発信したいという思いからブログを始める。

オブジェクト指向でなぜつくるのか』の輪読会も7月末から始まったので参加。

引っ越ししたのが落ち着いてコミュニティを意識し出したのがこの頃なのかもしれない。

隣の家が燃えた。

8月

ラクティスはJS。 カレンダーアプリやメモアプリを作成した。

Ruby以外の言語を学ぶのは初めてでめちゃくちゃ楽しかった。ただ、非同期処理!お前は別だ!

2つ目の輪読会であるRuby本(Ruby超入門とチェリー本)輪読会に参加。 今も続いてるくらいずっとやってる輪読会ですが、平日毎日やっているので自然と仲良くなれて、しんどい時などに支えてもらってます。ほんと、感謝しかない🙏

9月

ラクティスはnpm作成、Vue.js、アジャイルについての学習

AtCoderをはじめてみる。A問題やB問題を中心にやってましたが、B問題ですら難しいという現実。 Rubyの便利メソッドをたくさん知るきっかけになったのでよしとする。

退職してからは引きこもりを極めているので、自分の身に何も起きない。面白いことが全く起きない。散歩行った時に無駄に子供に絡まれるくらいで、ほんとにプログラミングしかしてないから何も起きない。プログラミングではエラーばっかり起きるのに。

リングフィットアドベンチャーをやらなくなった。

10月

ラクティスはチーム開発(スクラム開発)

現場Rails輪読会にRubyの章が終わるくらいから参加し始めた。これは厳密には9月から参加したかもしれないし、10月だったかもしれない。ただ、これくらいの時期。 現在も輪読会はやってるけど、初めからいましたよ感を出して参加してる輪読会の一つ。この輪読会のいいところはモブプロ形式で1つのアプリを作り上げていくので必然的にGitに詳しくなれるところ。

LT会初登壇。

大した発表はしてないけど、結果的にやってよかったなぁと思います。少しずつ自分の気持ちをアウトプットすることに慣れてきた気がする。

初モブプロ依頼。チーム開発で依頼されたコードレビューで全然わからなかったので依頼した。

それを皮切りに他の受講生の方とも生徒同士でモブプロしたり、少しずつモブプロ楽しい気持ちが芽生えてきた。 チーム開発でわからないことがあっても音声チャットで相談しながらモブプロしたり。今まで1人でもくもくと作業してきたので誰かと作業することが楽しかったのかもしれない。

教える・教わるのモブプロも楽けど、1つの問題に対してどんどん実装していくモブプロも楽しいことに気づく。

そのきっかけは@ikuma-tさん主催のこのモブプロに参加してからかな。

github.com

実家の冷蔵庫が壊れる。

11月

ラクティスはチーム開発、自作サービスを考え始める

最後のIssueがVueが絡む重めのIssueだったので結構時間かかったけど楽しかった。

github.com

そして、先月のモブプロの体験を活かして自分でもモブプロを主催してみることに。 https://github.com/haruguchi-yuma/janken

全2回のつもりをしてたんだけど、実は2回目は開催されていません。

輪読会メンバーでモブプロしたんですが、自作サービスが落ち着いたら考えようと思ってるうちに、メンバーの入れ替わりや、環境の変化、あとは自作サービスが落ち着くっていつだ問題。つまり自分の怠惰が原因で開催されてないので、どういう形かはわからないけど一旦蹴りをつけないといけないなという思い。

12月

ラクティスは自作サービス

イデア出しの段階でかなりつまづいて、結構辛かった記憶が、、、

ただ、アイデアだしで困っている人に一つアドバイスするとしたら的確なアドバイスがある。

それは、自作サービス進捗報告会に出ること。進捗なくてもいいし、アイデアが固まってなくてもいい。とにかく、今どんなこと考えているか話すだけでもいろいろアドバイスをいただけるのでおすすめ。

あとはアドベントカレンダーを書いたり伊藤さん主催の点字メーカープログラムに挑戦してみたりした。 このプログラムがTop Contributor賞を取るなんてこの頃は知らない。(未来予知)

まとめ

そんな感じで1年を振り返ってみましたが、ほとんどプログラミング漬けの毎日なのでプログラミングのことしか書くことがないことに気づきました。

来年の目標は「就職」なのでこれまたプログラミングですね。

あとは就職できてコロナが終息してたら旅行に行きたいです。

それではみなさん良いお年を。来年もよろしくお願いします。🙇‍♂️

困ったときはターミナルへ出力だ!プリントデバッグ入門

こんにちはフィヨルドブートキャンプで学習中のはるぐちです。

この記事はフィヨルドブートキャンプ Part 1 Advent Calendar 2021の14日目の記事になります。 前回はmasuyama13さんの「コードより先にコミットメッセージを書く」でした。

techblog.tebiki.co.jp

また、フィヨルドブートキャンプ Part 2 Advent Calendar 2021もあります。

対象読者

  • プログラミングを始めたばかりのあなた
  • フィヨルドブートキャンプ生
  • あの頃の自分

プログラミング経験がある人からすると「こんなん知ってるわ!!」となること間違いなしですが、過去の自分が知りたかったことをまとめた「超初心者向け」の内容ですので温かい眼差しで、目を細めて読んでいただけたら幸いです。

はじめに

「あれ?なんで動かないんだろ?」

プログラムを実行しようとすると必ずといっていいほど遭遇する(プログラムが思い通りに動かない)バグ。

僕もプログラミングを学習し始めた頃は、動かないプログラムに四苦八苦しました。(今でも)

英語で現れる読めないエラー。1文字のタイポのせいで空虚に消え去る1時間。変数の中身がnilだった時の絶望。「あぁ、無理だ。もう質問しようかなー」と分からないところを整理し始めた時に発見する解決方法。「絶対自分は間違ってないし」と強気に何度も実行するも「絶対的に自分が間違っている」現状。

いやですよね。プログラムが動かないのって。かっこよく1発で思い通りにプログラムを実行したいですよね。

安心してください!!1発でプログラムを動かそうなんてまぁ無理です!!

僕は2年間プログラミングを学習して(当初よりは)それなりにプログラムが書けるようになってきたとは思うんですが、プログラムは一向に動きません。ほとんど動きません。マジで全然動きません。たまに動きます。動くと可愛いです。

ということで、エラーを1回も吐かずに1発で実行できるプログラムを書くというのは土台無理な話なんですが、「プログラムが動かなくなった時に素早く原因を特定し、問題を解決できるようになる。」なら可能な気がします。そして、僕も、最初と比べたら問題解決までの時間が早くなった気がします。

この記事の方向性

プログラムが想定通り動かない時、原因を突き止め改善する作業ををデバッグといいます。デバッグをする手順としては以下のような手順が一般的です。

  1. プログラムが動かない、憤る
  2. エラーが出てないか確認する
  3. エラー文を見て怪しそうなところを調べる
  4. 怪しそうなところの変数やメソッドの戻り値が期待通りかどうか調べる
  5. 動く、喜ぶ

このようにデバッグをするときは、まずはエラー文を読め!ということになるんですが、この記事ではエラー文という謎の呪文をすっ飛ばして、とにかくターミナルに必要な情報を表示させるということに主眼を起きたいと思います。また、デバッグをするときはdebuggerなどの便利なツールもあるのですが、まずは最初に覚えたい「プリントデバッグ」という方法についてまとめていけたらなあと思います。

この方法を取得すると、エラーが出たときのみならず、コードを読むときに「この変数の中身なんだろう?」「このメソッドの戻り値点だっけ?」となった時も役立つと思います。

前置きが長くなりましたが、よろしくお願います。

ターミナルに出力して確かめる

メソッドを実行したときの戻り値や、変数の中身を知りたい時は「ターミナル(正確には標準出力)に出力するメソッド」を用います。次のコードを見てください。

# debug.rbというファイルに保存
numbers = [1, 2, 3, 4, 5]
even_numbers = numbers.select{ |num| num.even? }

ローカル変数even_numbersの中身に何が入っているのか知りたくなったとしましょう。(例が単純すぎて予測できますが、複雑なメソッドチェーンなどになっているときは分かりにくいですよね?ね?) この場合、pメソッドを使うとローカル変数even_numbersをターミナルに出力すことができます。

numbers = [1, 2, 3, 4, 5]

even_numbers = numbers.select{ |num| num.even? }
p even_numbers #デバッグのために追加

実行して確かめましょう

$ ruby debug.rb
[2, 4]

上の実行結果からeven_numbersは2と4というIntegerクラスのオブジェクトが入った配列であることがわかります。 ターミナル(標準出力)に出力するメソッドは他にもputsprintメソッドがありますが、それぞれ以下のような違いがあります。

p even_numbers
puts even_numbers
print even_numbers
$ ruby debug.rb
[2, 4]  # pメソッド(最後に改行されている)
2  # putsメソッド↓(配列の各要素が改行されている)
4
[2, 4] $ #printメソッド(改行されずにプロンプトが同一行にある)
メソッド 簡単な違い
p 配列の各要素を改行せずに表示し,全て出力した後で最後に改行が入る。データ構造(ここでは配列)がわかるので開発者にとって便利な情報が表示されている
puts 配列の各要素を改行して表示する。また、全て出力した後にも改行が入る。
print 1行で出力するため改行はない。上記では入力を受け付けるシェルのプロンプト$が同じ行に表示されているとこがわかりる。

色々違いがあることはわかったけど、どう使い分けるの?という疑問が湧いてくるかもしれません。 結論からいうとpメソッドは開発者にとって有益な情報を返してくれるので、基本的にはpメソッドを用いていれば大丈夫です。

pメソッドを使うとき注意点(結合度の違いに注意)

先ほどのコードを少し書き方を変えてpメソッドを使って式の評価結果を出力します。

numbers = [1, 2, 3, 4, 5]

# 例1
p numbers.select do |num|
  num.even?
end
=> #<Enumerator: [1, 2, 3, 4, 5]:select>

# 例2
p numbers.select do |num| num.even? end # 1行で書いてみたが、、、
=> #<Enumerator: [1, 2, 3, 4, 5]:select>

# 例3
p numbers.select{ |num| num.even? }
=> [2, 4]

do ~ endで記述(例1、2)したときと、{ ~ }で記述(例3)したときで出力結果が変わっています。どちらかというと{}で囲んだ時に出力されている情報が欲しいはずです。

つまり、上2つの実行ではnumbers.select do ~ endという式に対してpメソッドを用いて出力しようとしていますが、思っている通りの結果が返ってきません。

どうしてでしょうか?

これは {}よりdo ~ endの方が結合の度合いが強く、プログラムが以下のような解釈をしてしまっているせいです。

   ↓第一引数           ↓pメソッドに渡しているブロック
p(numbers.select) do |num| num.even? end
  • p メソッドの第一引数がnumbers.select
  • pメソッドにブロックを渡している(do ~ endの間)

このようなことを防ぐには最初に示した例のように、まず変数に代入し、その変数に対してpメソッドを使います。

even_numbers = 
  numbers.select do |num|
    num.even?
  end

p even_numbers
=> [2, 4]

メソッドチェーンの途中の結果を確認したい

メソッドをつなげて少し複雑な処理を1行でやってる(メソッドチェーン)場合はデバッグが面倒になります。 例えば、以下の例を見てみましょう。

names = ['sasaki', 'yamada', 'suzuki']

# メソッドをつなげて少し複雑な処理をしている
names.map{ |name| name + '!' }.map(&:upcase).sort.reverse

# 見やすく整えるとこんな感じ
names
  .map{ |name| name + '!' }
  .map(&:upcase)
  .sort
  .reverse
=> ["YAMADA!", "SUZUKI!", "SASAKI!"]

次の4つのことを行っています。

  1. 名前に!という文字列を連結
  2. 大文字にする
  3. 名前を昇順に並び替える
  4. 名前を降順に並び替える

このコードは正しくプログラムが動くためデバッグする必要はないのですが、もしプログラムが動かなかった場合どこのメソッドまでの処理が正しくて、どこのメソッドからの処理が間違っているのか問題の切り分けが難しい場合があります。

そんなとき、tapメソッドを用いるとメソッドチェーンの途中を確認するのに便利です。

names = ['sasaki', 'yamada', 'suzuki']
names
  .map{ |name| name + '!' }.tap{ |x| p x } # tapメソッドを追加
  .map(&:upcase)
  .sort.tap{ |x| p x } # tapメソッドを追加
  .reverse

=> ["sasaki!", "yamada!", "suzuki!"] # map{ |name| name + '!' }までの処理結果
=> ["SASAKI!", "SUZUKI!", "YAMADA!"] # sortまでの処理結果

るりまによるとtapメソッドは以下のように書いてあります。

self を引数としてブロックを評価し、self を返します。 メソッドチェインの途中で直ちに操作結果を表示するためにメソッドチェインに "入り込む" ことが、このメソッドの主目的です。
https://docs.ruby-lang.org/ja/latest/method/Object/i/tap.html

メソッドチェーンで処理が連なっているときはぜひtapメソッドを使ってみてください。

プリントデバッグしたけど見にくいなー

次のコードをみてください。

users = [ {id: 1, name: '太郎', address: '東京', age: 30 }, {id: 2, name: '次郎', address: '大阪', age: 20 }, {id: 3, name: '三郎', address: '名古屋', age: 10} ]

配列の中にハッシュが入っているなど、データが入れ子になっているとき、普通にデバッグしても見づらくよくわからない時があります。そんなときはppメソッドを使うと便利です。

p users
=> [{:id=>1, :name=>"太郎", :address=>"東京", :age=>30}, {:id=>2, :name=>"次郎", :address=>"大阪", :age=>20}, {:id=>3, :name=>"三郎", :address=>"名古屋", :age=>10}]

# 適度に整形して出力される
pp users
=> [{:id=>1, :name=>"太郎", :address=>"東京", :age=>30},
=> {:id=>2, :name=>"次郎", :address=>"大阪", :age=>20},
=> {:id=>3, :name=>"三郎", :address=>"名古屋", :age=>10}]

ppメソッドが見やすいように整形してくれていることがわかります。

【番外編】プリントデバッグ地獄に立ち向かう

プリントデバッグに慣れてくると、ある病を患ってしまいます。そう、「なんでもとりあえずpつけておく病」です。

この病は深刻で当時の僕をかなり苦しめました。pメソッドを使いすぎて本当に知りたい情報が埋もれてしまうのです。(不必要なpメソッドは消せばいいのですが、、、)

そんな時に便利なのが次の方法です。以下のコードはとりあえず用意したコードなので、意味がわからなくても大丈夫です。

def dot(moras)
  moras.map do |mora|
    p square = Array.new(6) { '-' }
    first_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverse
    p first_char
    p second_char
    p FIRST_CONVERSION_TABLE[first_char]&.each { |i| square[i] = 'o' }
    p SECOND_CONVERSION_TABLE[second_char]&.each { |i| square[i] = 'o' }
    p square
  end
end

これは、、、やってしまっています。pメソッドが多すぎてプリントデバッグ地獄が起こっています。

例えば変数squareの中身が知りたいとき装飾をつけることで目立たせることができます。

def dot(moras)
  moras.map do |mora|
    p square = Array.new(6) { '-' }
    first_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverse
    p first_char
    p second_char
    p FIRST_CONVERSION_TABLE[first_char]&.each { |i| square[i] = 'o' }
    p SECOND_CONVERSION_TABLE[second_char]&.each { |i| square[i] = 'o' }
    p '=' * 30  # 装飾追加
    p square
    p '=' * 30 # 装飾追加
  end
end

'====== ...'で挟み込んでいるので欲しい情報がわかりやすくなりました。

$ ruby sample_code.rb
["-", "-", "-", "-", "-", "-"]
"A"
nil
[0]
nil
"=============================="
["o", "-", "-", "-", "-", "-"]  # 本当に知りたかったsquareの中身
"=============================="
["-", "-", "-", "-", "-", "-"]
"A"
"K"
[0]
[5]
"=============================="
["o", "-", "-", "-", "-", "o"]  # 本当に知りたかったsquareの中身
"=============================="

とはいえ、不必要になったpメソッドはこまめに消しておきましょう。

【番外編】紛れ込むpメソッドを探すために。

一生懸命デバッグをしプログラムが思い通りの挙動をするようになりました。あとは提出するだけなのですが、デバッグ用に差し込んだpメソッドがどうやら紛れ込んでいるみたいです。 VScodeなどのIDEを使っているなら検索が使えますので、検索をして探し出しましょう。ここではVScodeので探してみます。

class Product
  TAX_RATE = 1.1
  
  attr_accessor :name, :price
  
  def total_price
    p price * TAX_RATE
  end

  def format_price
    "#{total_price}"
  end
end

上のコードからpメソッドを探し出したいとします。見たらわかりますが何百行もあると思ってください。検索をかけるにはVScodecommand + fを押します。

f:id:haruguchi_yuma:20211214100740p:plain

なんとpとつく単語が7つも出てきました。

そんなときは正規表現を使って\bp\bと検索してください。そのとき、正規表現を有効にするために.*のマークをonにします。

f:id:haruguchi_yuma:20211214100923p:plain

\bは単語の境界を表すメタ文字(特殊な役割の文字)でpだけにマッチするようになります。

最後に

いかがでしたでしょうか?プログラムが動かないとき、つい頭で原因を探ってしまいがちですが、実際に1つ1つプログラムの実行結果を調べることで解決に近づくのでぜひ使ってみてください!

改訂版・チェリー本発売記念 点字メーカープログラムに挑戦してみた!(Advent Calendar 2021/Qiita主催)

フィヨルドブートキャンプで学習中のはるぐちです。

伊藤淳一(@jnchito)さん著『プロを目指す人のためのRuby超入門/技術評論社』(通称チェリー本)の改訂2版が発売されたということでRubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念-で出題されている「点字メーカープログラム」に挑戦してみました。アドベントカレンダー12月6日担当です。 qiita.com

こういう企画に参加するのは初めてだったのですが、

  • チェリー本に育てられたといっても過言ではないので発売記念イベントを是非盛り上げたい
  • 自分の書いたコードを他人に見てもらうのは良い学習になる
  • 他の人の書いたコードを読むのは良い学習になる

という思いから参加してみました。 よろしくお願いします!!!

プログラム作成前に考えたこと

仕様の確認

今回の点字メーカープログラムでは入力された文字列に対して点字のフォーマット(3×2)に合わせた文字列で出力するというものです。 入力値は訓令式のローマ字で構成される文字列で、1音ずつ半角スペースで区切られています。また、日本語音節でいう清音(あ行、か行、…、ら行、わ行)から「ゐ」、「ゑ」、「を」を除いた45音で考えるということになっています。つまり、濁音や促音、長音といった清音以外は考慮不要になっています。

例) A HI RU SI MA U MA KI RI Nなど

また、文字の間にスペースが空いてないなどの異常系の入力は考慮不要になっています。(よかった。)

ということでこれらの入力を想定した最低限動くプログラムを作成することにしました。というか、長音とか促音とか入ってくると自分の力では解ききれない可能性が😅

方針を立てる

プログラムの流れを整理してざっくりと方針を立てます。

  1. 'A HI RU'などを['A' , 'HI', 'RU']と半角スペースで区切って配列に分解する。
  2. 配列をループさせながら何らかの変換ロジックに従って変換する
    • 1音はNを除き「母音」+「0以上の子音」の組み合わせになっているのでそれぞれを分解する。
    • や行を除いて母音と子音で変換が干渉することはないのでそれぞれの変換を重ね掛けする。(最大2回変換する)
  3. 点字のフォーマット(3×2)に合わせて出力する

実際の作成物もおおよそこのような形で作成することができました。

実際に作ったものはこちら

GitHub PR https://github.com/JunichiIto/tenji-maker-challenge/pull/15

ロジックの解説

メインメソッド

メインメソッドから見ていきましょう。メインメソッドはto_tenjiというメソッドになっています。

def to_tenji(text)
  moras = text.split(' ') #方針立て1
  squares = dot(moras) #方針立て2
  format_tenji(squares) #方針立て3
end

メインメソッドの中身は作成前に立てた方針と同じような作りになっています。

まず、入力された文字列をString#splitを使って半角スペースで区切って1音ずつ配列に格納しています。変数名は音節を表す「モーラ(mora)」の複数形です。

次に、TenjiMaker#dotという点を打つ変換メソッド(後述)を作って変換しています。 戻り値は二重配列になっていて例えばA HI RUであれば次のような戻り値になっています。

# dot(['A' , 'HI', 'RU'])の戻り値
[
  ["o", "-", "-", "-", "-", "-"], # A
  ["o", "o", "o", "-", "-", "o"], # HI
  ["o", "-", "-", "o", "o", "-"] #RU
]

最後にそれらをフォーマットするメソッドを用いて出力しています。(後述)

ということで、点を打つ変換メソッドの中身を見ていきましょう。

変換ロジック(dotメソッド)

点を打つ場所についての考え方

点を打つ場所を考えるために下のサイトを元に(3×2)の場所について考えました。

全視情協:点字とは – 点字のしくみによると

点字は、縦3点、横2点の6点の組み合わせで作られています。そして、この単位をマスと言います。下の図をみてください。この図の①②④の点を組み合わせて母音を表し③⑤⑥の点を組み合わせて子音を表します。

① ④

② ⑤

③ ⑥

とあります。

つまり、点字のマスは縦に1、2、3…と番号が付けられていることがわかります。 正直、配列の操作のしやすさだけでいうと横に1、2…といった方が操作しやすいとような気がしてここはどうするか迷いました。(下参照)

# 配列で操作するならこっちの方がいいけど、、、
① ②

③ ④

⑤ ⑥

これはデータ構造をプログラムの都合に合わせるか、現実の点字のしくみに合わせるかというトレードオフになっていると思うのですが、仕事としてプログラムを書いたことのない自分にはどっちに寄せるべきか判断できなかったので以下のように基準を作りました。

  • できるだけ、現実の仕組みに合わせる
  • できるだけシンプルに表現する

できるだけ。というところで妥協し前者の方法(縦に番号をつけていく)で番号づけを行いながら配列操作のため番号を⓪番から始めるということを落とし所にしました。 つまり下の図のようになります。

# 現実の点字の仕組みに即しつつ、配列操作のため0番からはじめる
⓪ ③

① ④

② ⑤

そして、全視情協:点字とは – 点字のしくみを参考に各音と点を打つ場所(インデックス)を対応づけるテーブルを作りました。

FIRST_CONVERSION_TABLE = {
  'A' => [0],
  'I' => [0, 1],
  'U' => [0, 3],
  'E' => [0, 1, 3],
  'O' => [1, 3],
  'N' => [2, 4, 5],
  'Y' => [3]
}.freeze

SECOND_CONVERSION_TABLE = {
  'K' => [5],
  'S' => [4, 5],
  'T' => [2, 4],
  'N' => [2],
  'H' => [2, 5],
  'M' => [2, 4, 5],
  'R' => [4],
  'A' => [2],
  'U' => [2, 5],
  'O' => [2, 4]
}.freeze

このテーブルをよく見るとA,U,O,Nがどちらのテーブルにも重複して存在していますが、これは「ん」と「な行」でNの変換位置が異なることや「や行」が母音の変換位置が変わることを考慮し、役割に応じてそれぞれのテーブルに分けています。こうすることによってif分での複雑な条件分岐を避けることができました。

1音ずつループを回して打点していく

点を打つロジックはこのようになっています。

def dot(moras)
  moras.map do |mora|
    square = Array.new(6) { '-' }
    first_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverse

    FIRST_CONVERSION_TABLE[first_char]&.each { |i| square[i] = 'o' }
    SECOND_CONVERSION_TABLE[second_char]&.each { |i| square[i] = 'o' }
    square
  end
end

このコード部分はmapメソッドで1音ずつ取り出し、点を打って配列にに格納するという作業を行なっています。

Array.new(6) { '-' }で基本となる1マス分の配列を作成しています。

[ '-', '-', '-', '-', '-', '-']`

ブロックを渡さずにArray.new(6, '-')とすると'-'という要素が同一オブジェクトになり、1つ変更すると全ての'-'が変更になってしまうので注意が必要です。

ポイントはfirst_char, second_char = mora.start_with?(/[WY]/) ? mora.chars : mora.chars.reverseの部分でString#charsで1音を1文字に分解するのですが、

  • 「わ行」や「や行」が入っている場合はA U Oといった母音はSECOND_CONVERSION_TABLEで変換してほしい
  • それ以外の音はA I U E Oといった母音はFIRST_COVERSION_TABEで変換してほしい

という比較的簡単な条件分岐になっています。

どのテーブルで変換するかを仕分けした後は、それぞれEnumarable#eachをつかって該当のインデックスに点(ここでは文字列oですが)を打っていきます。最後にループの内部で打点したsquaresという配列を返しています。

フォーマット部分

フォーマット部分は以下のようになっています。

def format_tenji(squares)
  transposed_squares = squares.map { |square| square.each_slice(3).to_a.transpose.map(&:join) }
  transposed_squares.transpose.map { |rows| rows.join(' ') }.join("\n")
end

もしかしたらかなり読みにくいかもしれません。😅

メソッド1行目に関して squares.map { |square| square.each_slice(3).to_a.transpose.map(&:join) }は縦に番号をつけた関係で、まず配列を2分割して行と列を入れ替える作業を行なっています。その後、内部の配列を結合して行ごとの配列にしていいます。

1音に対するフォーマットのイメージ図は以下の通りです。

# each_slice(3)3点ずつ2つに分ける
[[ 0, 1, 2] [3, 4, 5]]

# transposeで行列の置換

[
  [0, 3],
  [1, 4], 
  [2, 5]
],

# map(&:join)で内側の要素を結合する
[
  [03], # 1行目
  [14], # 2行目
  [25] # 3行目
]

メソッドの2行目 transposed_squares.transpose.map { |rows| rows.join(' ') }.join("\n")に関しては各音の1行目、2行目、3行目を揃えるためにtransposeして内部の要素を半角スペースで結合しています。

イメージ

# transposeで行列の置換
[ 
  [[03], [03], [03]...], # 1行目どうし
  [[14], [14], [14]...],  # 2行目どうし
  [[25], [25], [25]...] # 3行目どうし
]

# mapで行同士を結合
[
  [03 03 03...],
  [14 14 14...],
  [25 25 25...]
]

# join("\n") で改行しながら結合
[ 
  03 03 03 ... \n
  14 14 14 ...\n
  25 25 25 ...\n
]

最後にこれをputsして完成です。

作ってみての感想

アピールポイント

変換テーブルを2つ用意して、条件分岐の複雑さを回避したことでしょうか。 ただ、実際は促音や濁音などの清音以外の音が入ってくるのでこれは今回の仕様のみ使える方法だと割り切って考える必要がありそうですね。

フォーマット部分でややこしいことはしていますが、難しいことはしていないと思うので、ロジックの単純さがアピールポイントになると思います!!

苦労したところ

点字1マスの点の番号づけに苦労しました。

横に番号をつけていくと配列操作が楽になりフォーマットのロジックが簡単になりますが、変換テーブルを見たときにインデックスの意味がわからなくなるし、縦に番号をつけていくと変換テーブルの意味は分かりやすくなりますが、フォーマットするロジックは複雑になります。

点字の点の打ち方と比べてフォーマットは仕様によってコロコロ変わる恐れがあると思い、今回に関しては変わりにくい点字のインデックスの方をメンテナンスしやすいように、現実に即した番号づけをしました。

最後に

『プロを目指す人のためのRuby超入門』の改訂2版の発売おめでとうございます。現在フィヨルドブートキャンプでは初版の方で平日の夕方輪読会をしていますが、そこで一緒になって「あーでもない、こーでもない」と知識を探求する仲間に出会えました。この2版を使ってしっかり学んでいきたいと思います。

最後まで読んでくださってありがとうございました。まだまだ、アドベントカレンダーは続きますのでお楽しみください!!