プログラミング漫遊記

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

【Ruby】2重配列から特定の位置にある要素の配列を作る

先日、ド忘れして困ったので、忘備録として。

たとえば1つ目の要素だけを集めた配列を作りたいとする。 こんな感じ。

# 1つ目の要素だけを集めた配列を作るイメージ
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
=> [1, 4, 7]

Enumerable#mapを使うと簡単

ary = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

ary.map(&:first)
=> [1, 4, 7]

もう少し一般化する。 n番目の要素だけを集めた配列を作りたい場合

ary = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

ary.map { |a| a[n - 1] }

番外編としてArray#transposeを使う方法もある

ary = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ary.transpose[n - 1]

Enumerableモジュールのメソッドツアー - max_byメソッド

Enumerableモジュールのメソッドを1記事1メソッドで雑に紹介していくコーナー第3弾です。

前回はmaxメソッドを紹介しました。

haruguchi-yuma.hatenablog.com

今回はmaxメソッドにとても似ているmax_byメソッドについて紹介します。

max_byメソッド

まずはるりま*1の確認から。

module Enumerable (Ruby 3.1 リファレンスマニュアル)

各要素を順番にブロックに渡して実行し、その評価結果を <=> で比較して、最大であった値に対応する元の要素、もしくは最大の n 要素が降順で入った配列を返します。 引数を指定しない形式では要素が存在しなければ nil を返します。引数を指定する形式では、空の配列を返します。該当する要素が複数存在する場合、どの要素を返すかは不定です。 Enumerable#max と Enumerable#max_by の違いは Enumerable#sort と Enumerable#sort_by の違いと同じです。

「各要素をブロックに渡して実行し、〜」とあるようにブロック付きで呼ぶのが基本となるメソッドみたいです。 maxメソッドと同じく、なんらかの観点で比較したときの、最大の要素を引数の数だけ取得するのが基本の使い方になります。

ちなみに、ブロックなしで呼び出した場合はEnumeratorオブジェクトが返ってきます。

では挙動を確認していきます。

irbで挙動の確認

Ruby 3.1.2で実行しています。

ブロックありでの呼び出し

# 一の位が最大である要素を取得
[8, 32,4].max_by{ |n| n % 10}
# => 8

# イメージ
8 % 10 <=> 32 % 10で比較
8が大きい
8 % 10 <=> 4 % 10 で比較
8が大きい
8を返す

# 最大の要素がない場合はnil
[].max_by{ |n| n }
=> nil

引数を指定すると最大の要素を引数の数だけ取得することができます。 また、返り値は配列になります。

# 文字数で比較
%w(panda monkey dog cat).max_by(2) { |str| str.length }
=> ["monkey", "panda"]

