「AtCoder Beginner Contest - C問題」を使って学びのある記事を書けそう

chokudai さんがこう言っていて、確かにそうかもなぁと思った。

C問題を瞬時にキレイなコードで解けるようになると、業務コードも良い感じになるのかもしれない?(そうとは限らないけど)

で、自分はどうなのか?というと、なんとも判断ができない。

読みにくいコードは書かないように気をつけているつもり。回答速度が重要な競技プログラミングだけれど、

  • 意味のある変数名を考えてつける
  • 分離可能な処理はメソッドに切り出す

この2点は意識してやっている。 自分は短期記憶能力が極めて低いと思っているので、これやらないと自分が何やっているのかすぐに分からなくなってしまう。

自分なりには「そこそこ読みやすいな〜」と思いながらやっているけど、自分以外の人の視点で「キレイなコード」なのかは分からない。(「キレイなコード」の定義も人それぞれっぽいけど)

この疑問を解決するには自分のコードを自分以外の人に読んでもらう必要がある。

すでに GitHub には push してあるから誰でも読めるようにはなっているけど、もっと読みやすいコンテンツとして用意した方が良さそうだなぁ。この問題をどう解釈したか、とか、コードを書きながらどんなことを考えていたか、とか、もっとこう書けばよかった、とか。そういうのを書いた方が読み応えがありそうだし自分にとっても学びがありそう。

あと chokudai さんがこういうことも言っていて

ああ、他の人のコードをあまり読んでないなぁと思ったし、自分と同じように他の人のコードをあまり読んでない人はそれなりにたくさんいるだろうなと思ったので、「自分以外の人はこんなふうに解いていたよ」という感じでコードを貼り付けて解説してみるとコンテンツとしての価値が上がりそう。

「コード長」「実行時間」「メモリ」で sort できるから、それぞれのトップの提出コードを読んで解説してみると良さそう(読みやすいコードではないかもしれない。特にコード長が最小のやつ)

記事を書く場所はどこがいいだろ。内容的に Qiita が良いかなぁ。ここよりも幅広い人に学びを共有できそうな場所に投稿したいな。

ruby-jp slack で ActionText の質問に答えた

ruby-jp.slack.com | ruby-jp#support チャンネルで ActionText に関する質問があったので調べて答えた。 ActionText はまだちゃんと触っていなかったので、調べるきっかけになってよかった。

質問内容

rails6の ActionTextrich_text_area について質問したいです!!! has_rich_text を使ったフォームで Create するとエラーが出てしまいます( Update では正常に更新できます)。

エラー文
SQLite3::ConstraintException: NOT NULL constraint failed: articles.body

models/article.rb

class Article < ApplicationRecord
  has_rich_text :body
  validates :title, presence: true
  validates :body, presence: true
end

そのほか views と controllers のコードも貼り付けてくれていた。

調べたこと考えたこと

ActionText か〜。まだ触ってないな〜。と思ってまずRailsガイドをみた。

railsguides.jp

読んでみると

リッチテキストコンテンツは独自のRichTextモデルに保存され、このモデルはアプリケーションの既存のあらゆるActive Recordモデルと関連付けられます。

と書いてあるから、今回のケースで言うと Article モデルの body フィールドの実体は RichText モデルに保存されるってことだな。

つまり articles テーブルには body カラムは必要ない、ということだ。

で、質問のエラー文をみてみると NOT NULL constraint failed: articles.body とある。

ん、これは articles モデルに body カラムがあるってことだなぁ。

てことは body カラムを作る migration をしたのだろう。

そして質問者さんは「この body カラムをリッチテキストにしたい!」と考えて has_rich_text :body をつけたのだろう、と想像した。

さらに質問を読み返してみると

フォームで Create するとエラーが出てしまいます( Update では正常に更新できます)。

とある。

Create ができないのに Update ができる、とはどういうことか。

というか Create ができないのに、その Update の対象になっているレコードはどうやって作ったのか。

おそらくそのレコードは has_rich_text :body をつける前に作成したのだろうなと思った。 has_rich_text :body をつける前に作成したレコード( body カラムが not null なもの)であれば、has_rich_text :body をつけた後に Update しても not null 制約に掛からないのでエラーにならない。

回答

以上を踏まえて次の3点を回答した。

  1. 「もともと Article モデルにあった body カラムを rich_text にしようとしているのかな?」と推測したよ
  2. body カラムの実体は RichText モデルに保存されるから articles テーブルには body カラムは不要だよ
    • articles テーブルの body カラムを消す migration をすれば Create のときもエラーじゃなくなると思うよ
  3. Update がうまくいくのは has_rich_text :body をつける前に作ったレコードだからだと思うよ

質問者さんの反応は

推測通りです!笑 DBをRollbackし、Articleモデルからbodyを削除してMIgrationしたらエラーが消えました!

あってた。わーい。

めでたし。

ではない。

重ねて質問させていただきたいのですが、 bodyをnull: falseにしたい場合は、どうすればよろしいのでしょうか?

ですよね。

has_rich_textに指定したフィールドのnullを禁止するには

