ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

URLs are (not) alone

少し前にこんなエントリを書いた。

osa.hatenablog.com

要約するとこんな感じ。

  • URL はリソースを一意に特定するのがプライマリな役割だが、可読性が高くて指し示すリソースの内容がわかると嬉しい
    • /articles/2018/03/16 より /articles/great-article のほうが良いよねという話
  • 一方で、リソースの内容がわかるようにするとキーが意味を持つことになり、変更される可能性がある
    • /articles/great-article/articles/awesome-article に変更される可能性

リソースの管理が運営主体によって為されている場合はキーの変更リスクもある程度抑えられると思うが、UGC になると表記ゆれなどもあって変更頻度が結構高くなることが考える。

Cool URIs don't change なので URL は変わってほしくない。Web ってのはリンクなんだ。

このあたり、Wikipedia, Weblio, ニコニコ大百科, ピクシブ百科事典なんかは UGC だけど(マルチバイト文字の)タイトルを path に含めていてそれがベストプラクティス感がある。

ただ、やっぱり表記ゆれとかが怖いなーと思っていたら可読性について別の考え方が浮かんだ。

OGP に頼るという選択肢はどうだろう?

現代において、URL が貼られる場所(Twitter, Slack, Facebook, LINE, etc...)ではだいたい OGP を読み込んでタイトルや画像を展開してくれる。

URL が不変性を持ったままリソースの内容を表現できればそれに越したことはないが、内容を表現するために不変性が失われるのであれば URL に意味を乗せることはスパッと諦めて、OGP に意味を乗せれば良いのではないか。

一度この考え方でアプリケーションを作ってみようと思っている。

キー、その名状しがたきもの

URL とキーについて考えていたら、URL に含むキーについていろいろ思うところがあったので雑に dump しておく。

暗黙のうちに REST を前提にしてしまっているところはあるが、意識的に REST とは文脈を切り離して素朴に書いてみる。

重複する部分もあるが、URL 側とキー側、それぞれの観点から書いてみる。

URL に求めること

自分が URL に求めることはいくつかあるが主に以下のようなことを求めている

  • URL だけで目的のページにたどり着けること
  • ページの簡易的な説明になっていること
  • ASCII で構成されていること

それぞれの意図するところと重要度について簡単に書く。

[MUST] URL だけで目的のページにたどり着けること

あるページにたどり着くために「URL が指し示すページに行って、次に◯◯というボタンをクリックして……」みたいなことはやりたくない。

URL を入力したら一発で目的のページにたどり着いて欲しい。

(Addressable & Stateless であってほしい)

[SHOULD] ページの簡易的な説明になっていること

例としてブログの URL に含まれる path を考えてみる。

このとき、path が /articles/1234 となっているよりは /articles/introduction-to-key とかのほうが実際にページを見る前に内容が予測できて嬉しい。

[MAY] ASCII で構成されていること

状況によるが、可能であれば ASCII だけで構成されていると嬉しい。

ASCII 以外を URL に含めるとエンコードされて URL が長くなりがちだし、可読性も下がってしまう。

とはいえ、ASCII 以外を含めるメリットが大きいときもあるので、そういうときは無理に ASCII にはしない。

キーに求めること

URL の中で path に含まれるキーにあたる部分以外について上の条件を満たすのはわりと難しくない。キーをどうするかが悩ましい。

キーに求めることは以下のようなもの。

  • リソースを一意に特定できること
  • リソースを一意に特定する以外の意味を持たないこと

[MUST] リソースを一意に特定できること

これはキーの定義上必須。

ただし、path に含まれる文字列がそのまま物理的に保存されている必要性は必ずしもない。

[SHOULD] リソースを一意に特定する以外の意味を持たないこと

キーとはリソースなりレコードなりを一意に特定するものであり、それ未満でもそれ超過でもない。

仮に auto increment な primary key を作成順の判別に使っているとしたら、それはキーの目的外利用になる。そもそも、作成順は作成日時から導出できるので整数列にしてしまうということは情報を損失している。

人間は整数を見るとどうしても比較したりソートしたりしてしまうので、この観点からはキーは乱数(UUID v4 やランダム文字列)だと嬉しい。*1

対立とバランス

上に書いたものの中で真っ向から対立するものがある。

  • URLはページの簡易的な説明になっていること
  • キーはリソースを一意に特定する以外の意味を持たないこと

URL はページの簡易的な説明になっていてほしいけど、URL に含まれるキーはリソースを一意に特定する以外の意味を持ってほしくない。

このふたつの間でどういう風にバランスを取るかというのが良い URL/キーをつくる上で重要だと思っている。

いくつかの例を考えてみる。

例1:商品管理アプリケーション

小売店の商品を管理するアプリケーションを考えてみる。

この店舗が扱う商品すべてに JAN コード(バーコード)が付与されているなら JAN コードは商品を代表する属性として妥当なように思われる。

そこで JAN コードを path に含むキーとして採用して /products/4569951116179 のような path にするのが良いかもしれない。

例2:石コレクターのための石管理アプリケーション

そのへんで拾ってきた石を管理するアプリケーションを考えてみる。

石はこの上なく自然のものだが、石に自然キーはない。

そこで何かしらのサロゲートキーを用意してやる必要があるが、ここで auto increment な整数を使うとキーに必要以上の意味を持たせてしまうことになるため、この場合は /stones/5f498e23 のようなランダムな文字列をキーとして含む path が良いかもしれない。

例3:薄い本管理アプリケーション

薄い本を管理するアプリケーションを考える。