# 引数に負数を渡すとArgumentError
%w(panda monkey dog cat).max_by(-1) { |str| str.length }
`max_by': negative size (-1) (ArgumentError)

ちょっとだけ実用的な例を見てみましょう。 数学のテストの点数が一番高いユーザーのテスト結果を調べます。

students = [
  {name: 'taro', japanese: 30, math: 50, english: 90},
  {name: 'jiro', japanese: 80, math: 23, english: 20},
  {name: 'saburo', japanese: 88, math: 92, english: 79},
]

students.max_by { |student| student[:math] }
=> {:name=>"saburo", :japanese=>88, :math=>92, :english=>79}

ブロックなしで呼び出した場合

ブロックなしで呼び出した場合はEnumeratorオブジェクトを返します。

('a'..'c').max_by
=> <Enumerator: ...>


('a'..'c').max_by(2)
=> <Enumerator: ...>

maxメソッドとmax_byメソッドの違い

言葉で説明するのは難しいのですが、

maxメソッドは要素を2つずつ取り、明示的に<=>メソッドで比較を行います。(正確には左辺が大きいときは正の数、右辺が大きいときは負の数、等しいときは0を返せば<=>メソッドでなくても良いです)

一方、max_byメソッドは比較基準だけ示しておけば、内部的に<=>メソッドで比較を行ってくれます。

パフォーマンスの観点からどちらが優れているというのは未検証のため、言及は控えておきます。

# maxメソッドの場合
%w(panda monkey dog cat).max { |animal1, animal2| animal1.length <=> animal2.length }
=> "monkey"


# max_byメソッドの場合
%w(panda monkey dog cat).max_by { |animal| animal.length }
=> "monkey"

何かしらの観点で最大の要素を取得したい場合はmax_byを使った方が記述量は少なくすみそうです。

まとめ

  • max_byは最大の要素を取得する
  • ブロックを評価した後<=>で比較処理が行われる
  • 引数に正整数を渡すとその数だけ最大の要素を取得できる
  • ブロックなしで呼び出した場合はEnumeratorオブジェクトが返ってくる

以上です!

*1:Rubyリファレンスマニュアルの略

Enumerableモジュールのメソッドツアー - maxメソッド

Enumerableモジュールのメソッドを1記事1メソッドで雑に紹介していくコーナー第2弾です。

前回はall?メソッドを紹介しました。

haruguchi-yuma.hatenablog.com

今回はmaxメソッドについて紹介していきます。

maxメソッド

まずはるりま*1の確認から。

module Enumerable (Ruby 3.1 リファレンスマニュアル)

ブロックの有無で説明が分かれていました。

詳しい説明はるりまに譲るとして、雑に紹介するなら、最大の要素を引数の数だけ返すメソッドということになります。

最大の要素がなければ、nilを返します。

早速irbなどで試してみましょう。

irbで挙動の確認。

Ruby3.1.2で確認しています。

Enumerable#maxの説明ではありますが、ArrayやRangeクラスなどオーバーライドしているmaxメソッドも併せて紹介します。

ブロックなしの場合

ArrayやRangeオブジェクトに対して引数ありと引数なしでmaxメソッドを呼び出しています。

[1, 2, 3, 4, 5].max
=> 5

[1, 2, 3, 4, 5].max(2)
=> [5, 4]

(1..10).max
=> 10

(1..10).max(3)
=> [10, 9, 8]

('a'..'z').max
=> "z"

('a'..'a').max
=> 'a'

[].max
=> nil

引数なしの場合は返り値は最大の要素そのものが返ってきており、最大が見つからない(空配列など)場合はnilを返していることがわかります。 引数ありの場合は返り値は配列です。引数で指定した要素数だけ最大値を選び、降順に並べて返します。

# 引数なし
[].max
=> nil

(2..1).max
=> nil

# 引数あり
[].max(2)
=> []

(2..1).max(2)
=> []

上のコードを試して個人的にはびっくりしたポイントです。引数有りの場合は最大が見つからなくても配列を返すというのがわかりやすいです。

難しい話ですが、maxメソッドで最大の要素とは何かというと<=>で比較できるものみたいですね。 module Comparable (Ruby 3.1 リファレンスマニュアル)

100 <=> 1 # 比較できる
=> 1

1.2 <=> 1 # 比較できる
=> 1

'a' <=> 'aa' # 比較できる
=> -1

true <=> false # 比較できない
=> nil

1 <=> 'a' # 比較できない
=> nil

上の例でいくとbooleanは比較できないのでmaxメソッドで最大の要素は取得できません。(true <=> trueは0なので等価性は比較できる)

また、Integerとstringなど型が違う場合(Integerとfloatは除く)<=>で比較できないのでmaxメソッドは例外を発生させます。

 [true, false, true, false].max
=> comparison of TrueClass with false failed (ArgumentError)

[1, 'a', 3,5, false].max
=> comparison of String with 1 failed (ArgumentError)

Hashはmaxメソッドをオーバーライドしていません。

{a: 3, b: 2, c: 1}.max
=> [:c, 1]

{a: 3, b: 2, c: 1}.invert.max # invertはkey valueをひっくり返すメソッド
=> [3, :a]

{a: 3, b: 2, c: 1}.max(2)
=> [[:c, 1], [:b, 2]]

おそらくですが、keyで大小比較をして、[key, value]の配列(つまり要素)で返しているみたいです。

ブロック有りの場合

るりまの説明です。

ブロックの評価結果で各要素の大小判定を行い、最大の要素、もしくは最大の n 要素が入った降順の配列を返します。引数を指定しない形式では要素が存在しなければ nil を返します。引数を指定する形式では、空の配列を返します。ブロックの値は、a > b のとき正、 a == b のとき 0、a < b のとき負の整数を、期待しています。該当する要素が複数存在する場合、どの要素を返すかは不定です。

なんだか難しいですが、ブロックパラメータには要素が2つずつ入ってきます。この2つの要素をブロック内で比較して左辺が大きい場合は正の数を返し、左辺が小さい場合は負の数を返し、左辺も右辺も等しい場合は0を返すようにすればOKです。そうすることで「どの観点で最大値を取るか」を柔軟に指定することができます。

文字列なら通常は辞書順ですが、ここでは文字の長さで最大値を取りたいとします。つまり、文字列が長い方が最大であると定義します。

['monkey' 'Gorilla gorilla', 'cat'].max
=> "monkey" # この場合は辞書順

['monkey', 'Gorilla gorilla', 'cat'].max do |a, b|
  if  a.length > b.length
    1000 # 左辺が大きい場合 正の数を返す
  elsif a.length < b.length
    -1000 # 左辺が小さい場合 負の数を返す
  else
    0 # 等しい場合 0を返す
  end
end
=> "Gorilla gorilla"

今回は文字数で比較したいのでlengthメソッドを利用しています。

ブロックの値は、a > b のとき正、 a == b のとき 0、a < b のとき負の整数を、期待しています。

ブロックの評価で数値を返す部分は適当な数を返しましたが、本来<=>を使うともっと簡単に記述することができます。

['monkey', 'Gorilla gorilla', 'cat'].max { |a, b| a.length <=> b.length }
=> "Gorilla gorilla"

ブロックを渡すことで最大とは何か?自分で定義できるのがいいところですね。 下の例は文字列の最後の文字を辞書順に比較する例です。 (そんなことしたい場合はなさそう😅)

['monkey', 'Gorilla gorilla', 'cat'].max { |a, b| a[-1] <=> b[-1] }
=> 'monkey'

monkeyの最後の文字が'y'なのでうまくいってるようです。

まとめ

雑にまとめます。

  • maxメソッドは最大の要素を返す
  • 引数を指定するとその指定した引数の要素数だけ配列にして返す(降順)
  • ブロックを渡すことができる -ブロックでは2つの要素を比較し、左辺が大きければ正の数、左辺が小さければ負の数、左辺と右辺が等しければ0を返す
  • そういう決まりを守ることで最大とは何かを柔軟に定義できる

個人的にmax_byとよく混同してしまうので今回整理できてよかったです。

*1:Rubyリファレンスマニュアルの略

【Ruby】Enumerableモジュールのメソッドツアー - all?メソッド

こんにちは。はるぐちです。

Enumerableモジュールってご存知でしょうか。そうArrayとかHashとかにインクルードされている便利なやつ。

これのおかげで反復処理がスムーズにできると言っても過言ではないEnumerableモジュールですが、メソッドを全部知っているわけではないなーと思って1記事1メソッドでツアーをすることにしました。

ちゃんとした仕様の説明はRubyリファレンスマニュアル(以下るりま)をみてください。僕は雑に紹介していきます。

all?メソッド

module Enumerable (Ruby 3.1 リファレンスマニュアル)

all?メソッドは各要素を調べて、真偽値を返すメソッドです。

すべての要素が真である場合はtrue、1つでも偽である場合はfalseを返します。

言葉だけではイメージがつかないこともあると思うので、早速irbで遊んでみます。

irbで挙動の確認

Ruby3.1で確認しています。

引数なしの場合

引数なしの場合は各要素を真偽値に変換します。 Rubyの場合はnilfalseの場合はfalseになり、それ以外はtrueに変換されます。

[true, false, true].all?
=> false
[0, 'foo', nil].all?
=> false

基本的にrangeオブジェクトやhashオブジェクトに対して引数なしのall?メソッドを呼び出すのはあまり意味がなさそうです。

# なんでもtrueになる
(1..3).all? # 1も2も3もtrueなので返り値はtrue
=> true

{a: 1, b: nil}.all?
=> true

余談ですが、ハッシュのvalueに対してすべての値がtrueか確認したい時はHash#valuesメソッドを使って配列に変換するのが良さそうだと思いました。

# 配列に変換 valueだけ取り出す
{a: 1, b: false}.values
=> [1, false]


{a: 1, b: false}.values.all?
=> false

引数ありの場合

引数あり(ブロックなし)の場合は引数に対して===メソッドを呼び出し各要素と比較します。 ===メソッドの説明は大変なので割愛したい。(挙動がたくさんあって説明しきれないので、、、😅) class Object (Ruby 3.1 リファレンスマニュアル)

[1, 2, 3].all?(Integer)
=> true
[1.2, 2, 3].all?(Integer)
=> true

['foo', 'bar', 'baz'].all?(/\w+/)
=> true
['foo', 'bar', ''].all?(/\w+/)
=> false

これだけだとイメージしにくい方は以下にイメージ図を参考にしてください。

# イメージ図
[1.2, 2, 3].all?(Integer) の場合

# 各要素に対して順番に 引数 === 要素 を実行していく
Integer === 1.2  false
Integer === 2 true
Integer === 3 true

[false, true, true].all?
=> false

実際には1つでもfalseな要素があった場合はそれ以降の要素は確認されずにfalseが返されるみたいなのでこれはあくまでもイメージと思ってください。

ブロックありの場合

もう少し詳細に条件を絞り込みたい場合はブロック付きでメソッドを呼び出します。

ブロックパラメータは各要素で、ブロックの中でbooleanを返すような条件式を記述します。

# すべて3以上かどうかを調べる
(2..5).all? { |v| v >= 3}
=> false

# すべての要素が偶数かどうか調べる
[2, 4, 6, 8].all(&:even?)


# 全員成人かどうか調べる
users = [
  {name: 'taro', age: 40},
  {name: 'jiro', age: 30},
  {name: 'saburo', age: 20}
]

users.all? { |hash| hash[:age] >= 20 }
=> true

ブロックを用いる場合はHashでも使い道がありそうです。

{a: 1, b: 2}.all? { |k, v| Integer === v }
=> true
{a: 1, b: 2.0}.all? { |k, v| Integer === v }
=> false

まぁ、この例だったら配列に変換したほうがよさそうではありますね😅

引数とブロックを与えた場合

引数とブロックを与えた場合はどちらが優先されるのでしょうか?るりまには記述が見当たらなかったので自分で調べていきます。

[1, 2, 3].all?(Integer) { |v| v >= 2 }
warning: given block not used
true

ご覧の通り引数が優先されるみたいです。 warningが出てるので、ひょっとしたらall?メソッドの話ではなく引数とブロックが両立しないブロック付きメソッドの共通の仕様なのか?(未検証)

まとめ

雑にまとめます。

  • all?メソッドは各要素がすべて真ならtrue、そうでないならfalseを返すメソッド
  • 引数ありの場合は引数 === 要素で検証
  • ブロック付きの場合
    • ブロックパラメータ: 各要素
    • ブロックの中にはbooleanが返るような式を記述

【JS】イテレータ(iterator) / 反復可能オブジェクト(iterable object) ってなんだ?

こんにちは。はるぐちです。

最近JavaScriptイテレータについて学習したのでまとめておきたいと思います。 勘違いや誤り等ありましたら、コメントで指摘していただけると助かります🙏

イテレータとは

反復処理などでよく見かけるイテレータ。 わかってるようでわかってないのでまずは言葉の意味を理解するところから。

まずはMDNを見てみます。 イテレーターとジェネレーター - JavaScript | MDN

JavaScript では、イテレーターはシーケンスおよび潜在的には終了時の戻り値を定義するオブジェクトです。

なるほど、よくわからん😅となりますね。

めちゃくちゃざっくりいうと反復処理において、次の値を示すvalueプロパティと,反復処理の終了を表すdoneプロパティの2つのプロパティを返すnextメソッドを実装しているオブジェクトをイテレータと呼びます。

  • イテレータ
    • nextメソッドを持つ
      • 返り値:以下の2つのプロパティを持つオブジェクトを返す
        • value: 反復処理における次の値
        • dole: 反復処理における最後の値が消費されたかどうかを表す。反復処理が終わっていたらtrueになっている

nextメソッドの戻り値を確認してみる

Array.prototype.values()メソッドはイテレータを返すのでnext()メソッドがどのようになっているか、nodeで確かめてみます。

Array.prototype.values() - JavaScript | MDN

> const arr = ['a', 'b', 'c', 'd'];
undefined
> arr.values(); // イテレータが返ってることがわかる
Object [Array Iterator] {}
> const it = arr.values() // イテレータを変数に代入
undefined
> it.next(); // nextメソッドを呼び出すとオブジェクトが返ってくる
{ value: 'a', done: false }
> it.next(); 
{ value: 'b', done: false }
> it.next();
{ value: 'c', done: false }
> it.next();
{ value: 'd', done: false }
> it.next();
{ value: undefined, done: true } // 反復処理が終わったらdone: trueになる
> it.next();
{ value: undefined, done: true } // 反復処理が終わったら何回呼び出しても結果は同じ

イテレータfor ... of文で反復処理を行うことができる

配列からイテレータを取り出してfor ... of文で反復処理を行なってみます。

> const arr = ['foo', 'bar', 'baz'];
> const it = arr.values() // イテレータを代入

> for (const i of it) { // イテレータを反復処理する
// 内部的にイテレータからvalueが取り出される
    console.log(i.toUpperCase()); 
    }
//→FOO
//→BAR
//→BAZ

> it.next()
{ value: undefined, done: true } // 反復処理が終わったのでdone: true

このようにイテレータfor ... of文で反復処理を行うことができます。 (というか`for ... of文がイテレータを処理する実装になっていると言った方が正しいかもしれません。)