ActionText::Attribute をみると has_rich_text の引数は name だけでオプションは指定できない。

null を禁止したければカスタムバリデーションを書くかなぁ、と思った。

質問者さんの Article モデルを書き換える形で書くと、こんな感じかな。

class Article < ApplicationRecord
  has_rich_text :body
  validates :title, presence: true
  validate :body_required

  private
  def body_required
    errors.add(:body, "is required") unless body.body.present?
  end
end

body.body.present? となるのがなんとなく見栄えが悪いけど仕方ない。 なぜなら Ariticle モデルのフィールドとしての body は RichText モデルへの relation であり、 RichText モデルは body フィールドを持つので body.body になってしまう。

Article モデルのフィールド名を変えて body 以外(例えば content など)にするとこの見栄えの悪さを解消できる。

class Article < ApplicationRecord
  has_rich_text :body
  validates :title, presence: true
  validate :content_required

  private
  def content_required
    errors.add(:content, "is required") unless content.body.present?
  end
end

Action Text の概要 - Railsガイド の例でも content というフィールド名を使っているのでなんとなく良さそう。

思ったこと

ruby-jp 基本的には眺めているだけだったけど質問したり答えたりできる場があるのは便利だなぁと思った。

あと ActionText について調べるきっかけになってよかった。

おしまい。

なぜ競技プログラミングを楽しいと感じるのか

最近競技プログラミングを始めた。

まだ始めたばかりだが、なんだか楽しいなぁ〜、と感じている。

なぜそう感じるのか。

この問いに対して「競プロは『正解』があるから楽しいのでは?」説がある。

お仕事の話

年末に同僚とこんな話をした。

  • 普段お仕事をする中では、明確な『正解』を定義できないことの方が多いと思う。
  • その中で僕らは「正解とは言い切れないけど『だいたいよさそう』と思える範囲」に物事を持っていくためにお仕事をしている。
    • 『正解』というPoint(点)ではなくてArea(範囲)だよね
  • そしてその「『だいたいよさそう』と思える範囲」を決めることもお仕事。
  • つまり普段のお仕事で僕らはすごく高度なことをしているのでは。

なんとなくこの話がしっくりきた。

普段のお仕事って、結構疲れる。

「もっとこうした方がいいかな?」「あれをするともっと良いかも?」「そもそもやりたいことってなんだっけ?」

考えることがたくさんある(量)し、ずっと考え続けてる(時間)。

で、考えた末にやったとしても、「『だいたいよさそう』と思える範囲」に入るかどうかは分からないし、その範囲に入ったとしても、本当に良いことだったのかは分からない。

「不確実性」というやつかな?

お仕事が疲れるのは、そういう中でお仕事をしている(= すごく高度なことをしている)からなのかも、と思った。

その意味では、お仕事が疲れるのは健全なことかも。「仕事疲れた〜!」「いいじゃん!!お疲れ!ビール!!!」で良いなぁと。

競技プログラミングの話

一方で、競技プログラミングは明確な『正解』がある。

その正解に向かって、問題を解くためのコードを書けば良い。

あと、問題の状況が明確に定義されていることも、お仕事の現場とは大きく違うなぁと思った。(お仕事は状況の把握だけでかなり大変)

そう考えると競技プログラミングはずいぶんと「簡単なこと」に思える。

問題や正解を定義することなく「解くこと」だけに集中できる場は意外と少なくて、

『俺はいま解くことに集中しているぞ〜〜!うぉおおー!!』

というだけですでに楽しいし

『よっしゃ提出だ!WJ... どきどき... !』『うわっ、WAだ!』『なにっ!?TLEだと!?』『ヨシっ!AC!』

と、機械的に正解不正解を判断してくれるのも楽しいんだと思う。

お仕事へのヒント

競技プログラミングのエッセンスをお仕事に活かすことを考えてみる。

まずは「解くこと」だけに集中できる場を作ってみることかなぁ。

普段は「解くこと」と並行して「問題の定義」や「正解(よさそうなArea)の定義」を考えている気がするので、そこをしっかり分けること、かなぁ。

他にはなんだろ。よさそうかどうかの判断を機械的に(≒定量的に)できるようにすること、とかかなぁ。

競プロ、楽しいので継続的にやってみる

もともとプログラミングテスト・コーディングテストの類がすごく苦手で、前職で社内スキル調査の一環としてテストを受けた時1つもできなくて情けない気持ちになったことがある。確か計2時間くらいのテストだったんだけど、他のみんなは1時間くらいで数問を解いて業務に戻るなか、自分は2時間きっかり頑張って1問も解けなかったので「なんかもう無理...」となった。これが3年くらい前の話。

それ以降、この類のことは避けてきたんだけど、さすがにエンジニアとして人並みにはこういうこともできるようになりたいなぁという思いはずっとあった。

そんな自分が競プロを始めたきっかけは、社で開催されたバーチャルコンテストだった。

AtCoderの使い方やコンテストの説明(未経験者向け)・雑談 のあと、バーチャルコンテストを開催するよ〜、という感じでハードルを下げてくれていたので参加しやすかった。

