プログラミング漫遊記

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

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までフィードバックを頂けたら幸いです。とても喜びます。

UV study: Ruby LT会 Vol.3に登壇しました。

にゃんにゃんにゃん。

いわゆるネコの日にユニークビジョンさんのRubyのLT会に参加しました。

uniquevision.connpass.com

フィヨルドブートキャンプでのLT会は一度経験したことがあるんですが、外部でのLT会ということですごく緊張しました。以下感想を垂れ流します。

haruguchi-yuma.hatenablog.com

LT会で登壇した理由

純粋な気持ちから邪な気持ちまで大小様々な理由のうえ登壇することにしました。これが大人ということか。

  • 普段から輪読会で掘り下げて学習しているので技術ネタをアウトプットする場が欲しかった
  • 技術ネタを発表する責任感からちゃんと1次情報を調べて学習する癖がつくという期待(邪な気持ち)
  • フィヨルドブートキャンプの中でしか活動してないのでRubyコミュニティの方に名前を覚えてもらいたい(邪な気持ち)
  • 就職活動の足がかり、、、(邪な気持ち)

これでは筋斗雲には乗れないですね🌩

それに加えてこのLT会はVol.1-2でフィヨルド生が既に発表していたというのも大かった。やっぱり知っている人が過去に発表しているというのは安心感があります。 先人たちの勇気の上に成り立っている!感謝 🙏

発表内容

で、お前は何を発表したん?という声が聞こえてきそうなのでスライドを投下しておきます。

['a', 'b', 'c'].map(&:upcase) is 何? - Slidev

['a', 'b', 'c'].map(&:upcase)という初心者には摩訶不思議の呪文に見えるこのコードがどういう仕組みで動いているのか?というのを自分なりに調べて発表してみました。

ただ、このコードの動く仕組みというのはQiitaだったりZennだったりとネットの海に五万とあるので少し工夫したところもあります。

それは、自分で作ったコードを紹介したという点です。解説がメインなので自分の作ったコードはおまけなのですが、ずいぶん自分らしくなったと思います。

また、10分という時間制約の中でやむなく削った部分もあります。

  • Proc#callとyieldの違い
  • symbol.to_proc.callの第2引数以降について
  • Procオブジェクトの生成方法について
  • ブロックの中の処理を実行する方法(call以外)

と説明したいことは山ほどあったんですが、主題から逸れる部分は断腸の思いでカットしました。

procオブジェクトやブロックについてはまたわかったことをブログに書ければと思います。

その他

今回登壇以外にもツールで挑戦した部分があります。 それはslidevというマークダウンでスライドを記述するツールを使ったことです。 sli.dev

ドキュメントはid:ikmbearさんにより日本語にも翻訳されています。この日本語ドキュメントには大変お世話になりました。自身のスライドもかなりパクりました参考にさせてもらったのでここで感謝しておきたいと思います🙇

マークダウンで書けるのでスライド製作者はスライドの中身に注力できるというのが売りみたいですが、僕みたいな弱々マークダウナー(造語)には難しすぎた。 とはいえ慣れの部分もあるので何回かこれで書いて登壇を重ねていけばありがたさが実感できるような気がします。

【Ruby】selfキーワードとメソッド呼び出し

selfキーワードとメソッド呼び出しについて学習したのでまとめておきたいと思います。最初に言っておくとめちゃめちゃ長いです。

self

selfとは

るりまの記述を引用

るりま、擬似変数の項

self

現在のメソッドの実行主体。

じ、実行主体?🤔

どういうことでしょうか?インスタンスメソッドを作って試してみます。

class User
  def greeting
    p self
    puts 'hello'
  end
end

taro = User.new

taro.greeting
=> #<User:0x00000001070a48f0> selfの中身
=> hello

ここでの実行主体はtaroになります。

