ERBで'<%='の前にある空白文字を取り除くシンプルな方法は用意されていない...のか?

リファレンス

基本形
> ERB.new("<%= :hoge %>").result  # => "hoge"
> ERB.new("    <%= :hoge %>    \n").result  # => "    hoge    \n"
後ろを消したい

リファレンスによれば、trim_modeを"-"にした場合、「行末が-%>のとき改行を出力しない」。

> ERB.new("<%= :hoge %>\n", trim_mode: '-').result  # => "hoge\n"
> ERB.new("<%= :hoge -%>\n", trim_mode: '-').result  # => "hoge"

しかし、-%>が行末にない場合は後続のスペースも改行も消されない。

> ERB.new("<%= :hoge -%>    \n", trim_mode: '-').result  # => "hoge    \n"
前を消したい

リファレンスによると「行頭が<%-のとき行頭の空白文字を削除する」

> ERB.new("    <%- nil %>hoge", trim_mode:  '-').result  # => "hoge"

<%=の前にある空白文字って消せないのかな?

<%=-, <%-=のように組み合わせてもダメ。

> ERB.new("    <%=- :hoge %>", trim_mode:  '-').result
# => NoMethodError (undefined method `-@' for :hoge:Symbol)
> ERB.new("    <%-= :hoge %>", trim_mode:  '-').result
# => SyntaxError ((erb):1: syntax error, unexpected '=', expecting end-of-input)

どうやら<%=の前にある空白文字を取り除くシンプルな方法は用意されていないらしい。

同じ疑問をもっている方がいた。

ここで提示された解決策は以下。

> ERB.new("    <%- %><%= :hoge %>", trim_mode:  '-').result  # => "hoge"

強引さが否めない...。

ちなみに

Railsでメールのテンプレートを"xx.text.erb"でつくっていて、

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

みたいな場合にブロック内の行頭の空白文字を消したくて方法を探してたのだけど、これってわりと一般的なニーズなのでは?

(メソッド化するとか、<%をインデントしないとか、やりようはいくらでもあるとおもうけど。)

githubruby/rubyでissueを検索したけどなさそう。

issueを立てるに値する話だったらとっくに誰かがやってる気がするので、

  • 実はこの問題は解決できる
  • そもそもこの問題が生じるのがおかしい(やりたいことの設定がおかしい)

のどちらかな気がするけどどうなんでしょうか。

デメテルの法則

「ドットがつながったらあかんで」とかいうやつ、くらいの認識しかなかったが整理できたのでメモ。

結論としては、「ドットがつながったらあかんで」ではなく、「ドットがつながったときは不要な依存をつくっちゃう場合があるから、ほんとにそれでいいかよく考えなね」であると理解した。

飛び越えた依存

例えば以下のようなコードを考える。 (以下、barや@barはBarインスタンスであることを前提とし、bazについても同様。)

class Foo
  def foo_method
    @bar.baz.baz_method
  end
end

あるいは、こんなコード。

class Foo
  def foo_method(bar)
    bar.baz.baz_method
  end
end

Fooの中で@bar/barに対してBar#bazというメソッド呼び出しをしているので、FooはBarのインターフェースを知っている=FooはBarに依存していることになる。

これは以下のように図示できる。

Foo -> Bar

ではBarとBazの関係はどうだろうか。

ここでは、Bar#bazがBazインスタンスを返している。

そのばあい、確証はないけどBarはBazインスタンスに対してあれこれメソッド呼び出しをすることが多い。

