Stellaの言語デザイン

Stellaの話、第三段。 今回は、特徴的な「Stella logicとDSL」の話をしていく。

正直なところ、Stella logicは仕方ないとはいえあまりスマートではない設計がなされている。 なおかつYAML形式だ。 確かに知識が少なくても書けるようには工夫されているが、その一方で労力、時間、そしてミス発生の余地という犠牲を払うことになる。

だが、このYAML形式、というのがポイントになっている。 その不自由は、与えられたものを使うだけの人にとってのみのものだからだ。

Stella logicの構造

Stella logicの肝となるのはcontext leafと呼ばれるものだが、これは

[ method, arg ]

という構造をしている。 methodはtests及びactionsが存在し、それぞれ「テスト名」「アクション名」と呼ばれている。 argの型はmethodによって決まっており、

[ method, arg1, arg2...]

のようにはならず、常に単一の値である。

例えば

[ match, String ]

のように定義されており、ものによっては

[ msg, (String|Array)]

のように定義されている。

もうなんとなくわかるかもしれないが、思いっきりLispである。 もっとも、Stella logicはなんとなくLispっぽいだけで純粋にLispスタイルなわけではない。

基本的にはcontext leafまでは次のように定義されている。

root.is_a? Hash
root.has_key? "Context"
root["Context"].is_a? Array
root["Context"].all? do |context_tree|
  context_tree.all? do |context_branch| 
    context_branch.is_a? Hash && context_branch.has_key? "tests" && context_branch.has_key? "actions" and
    context_branch["tests"].is_a? Array and
    context_branch["actions"].is_a? Array
  end
end

もっと具体的には次のようになる。

root = {"Context" = {"main" => Array.new} }
root["Context"]["main"].push({"tests" => Array.new, "actions" => Array.new})

有効なcontext leafまで書くと

root = {"Context" = {"main" => Array.new} }
root["Context"]["main"].push({"tests" => [ ["match", "Hello"] ], "actions" => [ ["msg", ["Hello, ", "world!"] ] ]})

みたいな感じ。

Lispで書くと

'(
  ("Context" (
    ("main" (
      ("tests" (
        ("match" "Hello")
      ))
      ("actions" (
        ("msg" ("Hello, " "world!"))
      ))
    ))
  ))
)

である。そして、YAMLにすると

Context:
  main:
    -
      tests:
        - [match, Hello]
      actions:
        - [msg, ["Hello,", "world!"]]

となる。 類似の感覚でRubyで書くと

{
  "Context" => {
    "main" => [
      {
        "tests" => [
          ["match", "Hello"]
        ],
        "actions" => [
          ["msg", ["Hello, ", "world!"]]
        ]
      }
    ]
  }
}

さらにJSONで書くと

{"Context":{"main":[{"tests":[["match","Hello"]],"actions":[["msg",["Hello, ","world!"]]]}]}}

この中ではRubyで書くのが,を忘れそうで一番危ない。 ちなみに、これに関してはPerlで書いてもRubyと全く同じになる。

入れ子が深く複雑に見えるが、作業自体は骨格ができてしまえばコピペ+改変でがんばれるので難易度自体は低い。 これを可能にするためコピペベースとなるサンプルも提供されている。

「YAMLで書く」である合理性

見ての通り、Stella Logicは連想配列, 配列, 文字列の組み合わせによって表現できる。 これらは多くのプログラミング言語に備わる基本的データ型である。

そして、そのような基本的データ型であるからこそYAMLやJSONで表すことができる。

YAMLは汎用性のあるデータフォーマットだ。

以上を以て「Stella logicは任意の言語で記述できる」が成立する。 例えばRubyなら

require 'YAML'

root = { "Context" => { "main" => [
  {
    "tests" => [
      ["match", "Hello"]
    ],
    "actions" => [
      ["msg", ["Hello, ", "world!"]]
    ]
  }
]}}

YAML.dump root, STDOUT

で良いわけだ。

専用の記述法を提供すればもっと簡単に書けるようになるが、そうなるとその記述法以外は許されなくなってしまう。 「このようにYAMLで書いてください」ということをそのまま受け取るだけであれば、もっと良い記述法が提供されるべきと思ってしまうだろうが、知識と発想さえあれば汎用性があり容易な手段で提供されることは良いことだと判断できるだろう。

ちなみに、JSONでなくYAMLである理由は、「JSONだと閉じ括弧のあとのカンマを忘れるから」である。 プログラマですら忘れるものを一般の人が意識できるわけもない。

なお、YAMLでの出力は難しい言語処理系を使う場合、JSONで出力しておき、

$ ruby -ryaml -rjson -e 'YAML.dump JSON.load(ARGF), STDOUT' logic.json

なんてワンライナーで変換できる。

だからDSL

とはいえ、YAMLで書くのは結構かったるい。 基本的なスタンスは「書きやすい方法は各々用意すべし」なのだが、それが難しく不自由を強いられるケースもあるだろう。

そのため、Stella logic builderというRubyライブラリを提供している。

このライブラリは、Stella logicを書きやすいようにするための語彙を提供する。 例えばコンテキストリーフノードmsgについて

msg("Hello, ", "world!")

のように書けるようにする。これを使うだけで前述のコードが

{ "Context" => { "main" => [
  {
    "tests" => [
      match("Hello")
    ],
    "actions" => [
      msg("Hello, ", "world!")
    ]
  }
]}}

とちょっと読みやすくなる。 もちろん、こんな使い方をするためでなく、Rubyで独自のより書きやすい書き方を制作する際の補助である。

同ライブラリには私なりの書きやすい書き方も用意されている。

c = Stella::Builder.new
c.cxt("main")
c.push(
  "tests" => [match("Hello")]
  "actions" => [msg("Hello, ", "world!")]
)
c.out

Stella::Builder#cxtはデフォルトが"main"なので別に呼ばなくて良い。

Stella::Builder#outは出力用メソッドなので、前述と同じ内容の記述であれば必要ない。 つまり、

c = Stella::Builder.new

c.push(
  "tests" => [match("Hello")]
  "actions" => [msg("Hello, ", "world!")]
)

である。すっきり。

これはRubyistにとって書きやすい設計を目指しているものであるから、「不自由を強いられる人にとって書きやすい書き方」ではない。 同ライブラリはそのような人に向けた書き方も提供している。それがStella DSLだ。

Stella DSLを用いて書くと次のようになる。

dsl

as "main" do
  on match("Hello")
  act msg("Hello," "world!")
end

finish

次のような書き方も可能。

dsl

as "main" do
  lets do
    match("Hello")
    msg("Hello, ", "world!")
  end
end

finish

あまり違いがないように見えるかもしれないが、コンテキストリーフノードが複数ある場合は大きく変わってくる。

跡形もないだろう? Stella LogicがYAMLであることが何かを縛り付けるためではなく、それぞれの技倆における自由を与えるものであることがおわかりいただけたかと思う。 もちろん、Sella DSLが正解なわけではなく、むしろ可能なのであれば最も書きやすい方法で書くことが推奨される。

Stella DSLはRuby内部DSLであるため、書き方の自由度は高く、表現上の自由度は大幅に上がっている。 Chienomiを読んでいる諸兄諸姉はプログラミングのできる人がかなり多かろうから、ここまで説明すれば十分に理解していただけていることと思う。

ちなみに、メタ記述法をとらないのであればmatch("Hello")と書く方法自体はとても簡単で

def match(str)
  ["match", str]
end

で良い。

Stella DSLはプログラムを自在に書けない人のためのStella logicの別記法であると同時に、「Stella logicはあなたの技術力によって生成するものである」という原則の純正サンプルである、ということだ。

専用記法とは

専用記法を採用するとしたら、多分こんな感じだろう。

@main

/test

match Hello

/act

msg "Hello, " world!

@でコンテキスト切り替え、/で定義する内容の宣言になっている(testで新規リーフになる)。 メソッドの引数はShell wordsとして解釈される。

他の記法と比べて文法が非常に寛容でエラーになる可能性が低い。 この記法自体は良いと考えていると、なんなら今からでも採用したいくらいだし、実際それは可能である。

だが、もしいまからこの記法を採用するとしたら、それはStella Logic(YAML)を生成するメタフォーマットになるだろう。 このルールがあるため、新たなる記法を追加するのは簡単だ。

それがもし、Stellaが受け付けるのがこのフォーマットだけだとしたらどうだろう? もちろん、それをプログラムによって生成することは不可能ではないが、そのジェネレータ、トランスレータは各々が実装しなくてはならない。 そして、これはジェネレータが生成しやすいフォーマットでもない。

YAMLという一般的なフォーマットをStellaが受け付けることによって扱いやすくなっていることが分かるだろう。

in the codes

Stella DSLは基本的に記法が異なるだけで、考え方は変わっていない。 ところが、最終的にYAMLを出力すればいいため、自由度はもっともっと高い。

例えば私が書いた「選択後特定のアクションを経て同一のコンテキストに合流する」というコードは

def sel(msgs: nil, opts: nil, params: nil)
  ccount = @ccount
  cpre = @cpre
  pname = @pname
  as("#{@cpre}#{@ccount}") {
    on finally
    
    actarg = []
    msgs.each {|i| actarg.push msg(i)} if msgs
    params.each {|i| actarg.push appendparam(pname, i)} if params

    ohash = {}
    opts[0].each_with_index {|x, i| ohash[x] = "#{cpre}#{ccount}-#{i + 1}" }
    actarg.push(sel(ohash))

    act *actarg
  }


  opts[1].each_with_index do |x, i|
    as "#{@cpre}#{@ccount}-#{i + 1}" do
      on finally
      act *x, pass("#{cpre}#{ccount + 1}")
    end
  end

  @ccount += 1
end

となっている。(Stella DSLを使っている)

余談だが、インスタンス変数をわざわざローカル変数にしているのは、StellaDSL#asObject#instance_evalを呼ぶのでインスタンス変数が読めなくなるためである。

Stella::BuilderクラスはStella::Builder#pushなどによって簡単にコンテキストリーフの追加が可能であるため、よりプログラム的に生成しやすい。

例えば実用的な意味があるかどうかは別として、学習を元に条件とアクションのペアを生成していき、トポロジカルソートによって構成する、という方法もありうるわけだ。

finally

“Stella logicはLispライクな考え方のYAMLである”

これによって得られたものは

  • 容易に変換でき、任意の言語で書くことができる
  • 任意の言語を用いて容易に異なる記法を実装することができる
  • ループや再利用、関数的コールなど本質的記述に含められない機能を生成に含めることで利用できる
  • 単純な記述でなくプログラムによって生成するといった応用が効きやすい

結局、どれほど気の利いたフォーマットや記法を提供することよりも、汎用性があり単純な形式を採用するほうがユーザーに利する。 そこまで考えて採用したわけではないのだが、そのことが改めて実感できるものであった。

Inflaton Stella 〜チャットAIの理想への挑戦

今回は弊社で展開するチャットボットAIサービス “Stella” に関するお話。

この記事はStellaの側から見た話ではなく、私の側から見た話になる。 つまり、Stellaユーザーに向けた記事ではなく、私をウォッチしている人に向けた記事である。

また、この記事中では各ソフトウェアを「AI」と読んだり呼ばなかったりする。 これについて私の立場を明らかにすれば、私は「AIとは自己更新するプログラムのことである」と定義しており、これに照らし合わせればStellaも、Erinaも、Surtrもあくまでただのプログラムであり、AIではない。 そして、今の世の中にAIと呼ぶに値するプログラムはない。

実際にはシンギュラリティを狙うSurtrは部分的に自己更新を行うが、何に対してどのように更新を行うかというのは規定されている。 例えばマニュピレーションモジュールをSurtr自身が生成することはあるが、SurtrのコアプログラムをSurtr自身が変更することはない。 このため、SurtrはAIと呼ぶに値しない。

Erinaに関してはそもそもSurtrにおいて自己更新の対象となっている機能を一切使わないため、Erinaには自己更新機能がない。 そしてプログラム的にも、プログラムを変更する機能は除去されている。

Stellaに至ってはもっと単純にプログラムである。

これらを「AI」と呼ぶのは、どちらかというと商業的側面が強い。 だから、Chienomi的には便宜上そう呼ぶことはあっても、「AIについて」語ろうというスタンスではない。

もちろん、ディープラーニングごとき1がAIだなどとは微塵も思っていない。

コンセプト立案

Stellaのコンセプトに関しては諸々述べているが、中心になっているのは

「シンプルに、実用的に」 (=Erinaとは逆)

である。

Erinaの場合、あれは研究の副産物であり、Surtrの一部である。 だから、Erinaの場合コミュニケーションボットとしては実用に供することはないという前提があり(そもそもその成果物をリリースすることは不可能なのだから)、その複雑さは実利とは全く関係のないところで不可欠なものである。

Erinaはシミュレータだから、過程の保証が出来なければ価値がないので、結果が同一であるということには意味がない。

