20200204のRubyに関する記事は14件です。

面接でポートフォリオはなんのwebサーバーで動かしているのか聞かれてよく分からなかったので調べた

面接でポートフォリオはなんのwebサーバーを使っているのか聞かれました

プログラミング教室で教えてもらったのは、rails sとすればlocalhost:3000にアクセスすれば動く。そんな感じだったので、webサーバーやアプリケーションサーバーについて意識することはありませんでした。なんのサーバーを使っているのか気にしたことがありませんでした。なので、なにも答えられずww
基本が分かっていなかったので、一体どういうことか調べてみました。

webの仕組みをさらっと復習

webの仕組みは下の図のようになっています。
webの仕組み.png
クライアントが要求を出して、静的な処理ならwebサーバーがリクエストを返します。動的な処理の時、webサーバーはアプリケーションサーバーに処理を要求して、リクエストを返します。
非常にわかりやすい例があったので、引用します。

四則演算のアプリケーションが実装されているAPサーバでクライアントが「1+1は?」というリクエストを送ると、Webサーバはこれを「動的コンテンツ」と判別してAPサーバにリクエストを投げます。受け取ったAPサーバは四則演算ができるのでDBサーバにリクエストを投げなくても「1+1=2」という答えをWebサーバに返すことができます。
しかし、「リンゴとバナナの合計金額は?」というリクエストが飛んでくると、APサーバにはリンゴとバナナの値段のデータが存在しないため、DBサーバにリクエストする必要があります。そして、DBサーバからリンゴとバナナの値段が返ってきたらAPサーバは計算して結果をWebサーバに返します。
参考:ミドルウェアについて知ろう

Rubyのwebサーバーは?

自分でwebサーバーの設定をした記憶がなかったので、そもそもなぜlocalhost:3000にアクセスするとリクエストが返ってくるのかという疑問がでてきました。

webrickがwebサーバーみたいなもの

Rubyは標準でwebrickというwebサーバー用のフレームワークがあります。webサーバーとは違うようですが、webサーバーと同じような動きをすると認識しています。なので、手元で動かすだけなら、ApacheやNginxなどのwebサーバーは必要ないようです。webrickがあるから簡単にアプリケーションを動かすことができているという分けですね。

ApacheやNginxはどういう時に使うの?

ApacheやNginxは静的ファイルの配信やリクエストのバッファリング、レスポンスの圧縮など、アプリケーションではやらないHTTP上で起こる汎用的な問題を解決するミドルウェアです。
参考:Railsに標準装備されているWEBrickではなく、本番でNginxやApacheに変更するのは何故ですか?

とのことで実際に本番環境でパフォーマンスをあげるために使うというイメージで理解しました。
webサーバーはなくても動くみたいで、設計によって必要かどうか判断すると良いということで理解していますが、実際のところほとんどの場合webサーバーは使っているのではないでしょうか?

まとめ

webの仕組みについて改めて復習した次第です。webサーバーについて調べることができて少し理解が深まりました。

参考

webrickを使ったアプリケーションの起動
Rails開発におけるwebサーバーとアプリケーションサーバーの違い
webサーバー、アプリケーションサーバー、Rackといった仕様や概念と、WEBrick、Unicorn、Pumaといった実装の関係が頭の中で結びつきません
WEBrickを使ってみよう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

敷き詰めパズルの解をアルゴリズム「DLX」で探索

以前にアルゴリズム問題として敷き詰めパズルを社内のSlackで出題したのですが、軽く解説できるものではなかったので、記事に纏めることにしました。

地図の空き領域に、その下に列挙したピースを全て収めてください。ピースは回転させてもいいですが、裏返してはいけません。

snippet: puzzle#42
puzzle#42
###########
###.....###
##.###...##
#..#......#
#.........#
##.......##
###.....###
####...####
#####.#####
###########

.......
.#####.
.......
.#.....
.####..
.......
.##....
..###..
.......
.###...
..##...
.......
.#.....
.##....
..##...
.......
.#.....
.###...
..#....
.......
.##....
.##....
.......
.###...
..#....
.......
.##....
..##...
.......

※問題の出典:ニンテンドー3DS内蔵ソフト『すれちがいMii広場』 > 追加コンテンツ『すれちがい迷宮』 > ミニゲーム「パズルボックス」 > No.42


この手の問題を解くのに有用なアルゴリズム「DLX」について、日本語でのわかりやすい解説は既にいくつかあります(→参考リスト)。本記事の特徴は以下くらいですが、理解の足しになれば幸いです。

  • 「1次元の問題」で解法を手を動かして確認します
    • 単純な深さ優先探索
    • Algorithm X
  • DLXの実装をミスしにくいように工夫します
  • 最後に、冒頭の形式の問題を解くコードを添付します

1次元の場合で勉強

いきなり冒頭の問題を解くのはきついので、まずは簡略化した1次元の問題で解法を確認します(ピースの回転も不要とします)。

puzzle-1D
#.#.........#..#

#..#.##
#.#..##
##...##

この解のひとつは以下の通りです。どの列にも # がひとつだけある(重複も抜け落ちも無い)ことが確認できます。最下行は各ピースを異なる英文字で表したもので、コンパクトなため以降ではこの表記にします。

puzzle-1D-solution
puzzle-1D-solution
#.#.........#..# // 設問
: :         :  :
: :     #..#.##: // ピースaを置く位置
:#.#..##    :  : // ピースbを置く位置
: : ##...## :  : // ピースcを置く位置
: :         :  :
#b#bccbbacca#aa# // 各ピースを英文字で表した場合

単純な深さ優先探索

愚直に探索するならピースを片っ端から入れてみる方法があります。ピースがひとつ収まれば、空き領域と残りのピースが少なくなるため、より小さな敷き詰めパズルとして捉えられます。これを再帰的に繰り返して全ピースが埋まれば解が得られます。

探索の様子を以下に示します。ピースを収められなくなったら前のピースをずらしてまた試す、という繰り返しです。

search tree
#.#.........#..#
├── #a#.a.aa....#..#
│   ├── #a#babaabb..#..#
│   └── #a#.a.aab.b.#bb#
├── #.#a..a.aa..#..#
│   └── #.#a.babaabb#..#
├── #.#.a..a.aa.#..#
├── #.#..a..a.aa#..#
│   └── #b#b.abba.aa#..#
└── #.#.....a..a#aa#
    ├── #b#b..bba..a#aa#
    │   └── #b#bccbbacca#aa#
    │       └── // solution
    └── #.#.b.b.abba#aa#

ピースが収まる位置を見つけると言うと難しく感じるかもしれませんが、実際は「左端から全て試す」だけで十分でしょう。それも含めた図を以下に示します。 * で表したところはピースが重なっているため解にならず、次のピースを試すのをスキップすることになります。

