| @旅行/散歩

アルプス遠征で白馬に行ったとき、福岡からは松本経由で行った。前の記事に書いている通り台風の影響で縦走の予定がピストンとなったため、松本を一日観光する時間ができた。

松本には去年の秋に友達の結婚式で行っていて、そのとき市内を少し観光してすごくいいところだなと思った。前回は帰りの飛行機の時間の都合でできなかったことがいくつかあり非常に悔やまれたので、今回は前回できなかったことをやってきた。

宿泊

泊まったのは花月というホテルで、結構よいホテルのなはずなのにハイシーズンにもかかわらず低価格で泊まることができたが、部屋に入って窓を開けるとこの景色でなるほどという感じだった(これはこれで面白くてテンション上がった)。

IMG_6027.jpg

昼食 1

松本には昼過ぎに着いたのでホテルに荷物を置きに行き、 The Source Diner に行った。ここには前回も来ていて、カウンターの席に座るとお店の人が料理している様子を眺めながら食事することができる。自分も料理するの好きなので目の前で料理してくれるタイプの店はプロの慣れた手さばきが見られてとても楽しい。

DSC_5954.jpgDSC_5960.jpg

物色

あたりをうろつく。ナワテ通り外れの角にあるポーランド食器のセラミカがよくて、以前来たとき器を買うか迷ったが荷物になることや結構高いこと( 21cm の皿でも 3000 円から 5000 円くらいする)、帰りの飛行機まで時間が限られていたことで決心しきれず、買わずに帰ってきてしまった。あとあとこのことが非常に悔やまれて、もしまた松本に行く機会があればセラミカを訪ねて皿を買いたいと思っていた。

買いたい器に目星をつけたが今日は荷物になるので買わないでおく。

コーヒー 1

その後、セラミカの器でコーヒーが飲める喫茶店「カフェあげつち」でコーヒーを飲んだ。この喫茶店は 300 円でコーヒーが飲めてレトロな家具や調度品が飾ってあり休憩にちょうどよい。観光客向けというより地元の人が談笑しに来ている感じ。年季の入った建物を修復して公営の会議スペースとして利用しているようである。

松本城

そこから松本城に行った。前回も今回もお城の中には入らなかった。入り口に「ここから 30 分待ち」というような看板があったが、ああいうのがあると入るのをためらってしまう。

DSC_5962.jpg

お土産 2

夜は会社の仲良しおじさん連中と飲みに行くことにしていたので、ムラマサに行ってお土産を物色した。ここも前回来たときに気にはなったけど時間がなくて素通りしてしまった店(まつもと空港で売ってるだろうと思ってスルーしたら売ってなかった)で、寄らずに帰ったことを猛烈に後悔したので滞在中何度も訪ねて入念に物色した。シュークリームが名物のようだったが旅行者で冷蔵庫に保存することができないのでお店の人にお勧めを聞いて天守石垣サブレを買った。この手のクッキークリームサンド的なお菓子にはあまりテンションが上がらないのだが、帰ってきて食べてみたら確かにおいしかった。

DSC_5978.jpg

飲酒 1

花時計公園の横にある信州ゴールデン酒場に行った。山賊焼を食べたが焼きという名前なのに揚げてあるのにはびっくりした。あと付け合わせキャベツについてくる味噌が完全にう◯こに見えてやばかった。キンミヤハイボールがよいと foursquare の Tip にあったので頼んだらもろに甲類焼酎のアルコールにやられて悪酔いした。

IMG_6024.jpgDSC_5983.jpg

飲酒 2

二軒目はナワテ横丁にある彗星倶楽部という名前の店に行った。両腕にタトゥーを入れたお姉さんがやっててかっこよかった。つまみも全部うまかった。

IMG_6025IMG_6025

さらにもう一軒行ってそばを食べたけど泥酔し過ぎていてあまり思い出したくない…。

朝食

12:50 にバスセンター集合だったので早めにチェックアウトして朝食を食べに行ったが、目当てにしていたおきな堂はモーニング営業を取りやめており路頭に迷ってしまった。二日酔いで勘が鈍っており、ふらふらしながらナワテ通りの適当なパン屋に入ったところいまいちだった…。せっかくよいホテルに泊まってたのだから大人しくホテルの食堂のモーニングを食うべきだった。同僚によると「まるも」という喫茶店が良かったとのこと。