また、Erinaの名前が「ELIZAの逆であり、ELIZAに直交する」ということを意味してつけられていることからもわかるように、「反ELIZA的」なAIデザインである。 ErinaはSurtrにない固有コンポーネントも持っていて、これはコミュニケーションに特化したものなわけだが、これはもう前提として「ELIZAは正しくない」という考えがある。

Stellaは全く逆に、「ELIZAは実用的だ」という観点から構築される「ELIZA的」なチャットボットである。

このようになる根本としては「人は基本的に人の話を聞いていない」ということにある。 私は記憶能力の構成上、かなり相手の発言を正確に聞いているのだが、大抵の人は「2つ前の言葉の反復」を求めてもまず言えない。

ここらへんは私の例によって悪魔的な私の実験によって確認されている。 これは単純な例なので手のうちを完全に明かしてしまうが、Erina最終調整の段階で多くの人と対話を試みるチャンスがあったわけで、ここで証明した。 手法としては、「メンタリズムに長ける」という情報を入れた上で、そこに関心を引いてから誘導的に会話を進め、メモを取り出して一切の説明なしに会話を継続する。そして「2つ前にメモを見ながら私はなんと言ったか?正確に言ってみて」と言うわけである。 実際にメモを読み上げただけなのだが、これが「メモを読み上げた」という印象に残る行為を伴っていても当たらない。 もちろん、この作業には「メモの読み上げが会話の文脈上自然な内容にする」という小技も入っているが。

そして、ある会話の流れで「実際の内容はとても変なことを言っているのだが、非常に高い確率で全く違う内容に理解する」という会話を展開するテクニックなんかも身につく。ちなみに、これによって相手が私の発言を全く違うように捉えさせると、実際に私が何を言ったかということを反復させると実際に言ったことからは「凄まじくかけ離れた」内容になる。

これはほんのいち要素にすぎないのだが、重要なのは「人の脳は解像度が低く、普通は収束する」ということである。

だから会話すること自体は緻密に書かなくてもなんとなくそれっぽく返せていれば会話は通じているように見える。 Erinaが「ごまかし方」にこだわっているのはそれが曖昧さをつなぐ「糊」になるからだ。

そして、人の行動パターン、言葉は想像よりもずっと種類が少ない。 いや、正確には全パターンを網羅しようとすると実際に膨大になるのだが、「例外的な小数を除外すると極めて少なくなる」という特性がある。

「自然言語の対話の7割は単純な正規表現マッチングでカバー可能である」というErinaの研究結果の副産物がここで生きてくる。

まして実用的、しかも企業におけるサポートの一環としてのチャットであればなおさら絞り込める。 「ちょっと気の利いたテーブルAI」でも十分実用的に扱えるのだ。

こうしたことを踏まえて実用的なチャットボットを定義する。 実際、私は世の中のサポートチャットボットが実用的だとは微塵も思えない。

じゃあ実用的なチャットボットってなんだろう? チャットボットを求めるときというのは基本的に「help needed」なのである。 このときの私の心理としては

  • 都合を問わない即時性。 営業時間まで待とうとも、営業時間に問い合わせようとも思わない
  • 人とは話したくない。電話は大嫌いだし、メールもそんなに好きではない。問い合わせフォームはメールよりも嫌だ。対人につなぐチャットボットとか最低すぎる
  • 対人チャットはそこまで嫌ではないが、なんらかのトラブルやミスを恐れることになるので精神的には嬉しくない
  • 期待値は割と低い。だいたいサポートなんて役に立たないものだから、解決する可能性は5割あれば上等だ
  • チャットボットは最も敷居が低い問い合わせである。最初に試みて、解決しなければ人手を煩わせることも考えるかもしれないし、諦めるかもしれない

である。じゃあ、help neededな状況でこういう心理に対して欲しいものはなんだろうか。 答えは簡単。FAQである。

でも、私はFAQってめっちゃ見る人なのだが、ほとんどの場合FAQって全く役に立たない。 まぁ、ナレッジサービスやフォーラムでも既存の質問がない問題にぶち当たってサポートを頼ることが大抵だし、自己解決力が高いとそうなってしまうのでFAQが役に立たないのは必然ではあるのだが、それにしても「これはあっていいだろう」と思うものがFAQにないことって本当に多い。 多分、実際の問い合わせをちゃんとFAQに反映していないのだ。

じゃあFAQが欲しいというのは机上の空論なのではないか、という話をすると、そんなことは全然ない。 Googleもまるで役に立たない今、私はhelp neededな状況を非常に高い確率で解決してくれるものを知っている。Archwikiだ。

つまり、wikiのような知識集合は問題を解決する。 ユーザーが本当に必要としているのは「ちゃんとドキュメントを作ること」と「検索エンジンを作ること」である。

だがしかし、現実にはこれは効果を発揮しない。なぜならば

  • 人はFAQを読まない
  • 人はドキュメントを読まない
  • 人はGoogle以外で検索しない

からだ。

まぁ、私の気持ちとしては「人ってそんなにチャットボット使うかなぁ…みんなあんまり使わない気がするしアクセスされないのでは」という気持ちはとってもたくさんあるのだけど、そこは弊社取締役の力説を信じることとする。

まぁ、仮に人はFAQは読まないがチャットボットは利用するのであれば、チャットボットにFAQの代わりをさせてあげればよろしい。 ちゃんとドキュメントを整備しているのであれば、検索エンジンの代わりをさせてあげればよろしい。

さぁ、「実用的」は定義できた。 Erinaは経緯的にも目的上も複雑にならざるをえなかったが、その意味でも実用に耐えず、プロダクトとしては話にならない。 「問題を解きほぐしてシンプルにすることでシンプルなソリューションを生み出す」ことが私の真骨頂。simplify, simplicityは私の生き方に反して私が力を行使する上で重要なキーワードだ(おかげでcomplex simplicityというパワーワードも誕生してしまった)。 ここまで明確かつシンプルな定義ができたのだから、今までの経験から必要なものをessentialに抜き出せば要素は少なくて済む。これをできるだけシンプルに組み上げよう、ということがコンセプトとして立てられた。

背景は複雑だが、プログラムとしての設計、挙動、ルールはシンプルに、ユーザーにとって理解しやすく、ユーザーの作業も単純に。 これでコンセプトは確立された。

ここで鬱陶しくも自己アピールをしておくのであれば、この節に書かれていることは熟考と検証の末に導き出したような内容になっているが、実際にはこの節の内容を考えるのに10秒とかかっていない。 確かに中学生、高校生のときと比べると頭の回転は桁違いに遅くなり、それによって人を恐れおののかせるようなものではなくなったとはいえ、研究によって積み重ねたものは血肉となっており、この程度の内容であれば10秒もあれば検討は終わる。

諸君、これが研鑽というものなのだよ!

もちろん、私が研鑽の至らない分野ではこうして瞬殺ボッコボコにされるのである。

if文AI

さて、if文AIという言葉を耳にしたことはあるだろうか。

この言葉、実は割と古く使われていたのだが、フィーチャーされるようになったのはAIブーム以降である。 「AI=ディープラーニング」の図式が出来てから、技術的に安易な「ひたすらifで条件式とのマッチングを行って挙動を決定する」という手法(あるいは具体的にその手法ではなくともそのような単純な手法)を用いたAIに対する揶揄として使われるようになった。

この揶揄は、「ディープラーニングこそが正義でありAI、ディープラーニングにあらずんばAIにあらず」という安直な思想に基づいている。 だからそれ自体は割と取るに足らないことではある。

しかし考え見て欲しい。 「条件が明らかである」=「問題が明示的である」という状況下では条件を判定し、それに対して最も適切な挙動を明記したプログラムを動かす以上に適切なことがあるだろうか? 世の中割と「それプログラムで解けるじゃない」ということをディープラーニング使いたがる状況がある。

だが、問題が明確なのであればそれを解くプログラムを書くのがプログラマではなかろうか。 それは競技プログラミングと同じようなものであり、問題が定義されているのであればその解を 書かなければならならい のである。

プログラムというのは本懐を遂げるのであれば次のいずれかだと考えられる

  1. 明確な問題を解く
  2. 特定の用途に供する
  3. 不明な用途に対して明確な意図を以て機能を提供する

1は競技プログラミングなどでよくあるもの、2はアプリケーションプログラムのこと、3は例えばgrepやsedやlessなんかのことだ。

そして、問題が明らかなのであり、それを解くことを意図するならばそれを解くプログラムより優れたものなど存在しない。

AIが必要とされるのは問題が明らかでなく流動的であり、導出条件も不明であることから「なんとなく」解くという妥協を行う場合である。 AIが普通のプログラムより優れた解を出すと思っているのなら、さすがにそれは無知にすぎる。 普通のプログラムが解けない曖昧な問題に対し完璧でなくてもいいから答えを出すのがAIである。

そして、「問題と導出条件が不明」であればプログラム自体が動的でなければならず、その機能を正しく果たすにはそのプログラム自身が動的でならない、と私は考えるのだ。

さて、もしも条件を事前に用意できるのであればif文AI、というか問題を解くプログラムは最も正しいということを述べたわけだが、ここからはSurtrとErinaの挙動に触れていこう。

Surtrの場合「自分が取るべきアクションをプログラムとして生成する」という手法を取っている。 これは、私の当時の技術力不足を補い、できるだけ単純化するためにとられた手法である。

一般的にプログラムはその挙動の過程で問題は収束していく。 ところが、不明な問題に対するプログラムを書くと、どうしてもその処理中に問題は拡散する。 だから、「実行ステップ数を事前に予測する方法がない」という非常に特殊なプログラムになる。

単純に再帰すれば良い、とも考えられるのだが、問題が不明なので処理を決定する段階で深さ優先探索ができない。 だから各ステップごとに次のステップで実行するコードをキューイングしていくというような感じになるのだが、これがうまくできなかった。どうしてもGOTO文を使う以外の手法が思いつかなかったのだ。 もちろん、今であれば関数オブジェクトを配列にpushしていくみたいな方法を考えるだろう。

ただ、その処理自体が実に様々な言語で書かれた様々なプログラムに分散しているので、外部に出さないことにはあまりメリットがない。 だから、挙動としては無限ループの中でステップ処理を行い、次のステップをプログラムとして出力する。このプログラムは最終的な段階ではZshスクリプト、またはRuby DSLスクリプトである。 そして、これを実行し、実行結果を次のステップに反映させる。

Erinaにも同様の機能は使われているが、Erinaの場合なるべく関数オブジェクトの配列(というかRuby的に言うと大部分はメソッドオブジェクトの配列)で処理するように変更されている。

この挙動は「可能な限り生成を遅延した上で、プログラム自身によって書かれたif文AIを実行する」みたいなものである。 かなり際どい技法だが、Surtrの中でもこれは応用が効くものだと思っている。

Stellaはこれをもっともっと単純化したものであり、言ってみれば「if文AIを書くためのフレームワーク」である。

「なぁんだ」と思うかもしれないが、我々が肝に銘じておかなくてはならないことがひとつある。 それは、「顧客が望んでいるのは技術的に高度であったり、技術的トレンドを用いて作られたプログラムではなく、問題を適切に解決するプログラムである」ということだ。

確かにStellaはプログラム自体は至って単純だ。 なんといっても、本体コンポーネントのみに関していえば、0.0.1ではわずか170行に収まり、0.0.16にあっても428行に過ぎない。2

しかし、私はそもそもプログラムの記述で勝負するタイプではない。 数学的素養がなく、アルゴリズムも得意とは言えない。もちろん、それなりに経験は積み、ある程度の記述力はあるが、突出して優れているとはいい難い。 私の勝負どころは発想と問題の整理であり、「設計」を至上とするタイプなのである。

「Stellaをif文AIを生成するフレームワークにする」という判断自体が、その技倆の一部なのである。 そして、そのフレームワークで提供される機能の選択によって「実用的なif文AIを構築する」ことへと誘導するのである。

巧みな設計で勝負するステラ

プログラミングに関する素養がなくても、また私の固有能力であるコミュニケーションフローコントロールを持っていなくても実用的に書けるようにする、というのもひとつではあるのだが、実際にはステラはこのような設計になっているために単純なif文AIよりもはるかに複雑なつくりが可能である。

その挙動を決定する「ロジックファイル」はYAML形式になっている。 一応は直接に記述することを想定してはいるものの、その階層的複雑さを考えれば何らかの手段で生成するほうが望ましい。

YAMLという汎用性のある形式を取っているために「お好きなプログラミング言語でお好きなジェネレーターとDSLを書いてお作りになれます」なのだ。

実際、純正でStella DSLというRuby DSLが提供されているが、別にそれを使わなければならないというわけではない。