深さ優先探索の全容(122行)
search tree (all)
#.#.........#..#
├── *.#a.aa.....#..#
├── #a#.a.aa....#..#
│   ├── *a*.ab*a....#..#
│   ├── #*#ba.**....#..#
│   ├── #a*.*.a*b...#..#
│   ├── #a#babaabb..#..#
│   │   ├── **#ba**abb..#..#
│   │   ├── #**bab**bb..#..#
│   │   ├── #a**aba**b..#..#
│   │   ├── #a#**baa**..#..#
│   │   ├── #a#b**aab*c.#..#
│   │   ├── #a#ba**abbcc#..#
│   │   ├── #a#bab**bb.c*..#
│   │   ├── #a#baba**b..*c.#
│   │   ├── #a#babaa**..#cc#
│   │   └── #a#babaab*c.#.c*
│   ├── #a#.*.*a.bb.#..#
│   ├── #a#.aba*..bb#..#
│   ├── #a#.a.*ab..b*..#
│   ├── #a#.a.a*.b..*b.#
│   ├── #a#.a.aab.b.#bb#
│   │   ├── **#.ac*ab.b.#bb#
│   │   ├── #**.a.**b.b.#bb#
│   │   ├── #a*ca.a**.b.#bb#
│   │   ├── #a#c*.aa*cb.#bb#
│   │   ├── #a#.*caabc*.#bb#
│   │   ├── #a#.ac*ab.*c#bb#
│   │   ├── #a#.a.**b.bc*bb#
│   │   ├── #a#.a.a**.b.**b#
│   │   ├── #a#.a.aa*cb.#**#
│   │   └── #a#.a.aabc*.#b**
│   └── #a#.a.aa.b.b#.b*
├── #.*..a.aa...#..#
├── #.#a..a.aa..#..#
│   ├── *.*a.b*.aa..#..#
│   ├── #b#*..*baa..#..#
│   ├── #.*ab.ab*a..#..#
│   ├── #.#*.ba.**..#..#
│   ├── #.#ab.*.a*b.#..#
│   ├── #.#a.babaabb#..#
│   │   ├── *c#a.**baabb#..#
│   │   ├── #c*a.b**aabb#..#
│   │   ├── #.**.ba**abb#..#
│   │   ├── #.#*cbab**bb#..#
│   │   ├── #.#ac*aba**b#..#
│   │   ├── #.#a.**baa**#..#
│   │   ├── #.#a.b**aab**..#
│   │   ├── #.#a.ba**abb*c.#
│   │   ├── #.#a.bab**bb#cc#
│   │   └── #.#a.baba**b#.c*
│   ├── #.#a..*.*a.b*..#
│   ├── #.#a..aba*..*b.#
│   ├── #.#a..a.*ab.#bb#
│   └── #.#a..a.a*.b#.b*
├── #.#.a..a.aa.#..#
│   ├── *.*.abba.aa.#..#
│   ├── #b#ba.b*.aa.#..#
│   ├── #.*.*..*baa.#..#
│   ├── #.#bab.ab*a.#..#
│   ├── #.#.*.ba.**.#..#
│   ├── #.#.ab.*.a*b#..#
│   ├── #.#.a.babaab*..#
│   ├── #.#.a..*.*a.*b.#
│   ├── #.#.a..aba*.#bb#
│   └── #.#.a..a.*ab#.b*
├── #.#..a..a.aa#..#
│   ├── *.*..*b.a.aa#..#
│   ├── #b#b.abba.aa#..#
│   │   ├── **#b.**ba.aa#..#
│   │   ├── #**b.a**a.aa#..#
│   │   ├── #b**.ab**.aa#..#
│   │   ├── #b#*cabb*caa#..#
│   │   ├── #b#bc*bbac*a#..#
│   │   ├── #b#b.**ba.**#..#
│   │   ├── #b#b.a**a.a**..#
│   │   ├── #b#b.ab**.aa*c.#
│   │   ├── #b#b.abb*caa#cc#
│   │   └── #b#b.abbac*a#.c*
│   ├── #.*.ba.b*.aa#..#
│   ├── #.#b.*..*baa#..#
│   ├── #.#.bab.ab*a#..#
│   ├── #.#..*.ba.**#..#
│   ├── #.#..ab.*.a**..#
│   ├── #.#..a.babaa*b.#
│   ├── #.#..a..*.*a#bb#
│   └── #.#..a..aba*#.b*
├── #.#...a..a.a*..#
├── #.#....a..a.*a.#
├── #.#.....a..a#aa#
│   ├── *.*..bb.a..a#aa#
│   ├── #b#b..bba..a#aa#
│   │   ├── **#b.c*ba..a#aa#
│   │   ├── #**b..**a..a#aa#
│   │   ├── #b**..b**..a#aa#
│   │   ├── #b#*c.bb*c.a#aa#
│   │   ├── #b#bccbbacca#aa#
│   │   │   └── // solution
│   │   ├── #b#b.c*ba.c*#aa#
│   │   ├── #b#b..**a..**aa#
│   │   ├── #b#b..b**..a**a#
│   │   ├── #b#b..bb*c.a#**#
│   │   └── #b#b..bbacca#a**
│   ├── #.*.b..b*..a#aa#
│   ├── #.#b.b..*b.a#aa#
│   ├── #.#.b.b.abba#aa#
│   │   ├── *c#.bc*.abba#aa#
│   │   ├── #c*.b.*cabba#aa#
│   │   ├── #.*cb.bc*bba#aa#
│   │   ├── #.#c*.b.**ba#aa#
│   │   ├── #.#.*cb.a**a#aa#
│   │   ├── #.#.bc*.ab**#aa#
│   │   ├── #.#.b.*cabb**aa#
│   │   ├── #.#.b.bc*bba**a#
│   │   ├── #.#.b.b.**ba#**#
│   │   └── #.#.b.b.a**a#a**
│   ├── #.#..b.ba.b*#aa#
│   ├── #.#...b.*..**aa#
│   ├── #.#....bab.a**a#
│   ├── #.#.....*.ba#**#
│   └── #.#.....ab.*#a**
└── #.#......a..*.a*

コード

この探索をRubyで実装してみます。新しいピースが収まるか判定するためには、既に埋まっている位置を記録するデータ構造が必要です。位置と添字を対応させたboolean配列や整数(=ビット配列)という手もありますが、今回は埋まっている位置の番号を要素とする集合クラス1で行きます。 Solver#each_solution が再帰の部分で、再帰呼び出し前は問題を小さくするよう操作し、呼び出し後は復元操作をしています。

require "set"

class Solver
    def initialize(field)
        @field = field
        @filled_set = @field.set
        @pieces = []
        @selected_list = []
    end

    def add_piece(piece)
        @pieces << piece
        self
    end

    def each_solution(&block)
        return to_enum(__method__) unless block

        if @pieces.empty?
            block.call(@selected_list)
            return self
        end

        piece = @pieces.shift
        (0..(@field.width - piece.width)).each do |offset|
            piece_set = piece.set.to_set { |x| x + offset }
            next unless (@filled_set & piece_set).empty?

            @selected_list.push([piece, offset])
            @filled_set += piece_set
            each_solution(&block)
            @filled_set -= piece_set
            @selected_list.pop
        end
        @pieces.unshift(piece)

        self
    end
end

class MapData
    attr_reader :set, :width

    def initialize(str, char = "#")
        @set = str.each_char.with_index.select { |c, i| c == "#" }.to_set { |c, i| i }
        @char = char
        @width = str.length
    end

    def write(base = nil, offset = 0)
        base ||= "." * @width
        @set.each { |i| base[i + offset] = @char }
        base
    end
end

field = MapData.new("#.#.........#..#")
Solver.new(field)
        .add_piece(MapData.new("#..#.##", "a"))
        .add_piece(MapData.new("#.#..##", "b"))
        .add_piece(MapData.new("##...##", "c"))
        .each_solution do |selected_list|
            str = field.write
            selected_list.each { |piece, offset| piece.write(str, offset) }
            puts str
        end

効率

1次元の問題だとあまり気になりませんが、いきなりピースを中央に置いてみるというのは人間なら絶対にしない方法です。「ピースを順番に試す」というのは初期の分岐が増えてしまい、結果として探索木も大きくなりがちです。

もっと賢く、それでいてアルゴリズム(=コンピューターに指示する手順)としても複雑ではない解法を次で見ていきます。

Algorithm X

計算機科学の分野で有名なクヌース先生が論文の中で述べた、厳密被覆問題の全ての解を探索するアルゴリズムです。名前は「他にいいのが思いつかなかった」2らしいです。

厳密被覆問題への変換

厳密被覆(exact cover)は数学の集合(set)の用語です。専門用語だらけで翻訳に自信はありませんが、Wikipedia英語版には以下のように書いてあります。

In mathematics, given a collection S of subsets of a set X, an exact cover is a subcollection S* of S such that each element in X is contained in exactly one subset in S*. One says that each element in X is covered by exactly one subset in S*. An exact cover is a kind of cover.

数学において、集合 X の部分集合の族 S が与えられるとき、厳密被覆とは次の性質を満たす S の部分集合族 S* のことである: X の各要素は S* の1つの部分集合にのみ含まれる。 X の各要素は S* の厳密に1つの部分集合で被覆されると言う。厳密被覆は被覆の一種である。

わかりやすく配列で例を作れば、全体集合 x = [1, 2, 3] およびその部分集合のリスト s = [[1, 2], [1, 3], [2], [2, 3]] があるとき、リストから抽出した s_star = [[1, 3], [2]]x の要素をちょうどひとつずつ含んでいるため x の厳密被覆です。(厳密被覆になる選び方は1通りとは限りません)

敷き詰めパズルを厳密被覆問題と捉えるには、マス目の位置を要素とする集合に対し、候補となる部分集合としてピースの全配置を用意します。ただし、ピースは1回のみ使うという制約があるため、ピースの種類も集合の要素に追加します。

百聞は一見に如かずということで、実際に例題を変換したものを以下に示します。 | で分けた左16列 [A-P] はマス目、中3列 [a-c] はピースの種類、右1列 [z] は設問であることを表します。(設問を「置き方が1通りのみのピース」と捉えることで、問題サイズが大きくなる代わりに変換を楽にしています)

exact_cover_problem
ABCDEFGHIJKLMNOP|abc|z
______________________
#.#.........#..#|...|# 00 // selected
#..#.##.........|#..|. 10
.#..#.##........|#..|. 11
..#..#.##.......|#..|. 12
...#..#.##......|#..|. 13
....#..#.##.....|#..|. 14
.....#..#.##....|#..|. 15
......#..#.##...|#..|. 16
.......#..#.##..|#..|. 17
........#..#.##.|#..|. 18 // selected
.........#..#.##|#..|. 19
#.#..##.........|.#.|. 20
.#.#..##........|.#.|. 21 // selected
..#.#..##.......|.#.|. 22
...#.#..##......|.#.|. 23
....#.#..##.....|.#.|. 24
.....#.#..##....|.#.|. 25
......#.#..##...|.#.|. 26
.......#.#..##..|.#.|. 27
........#.#..##.|.#.|. 28
.........#.#..##|.#.|. 29
##...##.........|..#|. 30
.##...##........|..#|. 31
..##...##.......|..#|. 32
...##...##......|..#|. 33
....##...##.....|..#|. 34 // selected
.....##...##....|..#|. 35
......##...##...|..#|. 36
.......##...##..|..#|. 37
........##...##.|..#|. 38
.........##...##|..#|. 39