taroはUserクラスでnewされたインスタンスなので、正確には、Userクラスのインスタンス(上の例で言うとコイツ#<User:0x00000001070a48f0>)が実行主体ということになります。

メソッドを呼び出すときはレシーバ.メソッドという形式で呼び出しますが、このレシーバに当たるのが実行主体のようです。

「レシーバ」 =「 実行主体」

インスタンスメソッドとクラスメソッドのself

インスタンスメソッドの実行主体は、当然そのクラスのインスタンスなので、selfの中身はインスタンス自身となります。

では、クラスメソッドはどうでしょうか。確かめてみます。

class User
  # クラスメソッド
  def self.greeting
    p self
    puts 'hello'
  end
end

User.greeting
=> User
=> hello

クラスメソッドの実行主体はUserというクラス自身です。なのでselfの中身もUserということになります。 もちろんOrderというクラスに定義されているクラスメソッドの場合は、Orderとなりますし、Bookというクラスに定義されているクラスメソッドの場合はBookとなりますし、Hogeというクラ...(略)

いろんな場所でselfの中身を確かめてみる

1つ1つ確かめるのは面倒なので一気に確かめてみます。

# トップレベルでのself
p self

class Klass
  # クラス直下でのself
  p self 

  def instance_method # インスタンスメソッド
    p self
  end

  def self.class_method # クラスメソッド
    p self
  end
end

以下を表にまとめるとこんな感じです。

selfの場所 中身
トップレベ main
クラス定義式直下 Klass(クラス)
クラスメソッド定義内 Klass(クラス)
インスタンスメソッド定義内 #\<Klass:0x00007fb063a7ff60>(インスタンス)

mainというのはトップレベルでselfを表すものでp mainなどと参照はできません。

また、トップレベルでのself(main)のクラスを確認すると

self.class
=> Object

となるのでObjectクラスのインスタンスであることはわかります。ここでは難しく考えずに、華麗にスルーしておきます。

この関係性をもとにメソッド呼び出しについて考えてみます。

メソッド呼び出し

前提:レシーバの有無によるメソッド呼び出し

メソッドを呼び出すときってレシーバがある場合とない場合がありますよね?こんな感じです。

# レシーバなしでメソッドputsを呼び出してる
puts 'hello'

# レシーバありでメソッドを呼び出してる
'hello'.upcase

メソッド呼び出しについて「るりま」で調べると以下のような記述がありました。(強調は筆者によるもの)

メソッド呼び出し(super・ブロック付き・yield) (Ruby 3.1 リファレンスマニュアル)

メソッド呼び出し式はレシーバ(`.' の左側の式の値)のメソッドを呼び出します。レシーバが指定されない時は self のメソッドを呼び出します。

つまりレシーバなしで呼び出しているメソッドもそう見えるだけで暗黙的にレシーバ(self)のメソッドが呼ばれているということになります。

以下のコードはどちらでも同じです。

# selfに対してputsを呼び出す(明示的)
self.puts 'hello'

# レシーバ省略してるけど、selfに対してメソッドを呼び出している
puts 'hello'
「レシーバ省略されているときはselfがレシーバ」

以上のことを踏まえながらインスタンスメソッドやクラスメソッドないからメソッドを呼び出すことについて考えていきます。

インスタンスメソッドからインスタンスメソッドを呼び出す

特に意味のあるコードではないですが、問題を単純化するために以下のコードで考えます。インスタンス変数使えよ!とかそういう難しいことは考えません。

class User
  def greeting
    name + 'さん、ごきげんよう!'
  end

  def name
    '太郎'
  end
end

taro = User.new
taro.greeting
=> '太郎さん、ごきげんよう!'

greetingというインスタンスメソッドの中でnameというインスタンスメソッドを呼び出しているコードです。

インスタンスメソッドの中からインスタンスメソッドを呼び出すときはレシーバを省略できます。と習いましたが、ずっとなぜなんだろう?と思っていました。

ここまでのことが理解できると謎が解けます。

まず、さっきの話からレシーバが省略されている場合はselfが隠れています。明示的に記述していきましょう。

class User
  def greeting
    self.name + 'さん、ごきげんよう!'
  end
  
  def name
    '太郎'
  end
end

インスタンスメソッドはそのクラスのインスタンスがレシーバでないといけませんが、*1 ここでのselfはそのクラスのインスタンス自身だからインスタンスメソッドを呼び出せているということになります。

なので、クラスの外でself.nameまたは単にnameとすると(selfはmainなので)メソッドは呼び出せません。

class User
・・・略・・・
  def name
    '太郎'
  end
end

self.name => エラー # selfはObjectクラスのインスタンス
name => エラー

インスタンスメソッド内でインスタンスメソッドを呼び出すときにレシーバが必要ないのはインスタンスメソッド内のselfがインスタンス自身だからということになります。(インスタンス言い過ぎ)

インスタンスメソッドからクラスメソッドを呼び出す

以下のコードはダメな例です。

class User
  # これではクラスメソッドを呼び出せない
  def greeting
    name + 'さん、ごきげんよう!' ・・・(☆)
  end

  # クラスメソッド
  def self.name
    '太郎'
  end
end

クラスメソッドを呼び出す場合はクラス.メソッドという形になっていないとダメですがここでは(self.)name(☆のところ)の省略されているレシーバselfはインスタンス自身なのでクラスメソッドは呼び出せません。

呼び出す方法としては以下のような方法があります。

class User
  def greeting
    User.name # クラスを明示する
    self.class.name # self.class はUserとなる
    class.name # selfは省略できる
  end

  def self.name
    '太郎'
  end
end

どんどんいきましょう。

クラスメソッドからインスタンスメソッドを呼び出す

またもやダメなコード例から見てみましょう。

class User
  # これはではインスタンスメソッドを呼び出せない
  def self.greeting
    name + 'さん、ごきげんよう!'
  end

  def name
    '太郎'
  end
end

nameメソッドをレシーバなしで呼び出した場合、省略されているレシーバであるselfはクラス自身(User)になるのでクラスメソッドのnameが呼ばれることになります。

インスタンスメソッドのnameを呼び出すにはレシーバをインスタンスにする必要があるので以下のようになります。

class User

  def self.greeting
    User.new.name # ユーザークラスのインスタンスを生成してからnameメソッドを呼び出す(明示的)
    self.new.name # Userをselfにした
    new.name # selfは省略できる
  end

  def name
    '太郎'
  end
end

次!

クラスメソッドからクラスメソッドを呼び出す。

クラスメソッド内のselfはクラス自身なのでレシーバを省略して普通にメソッドを呼び出せます。

class User
  def self.greeting
     name + 'さん、ごきげんよう!'
  end

  def self.name
    '太郎'
  end
end

まとめ

  • selfとはメソッドの実行主体のこと
  • 実行主体とはレシーバ.メソッドのレシーバのこと
  • レシーバが省略されているメソッドはselfに対してメソッドを呼び出す。
  • 逆に言えばselfに対して呼び出せるメソッドはselfを両略できる
  • インスタンスメソッドのレシーバはそのクラスのインスタンスでないといけない(継承関係除く)
  • クラスメソッドのレシーバはそのクラスのクラス自身でないといけない

こんなに長い文章を読んでくださってありがとうございます。 読み飛ばして最後だけ読んでくれた方もありがとうございます。

*1:継承関係などは難しいので考慮してません