お土産 3

萬年屋というところで味噌を買った。信州味噌はなかなか九州では買う機会がないので面白い。二年熟成の味噌を買った。

IMG_6025

コーヒー 2

この日のメインイベントはセラミカでの器購入だったがオープンが11時なので開店まで暇をつぶす必要があった。朝食のコーヒーがいまいちだったので年季の入った外観だが今風の若い人が載ってそうなチャリンコが停めてある喫茶店に入ってコーヒーを飲んだ。

IMG_6025IMG_6025IMG_6025

セラミカで散財

11 時になり満を持してセラミカに行った。山小屋泊ですっかり朝が早くなったので 11 時はもう夕方みたいに感じる…。皿を三枚とマグカップを買った。グラタン皿も良さそうだったが流石に高すぎた(一万円オーバー)。カード払いしたが二回分割したかったのに何も聞かれず一括で決済されてしまった…。ただ買った皿はとてもよい。何でもおいしく感じる。

IMG_6031IMG_6031

昼食 2

ホテルの近くに有名なそば屋があるようだったのでセラミカで買い物をしたあと行ってみたが、 11:30 オープンで 11:35 に着いたにもかかわらずすでに満席で店の外まで列ができていた。今から並んではバスセンターの集合時刻に間に合わなくなるので仕方なく別のそば屋に入った。この日は暑かったが鴨南蛮が食べたくてだらだら汗をかきながら熱いそばを食べた。

IMG_6025

散策

そばを食べたあと、ホテルに預かってもらっていた荷物を受け取り、ホテル横の湧水場で湧き水をナルゲンボトルに汲んだ。松本は市内各所でこのように水が湧き出ているようである。重いザックを背負いバスセンターまで 10 分ほど歩いた。松本は標高 600m あるとはいえ 8 月の日中は暑く、大量に汗をかいた。

インスタグラムスポット

途中、ふとん屋のたたずまいが良すぎて写真を撮った。ここは去年来たときも見かけて写真を撮ったが、去年は一眼レフを持ってきておらず iPhone でしか写真を撮れなかったことをとても悔やんでいた。

IMG_6025 IMG_6025IMG_6025

感想

松本は岳都や学都、楽都と言われるが、確かにそうだなと思った。北アルプス登山の足がかりとなる街で市内から北アルプスの山々の姿を見ることができる。また信州大学の本部があり旧制高校の松本高等学校もあって学問の雰囲気も感じる。ナンバースクールがあった熊本(第五高等学校)よりも貫禄がある。加えて音楽が盛んなようで、街中に小澤征爾の音楽祭のポスターが貼ってあった。どうやら市をあげて音楽振興に取り組んでいるようである。

生まれ故郷の阿蘇もかつては阿蘇登山の拠点として駅前に旅館がいくつもあり栄えていたが、自動車が普及し日帰りで訪れることができるようになって 90 年代にはそれらの旅館は全て廃業してしまった。松本はきっとその時代から変わらず岳都として栄え続けているのだろう。山の高さや数が阿蘇とは全然違うとはいえ、山岳観光都市として参考にできる点は大いにあると思った。

アルプス登山がてらまた行きたい。

| @登山/ランニング

白馬岳

仕事で長野県、富山県、新潟県の県境に跨がる北アルプスの白馬岳に行った。山行記録はこちら。

当初は以下のように栂池高原 -> 白馬岳 -> 朝日岳 -> 蓮華温泉という縦走の予定だったが、台風が近づいており、急遽予定を短くして栂池高原 <-> 白馬岳のピストンとなった。 YAMAP の活動日記に書いているとおり体力不足で、15kg の荷物とカメラバッグが肩に食い込んでいたこと、また往路で転倒して左膝を岩場で強打したことによりかなり膝が痛む状態だったため、もし縦走で三日目があった場合は途中で離脱してしまっていたかもしれない。

夏の遠征登山2018_コース概要.jpeg