selected と書かれた行のリスト [00, 18, 21, 34] は、全20列をぴったり埋めて互いに重ならないので厳密被覆であり、従って例題の解です。

解法

まずは適当に列を選び、 # を持つ行=解の候補を抜き取ります。どの列を選んでも全ての解を見つけられますが、 # の個数が少ない列から考えると楽です。
z# 1個なのでこの列にします。抜き出した行は以下の通りです。

x-depth1-step1
ABCDEFGHIJKLMNOP|abc|z
______________________
#.#.........#..#|...|# 00

行は 00 一択なので、これを解の候補に選びます。
候補を選んだら、 # が被っている他の行は解にならないので消します。すると以下のようになります。(選んだ列 z は行を抜き出した時点で役目が終わるので先に消しています。)

x-depth1-step2.1
ABCDEFGHIJKLMNOP|abc
____________________
#.#.........#..#|... 00
____________________
.#..#.##........|#.. 11
...#..#.##......|#.. 13
....#..#.##.....|#.. 14
.....#..#.##....|#.. 15
........#..#.##.|#.. 18
.#.#..##........|.#. 21
...#.#..##......|.#. 23
....#.#..##.....|.#. 24
.....#.#..##....|.#. 25
........#.#..##.|.#. 28
...##...##......|..# 33
....##...##.....|..# 34
.....##...##....|..# 35
........##...##.|..# 38

そして候補に # がある列 [ACMP] 自体も消します。するとより小さな問題の形になります

x-depth1-step3.1
BDEFGHIJKLNO|abc
________________
#.#.##......|#.. 11
.#..#.##....|#.. 13
..#..#.##...|#.. 14
...#..#.##..|#.. 15
......#..###|#.. 18
##..##......|.#. 21
.#.#..##....|.#. 23
..#.#..##...|.#. 24
...#.#..##..|.#. 25
......#.#.##|.#. 28
.##...##....|..# 33
..##...##...|..# 34
...##...##..|..# 35
......##..##|..# 38

今度は列 B に注目すると解の候補が 1121 の2つあるので、ひとつずつ試す必要があります。

x-depth2-step1
BDEFGHIJKLNO|abc
________________
#.#.##......|#.. 11
##..##......|.#. 21

まずは 11 を候補に選んでみて、同様に # の被る行を消し、 # の列も消します。

x-depth2-step2.1
DEFGHIJKLNO|abc
_______________
.#.##......|#.. 11
_______________
#.#..##....|.#. 23
.....#.#.##|.#. 28
.....##..##|..# 38
x-depth2-step3.1
DFIJKLNO|bc
___________
####....|#. 23
..#.#.##|#. 28
..##..##|.# 38

すると # の無い列が現れました。これはもうどの行を選んでも解にならないということで、探索失敗です。「その列に注目すると解の候補が0個見つかる」と考えてもいいです。

問題を遡り、今度は 21 を候補に選んでみます。

x-depth2-step2.2
DEFGHIJKLNO|abc
_______________
#..##......|.#. 21
_______________
..#..#.##..|#.. 15
.....#..###|#.. 18
.##...##...|..# 34
.....##..##|..# 38
x-depth2-step3.2
EFIJKLNO|ac
___________
.##.##..|#. 15
..#..###|#. 18
##.##...|.# 34
..##..##|.# 38

E# 1個なので、これをもとに行 34 を選びます。

x-depth3-step1
EFIJKLNO|ac
___________
##.##...|.# 34
x-depth3-step2.1
FIJKLNO|ac
__________
#.##...|.# 34
__________
.#..###|#. 18
x-depth3-step3.1
ILNO|a
______
####|# 18

もう目視では答えが見えていますが、きちんと最後まで同じアルゴリズムを適用します。列 I をもとに行 18 を選びます。

x-depth4-step1
ILNO|a
______
####|# 18
x-depth4-step2.1
LNO|a
_____
###|# 18
_____
x-depth4-step3.1

列が無くなったため集合をぴったり埋められたことになり、選択した行 [00, 21, 34, 18] は解です(順不同)。他にも解があるか探すなら、探索を遡って他の候補を選び直せばいいです。今回は候補を選び尽くしたので、解はこの1個のみとわかります。

コード

実装は次節の「DLX」に回します。

効率

探索の順序を図にすると以下のようになります。候補の少ない列( _ で表現)から攻めることで非常に短く済んでいます。(「単純な深さ優先探索」のときは、列をピースの abc と順序固定で選んでいたことになります)

search tree
................|...|_
└── #_#.........#..#|...|#
    ├── #a#.a.aa..._#..#|a..|#
    └── #b#b_.bb....#..#|.b.|#
        └── #b#bccbb_cc.#..#|.bc|#
            └── #b#bccbbacca#aa#|abc|#
                └── // solution

表や図ではマス目とピースを区切っていますが、厳密被覆問題としては両者に区別はありません。これによって、「このマス目を埋められるピースはこれらだけ」と「このピースを置ける場所はここらだけ」という2通りの絞り込み方が自然とアルゴリズムに入っています。

DLX

見てきたAlgorithm Xは、表を作るのに 0/1true/false といった2値を記録できる2次元配列があれば済みます。ただ、深さ優先探索をする場合は探索を遡る方法が必要で、再帰呼び出しの度に現在の表を保存したり新しい表を作ったりすると、かなりメモリを消費してしまいます。

そこで別のデータ構造を使うことが多いです。要となるのはdancing linksと呼ばれる手法で、組み合わせてDLXと呼ばれます。Excelやスプレッドシートにある、行や列の「非表示/再表示」機能を思い浮かべるといいですが、表の一部を隠して小さくした後にまた元に戻すことができるため、新しい表を作る必要がありません。さらに表の2値のうち片方だけ記録すれば済み、今回のようにまばらな表なら効果的です。

dancing links

dancing linksは双方向連結リストの少し変わった使い方です。双方向連結リストからあるノード(要素)を取り除くときは両隣の参照先を変えるわけですが、取り除いたノードは両隣の参照を持っているため再び同じ位置に戻せます。ふつうは取り除いたノードは破棄するはずで、安全のため参照をnullなどにすることもあり、dancing linksの使い方は単純ながら意外です。

DLXでは表を再現するように縦横2次元の双方向連結リストになっていて、さらにリスト終端が無いほうが何かと便利なので循環リストにもなっています。連結リストは配列と異なり間をスキップしてもいいため、Algorithm Xで作った表の # 部分だけをノード化して互いに接続することで、メモリ効率も走査効率も良くなります。表のセル部だけだと扱いにくいので、列を管理するヘッダーノードとそれらを管理するマスターヘッダーノードもあります(行を管理するノードはありません)。図で見ると理解が速いので、参考リストにある元論文か日本語での解説を参照してください。

ノードを取り除いた連結リストはもちろん正常ですので、何段階でもノードの削除・復元ができます。注意事項として、基本的にノードの復元は削除と完全に逆順に行わなければなりません。これを間違えると連結リストの整合性が崩れてしまいます。当たり前のことを言っているようですが、DLXでは「縦横2次元の連結リストで」「ループによる走査を頻繁に行う」ため、1ヶ所でも間違えると原因の特定に苦労します。

コード

DLX::Client クラスから厳密被覆問題の表を操作することにします。単純な深さ優先探索のときの Solver クラスと似た構造です。

  • 表の列は 0 〜 N-1 の番号で区別し、 add_row メソッドの引数に # のある列番号のリストを与えて行を追加することにします
  • each_solution メソッドが再帰になっています
    • 基本ケースは表に列が無いときです
    • 選んだ列にある候補行毎に、その候補を抜いた小問題を作って解きます

処理速度よりも安全性・可読性を優先させるため、 Node クラスでは以下の工夫をしています。

  • データの削除と復元が逆順になることを保証するために、それぞれを別のメソッドとして定義するのではなく、同じメソッドにフラグを与えます
  • 併せて双方向連結リストのリンクも、各方向毎にメンバー変数を用意するのではなく配列に纏めることで、簡単に方向を切り替えられるようにします
    • わかりやすく横/縦と順/逆の2次元配列にします → @links = [[right, left], [down, up]]
  • 各ノードは役割毎にクラスを分け、アルゴリズムの実装ミスに気付きやすくします