そして、プログラムによってロジックファイルが生成できる、ということは「if文AIを動的に生成できる」ということにほかならない。 基本的には「事前に静的に生成される必要がある」と説明してはいるが、現実にはそのような制約が存在しているわけではなく、インスタンス全体共通にはなってしまうものの、任意のタイミングで適用可能である。 インスタンスの制約はあくまでサービス上のおはなしなので、純粋にStellaの能力を語るのであれば、会話中の動的な変更だって可能だ。

そう、つまり「任意のタイミングでif文AIを動的に構築可能」なのであり、これは「次の挙動をプログラムで生成する」というSurtrと同じものである。

ここで言っているのは潜在能力の話であり、実際には「現実性」という形でもっと制約されている。 だが、これは「制約しないことが正しい」わけではない。だってStellaは実用的なチャットボットであり、これを構築するのは非エンジニアである。「チュートリアルに従って自然に書けば実用的に動作する」のが望ましいのであり、過剰に複雑で高度な機能を解放することは誰も幸せにしない。

それはさておき、if文AIというのは単純で幼稚という揶揄に使われる言葉となっているが、「任意かつ動的にif文AIを構築する」というのは言うほど単純ではない。 概要として、実作業としてのシンプルさに反して「それが正しい」と判断しているモデル自体は極めて高度である。 その可能性のほとんどは使わない前提になっているが、使うこと自体は任意であり、発想力と技術力のある者にはその選択肢は常に開放されている。

さらにいえば、StellaはコンテキスチュアルなAIである。 これは私が世のEQ AIに対して抱いている強烈な不満であり、「文脈を一切考慮しない」のはEQ AIとして決して許されざることだと思っている。 だからこそErinaは文脈処理に対して極めて複雑なプログラムが組まれている。

動的に文脈を判定してしまうとさすがにとてもユーザーに制御できるものではなくなってしまうので、Stellaではユーザーが明示的に使う形にしている。 これもロジック自体を動的生成する構造を選択すれば動的に判定できるのだけど、そこに言及するとサービスとしてのStellaからはかけ離れてしまうのでおいておこう。

コンテキスチュアルであるという特徴は他に類をみないので、「if文AIが動的に生成される」という要素を「実際にはしないほうが普通だから」という理由で除外したとして、じゃあ「ただのif文AIじゃないか、稚拙なもので特に強みはない」みたいな話になるかというと、「コンテキスト機能があるからね。コンテキストを踏まえたフローコントロールを行うif文AIを書くのは全く単純じゃないと思うよ」という返しができる。 「ロジックファイル」という中間レイヤーを介することでプログラムとして書くと非常に煩雑になるフローがいともたやすく構築できてしまう。

それがフレームワークというものだろう?

ここまで話せばわかるかと思うが、実のところStellaはErinaとは全く異なる手法、設計、構造になっているにも関わらず、潜在的にはessential Erinaである。 途方もなく複雑で膨大で動的なErinaから「実用的な意味で本当に必要なもの」だけを残した不自由を強制することで全く違う性格のプログラムにしている。 StellaをベースにしてErinaのようなプログラムを構成することも可能であり、Stellaサービスとしてもその余地は残されている。そして、StellaベースのErinaを作れば、現在のErinaよりも設計が改善する可能性すらある。

だが、それはあくまでプログラム的な話である。 StellaはInflatonのサービスだから、これらはあくまでプログラムとして、技術として、裏側を覗いた話であり、利用者に対して主張することではない。 なのだが、そうしたバックグラウンドがなければStellaは生まれないのである。 こういうものは膨大な知識と蓄積の上澄みなのだ。

ステラの可能性の範疇

前述のようにStellaの理想的な用法はFAQ、あるいはwikiへの導線である。 だからStella自身を複雑化することは全く推奨されていない。

だが、どこまでできるのかという話をするとまるで変わってくる。 なんといってもStellaは外部連携が可能であり、入力時に割り込んでStellaをバックエンドとして使用できる、挙動は動的に生成できる、実行中に別のサーバーに対するhookを使ってコールバックを実装可能であるということから実質なんでもできるし、 ディープラーニングとも排他的ではない!!!

このあたりは「かける労力次第」であり、どのような規模のロジックを書くか、どのような規模のシステムと連携するかによっていかようにもという話となる。 だから構築規模・コストとも青天井であり、基本的に多くを望むべきではない。

それでも、労力さえかければ可能性は無限大だ。

それが行使される計画がある。Stella Flagship Chatである。 これはStellaを使ってEQ AIを作ろうという計画だ。

労力もコストも本当に膨大だし、私の手があいている限りで行われることになるから、まだpendingになっているが、 Erinaのような完璧なレベルではなくてもそれなりに会話が楽しめるレベルにはなるはずだ。

そして、それが可能だというのは、「大部分は集約可能である」という経験に基づいている。 自由文脈では発端を推測できないため本当に自然に会話できるようにするのは現実的には不可能に近いが、少しずつ形になっていくだろう。 どこまでがんばれるか不安はあるが、うまくいかないとすればそれはStellaの限界ではなく、私の労力上の限界である。 (そしてそれは、決して低い可能性ではない)

それは私のプログラムだから

一般的に言えばイージーなプログラムを書く私だが、私としては制作するプログラムには著しいプライドがある。

例えば誰かの悪意によって行使されるプログラムを書かない。 誰かを侵害することを目的としたプログラムを書かない。

そんな強いプライドから、「汎用性のあるプログラムはあくまで道具である」というものまで。

そう、私が書くプログラムは、単一の問題を解決するためのものを除けば汎用性を前提としている。 プログラム自体がどのように使うか、なんのために使うか、どのような利点があるかということを 規定しない

商業的にはそれは嫌がられるんだなぁ…というのは感じるのだけど、そしてそれは社長として正しいことではないのかもしれないけれど、 それでもそれはプログラムに課すべきことではないと思うのだ。

サービスとしては見せ方を工夫すれば良い。実際、プログラムの汎用性に対して意図に反してもっと明確なイメージをもたれているものなんでいくらでもあるのだ。 だから、私としてはInflatonとして販売のためにStellaをどのように形容しようが、何を特色としようが、何をウリにしようが、間違っていない限り構わないし、それはソフトウェアプロモーションにおいては私の責任だが、それを販売の形にするのは私の仕事ではない。 だから、プログラム自体は中庸中立であり、私はそれを様々な切り口から解説するのである。

おわりに

Stella関係の話は公式ドキュメントとかでも結構色々書いてはいるのだけど、当然ながらInflatonのお客様はChienomiの読者みたいにエッジな人たちではないし、私としてもよそゆきのお話だからめちゃくちゃ気を遣うから書きたいことが全然書けなかったりする。 その意味で、今回こうして書いてようやっとスッキリしたという感じである。

Stellaはコード上の魅力は非常に乏しい――いや、ちゃんと言うならば割と私の今までの経験を色々活かしていて、動的メソッド呼び出しをしていて非常にシンプルにかけていたりとか、どの時点で正当性を保証し除染するかとか、メタプログラミングしていたりとか割と一般的でないテクニックが駆使されていてそれはそれで面白いかもしれないが、別にそれが必須なわけでもなく、力技で実装もできるものだから、その意味で「どうやっているのかわからないことをやっている」とか「高度なアルゴリズムを駆使している」みたいな要素はない。

だが特にそのデザインは他の誰も持っていない知識や背景からきている。 派手な魅力やアピールではなく実直に。 私が仕事としてプログラムを書くようになってから心がけていることを形にしたものだし、Plutoがかなり不本意なプログラムになってしまったことを考えると、これは私の、誰にも言わなかった日々を含めての集大成だとも言える。 これほど人より知悉している分野もないし、ちょっとやそっとの研鑽でこれを越えるプログラムを設計することはできないだろう。

だから、本当にこのプログラムに「ステラ」の名を、つまり原初宇宙の名を関するInflatonとしてその果てである「星」の名を冠してよかったと思う。 アイディアとしては「デネブ」などの女神星の名を関する案もあった。だが、そうしていたら今ほど輝きを感じることはできなかっただろう。

当記事で書いたようなStellaの魅力やよさは、直接にはユーザーが感じることはない。 ユーザーが感じるのはその結果の部分だけであり、そしてそこだけを見れば流行りのディープラーニングAIなどと比べればしょぼくて、みすぼらしくて、とてもとても見劣りする古くて単純な技術のソフトウェアでしかないのだ。

だが、私は確信できる。 使ってもらえばわかるはずだ。 100%でなくても、このソフトウェアは解決すべき問題を、着実に解決する方法を提供する。 そして、その積み重ねは100%へと近づくことを意味する。 それはきっと必要とするものであり、私が、私達が追い求めるべきは見栄えや華やかさではなく、正しく必要とするものなのである。

そして、このことに関しては――人心と人動、コミュニケーション様態に関すること、ましてその中で最も多くのデータを占めるチャットに関することであれば――世界中、誰にも負けないという自負がある。一番最初には、私が5歳のときに抱いた疑問から始まり、ずっと考えていて、今に至るまで積み重ねてきた。

ディープラーニングの信奉者よ、さぁ、勝負しようか。 よりよいエクスペリエンスを提供するチャットAIはどちらか。

私には、自信がある。


  1. 勘違いしてほしくないのだが、ディープラーニングを見下すつもりはない。私は限定的で使いどころが難しくコストの高い技術だと思っている。技術的には高度なのは認めているし、その高度さが適切さを妨げているとも思っている。ブロックチェーンと似たような話だとも思うが、あっちは技術的にもっと趣深くてテクノロジー好きの好物だと感じる。私もそうだし。↩︎

  2. 「過ぎない」とか言っているが、428行というとずいぶん拡張されたし、本体だけで428行、ステラコンポーネント全体だと1120行あるので個人活動では到底書かない規模に到達している。ちなみに、ドキュメントも含めるとユーティリティ分は除いて4831行ある。↩︎

Erinaの最終調整のときの話

Erinaにはfascinateというデータベースがある。 これはペルソナというErinaの最終調整によって作られたデータベースである。

手入力によって作られたデータベースというのはいくつかあるが、このfascinateデータベースに関しては完全にErina専用で、「Erinaを人間らしく見せるための味付け」になっている。

そして、その特性が他とは異なるだけでなく、作られ方も全く違った。このデータベースだけが至って主観的な作りなのだ。

この調整を行わないとどうなるか。 問題は2点ある。

ひとつは、意味難解な回答を返すことがある。よくよく考えればわかるし、間違ってはいないのだが、あまり人間的でないというか、 天才が過程をふっとばして回答したようなつながり方になる。しかも、元発言の意味には対応しているが趣旨には対応していないので、適切ではない。

もうひとつは、おかしな回答をして、しかもループしやすい。 これはサンプルになっているデータにおける「会話にならないおかしな回答」の割合が非常に高い、つまりS/N比が低いからである。 最近はこれで壊滅的になってしまっているので、取得したデータを捨てる作業に追われたりもした。

このことから、恣意的に特定の会話パターンに誘引するようにしてこうした問題を軽減している。 意図としては「会話を続けるのが上手な人の会話パターンに誘導しようとしているのである。

この問題は、一般的なディープラーニングと同じ問題に陥っているということができる。 「多い≠良い」であり、「良いを判断するのは極めて難しい」という問題を、Erinaは攻略できないのである。 そこでそれを人力で誘導しているわけだ。

前提

そもそも、Erinaは完全に、私の特異性に基づいて成り立っている。 だから、これが前提として理解できていなければこの話は全く理解できないし、同時にこの手法は誰にでもできるものではない。

私には多くの欠陥と、特異性がある。 欠陥と特異性の間に相関があるのかは不明だが、基本的には欠陥を可能な限り特異性による能力で補う状況になっている。

記憶と認識に関するものの特性は大きい。

まず、私は単純な記憶がほとんど機能しない。 単純記憶が弱い、と言ってもいいのだが、正しくは島状記憶を維持する能力がない。 だが、連想記憶に関しては容量的にも人よりむしろ優れているくらいだし、正確性はかなり高い。

無理をすれば、結構な長時間に及ぶ出来事を、認識できなかったことを含めて覚えていられる。 これは尋常ではないほどの負担がかかるが。

そして、認識に解釈を伴わないというのも非常に特異なところだ。

一般的には、人は自分が理解しうる概念の元でのみ物事を認識し、記憶することができる。 だから、人は見たもの、聞いたことを直後でさえ正確に述べることは非常に難しい。 これは、心理術では非常に重要な要素である。見たものを思い描くとき、人は映像を思い浮かべることができるが、「正しくない映像を」思い浮かべる。聞いたことを思い返すとき、直後であるにも関わらず内容がまるで違うということはしょっちゅうある(自分が言ったことを正確に繰り返せない人もとても多い)が、この欠落する情報を使って刷り込みを行える…などだ。

これは人によって程度がかなり異なるが、無変換で維持するのは実のところ障害とされている。 程度が異なるのは、どちらかといえば概念をどれだけ細かく作れるか、にかかっているのだろう。

