- 投稿日:2020-02-04T23:35:40+09:00
面接でポートフォリオはなんのwebサーバーで動かしているのか聞かれてよく分からなかったので調べた
面接でポートフォリオはなんのwebサーバーを使っているのか聞かれました
プログラミング教室で教えてもらったのは、
rails sとすればlocalhost:3000にアクセスすれば動く。そんな感じだったので、webサーバーやアプリケーションサーバーについて意識することはありませんでした。なんのサーバーを使っているのか気にしたことがありませんでした。なので、なにも答えられずww
基本が分かっていなかったので、一体どういうことか調べてみました。webの仕組みをさらっと復習
webの仕組みは下の図のようになっています。
クライアントが要求を出して、静的な処理なら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を使ってみよう!
- 投稿日:2020-02-04T23:00:06+09:00
敷き詰めパズルの解をアルゴリズム「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_problemABCDEFGHIJKLMNOP|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-step1ABCDEFGHIJKLMNOP|abc|z ______________________ #.#.........#..#|...|# 00行は
00一択なので、これを解の候補に選びます。
候補を選んだら、#が被っている他の行は解にならないので消します。すると以下のようになります。(選んだ列zは行を抜き出した時点で役目が終わるので先に消しています。)x-depth1-step2.1ABCDEFGHIJKLMNOP|abc ____________________ #.#.........#..#|... 00 ____________________ .#..#.##........|#.. 11 ...#..#.##......|#.. 13 ....#..#.##.....|#.. 14 .....#..#.##....|#.. 15 ........#..#.##.|#.. 18 .#.#..##........|.#. 21 ...#.#..##......|.#. 23 ....#.#..##.....|.#. 24 .....#.#..##....|.#. 25 ........#.#..##.|.#. 28 ...##...##......|..# 33 ....##...##.....|..# 34 .....##...##....|..# 35 ........##...##.|..# 38そして候補に
#がある列[ACMP]自体も消します。するとより小さな問題の形になります。x-depth1-step3.1BDEFGHIJKLNO|abc ________________ #.#.##......|#.. 11 .#..#.##....|#.. 13 ..#..#.##...|#.. 14 ...#..#.##..|#.. 15 ......#..###|#.. 18 ##..##......|.#. 21 .#.#..##....|.#. 23 ..#.#..##...|.#. 24 ...#.#..##..|.#. 25 ......#.#.##|.#. 28 .##...##....|..# 33 ..##...##...|..# 34 ...##...##..|..# 35 ......##..##|..# 38今度は列
Bに注目すると解の候補が11と21の2つあるので、ひとつずつ試す必要があります。x-depth2-step1BDEFGHIJKLNO|abc ________________ #.#.##......|#.. 11 ##..##......|.#. 21まずは
11を候補に選んでみて、同様に#の被る行を消し、#の列も消します。x-depth2-step2.1DEFGHIJKLNO|abc _______________ .#.##......|#.. 11 _______________ #.#..##....|.#. 23 .....#.#.##|.#. 28 .....##..##|..# 38x-depth2-step3.1DFIJKLNO|bc ___________ ####....|#. 23 ..#.#.##|#. 28 ..##..##|.# 38すると
#の無い列が現れました。これはもうどの行を選んでも解にならないということで、探索失敗です。「その列に注目すると解の候補が0個見つかる」と考えてもいいです。問題を遡り、今度は
21を候補に選んでみます。x-depth2-step2.2DEFGHIJKLNO|abc _______________ #..##......|.#. 21 _______________ ..#..#.##..|#.. 15 .....#..###|#.. 18 .##...##...|..# 34 .....##..##|..# 38x-depth2-step3.2EFIJKLNO|ac ___________ .##.##..|#. 15 ..#..###|#. 18 ##.##...|.# 34 ..##..##|.# 38列
Eが#1個なので、これをもとに行34を選びます。x-depth3-step1EFIJKLNO|ac ___________ ##.##...|.# 34x-depth3-step2.1FIJKLNO|ac __________ #.##...|.# 34 __________ .#..###|#. 18x-depth3-step3.1ILNO|a ______ ####|# 18もう目視では答えが見えていますが、きちんと最後まで同じアルゴリズムを適用します。列
Iをもとに行18を選びます。x-depth4-step1ILNO|a ______ ####|# 18x-depth4-step2.1LNO|a _____ ###|# 18 _____x-depth4-step3.1列が無くなったため集合をぴったり埋められたことになり、選択した行
[00, 21, 34, 18]は解です(順不同)。他にも解があるか探すなら、探索を遡って他の候補を選び直せばいいです。今回は候補を選び尽くしたので、解はこの1個のみとわかります。コード
実装は次節の「DLX」に回します。
効率
探索の順序を図にすると以下のようになります。候補の少ない列(
_で表現)から攻めることで非常に短く済んでいます。(「単純な深さ優先探索」のときは、列をピースのa→b→cと順序固定で選んでいたことになります)search tree................|...|_ └── #_#.........#..#|...|# ├── #a#.a.aa..._#..#|a..|# └── #b#b_.bb....#..#|.b.|# └── #b#bccbb_cc.#..#|.bc|# └── #b#bccbbacca#aa#|abc|# └── // solution表や図ではマス目とピースを区切っていますが、厳密被覆問題としては両者に区別はありません。これによって、「このマス目を埋められるピースはこれらだけ」と「このピースを置ける場所はここらだけ」という2通りの絞り込み方が自然とアルゴリズムに入っています。
DLX
見てきたAlgorithm Xは、表を作るのに
0/1やtrue/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.rbmodule 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 end2次元の敷き詰めパズルに適用
2次元でも解き方は同じです。全てのマス目とピース種別(+設問)を要素とする集合を表の列として用意し、各ピースが可能な配置を表の行としてひたすら列挙します。配置はフィールドの左上から右下まで、加えて回転した場合も必要です。
冒頭の形式の入力をパースして解を列挙するプログラムを示します。(1次元にも回転ありで対応しています)
puzzle_box.rbrequire './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参考
- 元論文
- Knuth, Donald E. (2000), "Dancing links", Millenial Perspectives in Computer Science, pp. 187-214. (arXiv:cs/0011047 [cs.DS])
- Wikipedia英語版
- 日本語での解説
- 「厳密被覆」という日本語訳を使っているページ
- ゲーム情報
- 投稿日:2020-02-04T20:28:30+09:00
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
- 投稿日:2020-02-04T17:36:02+09:00
【Rails】Action Mailerのメール送信に失敗する
症状
Action Mailerを利用したメール送信処理で、以下のエラーを吐いてしまう。
なお以前は正常に送信することができていた。
Net::SMTPAuthenticationError (530 Authentication required):
ArgumentError (SMTP To address may not be blank: []):原因
環境変数を正常に読み込めていなかった。
SMTP認証情報やお問い合わせ宛先メールアドレスを環境変数に入れていたため、それらの情報を利用することができず、送信に失敗した。対処
1.アプリケーションサーバー停止
pumactl stop2.アプリケーションサーバー起動
rails s根本原因は?
不明。
EC2のインスタンスを止めたり、立ち上げたりしていたので、それが契機っぽいが。。
- 投稿日:2020-02-04T17:18:19+09:00
Heroku Aws::Sigv4::Errors::MissingCredentialsError 本番環境へのアップロード
初めての投稿で緊張しておりますが、ようやくエラー解除できたので、共有します!
rails5.2で作成したアプリケーションをHerokuへアップロードする際に出たエラーです。
エラーの主な原因としてAWSのS3機能とRails5.2以上から登場したCredentialの機能をHerokuに上手く設定できなかったことが原因みたい。(具体的な内容)
Herokuにアップロードし、heroku openをすると
こちらのエラーが出てきてなかなか開けません。ですので、heroku run rails cでエラーの内容を確認すると、以下のエラー内容が表示されます。(Aws::Sigv4::Errors::MissingCredentialsError)
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を選択します。
こちらを参考に、
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>エラーが出ない!嬉しい限りです。
そして、、、$ heroku open ▸ Error opening web browser. ▸ Error: Exited with code 3 ▸ ▸ Manually visit https://herokuapplication.herokuapp.com/ in your ▸ browser.Herokuにアクセスできるようになった!
以上より解決???Herokuに上げた後に直接環境変数を代入するのは初めてだったので、戸惑いましたが、なんとか完了!ターミナルからも本来はできると思うので、ここらへんの知識をもっと勉強しなくてはなりません。?
- 投稿日:2020-02-04T17:18:19+09:00
Aws::Sigv4::Errors::MissingCredentialsError 本番環境へのアップロード
初めての投稿で緊張しておりますが、ようやくエラー解除できたので、共有します!
rails5.2で作成したアプリケーションをHerokuへアップロードする際に出たエラーです。
エラーの主な原因としてAWSのS3機能とRails5.2以上から登場したCredentialの機能をHerokuに上手く設定できなかったことが原因みたい。(具体的な内容)
Herokuにアップロードし、heroku openをすると
こちらのエラーが出てきてなかなか開けません。ですので、heroku run rails cでエラーの内容を確認すると、以下のエラー内容が表示されます。(Aws::Sigv4::Errors::MissingCredentialsError)
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を選択します。
こちらを参考に、
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>エラーが出ない!嬉しい限りです。
そして、、、$ heroku open ▸ Error opening web browser. ▸ Error: Exited with code 3 ▸ ▸ Manually visit https://herokuapplication.herokuapp.com/ in your ▸ browser.Herokuにアクセスできるようになった!
以上より解決???Herokuに上げた後に直接環境変数を代入するのは初めてだったので、戸惑いましたが、なんとか完了!ターミナルからも本来はできると思うので、ここらへんの知識をもっと勉強しなくてはなりません。?
- 投稿日:2020-02-04T17:03:31+09:00
RailsのActionCableを使ってリアルタイムチャット機能を試してみた。
RailsのActionCableの機能を使って、リアルタイムチャットを作ってみた。
しかし、ActionCableの全容をつかむことが出来ず、わかる範囲での記述となります。
間違ったことを書いてある場合は、コメント欄で指摘していただきますと大変助かります。・目的
HTTP通信では、クライアントがホストに対してリクエストを申請して、初めてデータを取得することができる。
しかし、それでは複数のクライアントがホストにアクセスするような場合に、随時ホストにリクエストしなければデータが取得できない。そのため、ホストとクライアントが相互に監視し、データの変化に即座に反応するウェブページを作る。・実践
まず、事前準備としてRailsプロジェクトを生成します。
続いて、以下のコマンドでchannelを生成します。channel名はsend_msgです。rails g channel send_msg次に、ActionCable.serverをmountするための記述です。
routes.rbmount ActionCable.server => "/cable"chat_channel.rbでは、クライアント側のデータをホストに引き渡すための記述となります。
*ざっくり過ぎる表現
subscribedメソッド内でホストにストリームするための値として"room"を定め、send_msgメソッドのActionCable.server.broadcastの後ろにも共通の値が記述されています。これは、クライアントとホスト間の通信について、あらかじめコネクションのためのインスタンスが生成されており、これをsubscribed(購読と訳されていたが、予約的な意味合い?)して、サーバーにbroadcast(引き渡す的な)する。その具体的な内容はmessage:data['message']ですよ!と素直に読み取りました。
*勝手な解釈なので、お気をつけ下さい。chat_channel.rbclass 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.coffeeApp.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:datachat.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>・まとめ
Railsの機能や基礎技術に対する不理解が露呈する場面が多く、よくわからないけど動いているという状況だと思います。ホストとクライアント間の通信をリアルタイムにする機能について、あらためて向き合う必要があるようです。以上です。参考
Action Cable の概要 : https://railsguides.jp/action_cable_overview.html
Rails WebSocket Chat Real Time : https://www.youtube.com/watch?v=kJbuZecN1c8
- 投稿日:2020-02-04T14:36:58+09:00
config/initializeにカスタムバリデータを定義する
電話番号にバリデーションを使用する
telephone_numberというgemを使用してカスタムバリデーションを定義する。
まずはgemを定義
gem 'telephone_number'そしてbundle install!
$ bundleconfig/initializeに下記のように定義
config/initialize.rbmodule 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.rbvalidates :tel, tel: true参考記事
- 投稿日:2020-02-04T14:36:58+09:00
config/initializersにカスタムバリデータを定義する
電話番号にバリデーションを使用する
telephone_numberというgemを使用してカスタムバリデーションを定義する。
まずはgemを定義
gem 'telephone_number'そしてbundle install!
$ bundleconfig/initializeに下記のように定義
config/initializers/tel_validator.rbmodule 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.rbvalidates :tel, tel: true参考記事
- 投稿日:2020-02-04T12:17:26+09:00
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-versionVScode上でruby-rubocopを実行する時にデフォルトのruby環境(rbenv versionsコマンドを打った時のsystem)のままであり、rubocopがインストールされていない状況であった
解決方法
以下のどちらかの方法で解決できる
1.VScodeのruby実行環境を2.6.0にする
.ruby-versionを.vscodeがある~/に置く2. デフォルトのruby環境にrubocopをインストールする
- 投稿日:2020-02-04T12:04:56+09:00
コード書いたことない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) 用のコンテナを作っていきます。
まず、作業用のディレクトリを作っておきましょう。$ 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
DockerfileFROM 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命令でGemfile、Gemfile.lockをイメージ内にコピーして、bundle installを実行してます。bundle installはGemfileの内容に沿ってgemをインストールするコマンドです。Rails 自体もgemでインストールできるのでGemfileにrailsを記入しておけばこのbundle installの際にインストールされます。Gemfile.lockはすでにインストール済のgemのバージョンなどを管理して無闇にバージョンアップさせないようにしてくれます。最後に
COPY . $HOMEでローカルホストのファイルを一式イメージ内にコピーすることで Rails アプリケーションを起動させられる Docker image を作ることができます。Gemfile
Gemfilesource '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.ymlversion: '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まず、サービスとして
dbとwebの二つがあることがわかるかと思います。dbは文字通りデータベース用のコンテナ(サービス)、webは Rails アプリケーションを動作させるコンテナ(サービス)です。dbコンテナではimageにpostgres:12.1-alpineを指定しています。
docker-compose.ymlについては前回もお話したので、前回お話していない項目を中心にお話します。
environment: コンテナを起動する時に環境変数としてセットする。この場合、TZ(タイムゾーン)を東京にしてる。volumes: コンテナからホストのディレクトリをマウントしている。左がホストのパス、右がコンテナ内のパス。ホストのパスはこのdocker-compose.ymlの場所からの相対パスで書いてます。こう書くことで簡単にいえば、ホストのパスとコンテナのパスを同期しているイメージになり、ホストでファイルを編集すればコンテナ内にも反映され、コンテナ内でファイルが編集されればホストのファイルにも反映されるという関係を気づけます。コンテナはステートレスなので一度コンテナを削除して新しくコンテナを起動させた場合、最初のコンテナ(削除したコンテナ)内で変更されたデータは全てなかったことになってしまうのですが、ホストのディレクトリに同期しておくことで次に起動するコンテナもそのディレクトリをマウントするのでデータが永続化されるようになります。build:build: .でdocker-compose.ymlと同じディレクトリのDockerfileをbuildしたイメージを使ってコンテナを起動するようになります。depends_on: コンテナの依存関係を定義します。今回の場合『webはdbに依存している』と定義していることになりますが、これは次の2つが実現されます。
docker-compose upした時に、dbコンテナが起動してからwebコンテナを起動するdocker-compose up webでwebコンテナを起動させようとした場合、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 runはdocker-compose.ymlの定義にそって対象のコンテナを立ち上げてその中でコマンドを実行し、実行後にコンテナを停止するコマンドです。つまり、webコンテナを立ち上げてrails new . -fGTd postgresqlをwebコンテナ内で実行してくれます。
--rmオプションはコマンド実行後にコンテナを停止した後に削除までしてくれるオプションです。基本停止したコンテナは不要だと思うのでこのオプションをつけるのがよきかと思います。
--no-depsオプションはdocker-compose.ymlでdepends_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.ymlでDockerfileからのビルドが必要なサービスのイメージビルドをすべて実行してくれます。今回は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 } %> ...
host、username、passwordがデフォルトから新たに追加した項目です。
hostはDBのホスト名です。今 Rails アプリが稼働しているwebコンテナはdocker-compose.ymlが作る Docker ネットワークの中にいます。この中ではサービス名で名前解決してコンテナが相互に接続することができます。つまりhostとしてdbを設定することでdocker-compose.ymlでdbのサービス名で定義されたコンテナに接続できるようになるのです。
usernameとpasswordは何の値なのでしょうか?これはdbコンテナのイメージで指定したpostgresのデフォルト値です。passwordは同じホストからのアクセスであれば省略が可能になっています。postgresイメージのusernameとpasswordはそれぞれPOSTGRES_USERとPOSTGRES_PASSWORDの環境変数で定義することもできます。データベースの設定はここまでです。このデフォルトの設定値が開発環境(
development)、テスト環境(test)の設定値として反映されるようになっています。データベースを作成する
データベースの接続設定を定義したので、データベースを実際に作成していきます。
Rails ではデフォルトで本番環境(production)、開発環境(development)、テスト環境(test)の3つの環境(environment)が用意されています。特別に環境を指定しない場合、開発環境で挙動するようになっています。
データベースの作成はrails db:createコマンドを使いますが、このコマンドは開発環境とテスト環境用にデータベースを作成してくれます。本番環境用のデータベースを作成する場合は、RAILS_ENV=productionをオプションとしてつけます。
今回はまず開発環境向けにデータベースの作成を行いたいので、以下のコマンドを実行します。$ docker-compose run --rm rails db:createHello Ruby on Rails on Docker!!
ここまでで Hello world に必要な作業は全て完了しました。
コンテナを立ち上げて Hello world ページが表示されることを確認しましょう。$ docker-compose up -dRails アプリは少し起動に時間がかかります。すぐに
http://localhost:3000にアクセスしてもまだアプリケーションが起動していないこともありますので、その場合はdocker-compose logsコマンドを使ってアプリケーションの起動状態を確認してみましょう。$ docker-compose logs -f
-fオプションはログの変化をリアルタイムでコンソールに表示するためのオプションです。Rails アプリが起動した場合web_1 | Use Ctrl+C to stopというログが表示されます。この表示を確認したら
Ctrl+Cでdocker-compose logsから抜け出しましょう。では、
http://localhost:3000にアクセスしてみましょう!
このようなページが表示されたでしょうか?このページが Rails アプリケーションの第一歩、つまり Hello world です。おめでとうございます!これでもう『Rails は Hello world までならやったことあります』と自慢することができます!ここまでできたら後片付けをしておきましょう。
このままではコンテナが起動しっぱなしになってしまうので、最後にコンテナを停止させておきます。$ docker-compose downこれでコンテナを停止させたので、
http://localhost:3000にアクセスしても先ほどのページは表示されなくなっていることでしょう。まとめ
今回は、
Dockerfileやdocker-compose.ymlなどのファイルを作成し、Rails アプリケーションを稼働させられる Docker イメージ、Docker コンテナを作成してみました。
さらに、Rails アプリを新規作成して Hello world に成功しました!
まだまだアプリケーション開発のほんの入り口ですが、Docker を使って Web アプリを起動させることができただけでもかなり感動があると思いますし、ここまでさほど大変ではないことも感じてもらえたかなと思います。これこそが Rails や Docker の偉大なところですね。次回は、
scaffoldという Rails の便利機能を使って、サンプル Web アプリケーションを作ってみようと思います。このscaffoldで作成できるアプリケーションが Rails の基本的なアプリケーションの形になりまして、その中には Rails アプリケーションを語る上では避けられないRESTfulやMVCの要素が詰まっていますので、その辺りも合わせて学んでいけるようにしようと思います。では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Reference
P.S. 間違っているところ、抜けているところ、説明の仕方を変えるとよりわかりやすくなるところなどありましたら、優しくアドバイスいただけると助かります。
- 投稿日:2020-02-04T12:04:29+09:00
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_sandboxUser モデルを作る
nameを持つ User モデルを作成します。$ bin/rails g model User nameController と View を作る
User の controller と View を作ります。 View は手抜き実装で、 一覧画面 (index) と 登録画面 (new) だけです。
$ bin/rails g controller Users index newroutes を定義する
User 用にルーティングを定義します。 これまた、手抜き実装で、
index,new,createの3つだけにします。config/routes.rbRails.application.routes.draw do resources :users, only: %i[index new create] end一覧画面 (index) を作成する
一覧画面を作成します。これまた、
tableタグも使わない手抜き実装です。
Userのnameを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.rbclass 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 endfilter_parameters を指定して、 names パラメータの値がフィルタリングさせる
いよいよ、今回の機能を確認するために、
filter_parametersでnamesパラメータの値をフィルタリングさせます。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:migraterails 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) BEGINRails5 では
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
参考情報
- 投稿日:2020-02-04T10:08:21+09:00
Ruby コーティング規約について 構文編 2
はじめに
レイアウト編はこちらをクリック願います。
構文編 1 はこちらをクリック願います。
Rubyの基礎を学習中の方に向けて記載致します。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。構文について
① 本文が1行のときは、if修飾子を優先的に使う。
他の良い代替案としては&&を使った制御構文がある。
ワンライナーにする。qiita.rbstr = "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さいごに
毎日更新します。
皆様の復習等にご活用頂けますと幸いです。
- 投稿日:2020-02-04T08:58:44+09:00
文字列のハッシュロケットを直せと言われて「?」ってなった
修正前(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