dlx.rb
module DLX
    class Client
        def initialize(column_size)
            @root = RootNode.new
            @columns = Array.new(column_size) { ColumnNode.new(@root) }
            @selected_cells = []
        end

        def add_row(column_indices, data = nil)
            first_cell = nil
            column_indices.each do |i|
                cell = CellNode.new(first_cell, @columns[i], data)
                first_cell ||= cell
            end
            self
        end

        def each_solution(&block)
            return to_enum(__method__) unless block

            # find a column with the fewest cells
            column = @root.each_column.min_by do |column|
                column.size.nonzero? or break column
            end

            # no columns --> a solution is found
            unless column
                block.call(@selected_cells.map(&:data))
                return self
            end

            # depth-first search (recursive) when column.size > 0
            column.cover_rows(true)
            column.each_cell do |cell|
                @selected_cells.push(cell)
                cell.cover_other_columns_and_rows(true)
                each_solution(&block)
                cell.cover_other_columns_and_rows(false)
                @selected_cells.pop
            end
            column.cover_rows(false)

            self
        end
    end

    module Direction
        HORIZONTAL, VERTICAL = 0, 1
        FORWARD, BACKWARD = 0, 1

        def direction(forward)
            forward ? FORWARD : BACKWARD
        end
    end

    class Node
        include Direction

        attr_reader :links, :column  # links :: [[right, left], [down, up]]
        protected   :links, :column  # column != nil iff self is a cell node

        def initialize(row = nil, column = nil)
            @links = Array.new(2) { Array.new(2, self) }
            @column = column

            Array.new(2).tap do |headers|
                headers[HORIZONTAL] = row
                headers[VERTICAL] = column
            end.each_with_index do |header, dir_hv|
                next unless header
                @links[dir_hv][FORWARD] = header
                @links[dir_hv][BACKWARD] = header.links[dir_hv][BACKWARD]
                cover(false, dir_hv)
            end
        end

        def each(dir_hv, dir_fb, &block)
            return to_enum(__method__, dir_hv, dir_fb) unless block

            node = self
            block.call(node) until (node = node.links[dir_hv][dir_fb]).equal?(self)
            self
        end

        def cover(bool, dir_hv)
            column.update_size(bool) if dir_hv == VERTICAL

            node0, node1 = @links[dir_hv]
            node0.links[dir_hv][1] = bool ? node1 : self
            node1.links[dir_hv][0] = bool ? node0 : self
            self
        end
    end

    class CellNode < Node
        attr_reader :data

        def initialize(row, column, data = nil)
            super(row, column)
            @data = data
        end

        def cover_this_row(bool)
            each(HORIZONTAL, direction(bool)) { |cell| cell.cover(bool, VERTICAL) }
        end

        def cover_other_columns_and_rows(bool)
            each(HORIZONTAL, direction(bool)) { |cell| cell.column.cover_rows(bool) }
        end
    end

    class ColumnNode < Node
        attr_reader :size

        def initialize(root)
            super(root)
            @size = 0
        end

        def each_cell(&block)
            each(VERTICAL, FORWARD, &block)
        end

        def cover_rows(bool)
            cover(bool, HORIZONTAL)
            each(VERTICAL, direction(bool)) { |cell| cell.cover_this_row(bool) }
        end

        def update_size(bool)
            bool ? @size -= 1 : @size += 1
        end
    end

    class RootNode < Node
        def each_column(&block)
            each(HORIZONTAL, FORWARD, &block)
        end
    end

    private_constant :Direction, :Node, :CellNode, :ColumnNode, :RootNode
end

2次元の敷き詰めパズルに適用

2次元でも解き方は同じです。全てのマス目とピース種別(+設問)を要素とする集合を表の列として用意し、各ピースが可能な配置を表の行としてひたすら列挙します。配置はフィールドの左上から右下まで、加えて回転した場合も必要です。

冒頭の形式の入力をパースして解を列挙するプログラムを示します。(1次元にも回転ありで対応しています)

puzzle_box.rb
require './dlx.rb'

class MapData
    attr_reader :width, :height, :size

    def initialize(lines, char = "#")
        @cells = lines.map { |str| str.chomp.each_char.map { |c| c == "#" } }
        @char = char

        trim_cells
        @width = @cells[0].size
        @height = @cells.size
        @size = @width * @height
    end

    def each_position(field, &block)
        return to_enum(__method__) unless block

        cells, width, height = @cells, @width, @height
        4.times do |rot|
            ary = []
            cells.each_with_index do |row, j|
                row.each_with_index do |cell, i|
                    ary.push(i + j * field.width) if cell
                end
            end

            (0..(field.height - height)).each do |j|
                (0..(field.width - width)).each do |i|
                    offset = i + j * field.width
                    block.call(ary.map { |idx| idx + offset }, [i, j, rot])
                end
            end

            cells, width, height = rotate(cells), height, width
            break if cells == @cells
        end

        self
    end

    def write(*)
        raise NotImplementedError
    end

    private

    def rotate(cells)
        raise NotImplementedError
    end

    def trim_cells
        raise NotImplementedError
    end
end

class FieldData < MapData
    def write
        @cells.map do |row|
            row.map { |cell| cell ? @char : "." }.join
        end
    end

    private

    def rotate(cells)
        cells
    end

    def trim_cells
    end
end

class PieceData < MapData
    def write(base, left, top, rot)
        cells = rot.times.inject(@cells) { |c, _| rotate(c) }
        cells.each.with_index(top) do |row, j|
            row.each.with_index(left) do |cell, i|
                base[j][i] = @char if cell
            end
        end

        base
    end

    private

    def rotate(cells)
        cells.transpose.reverse!
    end

    def trim_cells
        4.times { @cells = rotate(@cells).drop_while(&:none?) }
    end
end


if $0 == __FILE__
    stream = $stdin.each_line
    field = FieldData.new(stream.take_while { |line| !line.start_with?("\n", "\r") })

    chunk_key = field.height > 1 || :_alone  # 2-D mode / 1-D mode
    pieces = stream.chunk { |line| line.include?("#") ? chunk_key : :_separator }
            .zip("a".."z")
            .map { |(_, sub_lines), char| PieceData.new(sub_lines, char) }
    pieces.push(field)

    dlx = DLX::Client.new(field.size + pieces.size)
    pieces.each.with_index(field.size) do |piece, idx|
        piece.each_position(field) do |cell_indices, pos|
            dlx.add_row(cell_indices.push(idx), [piece, pos])
        end
    end

    cnt = 0
    dlx.each_solution do |ary_data|
        cnt += 1
        basemap = field.write
        ary_data.each do |piece, pos|
            piece.write(basemap, *pos) if piece.is_a?(PieceData)
        end
        puts basemap
        puts
    end
    puts "#{cnt} solution#{"s" if cnt != 1} found."
end

参考


  1. 要素の重複が無いことを保証したリストです。とはいえ今回は普通の配列でも事足ります。 

  2. "I will call algorithm X for lack of a better name" 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

if current_user.admin? && !current_user?(user)で「!」が必要な理由

事象

railsチュートリアル「10.4.2 destroyアクション」において、
「現在のユーザーが管理者のときに限り」という条件を作るためにチュートリアルでは、下記のように記載されていました。

app/views/users/_user.html.erb
 <% if current_user.admin? && !current_user?(user) %>

2つの条件が成り立つ場合のif文になります。
右の条件において先頭に「!」が必要になる理由がわからなかったので記事にしました。
具体的には、現在のユーザーを「!」で否定すると条件が成り立たないのではと疑問に思っていました。

※左の条件は現在ログインしているユーザーが管理者権限を持つか判定しています。

!の意味

!はnot演算子です。
式の値が真である時偽を、偽である時真を返します。

!true=>false
!false=>true

!current_user?(user)が何を表すか

current_user?の判定を「!」で逆転しています。
ここでは、
current_user?がtrueならfalseで返し、
current_user?がfalseならtrueで返します。

忘れがちなeach文の存在

チュートリアルでややこしいことをやっているといま自分が何をやっているかわからなくなってしまうことが1周目だとちょくちょくありました。それゆえの失念だと思いますが、

そもそも、表題の条件文はeach文の中にあります。
下記がrender元のeach文です

app/views/users/index.html.erb
  <ul class="users">
     #省略型のeach文を表しています
   <%= render @users %>
  </ul>

下記がrender先になります。

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

このためuserの部分にユーザーの全レコードがそれぞれ入れられていく処理になります。
するとif分の結果として下記の4通りあるはずです。

①真&&真(=管理者 かつ userがcurrent_user)
②真&&偽(=管理者 かつ userがcurrent_userではない)
③偽&&真(=非管理者 かつ userがcurrent_user)
④偽&&偽(=非管理者 かつ userがcurrent_userではない)

そして!current_user?(user)
この条件はeach分によって割り当てられたuserが現在ログイン中のcurrent_user(=ここでは管理者)と同じかを判定しています。
管理者で無い限り!falseとなり、結果trueになります。

初見ではちょっとわかりづらい部分かと思います。
成り立つのは②の「管理者 かつ userがcurrent_userではない」時のみになります。