とはいえ苦手意識があったので、雑談までやってAtCoderのアカウントを作ったら、コンテストは参加せずに様子を少しだけ見て帰ろう、と思っていた。

けどコンテストの1問目をみると「与えられた3つの数字のうち小さい2つを足した結果を出す」くらいの問題があって、あ、こんな簡単なやつもあるんだ、と解き始めていた。

一度やり始めてしまうと面白くて、いまは社で毎週開催されている競プロもくもくランチにも参加している(主催の @purple_jwl さんに感謝)。

引き続きやっていこうと思う。

SUZURI New Items 2019

この記事は SUZURI Advent Calendar 2019 - Adventar の22日目の記事です。

2019年の1年間で SUZURI には10種類の新アイテムが追加され、7種類の既存アイテムがリニューアルされた。

  • ✨Tシャツ: 両面プリントに対応 (01/17)
  • ✨パーカー: 両面プリントに対応 (01/21)
  • ✨ロングスリーブTシャツ: 両面・両袖プリントに対応 (04/02)
  • 🆕アノラックパーカー (04/02)
  • 🆕ウォッシュTシャツ (04/02)
  • 🆕クージー (04/02)
  • ✨リンガーTシャツ: ボディリニューアル (07/01)
  • 🆕ウエストポーチ (07/08)
  • 🆕ジップパーカー (09/25)
  • ✨パーカー: サムネイル画像リニューアル (10/28)
  • ✨スウェット: サムネイル画像リニューアル (10/30)
  • 🆕きんちゃく (11/07)
  • 🆕ビッグショルダーバッグ (11/07)
  • 🆕ビッグシルエットパーカー (11/27)
  • 🆕ビッグシルエットスウェット (11/27)
  • 🆕グラス (12/05)
  • スマホケース: 仕上がりリニューアル (12/18)

抜け漏れないかな?たぶん大丈夫。

この記事では自分が開発に携わったアイテムについて、作りながら考えていたことやいま改めて思うことを書いていこうと思う。

✨Tシャツ: 両面プリントに対応

もともとオモテ面にしかプリントできなかったTシャツをウラ面にもプリントできるようにした。

こんな感じ。
スリ戸惑い中 / 忍者スリスリくん ( surisurikun )のTシャツ通販 ∞ SUZURI(スズリ)

オモテ ウラ
t-shirt-front t-shirt-back

両面プリントに関しては、すでに対応済みのコーチジャケットの実装があってそれを参考にしながら実装したので、大きな技術的課題はなかった。

ただ、新アイテムを追加するのは初めての経験だったので仕様の理解(そのためのコードリーディング)にかなり時間をかけたなぁ、という記憶がある。

あとTシャツは他のアイテムと比べてサイズの種類が多い(Mens S~XXXL, Ladies S~L, Kids 90~160)ため、単純にその分手間が掛かった。 実はこの時、単にウラ面に対応しただけではなくオモテ面画像のリニューアルも行なっていて、それも合わせると結構大変だった(特にデザイナさんは作業が多くて大変だったと思う)。

実装しながら思っていたことは、なんかこれミスったら影響でかいよなぁ〜、ということ。TシャツはSUZURIで一番売れるアイテムだから。リリース後に変な問題が起きないように、っていうのを一番考えていたかもしれない。問題なくリリースできて良かった。

案の定めちゃくちゃ売れて8月4日には2019年の累計受注枚数が10万枚を突破した。 pepabo.com

よくわからないけど10万枚はすごそう。
10万といえば10まんボルトだから、きっとすごいと思う。

🆕クージー

クージーって何?という感じだった。

缶やペットボトル飲料を入れて保温保冷するアイテムで、こんな感じ。

NINJA BEER / 忍者スリスリくん ( surisurikun )のクージー通販 ∞ SUZURI(スズリ)

正面 持ってる様子
koozie-front koozie-hold

円柱形のアイテムなのでクリエータさんがアップロードした画像を円柱形に合成する必要があった。

円柱形の合成処理は既存アイテムのマグカップにもあったが

  • 再利用できる形に切り出されていない
  • 変数名が length1 , height1 等になっていて可読性が低い

という問題があって、まずはこれを解決するためのリファクタリングをした。

具体的には、円柱形画像変換器として Cylinder というクラスを作り、マグカップの合成処理からこのクラスを使うようにした。クラス内ではとにかく意味のある変数名を定義した。 変数名を定義するのは意外と大変で、そもそも何やっているか分からないと名前をつけることができない。「あ、この height1 は "斜め上から見下ろした時に見えるプリント領域の高さ" のことなのか〜」「あ、この length1 は "円柱形の上面の楕円の、短い方の半径" のことね!」のように、内容を理解しながら名前をつけていった。

クージーの実装でもこの Cylinder クラスを使い、良い感じに実装することができた。

正直クージーの実装を担当することに決まった時はめちゃくちゃ不安だった。
「円柱形の合成処理なんて分からんわ〜」という感じだったし、参考としてマグカップの合成処理を読んでも「なるほど(全くわからん)」という状態だったのでヤバかった。