反復可能オブジェクト

次に反復可能オブジェクト(iterable object)についてみていきます。

安定のMDN反復処理プロトコル - JavaScript | MDN

反復可能プロトコルによって、 JavaScript のオブジェクトは反復動作を定義またはカスタマイズすることができます。例えば、 for...of 構造の中でどの値がループに使われるかです。一部の組み込み型は既定の反復動作を持つ組み込み反復可能オブジェクトで、これには Array や Map がありますが、他の型 (Object など) はそうではありません。

イテレータの説明よりはわかりやすいかな。😅

これもざっくり説明すると反復可能オブジェクトとはArrayMapStringのようなオブジェクトのことで、イテレータを持っているオブジェクトのことを指します。

(さっきも配列からイテレータを取り出しましたよね)

もう少し厳密にいうと@@iteratorメソッドを実装しているオブジェクトのことで、このメソッドは返り値にイテレータを返します。

@@iteratorメソッドはSymbol.iteratorというプロパティに定義されているので、自作クラスで反復可能オブジェクトを作りたい場合は[Symbol.iterator]メソッドを定義することになります。

  • 反復可能オブジェクト
    • [Symbol.iterator]メソッドを実装しているオブジェクト
      • 返り値はイテレータ(nextメソッドを実装してるオブジェクト)