まとめ

if current_user.admin? && !current_user?(user)
上記の文は管理者かつeach文によって割り当てられたuserが管理者(current_user)で無い限り成り立つことになります。

よって、管理者がユーザー一覧ページを開くと管理者以外のユーザーに対して削除ボタンを追加できるようになっています。
つまり管理者が自分自身を消せないようにもなっています。

参考

https://docs.ruby-lang.org/ja/latest/doc/symref.html
https://teratail.com/questions/198416

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Action Mailerのメール送信に失敗する

症状

Action Mailerを利用したメール送信処理で、以下のエラーを吐いてしまう。
なお以前は正常に送信することができていた。

Net::SMTPAuthenticationError (530 Authentication required):

ArgumentError (SMTP To address may not be blank: []):

原因

環境変数を正常に読み込めていなかった。
SMTP認証情報やお問い合わせ宛先メールアドレスを環境変数に入れていたため、それらの情報を利用することができず、送信に失敗した。

対処

1.アプリケーションサーバー停止
pumactl stop

2.アプリケーションサーバー起動
rails s

根本原因は?

不明。
EC2のインスタンスを止めたり、立ち上げたりしていたので、それが契機っぽいが。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Heroku Aws::Sigv4::Errors::MissingCredentialsError 本番環境へのアップロード

初めての投稿で緊張しておりますが、ようやくエラー解除できたので、共有します!:raised_hand:

rails5.2で作成したアプリケーションをHerokuへアップロードする際に出たエラーです。
エラーの主な原因としてAWSのS3機能とRails5.2以上から登場したCredentialの機能をHerokuに上手く設定できなかったことが原因みたい。:cry:

(具体的な内容)
Herokuにアップロードし、heroku openをすると
84308945cc9dcd981c2d383e42478eb7.png
こちらのエラーが出てきてなかなか開けません。

ですので、heroku run rails cでエラーの内容を確認すると、以下のエラー内容が表示されます。(Aws::Sigv4::Errors::MissingCredentialsError)
スクリーンショット (311).png

terminal.
missing credentials, provide credentials with one of the following options:
  - :access_key_id and :secret_access_key
  - :credentials
  - :credentials_provider

・access_key_id and secret_access_key
・credentials
・credentials_provider
のうちいずれかが上手く設定できていないみたい。

$ rails c
irb(main):001:0> Rails.application.credentials.secret_key_base
=>nil

ここが設定できていないのが原因のようだ
ここで同じような原因で悩む記事を発見しました!
rails エラー
(Aws::Sigv4::Errors::MissingCredentialsError)がわかりません。
https://teratail.com/questions/199552
こちらの方はHerokuのssettingからConfig Varsに
・AWS_ACCESS_KEY_ID
・AWS_SECRET_ACCESS_KEY
・AWS_DEFAULT_REGION
を設定したところ解決できたようです。

エラーが起こっているherokuに上げているアプリケーションのSetting(鍵マーク)→Config Varsを選択します。
スクリーンショット (312).png

こちらを参考に、
Config Varsのより
KEYにそれぞれAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_DEFAULT_REGIONを設定し、
VALUEにIAMから作成したシークレットキーとシークレットアクセスキー、リージョン(たぶん東京の人が多いはず)を入力していきます。
その後、

$ heroku run rails c 
Running rails c on ⬢ herokuapplication2020... up, run.3100 (Free)
Loading production environment (Rails 5.2.4.1)
irb(main):001:0> 

エラーが出ない!嬉しい限りです。:relaxed:
そして、、、

$ heroku open
 ▸    Error opening web browser.
 ▸    Error: Exited with code 3
 ▸    
 ▸    Manually visit https://herokuapplication.herokuapp.com/ in your
 ▸    browser.

Herokuにアクセスできるようになった!
以上より解決???

Herokuに上げた後に直接環境変数を代入するのは初めてだったので、戸惑いましたが、なんとか完了!ターミナルからも本来はできると思うので、ここらへんの知識をもっと勉強しなくてはなりません。?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Aws::Sigv4::Errors::MissingCredentialsError 本番環境へのアップロード

初めての投稿で緊張しておりますが、ようやくエラー解除できたので、共有します!:raised_hand:

rails5.2で作成したアプリケーションをHerokuへアップロードする際に出たエラーです。
エラーの主な原因としてAWSのS3機能とRails5.2以上から登場したCredentialの機能をHerokuに上手く設定できなかったことが原因みたい。:cry:

(具体的な内容)
Herokuにアップロードし、heroku openをすると
84308945cc9dcd981c2d383e42478eb7.png
こちらのエラーが出てきてなかなか開けません。

ですので、heroku run rails cでエラーの内容を確認すると、以下のエラー内容が表示されます。(Aws::Sigv4::Errors::MissingCredentialsError)
スクリーンショット (311).png

terminal.
missing credentials, provide credentials with one of the following options:
  - :access_key_id and :secret_access_key
  - :credentials
  - :credentials_provider

・access_key_id and secret_access_key
・credentials
・credentials_provider
のうちいずれかが上手く設定できていないみたい。

$ rails c
irb(main):001:0> Rails.application.credentials.secret_key_base
=>nil

ここが設定できていないのが原因のようだ
ここで同じような原因で悩む記事を発見しました!
rails エラー
(Aws::Sigv4::Errors::MissingCredentialsError)がわかりません。
https://teratail.com/questions/199552
こちらの方はHerokuのssettingからConfig Varsに
・AWS_ACCESS_KEY_ID
・AWS_SECRET_ACCESS_KEY
・AWS_DEFAULT_REGION
を設定したところ解決できたようです。

エラーが起こっているherokuに上げているアプリケーションのSetting(鍵マーク)→Config Varsを選択します。
スクリーンショット (312).png

こちらを参考に、
Config Varsのより
KEYにそれぞれAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_DEFAULT_REGIONを設定し、
VALUEにIAMから作成したシークレットキーとシークレットアクセスキー、リージョン(たぶん東京の人が多いはず)を入力していきます。
その後、

$ heroku run rails c 
Running rails c on ⬢ herokuapplication2020... up, run.3100 (Free)
Loading production environment (Rails 5.2.4.1)
irb(main):001:0> 

エラーが出ない!嬉しい限りです。:relaxed:
そして、、、

$ heroku open
 ▸    Error opening web browser.
 ▸    Error: Exited with code 3
 ▸    
 ▸    Manually visit https://herokuapplication.herokuapp.com/ in your
 ▸    browser.

Herokuにアクセスできるようになった!
以上より解決???

Herokuに上げた後に直接環境変数を代入するのは初めてだったので、戸惑いましたが、なんとか完了!ターミナルからも本来はできると思うので、ここらへんの知識をもっと勉強しなくてはなりません。?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのActionCableを使ってリアルタイムチャット機能を試してみた。

RailsのActionCableの機能を使って、リアルタイムチャットを作ってみた。

しかし、ActionCableの全容をつかむことが出来ず、わかる範囲での記述となります。
間違ったことを書いてある場合は、コメント欄で指摘していただきますと大変助かります。

・目的
HTTP通信では、クライアントがホストに対してリクエストを申請して、初めてデータを取得することができる。
しかし、それでは複数のクライアントがホストにアクセスするような場合に、随時ホストにリクエストしなければデータが取得できない。そのため、ホストとクライアントが相互に監視し、データの変化に即座に反応するウェブページを作る。

・実践
まず、事前準備としてRailsプロジェクトを生成します。
続いて、以下のコマンドでchannelを生成します。channel名はsend_msgです。

rails g channel send_msg

次に、ActionCable.serverをmountするための記述です。

routes.rb
mount ActionCable.server => "/cable"

chat_channel.rbでは、クライアント側のデータをホストに引き渡すための記述となります。
*ざっくり過ぎる表現
subscribedメソッド内でホストにストリームするための値として"room"を定め、send_msgメソッドのActionCable.server.broadcastの後ろにも共通の値が記述されています。

これは、クライアントとホスト間の通信について、あらかじめコネクションのためのインスタンスが生成されており、これをsubscribed(購読と訳されていたが、予約的な意味合い?)して、サーバーにbroadcast(引き渡す的な)する。その具体的な内容はmessage:data['message']ですよ!と素直に読み取りました。
*勝手な解釈なので、お気をつけ下さい。

chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def send_msg(data)
    ActionCable.server.broadcast "room", message:data['message']
  end
end

続いてchat.coffeeファイルについてです。
先ほどのchat_channel.rbのsend_msgメソッド内の処理で、サーバに引き渡すmessage:data['message']についてsend_msg:(data)の中に記述してあります。

chat.coffee
App.chat = App.cable.subscriptions.create "ChatChannel",
  connected: ->
    console.log("WEBSOCKET CLIANT CONNECTED")

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    console.log(data['message'])

  send_msg:(data) ->
    @perform 'send_msg', message:data