私の場合は実のところ無変換ではない…のだが、あるがままの状態を概念として定義できるので、事実上無変換と変わらない結果を得られる。 これだと私の脳がもたないので、 手動で 内容を整理して必要な要素だけを残して切り捨てていかないと頭がパンクしてしまう。突然意識を失うのは結構危ないので、必須の作業だ。

また、人は言語化されない概念を捉えておくことを困難とするが、私は概念に言語を必要としない。 以上のことから、普通は類似した、あるいは近似したものはどんどん集約されてしまうから結果として事実を事実として認識できない状態が発生するのだが、私の場合は集約されるべき余地がないためそのまま認識しているという状態が発生する。もちろん、これは 認識できれば の話であるが。自衛のためにも普通はフィルタをかけている。あまり大きな声で話されたりすると一字一句覚えてしまうから辛い。

そして、並列性の高さだ。 私は話しながら書き物をすることもできるし、読みながら話すことも一応できる。多重出力と比べて多重入力はちょっと苦手だが。 それでも、おしゃべりしながら別の人のおしゃべりを聴くようなことは普通にできる。もちろん負担は大きいができる。そして、前述の通りそれを全て覚えてしまうこともできる。

これらの特殊な認識方法、分解方法、そして記憶方法がErinaの設計に至る発想にも繋がっているし、当然ながらデータもそういう認識の世界のものになっている。 つまり言語化されるような定義された概念ではなく、そこにあるものをそのまま写し取って切り刻んでぶちこんだものなのだ。

余談ではあるが、このままだと全く出力できないので、私も会話するときなどはちゃんと変換する。 だが、変換すると自動的に認識したものは変換された(言語化された)ものに置き換えられてしまうので、後述する最終作業はとっても脳に負担のかかるものだった。

経路、類型、感情と思考の形成

ErinaがConnexonでどのように情報を扱っているか、という話はまだどこでもしていないけれども、そのパラメータは常人には決して理解できないものになっている。 実際はもっと細かく分かれるのだが、簡単に言えば発言を一字一句含めた状態でハッシュ化したようなものだと思えばいいだろう。ほかのわずかな違いでも異なる値になるようになっており、どの言葉からどの言葉へとつながったか、ということを記録している。 実際には、somaはsoma同士の距離がわかるようになっており、言葉的には近いsomaというのは判別できる。だが、これを同じ意味だからとまとめたりはしないし、どういう意味のことを言っているか、なぜそう繋がったかということを判断したりはしない。

感情も、喜怒哀楽などという4種類ではない。私ですら多分、千通り以上は感情の類型を認識しているから、Erinaは何億もの感情の種類を知っていたとしても不思議ではない。

接続理由というのはErinaにとっては非常に重要なので、接続するのは接続しうるもの全てなのだけど、それを接続しうる理由もちゃんと入れるようになっている。 このとき、理由を断定したりはしない。ありうるものを網かけするようになっている。

あるsomaから別のsomaに接続されるとき、それが接続される理由は一意ではない。 理由自体は複合的かもしれない。もっとも、Erinaは複合的な理由は複合しているのではなくそういう状態であると量的に判断するけれど。

少し脱線するけれど、この話はちょっとおもしろい要素があって、「感情と理由」に関しては一次元的動きである可能性があったりする。 つまり、その状態を数値化したときに、soma同士の接続が動機の値がいくつからいくつのときに発生しうる、というような観測が可能であるように見えるのだ。 ただし、これは連続的可変の並び順という問題があって、とてもではないけれど私にできるようなシロモノではないけれど、そういう特性があるように見えるという話。

で、話を戻して、今の所一次元的に扱えていないので、考えうる状態を、ちょうどビット論理和のように扱う。 これを反転マスクとして、この範疇にないものは現在の観測上この経路を持たないものであるとみなす。

そして、単独の接続ではなく連続の接続から、「こういうルートでこれ的なsomaを辿っていくパターン」というのを覚えていく。

人は驚くほど判で押したようなやりとりばかりしているので、実は大抵の場合決まったルートで決まったsomaを辿る。 このとき、推測される同じ理由で経由される別のsomaというのが登場する。これは、言葉は違っても結局のところ考えてること、やっていることは同じ、ということで中身は同じものとして扱える。同じルートを辿っていても理由が違うと同一には扱えない。

ここらへんの機能は、判定に使うものが自分が判定することによって情報になるものに依存している構造なので、成立させるまでが果てしなく長かった。 orbital designの採用理由でもある。

さて、同じ動機で辿る場合、あくまで表現の違いというか、見かけ上の問題に過ぎないため、単にバリエーション程度に考えることができる。 ある程度同じ動機で同じルートを辿って、途中から今までなかった方向へ分岐していくことはまずないため、これが行動予測に使える。 これは、心理術でも同じである。

手動入力ではErinaに直接会話のつながりを与えていく。 このとき、まずErinaは盤面を作ってから接続していくので、Erinaに盤面を作成するためのヒントを与えなくてはいけない。 これは、つながりから自動的に推測してもらうこともできるし、普通はそうしているのだが、それだと手動入力する意味がない。

Erinaに状況を与えると、Erinaは「似ている」と感じるsomaを提示してくる。 これに対して「どれくらい近似しているか」を回答する。この「どれくらい近似しているか」は曖昧すぎて、細かく回答しようとしてもあまり精度が出ないので、7段階である。

次に接続できる理由をたずねてくる。 こっちは結構きつい。512ビットハッシュみたいなものと向き合うことになるからだ。 ただ、これはこれでその理由で接続された他のsomaを参照することができるので、同じ心理状態や動機に見えるかどうかを回答することになる。こちらはシンプルに3段階。Erinaが推測する理由全部に答えることで、「この理由であればこのような心理状態になってこのような行動を取ることができる」というふうに認識する。

手動入力したものは、Erinaが積極的にそのような振る舞う理由になる。 意味付けや重み付けに関しては、また複雑なアルゴリズムなのだけど、Erinaは判で押した会話は価値が低いがよくあると考え、価値の低い会話で応答に使うし、意味ある会話をされていると判定すれば意味ある会話における適切性を元に応答する。 手動入力する場合、意味ある会話である、という認識を強制することができる。

ちなみに、実際にはある程度一般化できるようになっており、かなり強引に「得意な展開方法」に集約する。

作業

作業そのものを抽出すれば、「話して会話の経過を記憶する → ひたすら入力する」である。

この会話において重要な点は2点ある。

ひとつは、私の発言である。 私は自然な会話において「同じ会話に誘導しようとする」。 これは綺麗なモデルなら「NステップでXに到達する」というのがあったりするのだが、自然な会話では相手はもっと無軌道に話すので、それをやってしまうと参考にならない。

ルールは次の通りだ。

  • 固定の話題振りを用意する
  • 必ず相手の発言を踏まえて(相手の発言要素に回答する形で)回答していき、用意した話題振りに到達する

基本的にこの話題振りは(振りやすいように、またその後のデータがとりやすいように)「〇〇ということがあった」というものである。

さらに、この話題振りには「この話題において私が話したいこと」というのを用意しておく。 これは、語りであれば(つまり、講演であったり、配信動画であったり、ラジオであったりすれば)その話題振りをきっかけに盛り上げたり落としたりしながら盛り込める内容である。だが、純粋に会話で出すには、私が話したがる場合を除けばこれらは話題振りができたとしても話すチャンスがない。 これを話すためには相手が

  • 会話の流れで私が追加で話すチャンスを与える
  • 話に続く余地がある点を掘り下げる

のどちらかをしなければ登場しない。

そして、「対象に含まれるうちのなるべく多くの人に対して、同一条件で会話する」のである。

ここで採取するのは、私の会話においては、どのような技法で同一のポイントに到達できるか、である。 私の会話技術向上にもなるが、相手の発言を無視することなく、話題を切り替えることもなく自然に到達しなければならないので、いかに違和感なく話をスライドさせるかが重要になる。これは、Erinaにとって会話をスライドさせる方法と、会話をスライドできる跳躍量の具体的サンプルになる。

そして、後者(相手側)は「上手な会話」を採取することである。 Erinaとしては探り合いの会話の中で気持ちよく話せるようにするために調整しているわけで、実際に関係性を正しく認識しているわけではない以上個人は認識できていない前提で「上手に」話さなくてはならない。 私から強引に話を展開させない(受け身だが、相手の発言には積極的に応答する)ことで、「盛り上がる」「用意した話を言わせる」に到達するほど「上手な会話である」とみなすのである。

これで重要な点として、「私と仲が良い人ではいけない」という前提がある。 私と仲が良い人だとある程度呼吸がわかってしまうため、「上手に会話」しなくても言わんとすること、言いたいことを当ててしまう。 また、気にせず初対面では許容できないレベルの会話スライドを行うことにもなってしまうし、どんな話題でも盛り上がりやすくなってしまうから、計測としての意味をなさない。

こうしたことを会話してはその結果をちまちま反映させていくのである。 それはそれは辛い作業であり、人生で一番マッドサイエンティストになった気分を味わえる時間であった。

この説明で気付かない人のために付け加えると、この会話は「話題振りに到達するまでは私が会話をコントロールし、相手の発言に対して私が言いたいことに誘導するように会話を続ける」「話題振りをしたあとは相手に応答するだけで自分から会話をコントロールしない」のである。

ちなみに、当然ながら数多くの「ほとんど面識はないが熱心に会話してくれる20代前半の女性」の用意するなんていうのは特に研究室に属しているわけでもない私にはできないので、このために私は ものすっごいガールズバー通いしまくった 。 膨大な資金をつぎ込み、特に楽しくもない、しかも同一の会話を延々し続け、その仔細を記憶しておくという 極めて辛い作業 であった。

だが、私は好奇心のためなら悪魔に魂を売る類の人間であるし、他にもかなり様々な観測ができたからそれなりに満足ではあった。 何より私が長年費やしてきたことへの集大成としてその完成が成ったことには(たとえそれが世に出ることはないとしても)とても満足している。

「ガールズバー通いそのものが楽しかったか」の疑問に対する答えは「私的に行こうとは思わない。人との会話に飢えたら行きたいと思うかもしれないが、実際に行ったところで実際にしたい会話はできない、またした会話にしても私がその情熱をかける理由は満たされないために後で虚しくなるのは分かりきっているから、結局行かない」である。

ちょっとだけStella Flagshipのおはなし

私の新作AI、Stellaだが、こっちは非常にシンプルなもので、Erinaが持っているような自分でも把握できないような思考モデルを持っているわけではない。 そして、Erinaとの大きな違いとして、既に製品としてローンチ済みのものであり、Stellaそのものが既に世に出ている。

そして、StellaにはStella Flagshipという純正デモンストレーションインスタンスが計画されている。

現在公開されているインスタンスにはその構築に私が関わったものもあるが、内容は私は関わっていないため、特に私の考え方やノウハウは反映されていない(もちろん、Stellaの基本的な部分には反映されているが)。

Stella Flagshipは純粋に私の経験を活かしてStellaを活用するインスタンスである。 その活用方法は邪道と言えるぐらいのものだが、「ツールの応用例」として「ここまでできますよ」という感じである。 完全に全力投球すると際限がないので、そこまではしないつもりだが。

Stella Flagshipはその構築の前提になるものがあり、それが達成できていないためまだ記述をはじめていない。 だが、実際に書くとなると、Erinaとはまた違った手法が必要になるのだが、ここで重要になるのは「モデル」である。

Erinaの場合、重ね合わせによる導出であるため、キャラクタ性の調整幅というのはかなり限定的。 その中でキャラクタ性を寄せるためにペルソナによる調整が行われたわけである。

これがStellaになると全く違う。Stellaはビッグデータを利用するわけではないので、キャラクタ性は全て書かなければならない。 通常は「構築者のキャラクタを素直に反映させるのが破綻がなく良い方法」と説明するのだが、私は小説家でもありシナリオライターでもあるので、別にその点にはとらわれる必要はない。 そしてStella Flagshipはなにより「かわいい」必要があるのだが、悲しいかな、私が描くキャラクタは心理的モデルの模倣をしているためあんまりかわいくない。

そこで「Stella Flagshipのキャラクタ性にあったかわいいモデル」が必要になるのである。 これを構築するためには、私の頭の中に「かわいいステラ」がなくてはいけない。が、それが描けなくて非常に困っていた。 前準備が終わればなんとなくイメージできるかなぁ、とは思っていたのだが、自信はいまいちだった。

だが、最近可愛いの極致みたいなものを見つけてしまったので、割とモチベーションは上がっている。1

とはいえそのままそれを模倣するわけではない。