(たとえばCustomer#walletがWalletインスタンスを返す場合、CustomerはCustomer#payの中でWallet#withdrawを呼び出したりする。)

このとき、BarはBazに依存している。

Foo -> Bar -> Baz

そんで、上記の例だとFooはBaz#baz_methodを呼んでいるので、FooはBazのインターフェースを知っている=依存している。

Foo -> Bar -> Baz
  |            ^
  |____________|

FooはBarを飛び越えてBazにも依存しているわけだ。

それで、どうする?

この飛び越えた依存を解消するためにBarにメソッドを生やす、というやり方は明らかなので示さない。

(よくみるdelegateもつまりはBarにメソッドを生やす1つの方法だ。)

ここで問いたいのは、上記の飛び越えた依存が「解消すべきものなのかどうか」だ。

答えは、場合による。

飛び越えた依存を解消すべき場合

こちらの記事のPaperBoyの例は、飛び越えた依存を解消すべき場合だ。

依存関係を先程の図に当てはめると以下のようになる。

PaperBoy -> Customer -> Wallet
  |                       ^
  |_______________________|

これらのモデルによって表現されている現実においては、PaperBoyはWalletのことを知る必要はない。

よって、「PaperBoyはWalletのことを知らない=依存しない」形の設計によって現実を適切にモデリングできると判断できる。

不要な依存関係はつくらない方がよいし、それに「依存関係の不在」も含めてなるべく現実に近いモデリングをしておいた方が、今後現実に生じる出来事をモデルの中で表現しやすくなる。

だからこうすべきなのだ。

PaperBoy -> Customer -> Wallet
飛び越えた依存を解消すべきではない場合

上述の記事の後半に記載されているのは、必ずしも依存を解消すべきとは限らないことの良い例だ。(以下ではクラス名に多少の改変を加えている。)

OrdersCotroller -> Order -> Customer
  |                             ^
  |_____________________________|

ユースケース層に属しているOrdersControllerにとっては、OrderとCustomer両方のインターフェースを知っている=依存しているのはごく自然なことだ。

むしろ、モデルたちを並列的に扱うことが期待されるコントローラが、Customerに対する操作を逐一Order経由で行うことの方が不自然である。

デルメルの法則は私たちに「本当にその飛び越えた依存が存在していいの?」と問いかけるが、この場合は「もちろん。その依存関係は許容されるものだ」と胸を張って答えればいい。

デルメルの法則は絶対に従うべき掟ではなく、不注意な過ちを防ぐための注意書きに過ぎない。


もう1つ例を挙げよう。

Order -> Customer -> String
  |                    ^
  |____________________|

たとえばOrder#summaryの中でcustomer.name.upcaseなどを呼ぶ場合に、上記の依存関係が発生する。

この場合、Customer#name_in_upcaseを定義して、飛び越えた依存を解消すべきだろうか?

いや、OrderがRubyの組み込みクラスであるStringのインターフェースを知っているのはごく自然なことなので、この「飛び越えた依存」はあって然るべきものだ。

より実際的に言うならば、Stringは十分に安定的であり変更可能性が小さいので、その僅かなリスクのためにCustomer#name_in_upcaseを生やすというコストを払うのは賢明ではない。

総括

デメテルの法則は「こういう場合には注意した方がいいよ」というだけで、何をすべきかを指示するものではない。

注意したうえでどう判断するかは、状況次第である。

しかも、上記の例では依存を解消すべき/すべきでないことが明らかな例を挙げたが、そうした判断は必ずしも自明ではない。

「FooがBazを知っている形の設計にする」か「FooがBazを知らない形の設計にする」かは、多くの場合「そのドメインをどのようにモデリングするか」という、"決め"の問題になることも多いように思う。

以上適当なメモなので誤りなどあればご教示ください。

Promise

みんなPromiseさくっと理解してる気がするけどむずくないですか。

ようやくなんとなく見えたのでメモ。

Promise以前

XHRを考える。

const api = (url) => {
  const req = new XMLHttpRequest()

  req.open("GET", url)

  req.onload = () => {
    json = JSON.parse(req.response)
    console.log(json)
  }

  req.send()
}

取得した値を戻り値にすることはできないので、戻り値はundefined.

使ってみた結果が以下。(ログ出力されたオブジェクトは内容を抜粋して記載しています。以下同じ。)

api('https://swapi.co/api/people?search=R2-D2')
// <- undefined
// => {
//   results: [
//     {name: "R2-D2", height: "96", mass: "32", homeworld: "https://swapi.co/api/planets/8/", ...}
//   ]
// }

このままだと取得後に取得した値を元にして処理する、ということができない。

api('https://swapi.co/api/people?search=R2-D2').results[0].height
// <- TypeError: Cannot read property 'results' of undefined

戻り値はundefinedなのでそりゃそうです。

これを解決するためにコールバック関数という仕組みがあるが、それは今回は省略する。

Promise

以下のようにPromiseオブジェクト(を返す関数)を用意する。

const apip = (url) => {
  return new Promise((resolve, reject) => {
    const req = new XMLHttpRequest()
    
    req.open("GET", url)
    
    req.onload = () => {
      json = JSON.parse(req.response)
      resolve(json)
    }
    
    req.send()
  })
}

すると、取得した時点で、取得した結果を元に処理を続けることができる。

取得したjsonの一部をログ出力することもできるし、

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => console.log(json.results[0].homeworld))
// <- Promise {<pending>}
// => https://swapi.co/api/planets/8/