chat.coffeeにてApp.cableクラスのサブスクリプションが作られ、それがApp.chatに格納されています。
このApp.chatというインスタンスを用いてindex.html.erbからデータの送受信ができるようになります。

index.html
<h1>チャットルーム</h1><br>
<p id="chat"></p><br>
<p>メッセージ:</p>
<br>
<textarea id="msg" style="width:300px; height:80px;"></textarea>
<hr>
<button id="send">送信</button>
<script>
    $(document).ready(function(){
        /* メッセージ受信 */
        App.chat.received = function(data){
            $("#chat").append(data['message'] + "<br>")
        }

        $("#send").click(function(){
            msg = $("#msg").val()
            /* メッセージ送信 */
            App.chat.send_msg(msg)
        })
    })
</script>

ブラウザを二つ立ち上げて動作を確認できました。
actioncable.png

・まとめ
Railsの機能や基礎技術に対する不理解が露呈する場面が多く、よくわからないけど動いているという状況だと思います。ホストとクライアント間の通信をリアルタイムにする機能について、あらためて向き合う必要があるようです。以上です。

参考
Action Cable の概要 : https://railsguides.jp/action_cable_overview.html
Rails WebSocket Chat Real Time : https://www.youtube.com/watch?v=kJbuZecN1c8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

config/initializeにカスタムバリデータを定義する

電話番号にバリデーションを使用する

telephone_numberというgemを使用してカスタムバリデーションを定義する。

まずはgemを定義

gem 'telephone_number'

そしてbundle install!

$ bundle

config/initializeに下記のように定義

config/initialize.rb
module ActiveModel
  module Validations
    class TelValidator < EachValidator
      def validate_each(record, attribute, value)
        return if value.blank?

        record.errors.add attribute, :invalid unless TelephoneNumber.valid?(value, :jp)
      end

      module HelperMethods
        def validates_tel_of(*attr_names)
          validates_with TelValidator, _merge_attributes(attr_names)
        end
      end
    end
  end
end

そしてあとは個別のモデルで下記のようにすれば電話番号のバリデーションがかかってくれる。

hoge.rb
validates :tel, tel: true

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

config/initializersにカスタムバリデータを定義する

電話番号にバリデーションを使用する

telephone_numberというgemを使用してカスタムバリデーションを定義する。

まずはgemを定義

gem 'telephone_number'

そしてbundle install!

$ bundle

config/initializeに下記のように定義

config/initializers/tel_validator.rb
module ActiveModel
  module Validations
    class TelValidator < EachValidator
      def validate_each(record, attribute, value)
        return if value.blank?

        record.errors.add attribute, :invalid unless TelephoneNumber.valid?(value, :jp)
      end

      module HelperMethods
        def validates_tel_of(*attr_names)
          validates_with TelValidator, _merge_attributes(attr_names)
        end
      end
    end
  end
end

そしてあとは個別のモデルで下記のようにすれば電話番号のバリデーションがかかってくれる。

hoge.rb
validates :tel, tel: true

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VScodeでrubocopを使ったらエラーが出た

背景

・rbenvでrubyのバージョンを2.6.0に指定

system
* 2.6.0 (set by /PATH/)
2.7.0

・2.6.0の環境下でrubocopをインストール

gem install rubocop

・VSCode拡張機能であるruby-rubocopをインストール

エラー

VSCodeの右下に以下のようなエラーが出てきた