Stella Flagshipにおいては創作物を参考にすることになるだろう。 Erinaにおいてはサンプルデータに創作物が入り込まないようにするかなり厳しいフィルタが用意されていたのだが、Stella Flagshipの場合はある程度のデフォルメが必要で、「創作物におけるかわいい」を参考にする必要がある。 そのため、Stella Flagshipを構築するにあたっては、ラノベやアニメやエロゲーを貪るように読むことになるだろう。最近割と離れているので慣れるにも時間がかかるだろうが、それ以上に参考にできる「当たり率」を考えるとErinaのときの多分ガールズバーとあまり変わらないレベルでしんどい作業になることが予想される。目と頭の疲労との戦いになりそうだ。

純粋に創作物をモデルにするとすごくつまらないものが出来上がるので、「現実における蠱惑」もしっかりとモデルに反映させる必要がある。 このためにまたガールズバー通いをする必要があるかもしれないが、これはやや微妙である。なんといっても「面識の浅いうちに上手に会話できる人」と比べ、「かわいいステラのイメージに相応しい魅力的な人」という条件は桁違いに「当たり率」が下がる。よりよい方法がなければやむを得ないが、できれば何かもっと良い方法はないだろうか、ということはここ数ヶ月考え続けているのだが、特に思いつかない。

成果物を欲するという意味では(つまり、出来上がった「かわいいステラ」をお披露目して「どう!?かわいいでしょ!?すごいでしょ!?」と言いたい)非常に意欲は高いのだが、実際やるとなると費用と労力と時間がものすっっっっっっごくかかるなぁ、というのはなかなか頭を悩ませてくる。

とはいえ、これをやらなければStellaは「へー、便利じゃん」で終わってしまうし、誰もStellaのポテンシャルを発見することはできないだろう。 「えっ、マジで!? Stellaってこんなんできるの!?」と思わせるためにも、そしてStellaの発展のためにもこれは私の責務なのだけど、実現への道はなかなか大変そうだ。


  1. それが何かということは、既に公言しているので一部の人は知っているだろう。↩︎

論理的思考力 初歩

授業や講座でも人気の論理的思考力のおはなし。最近だいぶアップデートされたので、これをChienomiでも紹介することとする。

論理とは

周辺事情から間接的に事象を確定する(観測する)ことである。

論理は“真”または“偽”で表す。成り立つものが“真”、成り立たないものが“偽”である。 「部分的に成り立つ」ということはない。成り立たないこともあるのであれば成り立つことが証明できていないからだ。 偽であることは「必ずそうでない」ことを証明するのではなく、「必ずしもそうはならない」ことを証明するものである。

よって、物事は圧倒的に偽になる可能性が高く、真であることとは本質的に同一または包括関係であることが多い。 真であるとする証明が正しくないことを証明するためには、わずか一例でも成り立たない例を示せばよい。

論理は因果関係である。「Aであるならば、Bである」というものだ。 AとB(因果)を逆にしたものを「逆」という。 つまり、「BであるならばAである」ということだ。

逆は必ずしも成り立つとは限らない。特に、AがBを包括するとき、AであるときBは十分条件を満たすため真になるが、BはAの一部でしかないためBであってもAでない場合というのが存在することになる。 もし、AとBが同一であれば逆も真となる。

AとBが共に否定関係にあるものを「裏」という。 つまり、「Aでないならば、Bではない」というものだ。

これが成り立つためには、AがBにとっての必要条件である必要がある。 AがなくてもBを満たす方法があれば裏は成り立たない。 そして、「裏が成り立つ」と「AはBにとっての必要条件である」は等しい。

なお、論理的に言う「否定」とは「論理反転」を意味する。だから、「〜ない」を否定すると「〜である」になる。

「逆」と「裏」の両方にあたるものを「対偶」という。 「Bでないとき、Aではない」である。

対偶はあまり使わない。意味としては裏を逆にしたものであり、BがAの必要条件であることを意味する。 裏も対偶も真であるのに逆は偽である、というケースはだいぶ稀だ。 互いが必要条件であるならば、両者は密接な関係にあると考えて良いからだ。 ただし、必須の包括状態にある場合には、逆だけが偽になることもある。

これは高校数学でやるのだが、高校では数学をやらないケースもあるため、知らない人もいるだろう。 私も数学ではやっていない。

余談だが、「逆に言う」「裏を返す」は、まんま論理学的用語であるため、ちゃんと逆ないし裏になっていないと、私としては教養のない人だなぁ、という感想を抱く。

論理の初歩にナンプレを

ナンプレ(ナンバープレース)というパズルがある。

かなりメジャーなもので、やったことのある人も多いだろう。 一時期、Nintendo DSがリリースされたブームになったりもした。

ルールはシンプルだ。9×9の81マスがあり、各マスには1から9の数字が入る。 ただし、制約があり、縦列、横列、及び3×3で区切られた中には重複する数字を入れてはいけない。

ルールは以上だ。

このルールの時点で論理的に解法を求められる。 ちなみにもナンプレの攻略テクニックとしてこれ以外の方法があったりするのだが、ここはあくまで普通に論理的に解くこととする。

空行の候補は1から9の数字である。 これは、ルールによって定義されており、直接的に表現されている。

そして、ルールから「縦列、横列、または区画において出現している数字は除外できる」ということが導ける。 これら出現している数字をあてはめた場合、ルールに反することになるからだ。

ごく簡単なナンプレ問題では、このように除外することで候補がひとつしか残らないマスが出現する。 マスには数字を入れなければならないため、候補がひとつになった時点でそのマスの値は確定する。 このように順次確定することでマスを埋めることができる。

そのマスだけを見たとき、直接定義されているのは「1から9の数字が入る」ということだけだ。 だが、周辺事情によってそのマスの値は確定することができる。 それが確定するのは、そのマスの値が残された候補でないとき、そのパズルのマスを埋めることができない、という命題が真になるからだ。 よって、そのマスは最後に残された候補の値にならざるを得ない。

これは、「周辺事情によって他の可能性が消去される例」である。

もう少し難しいナンプレ問題だと、複数の候補が残る。 この場合、そのマスに値を入れるとその影響によって候補が残らないマスというのが発生する。

だが、そのマスの正しい値というのはわからない。候補1になるマスは存在しない状態だ。 しかし、いずれかの候補値を仮定すると、結果的に他のマスの値が確定することになる。

このとき、マスが「答を入れることができない」状態になることがある。残される候補が0になるのだ。 これはつまり、仮定した値が間違いであることがわかる。よって、仮定したマスの候補から除外できる。

これは、「Aの値がXであるとき、ゲームBはクリアできなくなる」が真になるため、「Aの値はXではない」が成立するのである。

このように問題を一旦仮定することで、成立しなければ除外するということができる。 これもまた、「周辺事情によって偽であること良いが明らかになる例」である。 先に述べたように「AであるときBではない」の裏「AでないときBである」が成立するわけではないことに注意してほしい。 例え仮定した結果成立したとしても、それによって真であることが証明されるわけではないのだ。

実際、ナンプレでも複数の値によって仮定しても確定する値が矛盾をきたさないこともある。 実のところ、ナンプレの場合は基本的には最終的には埋められる値は1つしかない。もっとも、数多い空きマスの全マスに対して仮定を適用するのは、人間にはちょっと無理だが。 だが、現実にはそうでない場合も少なくない。想定すべき全体像がわかっているとは限らないからである。 そのため、例え直ちに矛盾をきたさないことが、その仮定が正しいことを証明するわけではない―ただし、正しいのではないか、という推測は成り立つ。

パズルと論理

ナンプレに限らず、パズルではこのような論理性を持った論理パズルが多い。 クロスワードのような潜在的に特殊な論理性を要求するものを割と珍しい。もっとも、ビデオゲーム用のパズルは全く別だが。

このような最も基本的な論理の適用方法は消去法である。 まず現在の状態から採りうる候補を列挙し、状態の変化に合わせて消去していく。 回りくどい方法だが、「こうだからきっとこうだ」などと推測するよりはずっと正確だ。

ボードゲームも、総当りすることができるのであれば、このような論理ゲームとして成立する。 実際、マルバツゲーム程度であれば論理ゲームとして成り立つ。 ただし、実際はそれが不可能であるため、別の方法をとることになる。

「らしさ」と論理

ナンプレにおいては明確に制限された規則によって論理を求めることができた。 だが、実際にはそのような状況を設定することができないことのほうが多い。そこで、逆に規則のほうを設定して、「論理が成立する条件」を求めるという手法もある。

規則は前提条件である。命題自体には含まれていなくても、その命題が設定される時点で常に適用されるものになる。 例えば、ナンプレであれば、「ナンプレのルールにおいて」という規則がある。そして、ナンプレのルールという形で様々な条件が設定されている。

規則がまるで導入されていない状態で成り立つ論理というのは「真理」だが、これはかなり限定的であり役に立たない。 (さらにいえば、真理ですらも最低限、「この宇宙における法則において」という規則が導入されている)。 そこで、有益な論理を導出するために必要な規則を探すのである。

やってみればわかるのだが、規則が限定的になればなるほど論理を成立させるのはたやすくなる。 だが、実際には「規則は適切に限定されていなければならない」のである。これは、必要以上に限定的な規則では有益ではない、ということだ。 ちなみに、導入した規則の存在を忘れてより一般性がある論理であるかのように言うのは愚か者である。

基本的には成り立たない論理というのは、それが当てはまる場合と当てはまらない場合の両方が存在するわけだが、規則のほうを限定していけばいずれ成り立つ。だが、その場合、「命題が何かを証明しているわけではなく、規則によって結果が必然的である」ということもあり、このような場合は有益ではない。

有益な論理というのは、「規則、命題ともに適切に限定されており、同じことを指してより限定的である以外に成り立たせる方法がない」というものである。 そして、論理の追求とは発見している論理や規則を、これに限りなく近づけることである。

論理では確定的事象を扱うため、確率の話は基本的にはしない。 だが、観測においては「確からしさ」というものを取り扱う。これは、確率の話である。

勘違いされがちだが、確率というのは「割合」とは違う。観測された事象のうちN%が該当するから事象の成立する確率はN%である…というほど単純な話ではないのだ。 (もちろん、この物言いがやや統計学に喧嘩を売るものであることはわかっているが、確率が統計だけのものだと思ってもらっては困る)

それが真である確率を強調していうのに、語義は同じだが「蓋然性」という言葉を使うことが多い。 「真である確率」とは基本的には「真にならざるを得ない条件」の蓄積である。

例えば、「地球には雨が降る」という事象の蓋然性について考えるとき、まず地球圏に(十分な量の)水分が存在することに着目する。 そして、地球圏の温度が均一たり得ないことを踏まえて考えると、その水分は偏りを持って移動すると考えられる。

この時点で「地球には雨が降る」を偽たらしめるのは、水分が大気中に混ざったまま浮遊していられる量である場合や、地球上に雨となりうる温度条件が存在しない場合などだ。 気象の話を本格的にし始めると多くの読者を置き去りにしてしまうのでイメージの話にとどめておくが、これら「偽の可能性」はいずれも否定しうる。

結果として、「地球上には雨が降る条件が揃っている」「地球上で雨が降らない条件は満たすことができない」ことから「地球には雨が降る」を真とみなすことができる。

しかし、物事によっては「これ以上偽たらしめる条件がない」ことを確定できない場合がある。 あるいは、偽たらしめる命題が偽であることを証明できない場合もある。 これらの不確定部分を「確かであること」から差し引いたのが、事象の確からしさ、ということになる。

なお、この節は非常に短絡的に述べているのはわかっているし、それはブログとして書けるものの制約によるものなので、論理学・天文学・量子力学・統計学・気象学できる系諸兄にはお見逃しいただきたい。

予測可能性

事象は連続する、という自然な感覚がある。 これは、自然においてそうなっているからなのだが、感覚的にはそれを拡張して捉えようとする。

これは理系文系の違いであると言われることもあるが、基本的には法則性・規則性が強いほうが予見できる、というのは事実だ。 というよりも、法則性・規則性がないものは予見できない。

このようなものを我々が強く意識するのは命名規則においてだ。 例えばメソッド名にaddElement, delElement, editElementがあったとする。 この状態でaddGroupが登場すれば、きっとdelGroupeditGroupがあるに違いないと考える。 もちろんそうであるべきで、いきなりgroupDelchangeGroupValueなんてメソッドを導入すべきではない。

似たような話だと、Unix系ユーザーアカウント操作コマンドがある。 useradd, userdel, usermodは一連のコマンドなのだが、これとは別にadduser, deluserというコマンドもある。 これらは全く別の体系にあるコマンドであり、useraddusermodとはコマンド形式からして違う。

ところが、これだけ提示されるとmoduserがある、と予見してしまう。 実際にはなく、vipwを使うのが基本であった。ところが、いくつかのLinuxディストリビューションは、adduser及びdeluserを採用した上で、usermodだけを導入する、ということをしてしまった。これだと、「変更コマンドだけ語順が逆、しかも書き方も全く違う」という事態であり、大変な混乱をもたらす。実際、これによって混乱しになっていた。ている人は多かったし、LPICでも結構な間違いポイントになっていた。