アルプス、日本なのに「アルプス」みたいな名前は変だしいけすかない、山は生まれ故郷の阿蘇や久住が一番だ、という思いを持っていたけど、実際行ってみるとスケールが全然違った。盛夏でも雪が溶けずに残る雪渓、森林限界を超えたあとに広がる景色、稜線を境に長野側と富山・新潟側の両方の景色を眺めることができる高さ、山小屋泊、夜の星空の美しさ、全てにおいて圧倒されてしまった。そもそも日本アルプスという名前も日本人が勝手に名乗り始めたわけではなく、明治時代に来日した鉱山技師のウィリアム・ゴーランドというイギリス人が命名し、宣教師のウォルター・ウェストンがヨーロッパに名前を広めたものらしい。なんで Japanese Alps という英語表現がオリジナルということになる。

アルプスの素晴らしさは上に書いた通りだが、夏でも頂上付近は夜になるとダウンジャケットが必要になるくらい寒く、また天候が悪化すると下界よりはるかに厳しい気象条件となり、稜線や岩場を歩く際は凄まじい風に体を持って行かれそうになる。重い荷物を背負った状態でちょっと足運びを誤ると転倒して滑落し、傾斜が急な長野県側に落ちた場合はほぼほぼ命を落とすことになる。雨が降れば気温が下がり、雨具を持たない場合は夏でも低体温症になってしまう。

語弊を恐れずに言うと高い山に登るのは博打に近いよなぁと思った。すばらしい景色、他では見られない景色を見るために命を危険にさらして山に登に行っている感じ。どう考えても家でじっとしてテレビ見たり出かけるにしても街に行って買い物したりしてる方が安全じゃないですか。山に行かなければ死ぬ可能性はとても低い。『岳』という漫画を読むと、山に登って滑落し、手足をタコのようにぐにゃぐにゃに折り曲げて死んでいく人たちが沢山出てくるけど、そういう危険をおかしても行きたくなるようなギャンブルにも似た中毒性が山登りにはあるのではないかと思った。

| @WWW

2018 年 7 月 10 日に天神の Fusic 社で開催された Scrapbox Drinkup #5 に参加した。

Nota 社の皆さんが福岡に訪れてユーザーと情報交換するという趣旨のイベントのようだった。 Scrapbox はほとんど使ったことなかったが、 @masui 先生や @shokai さんの発表が聞けるということなので行ってみることにした。

Nota 社のイベント、他社の採用目的感あふれるイベントと異なり牧歌的な感じがとてもよかった。 Nota 社の皆さんも参加者と同じテーブルに座ってピザ食ったりビール飲んだりでざっくばらんな感じだった。

最初にパスタケさんの概要説明的な話があったあと、 @masui 先生や @shokai さんの発表を聞いて「使ってみたい!」という気持ちが強くなり、発表を聞きながら一つプロジェクトを作った。