反復可能オブジェクトはfor ... of文で反復処理ができる

for ... of文は反復可能オブジェクト(iterable object)に対して反復処理ができます。 (イテレータは反復可能オブジェクトでもあるので先ほどのイテレータの例も反復処理が可能だったと考えることができます。)

for...of - JavaScript | MDN

// 以下はStringの反復処理
const str = 'hello world';

for (const c of str) {
  console.log(c.toUpperCase());
}

H
E
L
L
O
 
W
O
R
L
D

自作クラスを反復可能にしてみる

最後に自作クラスを反復可能にしてみます。

やること

Setオブジェクトを再発明したGroupクラス(およびGroupIteratorクラス)を作ります。*1 Groupにはadd, delete, hasメソッドとfromという静的メソッドを用意します。

Setオブジェクトと挙動を合わせるため以下のような要件になっています。

  • Groupクラス
    • add : 引数を1つとり、その値を追加する。重複している値は無視する。
    • delete: 引数を1つとり、その値を削除する
    • has: 引数を1つとり、その値が存在するかどうか確かめる。Booleanを返す。
    • static from: 反復可能なオブジェクトを1つ受け取り、それを反復して生成されたすべての値を含むGroupオブジェクトを作成する。
    • ]symbol.iterator]: イテレータオブジェクトを返すメソッド -GroupIteratorクラス => イテレータオブジェクトを作成するクラス
    • next: {value: ... , done: ...}というオブジェクトを返すメソッド