薄い本には ISBN のようなものはないのでこれをキーにすることはできない。*2

とはいえ、本にはタイトルというその本を代表する属性がある。ただし、タイトルは重複する可能性があるのでそのままキーにすることはできないし、多くの場合、エンコーディングが必要な文字を含んでいるのでそのままだと path に含めて扱いにくい。そこで、本を代表する属性であるタイトルを元に新たにキーを考える。

たとえば /books/sekaiichi-kawaii-boku のような感じ。

キーの選び方

例1で触れたように、リソースを代表する属性がありそれがキーである場合、代表する属性を path に含めるキーにするのが良い気がする。

例2で触れたように、リソースを代表する属性がない場合、人工的で意味を見出しにくいキーを path に含めるのが良い気がする。

例3で触れたように、リソースを代表する属性はあるがそれがキーではない場合、代表する属性を元に人工的で意味のあるキーを path に含めるのが良い気がする。

例1は自然キーを使った path、例2は意味のない人工キーを使った path になっていて対照的。

例3はそれらの中間でリソースを代表する属性から意味のある人工キーを作り出している。いわば、半自然キーとでも言うべきものになっている。

おわりに

URL を考えるだけでもこんなに苦労するのでプログラミングはむずかしい。

書きながらキーについて思うところが増えてきたので、近いうちにまた何か書くかもしれない。

うぐぅ

*1:UUID やランダム文字列も比較・ソート可能だが、多くの人間はやらない。

*2:ISBN あればそれをキーにできるかというとそうでもないけど。

Ruby で 1 < 2 < 3 みたいな比較演算を書けるようにする試み

TL;DR

華麗に失敗した。

発端

  • そういえば Python ってたしか 1 < 2 && 2 < 3 じゃなく 1 < 2 < 3 みたいに書けるよな
  • Ruby でもいろいろ魔改造すれば似たようなことできないかな?

こんなモチベーションなので実用は目的にしていない。

何番煎じかもわからないけど、考えてみたかったので考えてみた。

問題をシンプルにする

仕様を絞る。

  • 対象クラスを絞って Integer だけにする
  • 境界値を含まない場合だけを考える(<, > だけ)
  • () による演算の優先順位変更とかも考えない

イデア

「左から順番に比較して、その結果を boolean じゃなく以前の 比較結果を保持するいい感じのオブジェクト で返せば良いのでは?」

左から順番に比較していくだけなら、

  • 前回の比較が true だったのかどうか
  • 前回の比較における右辺

が取れればいけるだろうという感じ。

初めに考えていた方針

  • Comparable をいい感じに魔改造すればいけそう
  • いい感じのオブジェクトTrueClass, FalseClass あたりを継承すれば良さそう

とぼんやり考えていたところ、

  • Comparable を変更しても Integer の挙動が変わらない
  • TrueClass, FalseClass が Singleton なので継承すると new できない

あたりの問題が起こったので、Comparable じゃなく Integer を直接触るようにしつつ、比較結果オブジェクトをそのまま条件式で使えるようにするのは諦めて to_b を呼んで boolean に変換するアプローチに変更。

実装

比較結果を保持するオブジェクトComparisonResult::TrueClass, ComparisonResult::FalseClass を作って、Integer#<, Integer#> がそれを返すように変更。

コードとしてはこんな感じ。

require 'singleton'

module ComparisonResult
  class TrueClass
    def initialize(left, right)
      @left = left
      @right = right
    end

    def to_b
      true
    end

    def <(other)
      @right < other
    end

    def >(other)
      @right > other
    end
  end

  class FalseClass
    include Singleton

    def to_b
      false
    end

    def <(_)
      self
    end

    def >(_)
      self
    end
  end
end

class Integer
  def <(other)
    if (self <=> other) == -1
      ComparisonResult::TrueClass.new(self, other)
    else
      ComparisonResult::FalseClass.instance
    end
  end

  def >(other)
    if (self <=> other) == 1
      ComparisonResult::TrueClass.new(self, other)
    else
      ComparisonResult::FalseClass.instance
    end
  end
end

# <
raise '1 < 2' unless (1 < 2).to_b == true
raise '2 < 1' unless (2 < 1).to_b == false
raise '1 < 2 < 3' unless (1 < 2 < 3).to_b == true
raise '1 < 3 < 2' unless (1 < 3 < 2).to_b == false
raise '2 < 1 < 3' unless (2 < 1 < 3).to_b == false
raise '2 < 3 < 1' unless (2 < 3 < 1).to_b == false
raise '3 < 1 < 2' unless (3 < 1 < 2).to_b == false
raise '3 < 2 < 1' unless (3 < 2 < 1).to_b == false

# >
raise '1 > 2' unless (1 > 2).to_b == false
raise '2 > 1' unless (2 > 1).to_b == true
raise '1 > 2 > 3' unless (1 > 2 > 3).to_b == false
raise '1 > 3 > 2' unless (1 > 3 > 2).to_b == false
raise '2 > 1 > 3' unless (2 > 1 > 3).to_b == false
raise '2 > 3 > 1' unless (2 > 3 > 1).to_b == false
raise '3 > 1 > 2' unless (3 > 1 > 2).to_b == false
raise '3 > 2 > 1' unless (3 > 2 > 1).to_b == true

感想

一応動く何かはできたけど、思っていたことは全然できなかった。

対象クラス・境界値・優先順位あたりはゴリゴリやればいけるはずだけど、 to_b が生えているのが「これはない……」という感じ。

Ruby を書きはじめて結構経ってるけど、Ruby 全然わかってないなということがわかった。