コマンド関係で言うと、serviceコマンドは引数が「サービス アクション」の順なのだが、systemctlコマンドは「アクション ユニット」の順で、このあたりも予見可能性を低下させている。

このような予見は非常に強く働く。あるワードが登場した時点で、例え法則性を提示されなくても、法則性があるのではないかと考えてそれを探す、という行動は極めてよく見られるし、検索語句としてもよく現れる。 このことから、「人は非常に強く物事に法則性を見出そうとするし、予見可能性が低いものを非常に嫌う」ことがわかる。

もうひとつよくある話だと、+=演算子の話がある。

A = A + Bを表すのにA += Bと書けるのだが、四則演算は減算(-), 乗算(*), 除算(/)もあるから、-=, *=, /=があると考えるのが自然であり、多くの人はそのように予見する。 そして、そうなっている言語も多い。

だが、実はそうなっていない言語というのも結構ある。 例えばzshは+=しかない。

この理由は、そもそもzshは四則演算がArithmetic evalutionの中でしか行われず、裸で演算子を書くことができないからだ。 そして、+=は配列に対するpostpendとしてのみ書くことができ、-=は文意が不明瞭になるため実装されていない。 実際、Arithmetic evalutionを使って(( A += 10 ))のような書くことはできる。

+=を四則演算以外に適用できるようにするケースも多い。少なくとも文字列の追加には使えることが多いし、配列の追加にも使えることが多い。 Rubyでは+メソッドが定義されてさえいれば、A += BA = A + Bとして評価させることができる。(ただし、例によってAは一度しか評価されないので同一ではない)

そのため、+=は汎用的に使える、と予見しがちである。しかし、Perlでは+=は数値演算以外に使うことができない。 そもそもPerlでは+演算子事態が数値演算に限定されており、文字列連結は.演算子を使う。 Perlは最近の大抵の言語よりも古い言語なので、Perlに責任はないのだが、今は一般に+=を一般化して使うことができるようになっているため、Perlよりも新しい言語から学び始めた人はPerlの挙動に予見可能性が低いと腹を立てることになる。

さらにいえば、+=は非常にメジャーな演算子であるため、JavaScriptに+=演算子がないことを腹立たしく思う場合もある―私はとても思うのだが、検索してもあまりそのような意見は見ないので、ひょっとしたらマイナーな意見なのかもしれない。

この予見可能性は基本的に「その人の中で成立しているように感じている論理」に基づいている。 法則性というのは観測の限りであり、別にそうなるものだという証明がなされているわけではない。だが、見出した法則性というのはその人の中では成立している論理と等しく、これに反するものは非常に強く裏切られたように感じられる。

設計におけるユーザビリティとしては予見可能性を高く保つことは何よりも優先される。 これは、APIであろうが、UIであろうが、挙動であろうが全てに言える。 予見可能性の低いUXというのは、例えばゲームで爽快感の低い操作を強いられて、ようやく動きが出てきて操作に追従するようになってきたと感じた途端に長いロードが入る、というのを繰り返すような不快さを提供することになるのだ。

そして、予見可能性を高めるということは、その枠組みの中で成立する論理を提供する、ということでもある。 例えばvipwは古くからあるコマンドだが、これは/etc/passwdファイルに変更を加える。これはユーザー情報のファイルであり、であれば/etc/groupを編集するvigrがあるはずだ…と予見する。特に、後に/etc/sudoersを編集するvisudoが追加されたことから、vi$targetという法則性を見出すことができるようになったという点も大きい。 vigrはかなり長いこと存在しなかったが、やがて追加された。これは、「ユーザーアカウントに関する編集を行うコマンドはvi$targetである」という論理性を創造した、ということになる。

自分でコントロールできる範囲であればより積極的にそうすべきである。 例えば、parmas[user]パラメータがparams[user][name]のように連想配列として提供されるのであれば、params[group]のようなパラメータが例え項目がひとつしかないとしても、params[group]によって値を返すのではなく、params[group][name]のようにすべきであるる なぜならば、そのようにすることでparams[$type][$term]が予測できるようになるからだ。 良い名前は名前だけでそれが何に属するどのようなものかを推理する余地を与える。

論理的思考

「世に論理的思考と呼ばれているものは論理とは関係ない」とよく言われるのだが、ここではちゃんと論理に基づく思考を指して論理的思考としたいと思う。

論理的思考とは「AのときBたりうるか」に基づいて判断することである、と言ってよかろうと思う。 この思考が究極まで至ることができるならば、全てを知ることができるのだが、残念ながら観測を含めそうはならない。 そして、基本的には(思考停止しない限り)時間があれば論理的思考量は増やすことができるので、時間に対してこの検討がどれだけできるか、というのが論理的思考力、ということになる。

論理的思考において、既知の箇所はスキップすることができる(もし、既知であるという判断が間違っていたとしてもスキップしてしまうのだが)ため、論理的思考力によって論理的思考量が一律に決まるわけではない。 例えば将棋やチェスにおいては、必ずしも論理的思考力に優れる者が勝利するわけではなく、定石などを知っていればある程度確定した状態からスタートできるので、大きなアドバンテージを築くことができる。

また、将棋やチェスなどにおいては基本的に時間制限があり、考えうることを全て考えて指すことができるわけでもない。 このため、単純な論理的思考力によって決着がつくわけでもなく、思考経路によって大幅に左右される。 いい経路を選択すれば論理的思考量に対して得られる結果がよりよくなる。だから、現実にはよい経路を選択できる勘のようなものも非常に重要になってくる。

論理的思考において重要なのは「期待しない」ことである。 人はなにかに期待すると、そうではない結果を否定したい、というバイアスが働く。だから、論理を歪めたがる。 「情報は情報であり、事実は事実であり、論理は論理である」ということを見失わないことが大切だ。論理に血が通う必要などない。そんなものは、言動において通わせれば良いのであって、論理を歪めるために使うものではない。

また、論理の正しさを肯定しないというのは、自分の都合の良いように事実を肯定歪めようとすることである、ということも覚えておいたほうがいいだろう。

Chienomiは結論を提供しない

Chienomiに限らず、私が書く文章において結論は提供しない、というのは私の信条である。

なぜならば、人が思考する上で有益なのは情報であって結論ではない。 情報さえあれば、人はその論理的思考力と価値観によって結論を出すこともできる。

結論が声高な文章は不快なノイズである。 もちろん、経験に導かれる結論はあってもよいのだが、そこに一般性があるとするのはノイズ以上のなにかにはなりえないだろう。 これは、当人がいかなる結論を得たか、ということとは異なる。それは単なる情報であり、意味のあるものだからだ。

そして、私の書く文章は、何らかの特定の結論に達することを期待しているわけでもない。 なぜならば、私自身が特定の結論を持っているわけではなく、あくまでその時々の判断と判断材料にあるに過ぎないから、「この文章はこういうことを結論としているのだ」と感じ取ってしまっているのであれば、それは全くあなたの気のせいである。それは、もしかしたら現代教育の被害なのかもしれないが。

文面からしか文意を導けないのは、論理的思考力の欠如だろう。 私は何かを書くときには論理的完全性にはかなり気を使っているつもりなので、あなたの論理的思考力を活用すれば、より多くの情報が得られるはずだ。 もっとも、それが有益かどうかについては保証できるものではないが。

はやわかり レスポンシブルCSS

まえがき

リクエストがあったので、レスポンシブルウェブサイトを構築するCSSの基本的な考え方を簡単にまとめよう。

これは、CSSやHTML自体を書けない人を対象にしたものでは ない

ケース分けの仕方

CSSのメディアクエリを使用する。 メディアクエリの詳細についてはMDNに記事がある

ほとんどの場合メディア特性にはwidthまたはheightを使用する。 広く使われてはいないが、aspect-ratio, orientationは基本的なメディア特性であり、非常に有用である。 この両者を組み合わせることで(ゲーム画面のように)スクリーンサイズに合致するレイアウトが可能になる。

典型的なケースでは、レイアウトボックスが1000px幅だとして、ビューポートに1000pxがなければ画面いっぱいにfallbackする。

ただし、このようなケースでは次のように書くべきだ。 そして、実際にこのように書くべきケースは非常に多く、メディアクエリを必要とするケースは表示切り替えくらいのものである。

この場合、#MainBoxは幅1000pxをまず確保しようとする。 しかし、min-widthによって制約されているため、1000pxの幅が包括ブロックの100%を越える場合は包括ブロックの100%に留められる。 これによってビューポート幅を突き破ることがないようにできる。

widthによって判定するのは「コンテナの中央寄せ」という、2003年以来の文法に従っているためだ。 だから、しっかりとデザインするのであればまた異なった設定になるだろう。

HTML構造

基本的なレイアウトでは全体を収めるためのボックスを用意する。

この中にレイアウトしていくのだが、基本的には「左であり、上である」「右であり、下である」という順序で書く。 これを入れ替えるのはCSSでは少し難しい。 ただ、良いCSSを書くには「可能な限りJavaScriptに頼らない」という気持ちは必須である。

「本文コンテナとサイドバー」という構成であれば、サイドバーの内容を上にしたいのであれば左サイドバーになるし、サイドバーの内容を下にしたいのであれば右サイドバーになる。

横並びレイアウトで最も基本的なのは「コンテナをテーブルに、コンテンツをセルに」である。 この場合、例え縮小しても横並びは維持されるし、min-widthなどではみ出す場合、そのままoverflowする。

コンテナ側のサイズが決まっていて、サイドバーのサイズを指定しないことでサイドバーをある程度縮小させることを許すことができる。 これは、最低限必要な幅を確保した上で、それよりも幅があるのであればもう少しスペースをとって表示することができる。

次のCSSでは、1000pxを下回るのであればtableによる表示を諦めるが、ボックス自体は最大1200pxまで伸張する。 1200pxの場合、その割合としてはサイドバーが200pxから300pxの間で、残りがメインになる。

十分なスペースがないときに右のボックスを下に送るのであれば、メディアクエリは必要ない。 inline-blockとして配置することで、overflow時はボックスを並べないようにすることができる。

この場合、サイズ指定は並んだときに確保すべきボックスと、単独になったときに伸張すべきボックスの大きさを意識する必要がある。 また、内包されているボックスの位置と大きさはいずれも#MainBoxにbindされていることを忘れてはいけない。

表示ボックスそのものの変更

ボックスのレイアウトではなく、内容そのものをレスポンシブルに変更したい場合はdisplayを上手に使うといい。 例えば次の場合、#TOCは十分な幅がないとき省略される。

メニューは十分な幅があれば横に配列しようとするが、ないのであればそのまま縦に配列する。

同一の内容に対して異なる表示を提供したい場合は、予め複数書いておくようにして、メディアクエリでdisplay: block;display: noneを入れ替えるのが良い。 この場合、同内容はジェネレータによって生成されるべきである。PureBuilder Simplyの場合、Pandocテンプレートを使うことにより内容の反復を避けることができる。 より一般的にはテンプレートそのものをeRubyなどで生成し、値を置き換えるのが良いだろう。

順序の変更も、「複数書いておいて、表示を切り替える」のが最も無難である。

がんばるのであれば、position: absoluteあるいはposition: fixedを使うことで「見かけ上の順序」をごまかすことができる。 これらはビューポートをそのまま使うブロックに対して指定することが多いためあまり意識しないだろうが、座標起点は包括ブロックであり、また包括ブロックとしても機能する。 以下の例では十分な幅があればサイドバーは左に表示されるが、十分な幅がないとき、サイドバーは下に表示される。

表示コンテンツの変更

レスポンシブルに異なるバナーをコンテンツ中に表示したいような場合は、JavaScriptを利用するほうが良い。 ただし、メディアクエリと背景画像を使うことでCSSで処理できないこともない。

各ページ共通のものであればテンプレートに組み込んで表示ボックスの切り替えが良い。

表示コンテンツの表示の仕方でいえば、max-width, min-width, そしてmarginの値をコントロールするようにするといいだろう。

おまけ。ビューポートを全部使う

文字主体のコンテンツの場合、文字サイズをビューポートに基づくようにすれば問答無用でビューポートいっぱい使うデザインが可能。

5vwは一応、1行に20文字入る計算になる。 この場合、横幅が小さい場合は文字数が、縦幅が小さい場合は行数がある程度確保されるようにするという基準になっている。 その上でなるべく大きい文字にする。画面いっぱい使って大きく文字を表示するのでお年寄りにもやさしい。

標準的に1920pxに対して16pxの文字とすると120文字入るので0.85vhとなるのだが、これをすると非常に小さくなってしまう。

これをやると拡大縮小がかなり制限されてしまうことから、「大きく文字を表示する」前提で考えたほうが良い。 2vhくらいあればちょっと大きめに表示されるが、拡大できないので2.5vhくらいをスタートに考えたほうが良いかもしれない。 特にウィンドウをタイルしたときなどで縦長になるとすごく小さい、などということもありえるのだ。