作成してみた

class Group {
  constructor () {
    this.values = [];
  }

  static from(obj) {
    const group = new Group();
    for (let element of obj) {
      group.add(element);
    }
    return group;
  }

  add(value) {
    if (!this.has(value)) {
      this.values.push(value);
    }
  }

  has(value) {
    return this.values.includes(value);
  }

  delete(value) {
    this.values = this.values.filter(function(ele){
      return ele != value;
    })
  }

  [Symbol.iterator]() {
    return new GroupIterator(this);
  }
}

class GroupIterator {
  constructor(group) {
    this.index = 0;
    this.group = group;
  }

  next() {
    if (this.index === this.group.values.length) return {done: true};

    let result = {value: this.group.values[this.index], done: false};
    this.index++;
    return result;
  }
}

できました。

反復処理してみる

作ったGroupクラスからオブジェクトを作成し、反復処理してみます。

for (let value of Group.from(["a", "b", "c"])) {
  console.log(value);
}
// → a
// → b
// → c

できていますね。 今回で言うとGroupクラスは配列で内部データを表しているのでイテレータを取り出すのはもっと簡単なんですが、わざわざ自分で作ってみて理解が深まりました。

参考

*1:『流麗なJAVASCRIPT』という書籍を参考にしています

「勝手にモブプロ」というのを勝手にやってみた