心折れそうだったけど「読んでるうちに理解できるはず」と信じて読み続けていたらだんだんと分かるようになってきたし、「途中までだけどここまで分かったかも」というのを誰かに聞いて欲しくてそれを聞いてくれる(&適宜良い感じの質問をくれる)仲間がいたのでなんとか乗り越えることができた。

(様子) koozie-yousu

この Cylinder クラスは最近リリースされたグラスにも活用されていて、あ〜実装しておいてよかった〜と思った。

ある冬の日 / 忍者スリスリくん ( surisurikun )のグラス通販 ∞ SUZURI(スズリ)

アングル1 アングル2
glass-1 glass-2

✨リンガーTシャツ: ボディリニューアル

デザインを印刷するTシャツ自体のことを「ボディ」と呼んでいる。 SUZURIのディレクター陣は常に面白いアイテムや良さそうなアパレルを探していて、この時は「今のやつよりも良さそうなシルエットと質感のリンガーTシャツを見つけたからリニューアルしよう」という話になった。

Yes. I am a NINJA. / 忍者スリスリくん ( surisurikun )のリンガーTシャツ通販 ∞ SUZURI(スズリ)

旧ボディ 新ボディ
ringer-t-shirt-black ringer-t-shirt-sumi

画像を差し替えて合成処理のパラメータを調整し直すだけなので、技術的に難しい課題は特になかった。

ただ今回はボディ差し替えと同時にカラー変更(旧ボディの4カラーを全て廃止し、新カラーに変更)も行なったため、旧カラーを表示したいシーン(買ったもの一覧画面など)でエラーにならないように、旧ボディの画像が表示されるようにする対応も必要だった。カラーによって合成処理を切り替えることで旧ボディも表示されるようにした。

ちなみに新カラーはスミ、バーガンディ、ジーパンブルー、ヘザーグレーの4種類で、個人的にはこの2色が好き。

ジーパンブルー ヘザーグレー
ringer-t-shirt-jeansblue ringer-t-shirt-heathergray

🆕ジップパーカー

その名の通りジップがついているパーカーのこと。個人的には、ジップがついているものを「パーカー」と呼ぶのだと思っていた節がある。(ジップがついていないパーカーのことは「ジップがついていないパーカー」と呼んでいた)

スライス / 忍者スリスリくん ( surisurikun )のジップパーカー通販 ∞ SUZURI(スズリ)
Jingu / 忍者スリスリくん ( surisurikun )のジップパーカー通販 ∞ SUZURI(スズリ)

オモテ ウラ
zip-hoodie1-front zip-hoodie1-back
zip-hoodie2-front zip-hoodie2-back

もともとSUZURIではパーカーのことを「フーディ」と呼んでいたけど、このジップパーカーのリリースを機に見直されたりした。

what-is-hoodie

🆕きんちゃく

小物を入れるきんちゃく袋。

ズッキュンズッキュン / 忍者スリスリくん ( surisurikun )のきんちゃく通販 ∞ SUZURI(スズリ)

グレー ベージュ
kinchaku-grey kinchaku-beige

合成の調整が難しかった。単純に画像を乗せるだけでは質感や色合いがうまく表現できないので、ざらつき感を出す加工をしたり色合いを調整した。

リリース後に、蛍光色を使ったデザインを合成すると微妙な感じになることが発覚し、どうしようかな〜と悩まされた。

社内ドキュメントに「こんな感じでやるといいかも?」とメモを残して私用で休暇に入ってしまったのだが、次に出勤した時には仲間がその感じで修正してリリースしてくれていた。ありがたかった。

🆕ビッグシルエットパーカー

ビッグなシルエットのパーカーのこと。

the page you were... / 忍者スリスリくん ( surisurikun )のビッグシルエットパーカー通販 ∞ SUZURI(スズリ)

オモテ ウラ
big-hoodie-front big-hoodie-back

(たまたまこの商品はウラ面のデザインがないけど、両面にプリント可能)

ビッグシルエットパーカーを触りがなら実装していた。

すごくふわふわで気持ちよかった。

この気持ち良さを合成で伝えたかったんだけど、なかなか難しかった。たぶん買ってない人には、このふわふわさは伝わっていないんじゃないかなぁ、と思う。

リリース当時もこんなこと言ってた。

まとめ

自分が実装に携わったのは以上の6アイテムだった。

改めてふりかえってみると、チームメンバーの仲間に大いに助けられてリリースすることができたのだなと感じた。

2020年もSUZURIは新しいアイテムの追加やリニューアルをたくさんやると思う。

楽しみ。

Rails CHANGELOG について10記事を書いたのでふりかえる

ふりかえりをしておく。

おさらい

8月末に RailsのCHANGELOG.mdのフィードを購読し始めた - tanaken’s blog

9月のコミットを memo: Rails CHANGELOG 2019-09 - tanaken’s blog でまとめたが、1ヶ月分を一気に書くのはダルそうと思ったのでざっくり週1のペースで書いてみることに決めた。

書いた記事