実は Drinkup 参加前に @ssig33 さんが書いていた Scrapbox についてのブログ記事(ssig33.com - Scrapbox Drinkup #4 にいってきた)を読んでいて「ふ〜ん」くらいに思っていたのだが、実際に利用事例やできることを知って、確かにこれは面白いものだなと思った。

@ssig33 さんが書いている通り、 Scrapbox は単に知識を集めて検索可能にするためのものではなく、コミュニケーションツールだなという感じがする。なので MTG の議事録などを複数人でとって理解が曖昧だったところを確認・補強したりするのに非常に向いてるだろうなと思った。リモートワーク主体のチームが Scrapbox を導入したら MTG の生産性が高まるのではないかと思う。

個人的に Scrapbox に対して良いなと思ったのは以下だ。

  • 共同編集
    複数人で同時に編集できる楽しさ
  • ドキュメント間のリンクが容易
    ハッシュタグやリンクは相互参照される
  • 動画や画像を簡単に貼れる
    コピペで画像をアップロードできる
  • 投稿・入力の敷居の低さ
    枠や升がなく、必須入力やバリデーションなどもほぼほぼなさそう
  • 検索

それぞれはすでにすぐれたツールが存在していると思うが、それが一つのツールですべて使えるようになっているところが便利なのだと思う。

以前アウトライナーについて記事を書いたことがあるが(アウトライナーで文章を書く - portal shit!)、その中で紹介したものに WorkFlowy というアウトラインプロセッサーがある。個人が思考を整理しながら文章を書くのに非常に適したソフトで、 Scrapbox に似ている側面があると思った。

WorkFlowy はアウトライナーからファイルやタイトル、ノードという概念の区別を取っ払い、すべての情報をノードとして扱いツリー構造にしてしまう。おかげですべての情報が容易に検索可能になり(情報がファイルで分割されない)、検索性も向上して知識の保存と参照が劇的にしやすくなる。 Scrapbox では階層構造を作ることはできないが、すべての情報はフラットな空間に書きためられ、それぞれの情報を非常に参照しやすいかたちでリンクさせることができる。

Scrapbox と WorkFlowy が大きく異なる点は、これらの情報を容易に共同編集できるかどうかだと思う。 WorkFlowy は非公開状態がデフォルトで、他者に閲覧を許可したり共同で編集したりするためにはその為に専用のリンクを発行して共有のレベルを設定しないといけない。 Scrapbox はデフォルトが公開状態で(少なくともプロジェクトの参加者の間では)、特に設定することなく情報を共有したり共同で編集したりできるようになっている。 URL は当然ながらドキュメントを作成したタイミングで生成される。わざわざ共有用 URL を発行する必要はない。デフォルトでは URL が存在しないため、 WorkFlowy ではドキュメント間のリンクもしづらい。

このように Scrapbox で初めて得られる文章編集体験というものが確実に存在すると思う。


個人的に作ったやつは会社の G Suite 内の Google Maps にあったランチスポット情報を集約したやつをエクスポートしたもの。Drinkup で @masui 先生が「ヘルプドキュメントの類を Scrapbox で構築したら便利なはず」という話をされていた1のに着想を得た。職場が福岡市の中心部でもマイナーなエリア(旧来の博多の街の中心部だけど天神と博多駅からも微妙に離れていて飲食店が少ない)にあり、食べログなどで探してもあまり有益な昼食スポットの情報が得られないため、これまで社内で情報をストックして共有していたが、この手のものは一社で作るよりも界隈にお勤めの皆さん全体で共有した方が効率がよいはず。 Scrapbox がこの用途にはピッタリなのではないかと思って作ってみた。

Spread Sheet や Google Maps の Data Table による管理では複数人からの情報を良い感じにまとめることができず、多くの人から情報を集める、という点で難があった。 Spread Sheet は入力しやすい UI であるとは言えないので、入力する側に根性や気合い、熱意が求められた。 Scrapbox であればテキストファイルに文章を入力するのと同程度の手軽さで情報を入力していくことができる。入力欄に枠や升があるわけではないので自由気ままに書くことができる。これは書き手にとって大いにストレスの軽減になると思われる。

もし福岡市地下鉄貝塚線呉服町駅周辺のランチスポット情報をお持ちの方がおられましたら以下のページから登録して情報共有しませんか。よろしくお願いいたします。


  1. 実際に Scrapbox のヘルプページは Scrapbox で作成されている。 Scrapbox ヘルプ 

| @技術/プログラミング

ヒトデさんの以下のツイートを目にして便利そうだと思ったので fish + peco + vim でやってみることにした。

以下のような fish 関数を追加した上でショートカットキーを bind しておいた。

function peco_gitlsfiles_vim
  git ls-files | peco --query "$LBUFFER" | read selected
  if [ $selected ]
    vim $selected
  end
end
function fish_user_key_bindings
  fish_vi_key_bindings
  bind \cg\cv peco_gitlsfiles_vim
  bind -M insert \cg\cv peco_gitlsfiles_vim
end

これまで一旦 vim を閉じてしまうとファイルを開きたいときには vim . して Unite で調べててたけど、いきなり git ls-files して peco して絞り込めるようになってとても便利になった。

2018-06-20 16.48.40.gif

| @WWW

| @ブログ

Facebook にウェブサイトの URL をはっつけるとき参照される HTML メタ情報の仕組みに Open Graph Protocol ってのがある。 Facebook に URL を貼ると bot が URL の内容を読みに行ってページの概要や画像を取得し Facebook 内に埋め込み表示するというもの。 Facebook を見ている人はリンク先の内容をクリックする前に概要を把握できるので、リンクをクリックして見たい情報じゃなかった、ということを避けられる。 Facebook が考案して策定した仕組みだけど、 Facebook に限らずいろんなサイトで OGP タグを出力してるし読み込んでる。 Twitter にも似た仕組みあって Twitter Card という。この辺の対応は結構前にやってた。

ただ自分のサイトが OGP タグを提供するだけではつまんないなと思ったので自分のブログにペロッと URL を貼ったときに相手先に OGP タグがあればそれを出力するようにしてみた。こんな感じ。

OGP Preview

しかしここで困ったことがあって、↑でリンクしてる Circle のサイトは HTTPS で配信されておらず、単純に Circle のサイトで og:image に指定されている画像を SSL 化されているこのブログで読み込むと Mixed Content になってしまう。せっかく HTTP/2 で配信していたのに台なしになってしまう。またそもそも og:image は Facebook でシャアされることを想定されていることがほとんどなので、画像サイズがデカすぎていい感じにスクエアに表示するためには CSS の小技を駆使したりする必要があった。

いい感じに解決する方法ないかなと調べていたら良いのが見つかった。

Go で書かれた Image Proxy Server で、 HTTPS Proxy は当然のこと動的リサイズもできる。使い方は簡単でバイナリを落としてきて動かすだけ。 Go なんで ImageMagick をどうしたりとかを考えなくて良い。 そもそも Docker イメージも提供されているので Docker をインストール済みなら docker run するだけでも動く。 めっちゃお手軽。

こいつのおかげで HTTPS で配信されていないサイトの OGP タグを読み込んでも Mixed Content にならずに済むようになった。また og:image は適切にリサイズできるようになった。画像変換サーバーとかは結構難しいやつで個人のブログでこんなに簡単に使えるものだとは思ってなかったので正直ビックリした。

AWS の登場で大企業じゃなくても CDN 使ったり仮想サーバーでウェブシステムを構築したりできるようになった。さらには Go や Docker といった技術のおかげで複数の込み入ったソフトウェアを組み合わせて構築していく必要があったシステムが、まるで jQuery を使うような感覚でポン付けで使える時代になってきている。とても素晴らしい。

ちなみに OGP の取得には open_graph_reader という gem を使っている(昔からある opengraph という gem はメンテナンスされておらず最近の Nokogiri で動かない)。 open_graph_reader の作者が結構 Opinionated な人で、以下のような Anti-featurs を掲げている。

open_graph_reader Anti-features

http://ogp.me/ の仕様に準拠していないサイトのことは完全無視というつくり。個人的にはこういう思想は好みだが、現実問題として使い勝手が悪い。例えば hitode909 さんのブログの OGP タグを取得しようとしたところ以下のようなエラーを出して取ってくれなかった。

スクリーンショット 2018-05-26 10.08.47.png

article:published_time は ISO8601 形式の datetime であるべき、とのこと。はてなブログはかなりシェアが大きくリンクする機会が多いので残念。

| @技術/プログラミング

A summer storm

問題点

Rails でデファクトスタンダードとなっているページネーション gem に Kaminari というのがある。

めっちゃ最高便利で大好きなのだけど、巨大なテーブルに対して COUNT 文を投げると遅いという問題にぶち当たった。このような巨大なテーブルで Kaminari を使うために COUNT 文を発行しない without_count というメソッドが用意されている( Kaminari 1.0.0 でやってくる 5 つの大きな変更 - Qiita )が、これを使うと next_pageprev_pagetotal_pages が取れなくなる(当たり前)。次のページがあるかどうかはばくち状態になってしまう。

本当は DB のスキーマを見直すべき(インデックスがちゃんと効くようにスキーマ変更するべき)だが、 Rails からもレガシーアプリからも同時に同じ DB にアクセスしており、並行運用しているような状況ではなかなか大胆な変更は実行できない。

DB 構造をなおせないとなるとキャッシュを思いつく。 HTML も Rails でレンダリングするのであれば partial cache などでページャー部分だけをキャッシュすれば良いが、 API 選任野郎と化した Rails ではビューのキャッシュはできない。

どうしたか

total_count をキャッシュする。公開範囲を設定できるようなリソースだと全員同一のキーでキャッシュするわけにはいかないのでユーザーごとにキーを作ってキャッシュする必要あり。全ユーザーの全リクエストでスロークエリになってたやつが 5 分に一回スロークエリになるくらいだったら何とか許容できる。

例えば以下のようなコントローラーがあったとする。 HeavyModel には数千万レコードあって、普通に COUNT 文を投げると遅い。 Paginatable という名前でモジュールを定義して、 render メソッドを上書きし、ページネーションを間に挟み込む。

class HeavyModelController < ApplicationController
  include Paginatable

  before_action :login_required

  def index
    resources = HeavyModel.all
    render json: resources, paginate: true
  end
end

モジュールはこんな感じ。車輪の再発明をしている感はあるが、 COUNT 文の結果が current_user 、コントローラーのクラス名、アクション名のそれぞれをつなげたものをキーにしてキャッシュされる。

module Paginatable
  def render(*args)
    options = args.extract_options!
    resources = options[:json]
    if options[:paginate]
      resources, meta = paginate(resources, cache_total_count: options[:cache_total_count])
      options[:json] = resources
      options[:meta] ||= {}
      options[:meta].merge!(meta)
    end
    args << options
    super(*args)
  end

  def paginate(resources, options = {})
    parse_params_for_pagination
    paginator = Paginator.new(
      resources:         resources,
      page:              @page,
      per:               @per,
      cache_total_count: options[:cache_total_count],
      cache_key:         total_count_cache_key
    )
    [paginator.resources, paginator.meta]
  end

  def total_count_cache_key
    @total_count_cache_key ||= "#{current_user&.id}_#{self.class.name}_#{action_name}_count"
  end

  class Paginator
    attr_reader :cache_total_count

    UNCOUNTABLE = -1

    def initialize(resources:, page:, per:, cache_total_count: false, cache_key: nil)
      @_resources        = resources
      @page              = page.to_i
      @per               = per.to_i
      @cache_total_count = cache_total_count
      @cache_key         = cache_key
    end

    def resources
      @resources ||= if cache_total_count?
                       @_resources.page(page).per(per).without_count
                     else
                       @_resources.page(page).per(per)
                     end
    end

    def page
      @page.zero? ? 1 : @page
    end

    def per
      @per.zero? ? Kaminari.config.default_per_page : @per
    end

    def meta
      {
        current_page: current_page,
        next_page:    next_page,
        prev_page:    prev_page,
        total_pages:  total_pages,
        total_count:  total_count
      }
    end

    alias cache_total_count? cache_total_count

    def paginatable?
      !cache_total_count? && resources.respond_to?(:total_count)
    end

    def current_page
      resources.current_page || page
    end

    def next_page
      paginatable? ? (resources.next_page || UNCOUNTABLE) : next_page_fallback
    end

    def next_page_fallback
      return UNCOUNTABLE if page < 1
      return UNCOUNTABLE if per > resources.length
      total_count_fallback > current_page * per ? current_page + 1 : UNCOUNTABLE
    end

    def prev_page
      paginatable? ? (resources.prev_page || UNCOUNTABLE) : prev_page_fallback
    end

    def prev_page_fallback
      return UNCOUNTABLE if page < 2
      (total_count_fallback.to_f / per).ceil >= page ? current_page - 1 : UNCOUNTABLE
    end

    def total_pages
      paginatable? ? resources.total_pages : (total_count.to_f / per).ceil
    end

    def total_count
      paginatable? ? resources.total_count : total_count_fallback
    end

    def total_count_fallback
      @total_count_fallback ||=
        begin
          cached_total_count = Rails.cache.read(@cache_key)
          if cached_total_count
            cached_total_count
          else
            real_total_count = @_resources.page(page).total_count
            Rails.cache.write(@cache_key, real_total_count, expires_in: 5.minutes)
            real_total_count
          end
        end
    end
  end
end

これで 5 分間はキャッシュが効くようになる。