rbenv: rubocop: command not found
The `rubocop' command exists in these Ruby versions: 2.6.0

問題の原因

以下の2つのファイルが置いてある場所に問題があった
~/.vscode
~/project/.ruby-version

VScode上でruby-rubocopを実行する時にデフォルトのruby環境(rbenv versionsコマンドを打った時のsystem)のままであり、rubocopがインストールされていない状況であった

解決方法

以下のどちらかの方法で解決できる

1.VScodeのruby実行環境を2.6.0にする
.ruby-versionを.vscodeがある~/に置く

2. デフォルトのruby環境にrubocopをインストールする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書いたことないPdMやPOに捧ぐ、Ruby on Rails on Dockerハンズオン vol.2 -Hello, Ruby on Rails on Docker-

この記事はなにか?
この記事はが社内のプログラミング未経験者、ビギナー向けに開催しているRuby on Rails on Dockerハンズオンの内容をまとめたものです。ていうかこの記事を基にそのままハンズオンします。ハンズオンは
1回の内容は喋りながらやると大体40~50分くらいになっています。お昼休みに有志でやっているからです。
現在進行形なので週1ペースで記事投稿していけるように頑張ります。
ビギナーの方のお役にたったり、同じように有志のハンズオンをしようとしている人の参考になれば幸いです。

他のハンズオンへのリンク
- Ruby on Rails on Dockerハンズオン vol.1 -Introduction-

$, #, >について
$: ローカルでコマンドを実行するときは、頭に$をつけています。
#: コンテナの中でコマンドを実行するときは、頭に#をつけています。
>: Rails console内でコマンド(Rubyプログラム)を実行するときは、頭に>をつけています。

はじめに

第二回目の今回は、Ruby on RailsをDockerコンテナで起動させるHello worldをやっていきます。

今日のゴール

  • Ruby on Rails on Docker で Hello world する

では早速、Ruby on Rails on Docker で Hello world していきましょう!

Hello, Ruby on Rails on Docker

今回は↓の図のように、Docker 上に Rails アプリケーション用のコンテナと PostgreSQL (database) 用のコンテナを作っていきます。
image.png
まず、作業用のディレクトリを作っておきましょう。

$ mkdir Handson
$ cd Handson

今後、このHandsonディレクトリをホームディレクトリとして話を進めますので、特に指定がない場合、Handsonディレクトリでコマンドを叩いたり、Handsonディレクトリから見た相対パスでファイルを編集していると思ってください。

では早速、Ruby on Rails on Docker な環境を構築するために以下の4つのファイルを作成していきます。

  • Dockerfile: Rails アプリ用の Docker image の元となる設計図
  • Gemfile: Rails アプリに必要なgemを記載するファイル
  • Gemfile.lock: Gemfileによってインストールされたgemのバージョン情報などを管理するファイル
  • docker-compose.yml: 今回のアプリをコンテナ起動させるための Dcoker Compose ファイル

Dockerfile

Dockerfile
FROM ruby:2.7.0-alpine3.11

ENV HOME="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR $HOME

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
      gcc \
      g++ \
      libc-dev \
      libxml2-dev \
      linux-headers \
      make \
      nodejs \
      postgresql \
      postgresql-dev \
      tzdata \
      yarn && \
    apk add --virtual build-packs --no-cache \
      build-base \
      curl-dev

COPY Gemfile $HOME
COPY Gemfile.lock $HOME

RUN bundle install && \
    apk del build-packs

COPY . $HOME
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

Dockerfile は初めて見る人にとっては「なんだこれ?」なものな気がしますが、読んでみると意外とシンプルです。
頭に大文字で書かれているのが命令と呼ばれるものでコマンドみたいなものです。
今回の Dockerfile では以下の命令を利用しています。

  • FROM: ベースイメージを定義する命令です。ruby イメージを指定しているので、元々 Ruby を使用できるイメージの上に Rails を動かす環境を作っていきます。タグの2.7.0-alpine3.11は2020.02.02時点で Ruby の最新バージョンを軽量ディストリビューションとしておなじみの alpine linux のこれまた最新バージョン3.11上で動かしているものを選んでいます。
  • ENV: 環境変数を定義する命令です。
  • WORKDIR: 作業ディレクトリを定義する命令です。ベースイメージ内に該当のディレクトリがない場合は、そのディレクトリを作成することもしてくれます。
  • COPY: ホストのファイルやディレクトリをイメージ内にコピーする命令です。
  • RUN: コマンドを実行する命令です。
  • EXPOSE: コンテナがリッスンするポートを宣言する命令です。 Rails ではデフォルトで 3000 番ポートを使用するので 3000 を指定してあげています。
  • CMD: ソフトウェアを実行するためのコマンドを定義する命令です。コンテナが起動する時に実行されるコマンドといった方がイメージ湧きやすいかもしれません。少し特別な書き方(["xxx", "xxx"]みたいな)をしますが、Rails アプリケーションを起動させるコマンドはrails server -b 0.0.0.0でして、それをCMDの記法で書いています。

あらかた命令と1行1行の内容について述べてしまいましたが、取りこぼしているところをキャッチアップ。

RUN宣言でapk ~と色々書いている9行目からの部分がありますが、apk addは Alpine linux でパッケージをインストールするコマンドです。
apk updateでパッケージリポジトリの最新のインデックス(インストールできる最新バージョンは何か)を取得してきて、apk upgradeですでにインストールしているパッケージで最新版にアップデートできるものをアップデートします。その後、apk addで Rails を起動するのに必要なパッケージをインストールしていきます。--no-cacheオプションはキャッシュを残さないようにするためのオプションです。不要なキャッシュを残さないことでコンテナ自体を軽量に保つことができます。(コンテナは軽量に保っておいた方がダウンロードに時間がかからなかったり、ホストのボリュームを圧迫しないのでよいとされています。)--virtualオプションはそのインストールしたパッケージ達を一つのグループとして名前づけしています。今回の例だとbuild-packsという名前をつけています。ここでインストールしたパッケージは Rails をビルドする上では必要なのですが起動させるためには不要なのであとでapk delで削除するために名前づけしています。

その後、COPY命令でGemfileGemfile.lockをイメージ内にコピーして、bundle installを実行してます。bundle installGemfileの内容に沿ってgemをインストールするコマンドです。Rails 自体もgemでインストールできるのでGemfilerailsを記入しておけばこのbundle installの際にインストールされます。Gemfile.lockはすでにインストール済のgemのバージョンなどを管理して無闇にバージョンアップさせないようにしてくれます。

最後にCOPY . $HOMEでローカルホストのファイルを一式イメージ内にコピーすることで Rails アプリケーションを起動させられる Docker image を作ることができます。

Gemfile

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'

Gemfileはかなりシンプルで、インストールするgemのソースとrailsgem をインストールすることを定義しています。Rails は2020.02.02時点で最新バージョンが 6.0.2 なのでまぁメジャーバージョンとして 6 のものをインストールしてくださいというような指定の仕方をしています。
Rails アプリケーションでは最初rails newコマンドでアプリに必要なgemやファイルをインストール・生成するため、初期ではこれほどシンプルなGemfileがあるだけで構わないのです。

Gemfileでバージョンを指定する表現方法はいくつかあります。gemはGitHubなどで公開されていることが多くて大体 README でこう Gemfile に記載してくれと書かれていることが多いのであんまり気にすることはないかもしれませんが一応紹介。

  • gem 'rails', '6.0.0': 絶対 6.0.0 をインストール(バージョンを定義)
  • gem 'rails', >= 6.0.0': 6.0.0 より最新のものをインストール(最低バージョンを定義)
  • gem 'rails', >= 6.0.0', < 6.0.2: 6.0.0 以上 6.0.2 未満のバージョンをインストール(バージョンの範囲を定義)
  • gem 'rails', '~> 6.0.0': 6.0.X のバージョンをインストール(マイナーバージョンを定義)

Gemfile.lock

Gemfile.lockは最初にbundle installされるときに書き込まれるので、最初は空ファイルで問題ありません。

$ touch Gemfile.lock

touchコマンドはファイルの更新日時を現在時刻に更新するためのコマンドですが、ファイルが存在しない場合は空ファイルを生成してくれるのでGemfile.lockを生成するために使いました。

docker-compose.yml

docker-compose.yml
version: '3'

services:
  db:
    image: postgres:12.1-alpine
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./tmp/db:/var/lib/postgresql/data

  web:
    build: .
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db

まず、サービスとしてdbwebの二つがあることがわかるかと思います。dbは文字通りデータベース用のコンテナ(サービス)、webは Rails アプリケーションを動作させるコンテナ(サービス)です。dbコンテナではimagepostgres:12.1-alpineを指定しています。
docker-compose.ymlについては前回もお話したので、前回お話していない項目を中心にお話します。

  • environment: コンテナを起動する時に環境変数としてセットする。この場合、TZ(タイムゾーン)を東京にしてる。
  • volumes: コンテナからホストのディレクトリをマウントしている。左がホストのパス、右がコンテナ内のパス。ホストのパスはこのdocker-compose.ymlの場所からの相対パスで書いてます。こう書くことで簡単にいえば、ホストのパスとコンテナのパスを同期しているイメージになり、ホストでファイルを編集すればコンテナ内にも反映され、コンテナ内でファイルが編集されればホストのファイルにも反映されるという関係を気づけます。コンテナはステートレスなので一度コンテナを削除して新しくコンテナを起動させた場合、最初のコンテナ(削除したコンテナ)内で変更されたデータは全てなかったことになってしまうのですが、ホストのディレクトリに同期しておくことで次に起動するコンテナもそのディレクトリをマウントするのでデータが永続化されるようになります。
  • build: build: .docker-compose.ymlと同じディレクトリのDockerfileをbuildしたイメージを使ってコンテナを起動するようになります。
  • depends_on: コンテナの依存関係を定義します。今回の場合『webdbに依存している』と定義していることになりますが、これは次の2つが実現されます。
    • docker-compose upした時に、dbコンテナが起動してからwebコンテナを起動する
    • docker-compose up webwebコンテナを起動させようとした場合、dbコンテナも起動させる

Rails アプリケーションを新規作成

まずはrails newコマンドで新規に Rails アプリを作成します。

$ docker-compose run --rm --no-deps web rails new . -fGTd postgresql
...
Webpacker successfully installed ??

いろいろな要素のあるコマンドですね。ちょっと順を追って説明します。
まずこのコマンドの大枠はdocker-compose run [options] <service name> <command>です。今回の例では、

  • [options]: --rm --no-deps
  • <service name>: web
  • <command>: rails new . -fGTd postgresql

となっています。docker-compose rundocker-compose.ymlの定義にそって対象のコンテナを立ち上げてその中でコマンドを実行し、実行後にコンテナを停止するコマンドです。つまり、webコンテナを立ち上げてrails new . -fGTd postgresqlwebコンテナ内で実行してくれます。
--rmオプションはコマンド実行後にコンテナを停止した後に削除までしてくれるオプションです。基本停止したコンテナは不要だと思うのでこのオプションをつけるのがよきかと思います。
--no-depsオプションはdocker-compose.ymldepends_onが定義されていたとしてもそれを無視してdocker-compose runを実行することができます。

次にwebコンテナで実行されるrails new . -fGTd postgresqlを見ていきましょう。
まずrails newは Rails アプリケーションを新規作成するためのコマンドです。.はアプリケーションを作成する場所を指していてカレントディレクトリ(コマンドが実行されたディレクトリ)を示しています。-fGTdはオプションなので一つずつ紐解きます。

  • -f: ファイルの上書きを強制する。Gemfileなどに上書きが走りますがいちいち Yes or No を聞かれないようにするためにつけています。
  • -G: Gitの初期設定をスキップします。Rails 6 からなんかこのオプションをつけないとまともにrails newできなかったのでつけてます。
  • -T: minitestという Rails でデフォルトでインストールされるテストフレームワークのインストールをスキップします。僕はRSpecというテストフレームワークをよく使っているのでこのオプションをつけて無駄にminitestがインストールされないようにしています。
  • -d: -d <database name>で利用するデータベースを指定します。今回はpostgresqlを指定。

Rails アプリケーションの新規作成ができたら一度イメージをビルドしておきましょう。

$ docker-compose build
...
Successfully tagged handson_web:latest

docker-compose buildコマンドはdocker-compose.ymlDockerfileからのビルドが必要なサービスのイメージビルドをすべて実行してくれます。今回はdbは DockerHub のイメージを使っているのでwebのみがビルドが必要なサービスとしてビルドされます。

DBの接続設定

ビルドが終わったら、Rails アプリケーションの DB 接続設定をコーディングしていきます。 DB の接続設定はconfig/database.ymlに記載します。 Rails では設定系のファイルはconfigディレクトリに格納されています。

config/database.yml
...
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
...

hostusernamepasswordがデフォルトから新たに追加した項目です。
hostはDBのホスト名です。今 Rails アプリが稼働しているwebコンテナはdocker-compose.ymlが作る Docker ネットワークの中にいます。この中ではサービス名で名前解決してコンテナが相互に接続することができます。つまりhostとしてdbを設定することでdocker-compose.ymldbのサービス名で定義されたコンテナに接続できるようになるのです。
usernamepasswordは何の値なのでしょうか?これはdbコンテナのイメージで指定したpostgresのデフォルト値です。passwordは同じホストからのアクセスであれば省略が可能になっています。postgresイメージのusernamepasswordはそれぞれPOSTGRES_USERPOSTGRES_PASSWORDの環境変数で定義することもできます。

データベースの設定はここまでです。このデフォルトの設定値が開発環境(development)、テスト環境(test)の設定値として反映されるようになっています。

データベースを作成する

データベースの接続設定を定義したので、データベースを実際に作成していきます。
Rails ではデフォルトで本番環境(production)、開発環境(development)、テスト環境(test)の3つの環境(environment)が用意されています。特別に環境を指定しない場合、開発環境で挙動するようになっています。
データベースの作成はrails db:createコマンドを使いますが、このコマンドは開発環境とテスト環境用にデータベースを作成してくれます。本番環境用のデータベースを作成する場合は、RAILS_ENV=productionをオプションとしてつけます。
今回はまず開発環境向けにデータベースの作成を行いたいので、以下のコマンドを実行します。

$ docker-compose run --rm rails db:create

Hello Ruby on Rails on Docker!!

ここまでで Hello world に必要な作業は全て完了しました。
コンテナを立ち上げて Hello world ページが表示されることを確認しましょう。

$ docker-compose up -d

Rails アプリは少し起動に時間がかかります。すぐにhttp://localhost:3000にアクセスしてもまだアプリケーションが起動していないこともありますので、その場合はdocker-compose logsコマンドを使ってアプリケーションの起動状態を確認してみましょう。

$ docker-compose logs -f

-fオプションはログの変化をリアルタイムでコンソールに表示するためのオプションです。Rails アプリが起動した場合

web_1 | Use Ctrl+C to stop

というログが表示されます。この表示を確認したらCtrl+Cdocker-compose logsから抜け出しましょう。

では、http://localhost:3000にアクセスしてみましょう!
image.png
このようなページが表示されたでしょうか?このページが Rails アプリケーションの第一歩、つまり Hello world です。おめでとうございます!これでもう『Rails は Hello world までならやったことあります』と自慢することができます!

ここまでできたら後片付けをしておきましょう。
このままではコンテナが起動しっぱなしになってしまうので、最後にコンテナを停止させておきます。

$ docker-compose down

これでコンテナを停止させたので、http://localhost:3000にアクセスしても先ほどのページは表示されなくなっていることでしょう。

まとめ

今回は、Dockerfiledocker-compose.ymlなどのファイルを作成し、Rails アプリケーションを稼働させられる Docker イメージ、Docker コンテナを作成してみました。
さらに、Rails アプリを新規作成して Hello world に成功しました!
まだまだアプリケーション開発のほんの入り口ですが、Docker を使って Web アプリを起動させることができただけでもかなり感動があると思いますし、ここまでさほど大変ではないことも感じてもらえたかなと思います。これこそが Rails や Docker の偉大なところですね。

次回は、scaffoldという Rails の便利機能を使って、サンプル Web アプリケーションを作ってみようと思います。このscaffoldで作成できるアプリケーションが Rails の基本的なアプリケーションの形になりまして、その中には Rails アプリケーションを語る上では避けられないRESTfulMVCの要素が詰まっていますので、その辺りも合わせて学んでいけるようにしようと思います。

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Reference

P.S. 間違っているところ、抜けているところ、説明の仕方を変えるとよりわかりやすくなるところなどありましたら、優しくアドバイスいただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す 119(filter_paramters 編)

はじめに

Rails 6 に追加された新機能を試す第119段。 今回は、filter_parameters 編です。
Rails 6 では、 filter_parameters で proc を使って、フィルターしたときに、パラメータが配列になっているときに、フィルターされないというバグが修正されています。

Ruby 2.6.5, Rails 6.0.2.1, Rails 5.2.4.1 で確認しました。 (Rails 6.0.0 でこの修正が入っています。)

$ rails --version
Rails 6.0.2.1

今回は、User を2つ登録する画面を作って、その動作を確認します。
なお、 filter_parameters の動作を確認するための手抜き実装になってます。

Rails プロジェクトを作る

Rails プロジェクトを新たに作成します。

$ rails new rails_sandbox
$ cd rails_sandbox

User モデルを作る

name を持つ User モデルを作成します。

$ bin/rails g model User name

Controller と View を作る

User の controller と View を作ります。 View は手抜き実装で、 一覧画面 (index) と 登録画面 (new) だけです。

$ bin/rails g controller Users index new

routes を定義する

User 用にルーティングを定義します。 これまた、手抜き実装で、 index, new, create の3つだけにします。

config/routes.rb
Rails.application.routes.draw do
  resources :users, only: %i[index new create]
end

一覧画面 (index) を作成する

一覧画面を作成します。これまた、 table タグも使わない手抜き実装です。

Username を1件ずつ表示します。

画面の下に、登録画面へのリンクを表示します。

app/views/users/index.html.erb
<h1>Users#index</h1>

<% @users.each do |user| %>
  <p>
    <%= user.name %>
  </p>
<% end %>

<%= link_to 'New', new_user_path %>

登録画面 (new) を作成する

登録画面を作成します。ここで、今回の機能を試すためにパラメータの値が配列になるようにします。
(結果として2件のデータを登録することになります。)

パラメータが配列となるように、form.text_field の引数が name="names[]" としていることに注意してください。
これが、今回の機能を試すために必要なことの1つです。

app/views/users/new.html.erb
<h1>Users#new</h1>

<%= form_with url: users_path do |form| %>
  <p>
    <%= form.text_field name="names[]" %>
  </p>
  <p>
    <%= form.text_field name="names[]" %>
  </p>
  <%= form.submit %>
<% end %>

UsersController を完成させる

今回の機能を確認するために、完成させる必要は無いのですが、一応、動作するように実装します。
これまた、手抜き実装です。

index メソッドでは、全データを取得します。
create メソッドでは、パラメータを元にして User をデータベースに保存し、一覧画面にリダイレクトします。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def new
  end

  def create
    params[:names].each do |name|
      User.create(name: name)
    end
    redirect_to users_path
  end
end

filter_parameters を指定して、 names パラメータの値がフィルタリングさせる

いよいよ、今回の機能を確認するために、 filter_parametersnames パラメータの値をフィルタリングさせます。

proc を追加していることに注意してください。

config/application.rb
...
module App
  class Application < Rails::Application
    ...
    config.filter_parameters << lambda do |key, value|
      if key =~ /names/
        value.replace('[FILTERED]') if value.respond_to?(:replace)
      end
    end
  end
end

マイグレーションを実行する

$ bin/rails db:create db:migrate

rails server を実行して、登録画面から登録する

bin/rails s を実行し、 http://localhost:3000/users/new にアクセスし、2つのフィールドに適当に入力して、 Save ボタンを押します。

このとき、コンソールでは、 names パラメータが、 [FILTERED] となっていることに注意してください。

Started POST "/users" for 192.168.16.1 at 2020-01-25 02:48:55 +0000
Processing by UsersController#create as JS
  Parameters: {"authenticity_token"=>"..., "names"=>["[FILTERED]", "[FILTERED]"], "commit"=>"Save "}
     (0.2ms)  BEGIN

Rails5 では

Rails 5.2.4.1 では、 フィルターされず、そのまま画面で入力した値が表示されてしまいます。

Started POST "/users" for 192.168.0.1 at 2020-01-25 01:42:25 +0000
Processing by UsersController#create as JS
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"..., "names"=>["aaa", "bbb"], "commit"=>"Save "}

あくまで手抜きのコードです

実際のアプリでは、複数のデータを1つの画面で登録する場合には、 Form オブジェクトを使ったり、Validation を追加したり、Strong Paramters を使ったりすると思います。
今回は、あくまでも、機能の確認のための実装なので、そういった点は、すっとばして、手を抜いてます。
このままコードを再利用することはオススメしません。

試したソース

https://github.com/suketa/rails_sandbox/tree/try119_filter_parameters_proc

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby コーティング規約について 構文編 2

はじめに

レイアウト編はこちらをクリック願います。
構文編 1 はこちらをクリック願います。
Rubyの基礎を学習中の方に向けて記載致します。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。

構文について

① 本文が1行のときは、if修飾子を優先的に使う。
  他の良い代替案としては&&を使った制御構文がある。
  ワンライナーにする。  

qiita.rb
str = "is he raigakun?"
#以下、実践します。


#悪い例 (長い)
if str.include?("?")
  puts "yes, he is."
end


# 良い例
puts "yes, he is." if str.include?("?")

#もしくは

if str.include?("?") && puts "yes, he is."

② thenは複数行にまたがるif/unlessでは使ってはいけない。
  私、使ってました・・・

qiita.rb
# 悪い例
if some_condition then
  # 本文省略
end

# 良い例
if some_condition
  # 本文省略
end

さいごに

毎日更新します。
皆様の復習等にご活用頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

文字列のハッシュロケットを直せと言われて「?」ってなった

修正前(application.html.haml)

application.html.haml
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/

<レビュー結果>
文字列のハッシュロケットを直しましょう
【参考】シンボルと文字列の違い

<考えたこと>
文字列をキーにしない方がいいんだな
=「""」or「''」で囲ったものをキーにしない方がいいんだな

修正1回目(application.html.haml)

application.html.haml
%meta{:content => "text/html; charset=UTF-8", :http-equiv => "Content-Type"}/

<レビュー結果>
文字列のハッシュロケットを直しましょう(全く同じ回答)

<考えたこと>
(キーをシンボル型に直したけど…?)
ハッシュロケットを使うなってこと?
→レビュワーに質問して認識のズレがないことを確認

修正2回目(application.html.haml)

シンボル型について調査
「"hoge":」は文字列型をキーにしている書き方だと思い込んでいたが、実際は「:」をつけることで、文字列ではなくシンボル型のキーになっている
"hoge": → :hoge(シンボル型)

application.html.haml
%meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/

<レビュー結果>
LGTM

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む