決めたこと

  • いつ書くのか = 毎週月曜
  • なにを書くのか = 前週の月曜から日曜の夜までの各コミットについての説明
    • 「このコミットはなにをしているか」or「このコミットのおかげでどうなるのか」をひとことで表現する(= 見出し)
    • コミット内容の説明をざっくりと書く
  • どのように書くのか
    • できるだけコードを併記する(引用元を書く)
    • 簡単に動作確認できるものは動作確認をする
    • わからないことがあって良い、という気持ちで書く

ふりかえり:YWT(やった・わかった・つぎやる)

Y:やったこと

W:わかったこと

  • 毎週4コミットくらいについて読んでコメント書くのは1時間あればできる(そんなにしんどくない)
    • 倍くらい(2時間くらい)の量だと結構しんどい
  • サンプルコードが書かれているコミットは読みやすい
  • 自分は自分自身が思っているよりも Rails について知らない
  • RailsCHANGELOG.md を読んでも、そこに書かれていること自体がすぐに役立つことはあまりない
  • Active Storage についてよくわかっていない(ということがわかった)
  • インターネットについてよくわかっていない(ということがわかった)
    • ネットワークとか

T:つぎやること

おわりに

やってみてわかったことがいっぱいあった(主に、わからないことがわかった)ので良かった。

何かをやるとき10回継続してみると何か分かる気がする(経験則)。毎日やる系だと10日間でちょうどいいし、平日だけやると2週間になってちょうどいい。毎週やる系だと2.5ヶ月やって、残りの0.5ヶ月でふりかえりをして「次の2.5ヶ月なにやろうか〜」となるのでちょうどいい。気がする。

「つぎやること」には書いてないけど他にもやりたいこと(お仕事で Rust を使うけどまだあまりやってないのでやりたい、自然言語処理で遊びたい、など)がたくさんあるので、時間の使い方をどうするのか考えたい。もうすぐ2020年だし。

忍者スリスリくんの影武者を作っている

この記事は GMOペパボ Advent Calendar 2019 - Qiita の15日目の記事です。

kagemusha0

オリジナルグッズ作成・販売サービスのSUZURI(スズリ) には、公式忍者の「忍者スリスリくん(@suzurijp)」がいる。

@suzurijp

私はスリスリくんのファンだ。

そんなスリスリくんについて、1つだけ気になっていることがある。

それは

follower

忍者のくせにフォロワー数が多すぎることだ。

控えめに言って隠密行動に向いていない。露出しすぎ。

だがここまで露出してしまったことを悔いても仕方がない。この状況を打破するためには、何か対策を打つ必要がある。

こんな時に有効なのが「影武者」だ。

敵の目を欺くために、大将などと同じ服装をさせた身代わりの武者。

出典: 影武者(かげむしゃ)の意味 - goo国語辞書

忍者スリスリくんにそっくりの影武者がいれば、この問題の解決策になる可能性がある。 私は忍者スリスリくんのファンなので、彼の身の安全を確保し、安心して隠密行動をしてもらうために「忍者カゲムシャくん」を作ることにした。

忍者カゲムシャくんとは

作ったものがこれだ。https://kagemushakun.herokuapp.com/

kagemusha1

カゲムシャくんの見た目は特殊メイク*1でスリスリくんそっくりに作った。98%の人はカゲムシャくんだと気づかないだろう。

そして何かメッセージをいれて「しゃべらせる」を押すと↓

kagemusha2

まるでスリスリくんのようにしゃべらせることができる。スリスリくんは「マス」などの表現が独特なのだ。

これで99%の人を欺くことができる。

さらに「あいさつさせる」にチェックを入れてしゃべらせると↓

kagemusha3

スリスリくんの独特なあいさつを再現することもできる。小粋だ。

これでスリスリくんと完全一致する。作った私ですらカゲムシャなのかホンモノなのか区別がつかなくなってきている。

ぜひ忍者カゲムシャくんで遊んでみてほしい。リンクを再掲しておく。 https://kagemushakun.herokuapp.com/

ここから先は忍者カゲムシャくんのカラクリを書いていく。(ここでページを閉じてもらっても構わない)

忍者カゲムシャくんのカラク

忍者カゲムシャくんのカラクリを紹介する。

ただし、この情報は極秘情報だ。なぜなら、この情報が拡散されるとカゲムシャのカラクリが露呈し、スリスリくんの身に危険が及ぶかもしれないからだ。

むやみやたらに拡散してはいけない。この約束を守ると誓ってくれる人は、このページの一番下にある☆を押してほしい。*2

ラクリの概要

忍者カゲムシャくんは Ruby on Rails 6.0 で動いている。DBはいまのところ必要ないので使っていない。Rails 6 から Webpacker がデフォルトになっているが、その辺も不要なので --skip-javascript をつけて skip した。画面の見た目はCSSフレームワークBulma (正確には bulma-rails) を使って簡単なデザインをしている。特に変わったことはしていない。