2月からフィヨルドブートキャンプで「勝手にモブプロ」という会をはじめてみて、ちょうど?17回目を終えたので感想をば。

勝手にモブプロ会とは?

僕(@haruguchi)がフィヨルドブートキャンプで勝手にやっているモブプロ会になります。 実際、参加募集している様子を見てもらった方が早いかもしれないです。

こんな感じ。

フィヨルドブートキャンプ内のDiscord ペアプロ・モブプロ相手募集チャンネルにて

普通のモブプロとは違い、前もってメンバーを集めず、勝手にやってるから勝手に参加してね!という非常に怠惰で傲慢なスタイルになります。なので、参加人数によってはペアプロになったり、最悪1人でやることになります。(おかげさまで今の所、ぼっちは回避)

モブプロのお題はAtCoder Problemsにある過去問のA,B問題を利用しています。

厳密には、不定期開催を謳っているのですが、実質毎週木曜日の16:00~17:00に固定されつつあります。

参加者は平均4〜5名ほどで結構流動的に変わります。(レギュラーもいます) Rubyラクティスを始めたばかりの方からメンターの方まで幅広く参加してもらっていて、初めましての方とも交流できて嬉しいです!

なんではじめたの?

きっかけはざっくり3点

  1. 気軽にワイワイできる環境が欲しい(遊び友達が欲しい)
  2. モブプロの楽しさを広めたい
  3. 何か企画してみたかった

気軽にワイワイできる環境が欲しい(遊び友達が欲しい)

フィヨルドブートキャンプのメンバーにトミー(id:eatplaynap329)さんという方がいるのですが、チーム開発最初のIssueをモブプロ形式でワイワイやってるのが羨ましかったというのがきっかけです。 当時のモブプロ会の感想です。

余談ですが、トミーさんはいい意味でクレイジーなので結局ほとんどモブプロでIssueをやってしまいました。もはや恐怖すら覚えます😱笑

eatplaynap329.hatenablog.jp