取得したjsonを元に再度APIを叩くこともできる。

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => api(json.results[0].homeworld))
// <- Promise {<pending>}
// => {
//   name: "Naboo", diameter: "12120", resident: [...], ...
// }

何が起きているのだろうか?

  • apipは、その中でPromiseのコンストラクタを呼び出す。
  • すると、コンストラクタに渡した関数が実行される。つまりリクエストが送信される。
  • 実行後、Promiseオブジェクトが生成される。これをp1と呼ぼう。
  • Promiseオブジェクトは、resolve()が実行されるまではpending状態、resolve()が実行されるとresolved状態となる。
  • p1は生成時点ではまだresolve()が実行されていないので、pending状態である。

  • で、then()もまた別のPromiseを生成する。p2と呼ぼう。
  • p2は、ハンドラー関数の実行が終わって値を返したときに、ハンドラー関数の戻り値を引数としてresolve()を呼び出す=resolvedになる。
  • (ただしハンドラー関数がPromiseオブジェクトを返すときは異なる。これについては後述する。)
  • ハンドラ関数が呼ばれるのはいつかというと、then()のレシーバであるPromiseオブジェクト(=p1)がresolve()を実行するとき。
  • 前述のとおり、p1はまだpendingなので、p2のハンドラ関数も呼び出されていない。
  • よって、p2も生成されたときはpending状態である。

  • しばらくするとp1で実行していたリクエストのレスポンスが返ってくる。
  • onloadで登録されていた関数が呼ばれ、resolve(json)が実行される。つまり次に登録されたp2のハンドラー関数が呼び出される。
  • この時点でp1がresolvedになる。
  • p2のハンドラー関数が実行され終えると、p2もresolvedになる。

then()をチェーンする

1つめのthen()で登録したハンドラー関数の実行が終わった後で、さらに処理をチェーンさせたい場合がある。

例えば以下のように。

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => json.results[0].homeworld)
  .then(string => "R2-D2の故郷の情報は " + string)
  .then(string => string + " から取得できます。")
  .then(string => console.log(string))
// <- Promise {<pending>}
// => R2-D2の故郷の情報は https://swapi.co/api/planets/8/ から取得できます。

n個めのthenのハンドラー関数が実行された直後の時点で、n+1個めのthenのハンドラー関数の引数が決定できるのでうまくいく。

しかし例えば、1つめのthenのハンドラー関数で再度APIを叩き、その結果を出力したい場合はどうだろうか。

以下のコードはうまく動作しない。

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => api(json.results[0].homeworld))
  .then(json => console.log(json.name))
// <- Promise {<pending>}
// <- TypeError: Cannot read property 'name' of undefined

1つめのthen()のハンドラー関数が呼ばれたときに、その戻り値がundefinedなので、2つめのthenのハンドラー関数がundefinedを引数として直ちに実行されてしまうからだ。

この問題を回避し、あるthen()のハンドラー関数が非同期的に実行完了するまで、次のthen()のハンドラー関数の実行を待つ、という仕組みが存在する。

そのためには、ハンドラー関数の戻り値をPromiseオブジェクトにすればよい。

チェーンされたthen()のハンドラー関数の実行タイミングを制御する

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => apip(json.results[0].homeworld))
  .then(json => console.log(json.name))
// <- Promise {<pending>}
// => Naboo

このコード例では、1つ目のthen()のハンドラー関数に含まれるHTTP通信が完了するのを待ってから、2つ目のthen()のハンドラー関数を実行し始めることができている。

ここで何が起きているか。

  • 先程と同様、p1, p2, p3とPromiseオブジェクトが生成される。
  • p2のハンドラー関数が実行されるところまでは同じである。
  • p2のハンドラー関数は、先程とは異なり、Promiseオブジェクトを返す。これをp2-0と呼ぼう。
  • (p2-0は生成直後にはpending状態である。)
  • p2は、ハンドラー関数の戻り値がPromiseオブジェクト(=p2-0)である場合、その状態及びresolveする値を、戻り値のPromiseオブジェクト(=p2-0)に依存する。
  • p2-0はまだpending状態なので、p2もpending状態となる。
  • よってp3のハンドラ関数も呼ばれない。

  • しばらくすると、p2-0で送信していたリクエストのレスポンスが返ってくる。
  • onloadで登録していたresolve(json)が実行され、p2-0はresolvedになる。
  • p2-0に依存しているp2においても、jsonを引数としてresolve(json)が実行される、すなわちp3のハンドラ関数がjsonを引数として実行される。