カゲムシャくんの機能的な特徴はスリスリくんに似せた「しゃべり」と「あいさつ」だ。 これは suri_lang という rubygem を開発することで実現した。 この自作 gem について紹介していくことで、カゲムシャくんのカラクリを明らかにする。

ラクリ①「しゃべり」

スリスリくんのtwitterおよび筆者が独自ルートで手に入れた情報*3を元に、しゃべりの特徴を分析すると

  • 語尾の表記にカタカナを使う傾向がある
  • 次のような一部の単語はひらがなを使う傾向がある
    • 「等」「事」などの一部の名詞
    • 「是非」「沢山」などの一部の副詞
    • 「及び」「但し」などの一部の接続詞

ということが分かる。特定の単語を上記の法則に則って変換するだけで、まあまあスリスリくんっぽくなると予想できる。

これを実現するための手順は次の通りだ。

  1. 変換したい単語のリストを用意する
  2. しゃべらせたい文章を単語に分割(= 形態素解析)する
  3. 分割した単語をひとつずつ、変換したい単語のリストと比較する
  4. 変換したい単語と一致したら変換する

1. はコツコツ*4やる。
私はスリスリくんのファンなのでまったく苦にならずにリストを用意することができた。

2.MeCab という形態素解析エンジンを用いる。
厳密には MeCabRuby で扱いやすくした natto という gem を使って形態素解析をする。

natto を使うとこのような実装で形態素解析をすることができる。詳しい解説は natto/README.md at master · buruzaemon/nattoHome · buruzaemon/natto Wiki を参照してほしい。

require 'natto'

nm = Natto::MeCab.new
nm.enum_parse('私の名前は田中です').each do |elm| 
  print "#{elm.surface}\t#{elm.feature}\n"
end
# 私    名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
# の    助詞,連体化,*,*,*,*,の,ノ,ノ
# 名前  名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
# は    助詞,係助詞,*,*,*,*,は,ハ,ワ
# 田中  名詞,固有名詞,人名,姓,*,*,田中,タナカ,タナカ
# です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
#       BOS/EOS,*,*,*,*,*,*,*,*

3.4. はやや荒い実装だ。
まず 1. のリストから key が 変換前の単語,その品詞 , value変換後の単語 となる Hash オブジェクトを作成しておく。 そして 2. で分割した単語とその品詞を使って、その Hash にアクセスし、該当のキーがあれば 変換後の単語 を、なければ 変換前の単語 を返す。

require "natto"

module SuriLang
  class Translator
    DICTIONARY = {
      'です,助動詞': 'デス',
      'ます,助動詞': 'マス',
      # ... 略 ...
    }

    def self.translate(text)
      nm = Natto::MeCab.new('-F%m,%f[0]')
      features = nm.enum_parse(text)
                   .select{|n| !n.is_eos?}
                   .map{|n| n.feature}

      features.map{|feature| to_suri_word(feature)}.join
    end

    private

    def self.to_suri_word(feature)
      DICTIONARY[feature.to_sym] || feature.split(',')[0]
    end
  end
end

実際に suri_lang をこのように使ってスリスリくんのようにしゃべらせることができる。

require 'suri_lang'

SuriLang::Translator.translate('SUZURIの忍者スリスリくんです')
# => "SUZURIの忍者スリスリくんデス" 

ラクリ②「あいさつ」

私はスリスリくんのファンなので 忍者スリスリくん ( surisurikun )のジャーナルズ ∞ SUZURI(スズリ) をよく読んでいる。

すると、ある特徴に気づく。記事冒頭の小粋なあいさつだ。

例えばこれだ。

石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。

引用: 忍者スリスリくん ( surisurikun ) の「 【終了】ビキビキ!800円引きセール 」というジャーナル ∞ SUZURI(スズリ)

意味はわからないが、なんとなく可愛い。

また、例えばこのように

そうめんツルツル!忍者スリスリくんデス。

引用: 忍者スリスリくん ( surisurikun ) の「 人気のTシャツに学ぶ!デザインのヒント 」というジャーナル ∞ SUZURI(スズリ)

夏の風物詩である「そうめん」を使った、季節感のあるあいさつもある。どうやら季節に合わせてあいさつを変えているようだ。

これを実現するための手順は次の通りだ。

  1. 忍者スリスリくんのジャーナルから冒頭のあいさつをリストアップする
  2. あいさつをyamlファイルにしておく
  3. ランダムにあいさつを表示する

1. はコツコツ*5やる。
私はスリスリくんのファンなのでまったく苦にならずにリストアップすることができた。

2. は以下のようなyamlファイルを作った。

- message: 石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。
  source: https://suzuri.jp/surisurikun/journals/2019-05-07
  season: none
- message: そうめんツルツル!忍者スリスリくんデス。
  source: https://suzuri.jp/surisurikun/journals/2019-07-02
  season: summer
- ...

3.message, source, seasonインスタンス変数とする Greeter クラスを実装し、yaml を読み込んで作られる Array からランダムに1つ取り出して Greeter クラスのインスタンスを作るようにした。

実際に suri_lang をこのように使うとあいさつさせることができる。

require 'suri_lang'