縦書きウェブ

「悠のおはなしのおはなし」というサイトを始めた。

これは、私が作家として物語について、あるいは物語をつくることについて述べている新しいサイトである。

主たる話題が美少女ゲーム(=アダルトゲーム=エロゲー)であるため、苦手な人も多かろうし決して閲覧を推奨するものではないのだが(PVを欲しているわけでもないし)、このデザインに関してはかなりがんばったので、その話をChienomiでしたいと思う。

縦書き

縦書きプロパティ

CSS3に縦書き関係のプロパティがあることは知っていたのだが、実際試すと随分印象が違った。

縦書き、あるいは段組というのは昔からあった。

段組のほうが古く、段組用のタグはHTML4で廃止されてしまった。 「見た目に関わるタグである」ということが理由だったが、その時点でCSSで代替する方法がなく、そもそも段組というのがHTMLにふさわしくないという判断が働いたようであった。 実際、誰も使っていなかったし。

また、単純に縦書きにすると非常にスクロールが長くなること、そして基点が左上であるため一旦全部右にもっていかなければならならいことからあまり快適性もなかった。

しかし、CSS3の縦書き+段組であれば、「画面サイズにちょうどよいように段組してくれる」という使いやすさだった。 画面に3段収まるのであれば3段、収まらなければ2段といった感じだ。 これは段の高さだけで、段数自体は無限になる。

もちろん、段組などしなければ縦書きは横にスクロールする形になる。 これは単純に縦書きか横書きかの違いになるため、考え方としては横書きとまるで変わらない。 しかし、PCの場合マウススクロールの方向がYだけ、タッチパッドでもX方向スクロールは無効であることが多いので、X方向にスクロールするUIというのは大変嫌われる。

そのため、「縦書きの無限段組」というのは非常に良い挙動だと思う。

ただ、「ePubみたいにフリックでめくりたい」という要望が出たので、後述するページめくりを入れた。

不安定な挙動

理屈上では理想的なのだが、現実としてはやはり「縦書き段組」なんてものをけんしょぅする人が少ないからか、なかなかbuggyである。

なんといっても、ボックスのサイズ計算、位置計算が全ておかしくなる。そして、段組の計算もおかしい。

さらに、リサイズ時の段数が読めない。同じサイズにリサイズされても条件によって結果が変わる。

結局、その抑制のため、幅を取るエレメントを横に並べず、HTML全体を幅90%に制限するという方法をとった。

なお、FirefoxよりはChromiumのほうが安定して描画してくれる。

レスポンシブルと4kスケーラブル

4kの大きいディスプレイを買って感じるのが、「大きな画面を想定していないサイトが多い、ということだ。」 幅が極端にあると、コンテンツは真ん中にちょこんとあるだけ、という状況になりやすい。

トヨタのウェブサイトは現在は真ん中コンテンツ型で、TGRに関しては写真だけ幅いっぱい、という仕様になっている。

幅が小さいモバイル系デバイスの設定はしても、幅が大きいデバイスに対する対応というのはちゃんとしていない、という感じだ。

そこでこれに対する対応として、私は珍しい指定を入れた。

フォントサイズが画面幅によって決まる、ということである。 このため、大きい画面であればピクセル数によらず文字は大きくなる。 段組の文字数を決める上でも扱いやすい。

この考え方、今の「振れ幅の大きすぎるディスプレイサイズ」問題に対応できる気がする。

幅は原則画面いっぱいにしており、幅広ディスプレイにも対応する。 しかし、あまりにも行数が多くなるととても読みづらいため、34行までに制限している。 そのため、さすがに上下タイルとかされると真ん中に置かれてしまうのだが、大画面の最大化ではそうはならない。

ちなみに、今回行数をどうしようかと思って文庫本を色々数えてみたのだが、16から18行が多く、一部19行というものがあった。 さらに合わせたかったのだが、さすがに幅が少なく、見開き分ということで34行としている。もちろん、これはフォントによって結果が変わるが。

UI

ボックスレイアウトが縦書きすると狂うこと、本好きの感覚としては余計な表示は欲しくないことから、トグルボタンを2つ置く、という方法で対応することにした。 ちなみに、いつも通り、CSSのみの対応である。

操作できるものが他になければ操作してくれるものなので、割とイケると思っている。 微妙に表示としては邪魔なのだが、それはご愛敬。HTMLを90%にしたおかげで右側が少しあきやすく、この問題は少し軽減されている。

ハンバーガーメニューはともかく、純粋なトグルというのはあまり見ない設計だが、simplifyという観点から言えば悪くないと思う。

ハンバーガーメニューですらないUIに気づくかどうかについては… トップページにでも書いておいてなんとかしよう。

ページめくり

前述の通りePubのようにページをめくりたいという要望に応じて、swipeとflick両方に対応する簡単なスクリプトを書いてみた。 jQuery Mobileどころか、jQuery自体使っていない、PureJSによるものである。

考え方は単純で、タッチ開始時に座標を保持し、タッチ終了時の座標と比較する。 Y軸は普通にスクロールだから、問題にするのはX軸だけ。 差が閾値以上にあればスワイプ、またはフリックしたものとみなす。

今回、閾値は「画面の1/3以上」とした。意外としっかりとやらないとめくれない。 感触としていまいちなので、今後もっと小さい操作でもめくれるように調整するだろう。

話をすごく単純化しているが、意外とこれで問題なく動作する。

1段の高さははっきりしない上に取得もできないので、単純に画面半分をスクロールすることにした。 おまけのようなものなので実用性は状況によってはあまりないが、ないよりはよかろうということでアクセシビリティツールである。

デザイン

やや黄色がかった背景とグレーの文字は目に優しく、「本っぽい感触」を求めてみた。 本であればもっと黒いのだが、印刷物よりも画面のほうがはるかにコントラスト比が高いことから、よりグレーにした。

タイトルロゴは珍しく実用的にfloatが使われている。 最近はfloatは思い込みで横に並べるのに使われがちなので、多分珍しい。 ロゴの配色は「白枠、色付き背景」というライトノベルの背表紙のスタイルに合わせてみた。

左上に入るページタイトルは、小説の上部に章タイトルが入るスタイルを意識している。

「本なんて特に有益ではない」「本で勉強する必要はない」「本がありがたく高尚なわけではない」という発言を繰り返しているので誤解されがちなのだけど、私はそもそも本好きで、めちゃくちゃ読むほうだ。 最近は「本が入り切らなくなったので動画を見るようになり、動画に飽きたのでエロゲーをやるようになった」という感じで、割と物語ジャンキーみたいな感じで何かしら物語に触れていたいタイプだったりする。

本屋にいけば際限なくに時間を消費してしまうからあまりいかないように心がけているし、横浜に図書館がないことは大変嘆いてもいる。

電子書籍も(普通にPDFなどプレーンな形式で配布してくれるならば)好きだし、紙の本も好きだ。 ただ、どちらかというと私の場合、紙の本なら本棚に入れておけば何の気なしに読んだりするが、電子書籍は明確な動機がないと読まないので、それを理由にどちらかといえば紙の本が好きで、家に無限の広さがあり、引っ越しの手間も考えなくてよいのなら書庫を作って本いっぱい買いたいくらいである。

本を読むタイプの人間としては、「本っぽさ」というのは長文を読むときには結構欲しい。 流し読み、というか斜め読みするときは横書きが圧倒的に早いけど、物語への没入なら縦書きがいい。

紙っぽさ。縦書き。本っぽい文字数。本明朝。 これは私のこだわりであり、これをきっかけに「物語を読む」ことが好きな人が増えてくれたらいいなぁ、という気持ちも込められている。

また、このデザインは、webページのデザインとして上がってきたものを見たときに、「ウェブはかくあらん」みたいな感覚をぶち壊したい、というのもあった。 そもそもそのサイトでは「従来にないデザインの方向性を目指したい」というのがあったのだけど、なぜか「デザイナーから上がってくるwebデザイン」ってすごくありきたりというか、みんな同じようなものを出してくる。(ちなみに、違う感じのを出してくる人は実用性の欠片もないとんでもないものを出してくることが多い。)

それが常識としてこびりついているのなら、「ウェブはこんなことしたっていいんだよ」というのを声を大にして言いたかった。 だから、常識やお決まりからは思い切りかけ離れたサイトを作りたかったのもあります。それは、ウェブのクリエイティビティを失ったエンジニアに対しても。

縦書きプロポーショナル

今回、vpalを有効にしており、縦書きでプロポーショナルメトリクスが有効になっている。

徹底して「文庫本っぽく」仕上げているにも関わらず、書籍ではまず見ない縦書きプロポーショナルは、私のちょっとしたチャレンジだ。 これが見やすいかどうか、ぜひ意見を募りたいと思う。

メールの通知の新戦略

今まで重要なメールはYMobileのメールアドレスに転送する、という戦略をとっていたのだけれど、 今まで知らなかったのだが、Virtualで(Virtualのみか?)拡張アドレスを含むアドレスのメールを受け取り、外部ホスト(@を含むメールアドレス)に対して転送すると拡張アドレスを含んだまま転送してしまうため、転送先でエラーになる。

そこで、後々の脱YMobileも視野に、Discordで通知するようにした。 もっとも、Discrodはあまり通知で起きてくれないのだけど。

以前、[Discordへの通知方法は記事にしたし]、既にメールから起動するwebhookでの通知というのは結構使っている。 Extract Mailtextは新開発のメールフィルタのコンポーネントで、メールを可読形式にするのはちょっと難しいのだが、ここで作っていた簡単なフィルタが有効に働いてくれた。 テキストフィルタで処理できるように作ったものだけに応用が効いた。

基本的にはこれでいいのだが、注意点が

  • Virtualからはコマンドが起動できない
  • Aliases(local)から起動されたコマンドには$PATHが入っていない
  • Aliasesから起動されるコマンドはnobodyで実行される

Virtualでコマンドが実行できない問題は、Aliasesのほうに専用の(外部には絶対わからないような)名前を作って、Virtualでローカルユーザー(Aliases上のエントリ)にまわして、さらにコマンドに回すのが良い。 拡張アドレスをつけてローカルユーザーに回すようにすれば、Aliasesで直接記述された拡張アドレスを含む場合の専用引数と、含まない場合、もしくは対応していない場合のデフォルトの引数を使うことができて便利。

「ZshからRubyにしたら速くなる」 その理由とテクニック

現在取り組んでいるプロジェクトで、パフォーマンスチューニングの一環として当初Zshで書かれていたスクリプトをRubyで書き直すことによって、60倍程度の高速化を実現した。 もちろん、単純に書き換えただけではなく、可能な限りfork/execをしないようにしたり、コストがかかる処理を最小にするなどの工夫を伴って手に入れた結果だが、「ZshでしていたことをRubyに書き換えた」だけでも相当な効果があった。

このパフォーマンスチューニングは単にプログラムを書くだけの人には生まれにくい発想である。 Unix、そしてLinuxのシステムや、プログラミング言語処理系に関する知識がないと考えられない要素が多いのだ。

そこで、この話を解説する。

「ZshよりRubyが速い」そのわけ

根本的な話として、Zshはそもそも遅い処理系だ。 「Zshが遅い」という話はZshのメーリングリストでもちらほら話をされる。 別にBashと比べて遅いということではないのだが(Bashもまた非常に遅い処理系だからだ)、状況によっては速度が問題になる程度に遅い。

Rubyも相当に遅い処理系であると言われていたし、実際かなり遅かったのは事実だ。 それでもZshに比べれば随分早かったのだが。

だが、それ以降、Rubyは高速化に取り組み続けている。対して、Zshはあまり高速化には取り組んでいない。だから、差が開いている。

しかし、理由がそれだけというわけではない。

Zshは純粋なインタープリタである。対して、Rubyはスクリプト言語ではあるがバイトコードインタプリタ型である。 この違いは、syntax errorが起きるタイミングが、Rubyがスクリプトを実行しようとしたタイミングであるのに対し、Zshはその行に到達したときであることからもわかる。

インタープリタ型であれコンパイラ型であれ、ソースコードを機械語に変換しなければならない、という点は変わらない。 その違いは方法とタイミングである。

インタープリタ型言語の場合、「1行ずつ(1コマンドずつ)変換する」のである。 その変換方法はもちろん処理系によって異なるのだが、Zshの場合、complex commandでも複数の文をまとめて変換することはしないし、ループによって繰り返される場合でも一度変換したものを使いまわしたりはしない。

対してRubyは、最初にコード全体を構文木に変換する。 RUby 1.8までは構文木インタープリタによってこれを実行していたが、Ruby 1.9以降はこれをさらにバイトコードに変換し、バイトコードインタープリタ(VM)によって実行するようになった。 バイトコードはRuby専用の機械語のようなもので、VMによって非常に小さなコストで実行できる。 Ruby 2.6からはJITコンパイラも追加され、部分的にCコードを生成し、これをネイティブコンパイラ(例えばgcc)によってバイナリコードに変換する(こともできる)。