これでどうやらうまくできたみたいです。

thenが3つ以上の場合にも同様の方法でチェーンしていけるはず。

おまけ: Promise.all

力尽きたのでコード例のみ。

apip('https://swapi.co/api/people?search=R2-D2')
  .then(json => apip(json.results[0].homeworld))
  .then(json => {
    return Promise.all(
      json.residents.map(resident => apip(resident))
    )
  }).then(residents => {
    console.log(residents.map(resident => resident.name))
  })
// <- Promise {<pending>}
// => ["R2-D2", "Palpatine", "Jar Jar Binks", "Roos Tarpals", "Rugor Nass", ...]

クラス変数とインスタンス変数

職場で「クラス変数とクラスインスタンス変数の違い」が話題にあがっていて、以前理解したよな、と思いつつ咄嗟に整理して述べられないなと思ったので、思い出しつつまとめてみる。

基本的にはクラス変数とインスタンス変数というものがある。

まずサンプルコード。

class Parent
  @@var = 'class variable of Parent'
  @var = 'instance variable of Parent (so called class instance variable)'

  class << self
    def at_at_var
      @@var
    end

    def at_var
      @var
    end
  end

  def initialize
    @var = 'instance variable of parent'
  end

  def at_at_var
    @@var
  end

  def at_var
    @var
  end
end

インスタンス変数とは

あるインスタンスが保持する変数。そのインスタンスにおいて利用できる。@{変数名}により指示される。

たとえば、parentという「Parentクラスのインスタンス」は@var(='instance variable')というインスタンス変数をもっている。

parent = Parent.new
parent.at_var  # => "instance variable of parent"

同様に、Parentという「Classクラスのインスタンス」は@var(='instance variable of Parent (so called class instance variable)')というインスタンス変数をもっている。

Parent.at_var  # => "instance variable of Parent (so called class instance variable)"

後者のように「Classクラスのインスタンス」が持つインスタンス変数のことを「クラスインスタンス変数」と呼ぶことがある。

クラス変数とは

あるクラスが保持する変数。@@{変数名}により指示される。

当然ながらParentクラスは自身のクラス変数を利用できる。

Parent.at_at_var  # => "class variable of Parent"

クラス変数はこれに加えて、以下の特徴をもつ。

1. Parentクラスのインスタンスから、Parentクラスのクラス変数にアクセスできる
parent.at_at_var  # => "class variable of Parent"
2. Parentクラスを継承したChildクラスから、継承元であるParentクラスのクラス変数にアクセスできる
class Child < Parent
  def initialize
    @var = 'instance variable of child'
  end
end
Child.at_at_var  # => "class variable of Parent"
3. (上記の合わせ技として)Parenetクラスを継承したChildクラスのインスタンスから、Childクラスの継承元であるParentのクラス変数にアクセスできる
child = Child.new
child.at_at_var  # => "class variable of Parent"

おまけ

仕組みはこれだけだと思う。最後に念のため、上で述べたクラス変数の「色んなところからアクセスできる性質」は、クラスインスタンス変数には与えられていないことを確認する。

1. Parentクラスのインスタンスから、Parentクラスのクラスインスタンス変数にはアクセスできない
parent.at_var  # => "instance variable of parent"

@varはparentのインスタンス変数を指してしまっている。

2. Parentクラスを継承したChildクラスから、継承元であるParentクラスのクラスインスタンス変数にはアクセスできない
Child.at_var  # => nil

@varは「ChildというClassクラスのインスタンスが持つインスタンス変数」を指してしまっている。(これは宣言していないのでnilが返っている。)

3. Parenetクラスを継承したChildクラスのインスタンスから、Childクラスの継承元であるParentのクラスインスタンス変数にはアクセスできない
child.at_var  # => "instance variable of child"

@varはchildのインスタンス変数を指してしまっている。

丁寧に言語化すると「ClassクラスのインスタンスであるHogeクラスのクラスインスタンス変数」とか、どうしても早口言葉っぽくなるな...。

以上です。

DB設計なんじゃらほい

[2019/02/17 追記]

下記の文章ではDB設計とドメインモデリングを混同しており、根本的な誤りを含んでいることに気づいた。

修正しておらず誤ったままだがメモとして残しておく。