greeter = SuriLang::Greeter.random_build
greeter.message
# => "石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。" 

忍者カゲムシャくんの今後の展望

以上が忍者カゲムシャくんのカラクリだ。長々と書いてきたが、大したことはしていない。

今後の展望として、改良点を2つ考えている。

改良点①「入力された文章の季節に合わせてあいさつをさせる」

例えばこのように、冬の内容の文章を入力しても、真夏のようなあいさつをしてくる場合がある。

kagemusha4

忍者スリスリくんは賢いので、このような季節が食い違う発言はしない。

これについては、「入力された文章がどの季節っぽいか」を判定するカラクを実装しようと考えている。

具体的には、次のような手順を想定している

  1. なんらかの文章データを用いて、なるべく多くの単語の分散表現(ベクトル)を用意する
  2. 各季節を代表する単語(例えば、春を代表する単語として「桜」など)をリストアップする
  3. 入力された文章を構成する単語のベクトルと各季節を代表する単語のベクトルとを比較し、ベクトルの類似度を数値化する
  4. 各季節ごとに類似度の合計値を算出し、類似度が低い季節を特定する
  5. 類似度が低い季節のあいさつは選択されないようにする

この手順でいい感じになるかはやってみないと分からない。

1. の「文章データ」を決めかねている。「スリスリくんがしゃべる単語」という意味ではスリスリくんのジャーナルやtwitterでの発言を対象にすると良さそうだが、データ量が限られているため語彙数が少ない懸念がある。そうなると入力された文章の単語の多くが「未知語」と判定されてしまう可能性があり、各季節との類似度判定の精度が低くなりそうだ。

単語の分散表現の獲得については、(Deep Learning の勉強も兼ねて)推論ベースのアプローチを取るつもりだ。具体的には CBOW(continuous bag-of-words) モデルの word2vec を実装してみる。

2. の「各季節を代表する単語」の選び方も決めかねている。主観で良いのか?和歌の季語を活用してみるか?などを考えている。

3. 4. 5. は、そもそも単語ごとの類似度の数値化をすることで文章の季節感を表現できるのか?という懸念がある。例えば文章中に否定語が入っている場合に、想定外に季節の類似度が評価されてしまう可能性がある。(「今は桜が咲いていない」= 「桜」という単語が評価されて「春」だと判断されてしまう、など)

とはいえ、やってみないと分からないので、やってみてから考えようと思う。

改良点②「リスト取得の自動化」

今回は「しゃべり」と「あいさつ」に必要なデータを手動で獲得したが、これではメンテナンスコストが掛かりそうだ。

「しゃべり」に関しては、スリスリくんの発言から特徴的なカタカナ表記やひらがな表記を、 「あいさつ」に関しては、スリスリくんのジャーナルから冒頭文の数行を、 それぞれ自動で抽出できるようにしていこうと考えている。

具体的な手順は検討中だ。

まとめ

この記事では、忍者スリスリくんの影武者である「忍者カゲムシャくん」を紹介し、そのカラクリについて説明した。 今後も「忍者カゲムシャくん」の改良を続けていくので暖かい目で見守ってほしい。

*1:画像加工とも呼ばれる。怒られないか心配だ。

*2:押してくれると筆者がよろこぶ。このもう少し下に☆があるはず。SNSシェアでも可。

*3:実は社内にいい感じのドキュメントがある。入社したら読める。we are hiring!

*4:小さなことからコツコツと。大切にしていることだ。

*5:小さなことからコツコツと。大事なことだから2回言った。

Rails CHANGELOG "2019-12-02".."2019-12-09"

この期間の CHANGELOG.md へのコミットは11件。

railtiesに関して3件、ActiveStorageに関して4件、ActiveRecordに関して1件、ActionViewに関して1件、ActionPackに関して2件。

多いすね。

railties

各環境の設定ファイルで autoload_paths, autoload_once_paths, eager_load_paths を指定できるようになった

コミット: registers 43863bf in the CHANGELOG [skip ci] · rails/rails@76bafb0

実装の中身は let environments configure load-related paths · rails/rails@43863bf これ。

このコミットは CHANGELOG に書いとくね〜 というやつ。