モブプロの楽しさを広めたい

現在フィヨルドブートキャンプでは、気軽にモブプロ・ペアプロができる環境にあります。

傾向としては分からないところをメンターの方と一緒にペアプロ(モブプロ)して解決する「問題解決型」のスタイルが主流になっています。(勝手に名付けた)

僕としては、問題解決のためのモブプロだけでなく、何かお題をみんなで一緒に解いていく遊びメインのモブプロも流行って欲しいなーと思って始めました。

これは、昨年にはじめて参加したikumaさん(id:ikmbaer)のモブプロ会が楽しかったという経験からきています。 その会の詳細は以下のブログの10月にまとめてあります。

haruguchi-yuma.hatenablog.com

何か企画してみたかった

フィヨルドブートキャンプでは日々いろんな人がいろんなことを企画しています。 僕は参加することが多かったのですが、自分も何か企画してみたいという思いがありました。

で、何を企画しようかな🤔ということになるわけです。

輪読会は開催してから終わるまでの期間が長く、気軽さの面では少し難しい。 就職してからも続けられる自信がないなどの理由から手軽さを取ってモブプロにしました。

工夫したこと

工夫というほどでもないですが、以下のことにこだわって開催しています。

  • 再現性があること

再現性があること

モブプロの楽しさを広めたいので、誰でも真似できるということにこだわっています。

まず何かを企画した時に、一番気になるのが参加者がいるのか?という問題ですが「勝手にやる」スタンスを取ることで解決しています。

元々自分がやりたかったAtCoderを解くと言うのをテーマにしているので、最悪1人でも当初の目的通りAtCoderの問題を解く練習をすればいいようになっています。

他には、主催者の負担をできるだけ減らすと言うことにこだわっています。 実際ホストの僕がやることは、ほとんどありません。

開催の3日前くらいに「勝手にモブプロやります〜」と声をかけたり、問題を解き終わったら「次誰かドライバーやりたい人いますか〜?」と呼びかけるだけです。

一度始まればみんなと遊ぶだけなんでとても気楽です。

また、お題を考えるのが大変なのでAtCoderというのは良い案だったなと思います。1問あたり15分から30分くらいなのでちょうどいい感じですし、A,Bくらいの難易度だと数学的な知識もほとんどなしで解けるのもポイントです。一度お題を作ってモブプロ会をしたことがあるんですが、作問に3、4時間くらいかかってコンスタントにするのは不可能だなと感じました。

良かったこと

やってから気づいた良かったことがいくつかあります。

  • いろんな人と接点ができた
  • 人前でコードを書くのが恥ずかしくなくなった
  • 人と話す練習になった
  • 自分の考えを言語化する癖がついた
  • コードリーディングする機会が増えた
  • るりまを見る練習になる

あと、本当にはじめての場合は画面共有の練習になったりしていいかもです。

あと、これをきっかけにかは分からないですが、sugieさんはAtCoderの問題を解いたブログをまめに更新していてすごいです。みんなに見てほしい。

sugie.co

最後に

勝手にやってるとはいえ参加者がいて楽しくなっている会ですので、いつも参加してくださる人には感謝です🙇‍♂️

今後の方針としては、AtCoderにこだわらず何かお題を決めて大きめのプログラムを作る回が臨時的にあってもいいかなぁなんて思っている次第です。

「勝手にモブプロ」が気になると言う方は見学だけでも大丈夫なので是非勝手に見学してみてください。

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

はじめに

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

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

reread-book.herokuapp.com

github.com

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

自己紹介

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

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

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

re:Readの紹介

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

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

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

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

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

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

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

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

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

使い方

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

ログインして書籍を登録

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

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

読み返したい本リスト

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

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

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

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

気になりポイント

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

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

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

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

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

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

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

Googleカレンダー

解決したい問題

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

こんな人に使ってほしい

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

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

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

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

今後の予定

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

機能面

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

技術面

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

苦労したポイント

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

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

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

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

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

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

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

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

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

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

画像アップロード Shrine

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

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

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

shrinerb.com

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 最後に

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

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