DB設計ってのをほぼやったことがない。 概念設計/論理設計/物理設計などという言葉が錯綜していて混乱したが、以下の5つに分けると理解しやすい気がしたのでメモ。

設計の種類

1. ドメインモデル
  • 業務はどのようにモデル化されるか
  • 業務はどのような概念とその相互作用によって十分に表現されるか
2. entityやらvalue objectやら
  • システム上でどのようなオブジェクトの相互作用によって処理を実現するか
  • それぞれのオブジェクトのattributeはアプリケーション上でどういう制約をもつか
  • オブジェクト指向の世界の在り方」といえるかも
3. オブジェクトとデータベースの対応づけ
  • 「entityやらvalue objectやら」と「データベースの構造」をどうやって対応づけるか
  • ActiveRecordパターンで1対1対応させる」とか「Repositoryでよしなに」とかの選択肢があると思う。
  • インピーダンスミスマッチとの向き合い方」と言えるかも。
4. データベースの構造
  • RDBにするかKVSにするか
  • どういうテーブルがあってどういうカラムをもつか
  • それらのカラムがRDB上でどういう制約をもつか
  • RDBの場合)「リレーショナル指向の世界の在り方」と言えるかも。
5. ミドルウェア&ハードウェア

注意事項

  • 1と2は似ているようでいて実は違う。
    • 「どのようなドメインでも様々なモデルで表せます。そしてどのようなモデルでも様々な方法でコードに落とし込めます。」*1
  • レイヤーでいうと上記の1=>5の順だけど、設計作業の順番は必ずしも1=>5ではないと思う。
  • 各項目は必ずしも互いに独立していない
  • 2,4,5は意識的に考えないとつくれないが、3は割と無意識的に決まっちゃうこともあり、1については(少なくとも意識的には)全く考えないこともできる。

具体例

あるプロジェクトで、暗黙の前提として「RailsだからActiveRecordパターンね」という決めがあったとすると、それで予め3が決まる。

すると2と4は基本的に直結する*2ので、2と4の作業はひとつの設計作業として行われる。

1はよく考えなかったとする。

この場合、やった設計作業は「テーブル/クラスの構造を決める」と「ミドルウェア&ハードウェアのことを決める」の2つになる。

もちろん別のプロジェクトでは1,2,3,4,5を別々に考えて、5つをくっきり区切って設計作業を行うということもありうる。

「XX設計」との対応

まあムリに対応づけることもないけど、ぱっとでてくるサイトでの説明と見比べると、以下のような感じかと思う。

  • 「概念設計」 <=> 1. ドメインモデル
  • 「論理設計」 <=> 2. entityやらvalue objectやら + 3. オブジェクトとデータベースの対応づけ + 4. データベースの構造
  • 「物理設計」 <=> 5. ミドルウェア&ハードウェア

...というようなことをつらつら考えたが、DB設計といふものをやったことないからまずは実践してみませう。

*1:"Domain Driven Design Quickly"

*2:直結しなくなるのはSTIとかくらいか

Rspecでuse_transactional_fixturesはexampleのみにtransactionを張る

Rspecの基本的なことを知らなかったのでメモ。

rails_helper.rb

RSpec.configure do |config|
  config.use_transactional_fixtures = true
end

1つ1つのテストにtransactionを張って、テスト終了時に必ずrollbackしてくれる設定。

user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do

  # 普通にexample内に書かれてたらrollbackされる
  example 'create a user in an example' do
    ichiro = FactoryBot.create(:user, first_name: 'ichiro')
    expect(ichiro.first_name).to eq 'ichiro'
  end

  # beforeブロック内もexampleに入ってから評価されるのでrollbackされる
  describe 'create a user in a before block' do
    before { FactoryBot.create(:user, first_name: 'jiro') }
    it { expect(User.last.first_name).to eq 'jiro' }
  end

  # letブロック内もexampleに入ってから評価されるのでrollbackされる
  describe 'create a user in a let block' do
    let(:saburo) { FactoryBot.create(:user, first_name: 'saburo') }
    it { expect(saburo.first_name).to eq 'saburo' }
  end

  # let!ブロック内もよくわからんがtransactionの内側らしい
  describe 'create a user in a let block' do
    let!(:shiro) { FactoryBot.create(:user, first_name: 'shiro') }
    it { expect(shiro.first_name).to eq 'shiro' }
  end

  # describeに直接書くとexample内ではないのでtransactionは張られておらず、rollbackされない。
  FactoryBot.create(:user, first_name: 'goro')