config/environments/*.rb にこんな感じで書けば良い

# In config/application.rb, for example.
require "#{Rails.root}/lib/my_app/config"

# In config/environments/development.rb, for example.
config.foo = MyApp::Config.foo

引用: registers 43863bf in the CHANGELOG [skip ci] · rails/rails@76bafb0

Rails.application.config_for の戻り値に対して、文字列キーでのアクセスを禁止した

コミット: Remove deprecated non-symbol access to nested config_for hashes · rails/rails@e5e9c55

この修正前は deprecated になっていた。 シンボルキー以外でアクセスすると warning が出る。

ActiveSupport::Deprecation.warn(<<~MESSAGE.squish)
  Accessing hashes returned from config_for by non-symbol keys
  is deprecated and will be removed in Rails 6.1.
  Use symbols for access instead.
MESSAGE

引用: Remove deprecated non-symbol access to nested config_for hashes · rails/rails@e5e9c55

この修正後はシンボルキーのみでアクセスできるようになる。

どうしても文字列キーでアクセスしたければ with_indifferent_access を使えば?みたいな話も書いてある。

shared_configdeep_merge するようにした

コミット: Add CHANGELOG entry for 4d858b3f2a · rails/rails@2b17c11

詳細は Rails::Application#config_for merges shared configuration deeply by kirikiriyamama · Pull Request #37913 · rails/rails

このようなyamlがあった場合、

# config/example.yml
shared:
  foo:
    bar:
      baz: 1
development:
  foo:
    bar:
      qux: 2

引用: Add CHANGELOG entry for 4d858b3f2a · rails/rails@2b17c11

config_for を使うと baz 消えてしまっていた。baz が消えないように修正されている。

# Previously
Rails.application.config_for(:example)[:foo][:bar] #=> { qux: 2 }
# Now
Rails.application.config_for(:example)[:foo][:bar] #=> { baz: 1, qux: 2 }

引用: Add CHANGELOG entry for 4d858b3f2a · rails/rails@2b17c11

deep_merge に関してはこちら

ActiveStorage

Variant#service_urlPreview#service_url が非推奨になり、 #url が推奨になった

コミット: Deprecate `service_url` in favour of `url` · rails/rails@235f263

「Blobと一貫性を持たせようぜ」的なことが書いてあったので過去のコミットを掘り出した。
Permanent URLs for public storage blobs · rails/rails@feab703
Rails CHANGELOG "2019-10-07".."2019-10-13" - tanaken’s blog で紹介していた。

このときも #service_url を非推奨にして #url を推奨にしているので、合わせようぜ〜という話。

Blob の variant を DB に保存するようにした

コミット: Track Active Storage variants in the database · rails/rails@7d0327b

ActiveStorage::Blob::Representable でDBを使っているなら ActiveStorage::VariantWithRecord を使うように修正されている

def variant(transformations)
  if variable?
    variant_class.new(self, transformations)
  else
    raise ActiveStorage::InvariableError
  end
end

# 中略

private
  def variant_class
    ActiveStorage.track_variants ? ActiveStorage::VariantWithRecord : ActiveStorage::Variant
  end

抜粋: rails/representable.rb at 7d0327bbbf9fdb1f7f988ebb4e86649a776d1c65 · rails/rails

PublicDiskController を削除し DiskController に統一した

コミット: Use DiskController for both public and private files · rails/rails@fbb83d7

リファクタリング

DiskService で作られたURLに service_name フィールドを追加することで、DiskController は複数のサービスを処理できるようになっている。

DiskService のクエリパラメータのうち、dispositioncontent_type は未使用だったので削除した

コミット: Remove query params in DiskService · rails/rails@2e15092

これもリファクタリング。

ActiveRecord

connect_to メソッドにキーワード引数 database を渡すと warning が出るようにした

コミット: Deprecate `database` kwarg from `connected_to` without replacement · rails/rails@254ba46

コミットメッセージによれば、キーワード引数 database をシャーディング(データベースを水平方向に分割すること)の用途で使おうとし、それに関する多くのバグレポートがあったとのこと。で、検討の結果このキーワード引数 database は一旦削除することにしたとのことで、今回 deprecated になった。

シャーディングをサポートする機能を追加するプランはあるらしい。

いまこのキーワード引数 database があるとなんかいろいろしんどいんだろうなぁ。バグレポートを無視するわけにもいかないだろうし。

ActionView

TagBuilder で条件による値の出し分けをできるようにした

コミット: Add support for conditional values to TagBuilder · rails/rails@f1c63d8

修正前はこう書かないと行けなかったところを

content_tag(
  "My username",
  class: "always #{'sometimes' if current_user.special?} another"
)

引用: Add support for conditional values to TagBuilder by joelhawksley · Pull Request #37872 · rails/rails

こう書けるようにした

content_tag(
  "My username",
  class: ["always", "another", { 'sometimes' => current_user.special? }]
)

引用: Add support for conditional values to TagBuilder by joelhawksley · Pull Request #37872 · rails/rails

ActionPack

system test の driver オプションを non-headless ブラウザ用の selenium driver に渡すようにした

コミット: forward system test driver options configured · rails/rails@769188e

コミットメッセージを見る限りそんな感じ。コードベースではあんまり理解できてない。

config/routes.rb の中で外部のroutesファイルをロードできるようにした

コミット: Bring back feature that allows loading external route iles: · rails/rails@33bf253

使い方はこんな感じ。 draw メソッドが生えている。

# config/routes.rb

Rails.application.routes.draw do
  draw(:admin)
end

# config/routes/admin.rb

get :foo, to: 'foo#bar'

引用: Bring back feature that allows loading external route iles: · rails/rails@33bf253

ルーティングがたくさんあるアプリケーションだと有用そう。

コミットメッセージによると2012年にこの機能はあったんだけど、リバートされていたとのこと。
Revert "Allow loading external route files from the router" · rails/rails@5e7d6bb

やっぱり便利っぽいから入れようぜ、的な議論があったらしい。