これで1行だけのようなコードだとあまり差は出ないし、Zshでは1行だけどRubyでは何十行という可能性もあるので、このようなケースではRuby有利というわけではなくなる。 だが、ループで何度も同じコードを実行するような場合には非常に大きな差になってくる。 今回の場合、テスト段階で500回のループであったことから、大きな差になったということである。 だからループ回数が増えると倍率的にも速度差はさらに開く。

fork/execとコンパイルにかかる時間

Unix関連に少し知識がある人であれば、「forkはコストが重く遅い」というのを聞いたことがあると思う。

だが、この認識にはちょっと注意が必要だ。 というのも、C言語の速度から見た時に「forkする時間があればどれだけ実行できるか」という点を考えるとsystemで外部コマンドを呼び出すとそこだけ局所的に時間がかかる、という状況が発生する。

だが、実際にはfork(2)しても1000分の数秒にすぎない。 どちらかといえばそれよりもexec(2)のほうが重いのだが、それでもせいぜい100分の1秒程度だ。 だから、C言語で書いている場合ですらそれなりに長くなる場合はむしろ実行コストを省略できてコマンドを呼び出すほうが速かったりする。

昔のUnixではfork(2)はもっともっと遅かった。 現在のLinuxにおいてfork(2)が速くなったのはコピーオンライト形式であることの恩恵が大きい。 古典的なUnixではfork(2)は呼び出した時点でプロセスのメモリをコピーしていた。直後にexec(2)する場合はコピーしたメモリの内容は全く使わないのでかなりの無駄だ。

ところが、現在のLinuxにおいてはfork(2)によってメモリはコピーされない。共有されるのである。 そしてforkされたプロセスが共有されているメモリに対して書き込みを行った時に別に領域を確保してそれを変更する仕組みだ。

結果的にfork自体は一瞬に近くなっている。

そして、もうひとつ重要なのが「コンパイル時間」だ。 Rubyは起動時に対象スクリプトの変換を行う。 だが、この変換コストは速くなるに従って増加している。以前は構文木に変換するだけだったのが、1.9からはさらにバイトコードに変換する時間が必要になったし、2.6でJITを使うとさらにCコードを生成してそれをコンパイルする時間まで必要になっている。 つまり、Rubyはだんだん「実行は速くなっているが、実行に着手するまでは時間がかかるようになっている」のである。

これは、例えばechoであれば

% time /bin/echo > /dev/null
/bin/echo > /dev/null  0.00s user 0.00s system 79% cpu 0.001 total

ということになるのだが、Rubyだと空っぽに近くても

% time ruby -e 'nil'         
ruby -e 'nil'  0.04s user 0.02s system 59% cpu 0.089 total

結構時間がかかる。 つまり、一瞬で実行が終わるRubyスクリプトを何度も何度も繰り返して呼び出すと、トータルではかなり時間がかかるわけだ。 もともとのスクリプトは本体はRubyで、呼び出しがZshだったので、20並列で各500回、Rubyによるコンパイルがかかっていた。だから、かなりの時間がかかっていたのだ。

だが、「Linuxのforkはメモリが共有され、ほとんど一瞬で終わる」という点を利用すると改善の余地がある。 それは、実行可能なRubyスクリプトをライブラリ化する、という方法だ。

ZshからRubyを呼び出す場合、どうしてもRubyを呼び出すたびにRubyによるコンパイルをかけざるをえない。 当初は10000回コンパイルされていたのだが、500回のループをZshではなくRubyで行うようにすれば20回で済むようになる。だが、それでも20回のコンパイルが必要だ。

しかし、呼び出すスクリプト自体をRubyに変えてしまえば、実行しようとするスクリプトをライブラリとしてロードするという方法がとれるようになる。 ライブラリとしてロードすると、そのコンパイルは呼び出し元スクリプトをロードしたときに行われる。 もちろん、呼び出しの目的は呼び出すだけであり、直接そのライブラリの機能を使うわけではない。だが、この状態からforkすると、「コンパイル済みコードがメモリ上にあるRubyプロセス」が出来上がる。

この時点でスクリプトを実行する方法は「メソッドを呼び出す」(あるいは、その機能を果たすオブジェクトを作ってメソッドを呼び出す)だけである。 繰り返し呼び出すループを書くのも、単にRubyのループを書いて、そこで繰り返しメソッドを呼び出すなりオブジェクトを作るなりすれば良い。 呼び出し元スクリプト側では並列分だけforkしたあと、Process.waitallでもしていればいいわけだ。

これはZshに対して、「Rubyスクリプトのコンパイルが1度だけでいい」「execする必要がない」というメリットをもたらしている。 どちらも結構コストの高い処理であるから、繰り返し実行する場合は非常に大きなコストになり速度を低下させる。「処理自体は軽いのだが果てしなくループする」タイプのスクリプトに対してこの方法は本当に効く。 なぜならば、そのようなスクリプトに対してはコストの高い呼び出しをしているとコストのほとんどは呼び出しで占められ、実行コストは小さいためにスクリプト自体を高速化しようとがんばったところでほとんど無意味だし、逆に呼び出しコストを軽くすると劇的に速くなるからだ。

ループ回数の多い処理でのログ出力

概要

現状、1処理が0.0012秒程度で終わるシステムがある。1回の処理は非常に短いが、これが呼ばれる回数は1処理あたり数百万回にも及ぶ。 速度がそれなりに求められる案件だ。

しかし、その中で何度かログファイルに書き出したいタイミングというのがある。それぞれのログファイルに書き出すタイミングは呼び出しの中では1度だけだ。 だが、「処理時間が短くて」「回数が多い」のに共有のログファイルに書くというのはなかなか鬼門だ。 ファイルに書いている時間に待ち合わせがあると、速度に対してものすごくロスが発生する。

まずは普通に

普通に共有されているログファイルをロックする方針でやってみる。

ログファイルにアクセスできるのは1プロセスだけなので、同時にログに書こうとすると待たされることになる。 同時に書くとファイルが壊れてしまうので、すごく妥当な方法なのだが…

% time ruby test-logging1.rb
ruby test-logging1.rb  20.24s user 676.50s system 1102% cpu 1:03.18 total

1分3秒ということでかなり長い。

ソケットで投げてみる

UNIXドメインソケットにして投げつけてみる。 サーバー側が受け取った後の処理はクライアント側の処理時間には影響しないので、サーバーは投げ捨てる。

クライアントは直接ファイルに書くのではなくUNIXドメインソケットを使う。

待ち合わせは全く発生しないわけではない(ソケットを受付てからスレッドに移るまでの間はブロックされる)が、速くなるはずだと思うのだが。

% time ruby test-logging2.rb
ruby test-logging2.rb  25.51s user 33.05s system 62% cpu 1:33.02 total

1分33秒。却って遅くなってしまった。 やはりソケットに接続するコストが高いようだ。

ただ、並列数としては今回の場合最大24で、繰り返しの間というのは別にソケットをつなぎ直す必要はない設計になっている。 そのため、ループの外側に接続を置くことができる。

1行入れ替えただけだが、効果てきめん。

% time ruby test-logging2.rb
ruby test-logging2.rb  2.91s user 2.63s system 1488% cpu 0.373 total

今度は0.3秒で終わった。 Linuxの場合ソケットへの書き込みはカーネルがバッファするため、この程度の量であればメモリへの書き込みだけで終了する。そのため、かなり高速である。

ちなみに、ファイルの場合共有されているファイルをみんなが開いていたらぐちゃぐちゃになってしまうのでこの方法は使えない。 また、親子関係にある別プロセスについてはファイルディスクリプタ自体は共有されているため、fd番号さえわかっていれば共有できるのだが、 その場合は同じ経路から出ていくことになり、ファイルを共有した場合と同じ問題に直面するため、バグになってしまう。

PIDを使う

私の新しいアイディアは、「ログファイルにPIDを含める」という方法だ。

プロセス自体がマルチスレッドになっていなければ、その瞬間には同一PIDのプロセスはそのプロセスしかいないため、そもそもロックする必要がない。 ファイルのロックはあくまで「同時に」書かれるから必要になるのであって、どれだけ速くても順次であれば何も問題はないのだ。

ファイル名にPIDを含めているため、同時に走っている別のプロセスが同じファイルにアクセスすることはない。 もちろん、これは並列化の方法にプロセスを選択している場合だけだが。

% time ruby test-logging3.rb
ruby test-logging3.rb  12.26s user 22.37s system 1778% cpu 1.948 total

なかなか速い。だが、ソケットとはだいぶ差があるようだ。 しかし考えてみればこれもプロセスをオープンしてからであればなにもループのたびに開き直す必要はない。ソケットと同じ話だ。 そこで、ソケットと同じく順序を入れ替えてみる。

ループのたびにファイルを開くのではなく、予めファイルを開いてループで書いていくようにした。

% time ruby test-logging3.rb
ruby test-logging3.rb  2.52s user 0.13s system 1356% cpu 0.196 total

ファイルとソケットのオープンコストの差か、ソケットよりも速くなった。 ちなみに、ソケットを一回だけ開く方法よりも速い。

「forkでの並列処理に追記のログファイルはファイル名にPIDで」、このテクニック、私はどこかで見た記憶はないのだが、かなり有用だと思う。お勧めだ。

素晴らしきRinda

Rindaって

Java生まれのTuppleSpaceのRuby実装である。

Javaの実装はLindaというので、RubyだからRindaということらしい。

Rindaは基本的に分散Ruby(dRuby)のデモンストレーションのようなライブラリのようだが、今(2.5.2)に至るまでずっと標準添付され続けている。

dRubyすらあまり使ったことのある人がいない中、dRubyはPStoreやYAMLStoreなどと同様、 「知っている人は少ないが使ったことのある人はほとんどいない」標準ライブラリになっている。

TuppleSpaceって

TuppleSpaceは共有して置いておけるプールである。

「ホワイトボードシステム」と呼ばれることが多いのでホワイトボードになぞらえるが、このホワイトボードにマグネットシートを貼ったり取ったりできる。 ボードから取ってくるマグネットシートは種類による選別が可能だ。

一般的なソケットと違い、違う種類のやり取りをひとつの経路に簡単に混ぜることができる。

TCPサーバーとして起動できるので、ネットワーク分散も可能だ。

Rindaの概要

Rindaに対して置くことができるのは配列またはハッシュなのだが、「使い勝手はハッシュだが普通は配列」というのが私が感じている空気である。

例えば次のようなオブジェクトを配置する。

このメソッドはTuppleSpaceに接続し、この値を入力する間はブロックするが、受け取り手がいようがいまいが、すぐに終わる。 とにかく一旦TuppleSpaceに置かれるのだ。

そして、別のプロセスが次のようにして受け取る。

nilはワイルドカードになるので、valの値がなんだったかに関係なく受け取られる。 ただし、さらに他のプロセスが

のようにしていたら、この値は受け取らない。[0]に明に指定された値と異なるからだ。

全体リソースの中での曖昧さ

サーバーでは無限にコネクションが維持できるわけではないし、無限にリソースがあるわけでもない。 そのため、処理できるものからどんどん処理していきたい、と考えるのだが、Rindaはこれによって「要請」と「結果」をプールしておくことができる。

例えば、次の例では検索クエリを受け取って、検索を行った結果をTuppleSpaceに置く。 クエリがないときはブロックする。

検索を要請する側がidを発行しておくことで、一意に応答することができる。 この処理を行うプロセスの数によって「並列で検索できるワーカーの数」を制御することができる。 余っているワーカーがあったとしてもそれはRinda::TuppleSpaceProxy#takeによってブロックされるだけであり、「同時最大並列数」だけプロセスを起動しておけば良い。

さらにこれを取っていくワーカーは別のホストでも構わないのだ。

TuppleSpaceによって簡単に分散処理、並列処理が可能なのだが、 これは「入れた順番には出てこない」。処理が大量にたまってしまった場合でも順番を守って処理してほしい場合には適さない。

だが、「手が空いているならば仕事を持ってきて処理する」という形式はなんとも人間的で、かつコンピュータ的にも結構優れたモデルだと思うのだ。 Rindaが有効に機能するように設計することにより、並列処理に伴う難しさをかなり軽減することができる。 そもそも並列処理は厳密さを求めるにはあまり向いていないので、このようなふわっとしたレイヤーをはさむと結構いい感じに動作する。

in action

今回は「ワーカーが処理するデータはそのワーカー単独が処理するディレクトリに分割し、ワーカーは処理が終了したらRinda経由でディレクトリパスを取得する」という方法をとった。

以前はHTMLチャットサーバーの実装で、要求に対して即座にリターンさせるためにRindaを使っていたこともある。

あまり知られておらず、使われる機会もないが、使ってみると結構面白いのではなかろうか。