成果物
プルリクから実装箇所を抜粋
class TenjiMaker
DOT = 'o'.freeze
DASH = '-'.freeze
Point = Struct.new(:i, :j)
ZERO_ZERO, ZERO_ONE = Point.new(0, 0), Point.new(0, 1)
ONE_ZERO, ONE_ONE = Point.new(1, 0), Point.new(1, 1)
TWO_ZERO, TWO_ONE = Point.new(2, 0), Point.new(2, 1)
def to_tenji(text)
result = text.split.map do |s|
/(?<consonant>[KSTNHMYRW]|)(?<vowel>[AIUEO]|)/ =~ s
tenji_unit = [
[DASH, DASH],
[DASH, DASH],
[DASH, DASH]
]
!consonant.empty? && \
_points_consonant(consonant, vowel)&.each { |point| tenji_unit[point.i][point.j] = DOT }
!vowel.empty? && \
_points_vowel(consonant, vowel).each { |point| tenji_unit[point.i][point.j] = DOT }
tenji_unit
end
_to_string(result)
end
private
def _points_consonant(consonant, vowel)
case consonant
when 'K'; [TWO_ONE]
when 'S'; [ONE_ONE, TWO_ONE]
when 'T'; [ONE_ONE, TWO_ZERO]
when 'N'; vowel.empty? ? [ONE_ONE, TWO_ZERO, TWO_ONE] : [TWO_ZERO]
when 'H'; [TWO_ZERO, TWO_ONE]
when 'M'; [ONE_ONE, TWO_ZERO, TWO_ONE]
when 'Y'; [ZERO_ONE]
when 'R'; [ONE_ONE]
when 'W'; nil
end
end
def _points_vowel(consonant, vowel)
if %w[W Y].include?(consonant)
case vowel
when 'A'; [TWO_ZERO]
when 'U'; [TWO_ZERO, TWO_ONE]
when 'O'; [ONE_ONE, TWO_ZERO]
end
else
case vowel
when 'A'; [ZERO_ZERO]
when 'I'; [ZERO_ZERO, ONE_ZERO]
when 'U'; [ZERO_ZERO, ZERO_ONE]
when 'E'; [ZERO_ZERO, ZERO_ONE, ONE_ZERO]
when 'O'; [ZERO_ONE, ONE_ZERO]
end
end
end
def _to_string(result)
line1, line2, line3 = '', '', ''
result.each do |tenji_unit|
line1 += "#{tenji_unit[0][0]}#{tenji_unit[0][1]} "
line2 += "#{tenji_unit[1][0]}#{tenji_unit[1][1]} "
line3 += "#{tenji_unit[2][0]}#{tenji_unit[2][1]} "
end
"#{line1.strip}\n#{line2.strip}\n#{line3.strip}"
end
end
ロジックの解説
「どのような考えのもと、なぜそのコードを書いたのか」というのを実際にプログラムを作成した手順に沿って説明します。メソッドの定義や使い方は公式ドキュメントを見ていただければと思います。
手順1点字の仕様確認
点字の仕組みについては以下からご確認いただけます。
点字は、縦3点、横2点の6点の組み合わせで作られています。
この一行を見た時に、「マスを3 × 2の行列に見立てて点字を作り、最後にString型に変換すればよさそう」というざっくりとした実装計画を立てました。高校や大学の数学で行列にこの説明でわかるのかもしれないですが、中には「なんでそんな考えになるの?」と思う人や、「行列って何?」という方もいらっしゃるかもしれませんのでそういった方向けに間単に説明したいと思います。
行列とは
まずはwikipediaから引用
数学の線型代数学周辺分野における行列(ぎょうれつ、英: matrix)は、数や記号や式などを縦と横に矩形状に配列したものである。
こういうのですね。
これは2つの行と3つの列からなるので(2,3)型または2×3型の行列と呼ばれます。
\begin{bmatrix}
a & b & c \\
d & e & f
\end{bmatrix}
この記事を読む上では「縦と横に数字や記号並べたもの」という理解でも(厳密な説明ではないが)一旦はokです。
この行列というものが数学で方程式を解いたり、座標を変換したり、さらには物理学や化学などあらゆる分野で使われています。youtubeでわかりやすく解説してくれてる方がいるのでより詳しく知りたい方は見てみるのもいいかもしれません。
行列と配列
ここが少し大事です。プログラミングで数学的な計算をできる、とういことはみなさんご存知かと思いますがベクトルや行列を表すのに一般に配列というデータ構造を使います。RubyにもMatrixという行列を扱うためのクラスが用意されていたり、Array#transoposeという配列を行列と見立てて列と行を入れ替えるメソッドが用意されていたりします。「行列は配列を使って表せる」ということと「点字ブロックが3×2型の行列に見立てられそう」という2点を合わせて冒頭の「マスを3 × 2の行列に見立てて点字を作り、最後にString型に変換すればよさそう」という考えに至りました。
この考え方はボードゲームや平面上のグラフなど、2次元上の問題を考える際にも応用できます。
https://www.rubyguides.com/2019/01/ruby-matrix/
手順2: 実装計画
手順1 点字の仕様確認を見ていただくとわかるかと思いますが、点字は基本的に子音と母音の組み合わせでできています。つまり、子音と母音を別個で考えてロジックを組むことができます。このことに留意して作成したフローチャートが以下です。
複雑なロジックを組むような際はいきなりコードを書き始めるのでなく、最初に時間をとって図や擬似コードのようなものを簡単に書いておくと後々の実装が楽になることもあります。また、業務でコードを書く際には実装前にチーム内での齟齬を発生するのを防止するのにも使えるので僕はこのような図を使うことがあります。
手順3: 実装
基本的にはフローチャートの順番通りに処理が行われていると思います。(to_tenji
メソッドとフローチャートを照らし合わせてみてください)その中でいくつか重要だと思う箇所をピックアップして解説していきます。
定数定義
クラス全体で使う固定値は定数として定義します。これは「変更に強いコード」を書く上で役に立ちます。例えば点字の出力部分をDOT
とDASH
という定数に代入していますが、点字の出力をo
ではなく●
に変えたいという状況でも定数部分を変更するだけですみます。
Point
は点字を行列に見立てた時の成分です。行列を一般化する際に第 i 行目、j 列目の成分を行列の (i, j) 成分と呼ぶことが多いのでこのような書き方をしています。行列の成分は1からスタートですが配列のインデックスは0からであることにご注意ください。
DOT = 'o'.freeze
DASH = '-'.freeze
Point = Struct.new(:i, :j)
ZERO_ZERO, ZERO_ONE = Point.new(0, 0), Point.new(0, 1)
ONE_ZERO, ONE_ONE = Point.new(1, 0), Point.new(1, 1)
TWO_ZERO, TWO_ONE = Point.new(2, 0), Point.new(2, 1)
正規表現
/(?<consonant>[KSTNHMYRW]|)(?<vowel>[AIUEO]|)/ =~ s
アルファベットを子音と母音に分ける際に正規表現を使いました。「ある規則に基づいて文字列を操作したい」という正規表現を使うことが役に立つことがあります。ちなみに上記のコードは正規表現のキャプチャの名前をローカル変数(consonant
, vowel
)に割り当てているのですがこの機能はチェリー本で知ったものをそのまま使いました
nilと空文字の扱い
!consonant.empty? && \
_points_consonant(consonant, vowel)&.each { |point| tenji_unit[point.i][point.j] = DOT }
!vowel.empty? && \
_points_vowel(consonant, vowel).each { |point| tenji_unit[point.i][point.j] = DOT }
まず、a && b
というのはaとbの積集合を表し、aとbがtrueのときにtrue, それ以外はfalseを返します。
上記のコードでは!consonant.empty?
と!vowel.empty?
がfalseになった時点で&&に続く処理は行わずにfalseを返します。ここで、先に説明した正規表現のキャプチャではマッチする文字列がなかった場合にnil
ではなく""
(空文字)を返します。rubyではfalseまたはnilが偽、それ以外は真という扱いなのでconsonant &&
やvowel &&
のような書き方では常にtrueを返してしまい、意図した挙動を実装できません。
次に、 _points_consonant(consonant, vowel)&.each
の箇所でいわゆる「ぼっち演算子」が登場していますがこれは「ワ行」のときに_points_consonant
がnilを返す実装になっているためです。
プライベートメソッド
_points_consonant
, _points_vowel
, _to_string
をprivateメソッドとして定義しています。これによりこれらのメソッドは外から呼び出すことがない、クラス内部のみで使えるメソッドであることを明示的に宣言できます。第三者にクラスを使ってもらうことを考えたときにprivateとpublicを明確にわけることはコードの見通しを良くする上で非常に重要です。
工夫したところ
「(コメントなしで)点字の仕様が視覚的にわかるようにしたい」というのをテーマにコードを書きました。点字が2行3列であることを意識してコードの所々にこの形を取り入れました。伝わっていたようでしたら幸いです。
苦労したところ
プルリクエストの最後のコミットが「リファクタリング」となっているのですがここが非常に苦労しました。というか今でも完全には納得できた実装にはなっていません。愚直に条件分岐している分コード量が多くなってしまうのですが可読性を保った上でコード量を減らす、というのが難しかったです。「こういう書き方あるよ」みたいものがありましたらコメント是非コメントください!
伊藤さんにメッセージ
qitaの記事やチェリー本、RspecによるRailsテスト入門など、伊藤さんの発信してくださる情報にはいつもお世話になっております。
特にチェリー本はエンジニア転職する際の勉強用に購入したのですが、今でも仕事でRubyを書く上での土台になっています。
今回このイベントに参加させていただき、アウトプットをすることで知識の定着を確かめることができました。ありがとうございました!
2022/1/30 追記
golangでも実装してみました。
https://github.com/shuntagami/tenji-maker-go