end

user_spec.rb実行前のtestDB

mysql> select first_name from users;
Empty set (0.00 sec)

user_spec.rb実行後のtestDB

mysql> select first_name from users;
+------------+
| first_name |
+------------+
| goro       |
+------------+
1 row in set (0.00 sec)

IOインスタンスの読み書きモード

Kernel.#openIO.openはだいたい同じ。IOインスタンスを返す。

第二引数で指定する読み書きモードについてメモ。

各種モードの違い

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
r 1 0 0 -
r+ 1 1 0 先頭
w 0 1 1 先頭
w+ 1 1 1 先頭
a 0 1 0 末尾
a+ 1 1 0 末尾

r

デフォルト値。

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
r 1 0 0 -

_

# 読み込みできる(内容が残っている)
> open('alphabets.txt', 'r') {|f| f.read }
=> "abc\n"

# 書き込みできない
> open('alphabets.txt', 'r') {|f| f.write('def') }
IOError: not opened for writing

r+

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
r+ 1 1 0 先頭

_

# 読み込み可能(内容が残っている)
> open('alphabets.txt', 'r+') {|f| f.read }
=> "abc\n"

# 書き込み可能(書き込み位置=先頭から上書きされる)
> open('alphabets.txt', 'r+') {|f| f.write('de') }
=> 2
> open('alphabets.txt', 'r+') {|f| f.read }
=> "dec\n"

w

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
w 0 1 1 先頭

_

# 読み込みできない
> open('alphabets.txt', 'w') {|f| f.read }
IOError: not opened for reading

# 書き込みできる(既存の内容は開いた時点で削除されているため、白紙状態に書き込み)
> open('alphabets.txt', 'w') {|f| f.write('de') }
=> 2
> exit
% cat alphabets.txt
=> de%

w+

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
w+ 1 1 1 先頭

_

# 読み込みも可能(ただし既存の内容は開いた時点で削除されている)
> open('alphabets.txt', 'w+') {|f| f.read }
=> ""

# 書き込みできる(既存の内容は開いた時点で削除されているため、白紙状態に書き込み)
> open('alphabets.txt', 'w+') {|f| f.write('de') }
=> 2
> exit
% cat alphabets.txt
de%

a

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
a 0 1 0 末尾

_

# 読み込みできない
> open('alphabets.txt', 'a') {|f| f.read }
IOError: not opened for reading

# 書き込みできる(末尾に追記)
> open('alphabets.txt', 'a') {|f| f.write('fgh') }
=> 3
> exit
% cat alphabets.txt
defgh%

a+

読み込み可能 書き込み可能 開いた時点で
内容を白紙にする
書き込み
開始位置
a+ 1 1 0 末尾

_

# 読み込みできる
> open('alphabets.txt', 'a+') {|f| f.read }
=> "defgh"

# 書き込みできる(末尾に追記)
> open('alphabets.txt', 'a+') {|f| f.write('ijk') }
=> 3
> exit
% cat alphabets.txt
defghijk%

ファイルポインタに関する考察

IO#posは「ファイルポインタの現在の位置」を返す。*1

確かめてみる。

open('alphabets.txt', 'r') {|f| f.read }  # => "abcde\n"

open('alphabets.txt', 'r+') do |f|
  p f.pos        # => 0
  p f.gets(2)    # => "ab"
  p f.pos        # => 2
  f.write('x')
  p f.pos        # => 3
end

open('alphabets.txt', 'r') {|f| f.read }  # => "abxde\n"

open('alphabets.txt', 'a+') do |f|
  p f.pos        # => 0
  p f.gets(2)
  p f.pos        # => 2
  f.write('x')
  p f.pos        # => 7
  p f.gets       # => nil
end

open('alphabets.txt', 'r') {|f| f.read }  # => "abxde\nx"

意外だったのは、a+でもr+と同様にファイルポインタは先頭から始まり、読み込むにつれて進んでいくことだ。

writeしたときにr+だと素直にその時点でのファイルポインタの位置から書き込むが、a+だと突然posが末尾に移動したうえで書き込みをしている模様。

そして書き込みを終えたら元の位置に戻る訳ではなく、末尾のままとなる。そのため読み込むことはできなくなる。

リファレンスなどでもこの辺りの説明は見当たらなかった。 Rubyの実装をみればわかるのだろうが、まだちと難しいので宿題にしておこう。