たのしい人生

【小ネタ】Ubuntu で boot 時に failed to resume link や SATA Link down、AHCI controller unavailable! などと表示されて妙に時間がかかる問題に対処する

Ubuntu で起動 (Splash) に妙に時間がかかったので調べました。Splash を表示せずに起動時のメッセージを見ると

f:id:biacco42:20200514010742p:plain

failed to resume linkSATA Link downAHCI controller unavailable! など明らかにあんまりよろしくないメッセージが出て時間がかかっていました。

結論としては、PCIe の SATA 拡張ボードに DVD ドライブを接続していたのが原因でした。この拡張ボードは Asmedia 製のコントローラを搭載していたのですが、どうもこの拡張ボードに DVD ドライブを接続していると起動時に上記のメッセージが表示されて起動に時間がかかります。M/B の SATA ポートに DVD ドライブを接続し、拡張ボードの方に SSD を接続したら問題が解消しました。

おしまい。

Ubuntu 環境で Blue Yeti のマイクのモニタ (入力フィードバック・ループバック) が聞こえない場合の設定 / モニタをオフにする方法 / モニタ音量調節方法

以前書いた Windows 環境で Blue Yeti のマイクのモニタ (入力フィードバック) が聞こえない場合の設定 / モニタをオフにする方法 - たのしい人生Ubuntu 版です。やってることはほとんど同じです。

問題 - モニタ音声 (フィードバック音声) が聞こえない・音量が小さい

USB 接続で高音質な音声入出力が可能な Blue Yeti Microphone。3.5 mm のステレオミニジャックがついており、PC 等の出力デバイスの音を聞きながら 遅延ゼロで入力音声をミックスしてモニタリングできる機能 が非常に便利な使い勝手のいいマイクで、podcast をオンラインで収録したり YouTube 配信したりゲームのボイスチャットに利用したりされています。

↓後継機種

Blue Yeti で検索すると一般的にこの 入力音声をモニタリングする機能はオフにできない と書いてあります。ですが、Ubuntu で場合によってはこのモニタリング音声が聞こえない場合があります。ヘッドホンをした状態で自分の声がフィードバックされないとしゃべるのが非常に困難で問題になります。

解決法 (or Blue Yeti のモニタ音声をオン / オフ / 音量調整する方法)

UbuntuLinux distribution ではオーディオデバイスの抽象的なコントロールとして PulseAudio という仕組みが使われていることが多いですが、PulseAudio では 再生デバイスのマイク音量 (Windows 環境で Blue Yeti のマイクのモニタ (入力フィードバック) が聞こえない場合の設定 / モニタをオフにする方法 - たのしい人生 参照) がうまいこといじれなさそう?でした。

Ubuntu で「再生デバイスのマイク音量」を操作するためには alsamixer を使うのが正解です。

$ alsamixer

ターミナル上で alsamixer の画面が表示されます。

f:id:biacco42:20200513235628p:plain

F6 キーでデバイスを選択します。今回は Yeti Stereo Microphone を選択します。

f:id:biacco42:20200513235827p:plain

選択すると Yeti Stereo Microphone の設定画面になります。F3 キーで Playback デバイスを選択します。Capture デバイスではないです。

f:id:biacco42:20200514000054p:plain

Playback デバイスの Mic がマイクループバックの音量なのでここで音量調節ができます。

また、ここで Speaker の音量を上げるとループバックの音量も大きくなりますが、Gnome Shell 等の音量調整ではループバックの音量は変わらないため、環境で再生される音量とマイクループバックの音量の比率を間接的に調整することができます。

以上です。お疲れさまでした。

Ubuntu でデスクトップをキャプチャして仮想カメラデバイスとして流す話

デスクトップをキャプチャしてストリームできると Discord などの通話で使ったりできて便利なのでやり方をまとめます。

v4l2loopback のインストール

v4l2loopback という module をインストールすると仮想カメラデバイスが作れます。Ubuntu は公式リポジトリホスティングしてくれています。

$ sudo apt install v4l2loopback-dkms

でインストールできます。

v4l2loopback の読み込み

まず v4l2loopback をロードする前に、現在のビデオデバイスを確認しておきます。

$ ls /dev/video*
/dev/video0  /dev/video1  /dev/video2  /dev/video3

確認したら以下のコマンドで v4l2loopback をロードします。

$ sudo modprobe v4l2loopback

その後、ビデオデバイス一覧を再確認してみます。

$ ls /dev/video*
/dev/video0  /dev/video1  /dev/video2  /dev/video3  /dev/video4

video4 が v4l2loopback で導入された仮想カメラデバイスになります。

Discord などで取り込めるように ffmpeg でデスクトップをキャプチャして v4l2loopback のデバイスに流し込む

なにもしなければ v4l2loopback デバイスは単なる空っぽのデバイスなので、ffmpeg で画面をキャプチャーして v4l2loopback に流し込みます。その際にキャプチャー範囲や出力解像度を調整します。

ffmpeg -f x11grab -r 15 -probesize 128M -s 3840x2160 -i :0.0+1920.0 -vcodec rawvideo -pix_fmt yuv420p -f v4l2 -vf 'scale=1280:720' /dev/video4

この状態で Discord などを開くと仮想カメラデバイスが選択でき、ffmpeg がいい感じにデスクトップキャプチャしてくれていればいい感じに表示されます。(Discord などは Web Camera として認識しているので手元では左右反転プレビューされますが、相手には正しい向きで表示されます)

f:id:biacco42:20200512233244p:plain

便利になりました。

お絵かき / Kindle 視点で iPad Pro 2020 (Gen. 4) 12.9 inch レビューと iPad Pro 2017 (Gen. 2) からの乗り換えや Apple Pencil 2 と保護ケース・シートの話

TL;DR

iPad Pro 2017 (Gen. 2) から 10.5 inch から iPad Pro 2020 (Gen. 4) 12.9 inch に乗り換えました。世代や画面サイズの変化についてレビューします。結論から言うと Apple Pencil 2 がめちゃくちゃによいし、それを活用できるようにすると最高。

iPad Pro 2020 12.9 inch を買いました

もともと iPad Pro 2017 10.5 inch をお絵かきするぞ!と思って購入し初代 Apple Pencil を iPad Pro に挿して充電する日々を送っていたのですが、Apple Pencil 2 の本体に磁力固定・無接点給電が羨ましすぎて 2020 の発表と同時に即購入しました。そんな iPad Pro 2020 12.9 inch を 2 ヶ月弱使ってみてのレビューをお届けします。

f:id:biacco42:20200510164136j:plainf:id:biacco42:20200510165228j:plain

お絵かきなら 10.5 inch でも 12.9 inch でも実は大差ない

私は結構画面に寄って描いてしまいがちなので、体感としては 10.5 inch でも 12.9 inch でもそれほど絵の描きやすさは変わらない印象でした。ただ、同じ視界いっぱいにキャンバスを映す場合、12.9 inch だと相対的に顔を離して絵を描くことができるので、そういう意味で目に優しいのは 12.9 inch のほうかもしれません。

f:id:biacco42:20200510165856j:plainf:id:biacco42:20200510165405j:plain
10.5 inch と 12.9 inch のサイズ感の比較。10.5 inch はベゼルが広いためもう一回り画面が小さい。

画面の大きさで絵の描きやすさが変わってしまうか気にしている人に関しては「正直どっちでも描けるよ (たぶん)」といった感じなので、画面サイズに関しては他の理由で決めても構わないと思います。

f:id:biacco42:20200510170010j:plainf:id:biacco42:20200510170006j:plain
デスクに置いてみた感じ。iPad Pro 2020 (と 2018) はベゼルが細いので思ったより小さく感じる。

Kindle でマンガ読むなら 12.9 inch の横持ち見開き表示は 10.5 inch よりかなりいい

Kindle でマンガを読むならできるだけタブレットで、それも横画面で見開きに表示にするのが製本版のマンガ体験に近く非常におすすめなのですが、特に iPad Pro に関しては断然 12.9 inch モデルがおすすめです。

f:id:biacco42:20200510173123j:plain

写真はつくみず先生の少女終末旅行① [つくみず (2014) 少女終末旅行① 新潮社] なのですが、10.5 inch モデルだと原本よりひと回り小さくなってしまいます。もちろん元の本のサイズにもよるのですが、12.9 inch の方が体験としては「マンガ的」なので、Kindle でマンガを読もうと思っている場合は個人的には 12.9 inch モデルのほうがおすすめです。

10.5 inch と 12.9 inch で重さは結構変わるので気になる人はそこは注意

上記 Kindle の項では 12.9 inch がおすすめと書きましたが、10.5 inch と 12.9 inch の重量は無視できない差があります。iPad Pro 2020 の 11 inch モデルは 471 g、12.9 inch モデルは 641 g で、12.9 inch モデルだと重さおよそ1.4 倍弱、スマホ 1 台分程度重くなります。

私の持っている iPad Pro 2017 10.5 inch は 469 g で 11 inch モデルとほぼ同じ重さなのですが、12.9 inch の iPad Pro と持ち比べると明確に重さの違いが感じられます。重量が気になる方はこの点は注意したほうがいいでしょう。11 inch モデルと 12.9 inch モデルの隠れた最大の違いと言っても過言ではないです。

Apple Pencil 2 はめちゃくちゃ進化してるのでコレのために買ってもいい

ここからが本題で、iPad Pro をお絵かきデバイスたらしめている Apple Pencil についてです。

もともと所有していた iPad Pro 2017 は初代 Apple Pencil で、初代の時点でもなめらかな書き味に感動したのですがいくつか問題点がありました。対して iPad Pro 2018 (Gen. 3) 以降で採用されている Apple Pencil 2 はそのほとんどの問題が解決しており、Apple Pencil 2 のためだけに iPad Pro 2020 を買ってもいいぐらいです。

感度が改善されている

初代 Apple Pencil は画面に触れる前、空中に浮いた状態でも線の入力がされてしまう「負の感圧」が結構あったのですが、Apple Pencil 2 ではこれが改善されています。ほぼチューニング無しで画面に触れるぐらいから入力が始まるので、より自然に描けます。

f:id:biacco42:20200510183637j:plain

また、感圧の分解能も初代 Apple Pencil より高くなっているらしいのですが、正直そこは私の能力ではあまりわかりませんでした。また「ペン先のブレ」は初代 Apple Pencil / Apple Pencil 2 共に Wacom の液晶タブレット等に比べると大きいように感じるので、究極を求めるなら気になるかもしれません。

充電が iPad にくっつけるだけ

説明不要です。これが最高に便利すぎます。初代 Apple Pencil を使っているときは、朝オフィスでおもむろに iPad Pro に初代 Apple Pencil を刺して充電するのが日課だったのですが、Apple Pencil 2 では iPad Pro 本体にくっつけておくだけで常にフル充電、使いたいときにサッと使えます。

iPad Pro をお絵かきの道具や手書きメモとして使うのであればこの「いつでも使える確信がある」というのは非常に重要です。精神的なめんどくささがあると絶対に使わなくなります。

f:id:biacco42:20200510180812j:plain
新旧 Apple Pencil 充電の姿

転がらないデザイン

地味にこれがめちゃくちゃ嬉しいのですが、Apple Pencil 2 では円柱のいち部分だけが平面になったデザインをしています。

f:id:biacco42:20200510181606j:plain
基本的に円柱だが一部だけ平面になっている (かしこい!)

初代 Apple Pencil は完全な円柱だったため少しでも平面が出ていない机などに置くとコロコロコロ…と転がっていってしまう不安定さが非常にストレスでした。Apple Pencil はそれ単体で 1 万円程度するため、机の上から落としたりするのがめちゃくちゃ怖いんですよね。なので Apple Pencil 2 のデザインは非常にありがたいです。

また表面仕上も変わって、初代のツルツルな質感から Apple Pencil 2 ではサラサラマットな仕上げに変更されました。これは好みもあるでしょうが、悪くないです。

ダブルタップによるツール切り替え

Apple Pencil 2 ではペンのダブルタップによるツール切り替えができるようになりました。標準ではペンなどの書き込み系ツールと消しゴムが交互に切り替わるようになっています。これがかなり便利で、絵を描きながらツールパレットに意識を移動することなく消しゴムツールに持ち替えて修正することができるようになりました。うれしい。

注意点としては、「ダブルタップ」というとついついスマホタブレットのようなタッチパネルを想像してしまいますが、Apple Pencil 2 のダブルタップ検出はおそらく加速度ベースで、指を「トントン」とする衝撃を計測しています。そのためダブルタップする場所は意味ありげな平面部分でなくても、先端寄りなら本体のどこでも大丈夫です。

重心位置が少し下がった?

微妙な違いですが、初代 Apple Pencil はやや重心位置が高く指先でペンを動かすとペンの後ろ側の重心にやや振り回される印象がありました。Apple Pencil 2 では初代より長さが少し短くなり、重心位置が下がっているようです。重量は変わっていないらしいのですが、使ってみると Apple Pencil 2 の方がやや軽快に使えます。

f:id:biacco42:20200510184201j:plain
ほんのちょっぴり短くなった Apple Pencil 2

Apple Pencil 2 の磁力で本体固定・充電は最高だけど縦持ちすると手に干渉する

ここまで Apple Pencil 2 の魅力を書いてきたのですが、欠点もあります。結構致命的なのが、Apple Pencil 2 を iPad Pro 本体につけた状態で縦持ちすると、微妙に手とぶつかってしまう問題です。

f:id:biacco42:20200510195634j:plain:w400

とくにソフトウェアキーボードで入力しようとすると Apple Pencil 2 が外れかけることもしばしばあります。机の上からならまだしも、手の高さから、たとえばアスファルトApple Pencil 2 を落としてしまうのはあまり想像したくありません。一方で、Apple Pencil 2 を iPad Pro 本体にくっつけておくのは充電の機能も兼ねているため、Apple Pencil 2 だけペンケースに入れるといった運用は本来の利便性を損ねてしまうほか、サッとメモをとる、絵を描くといったことができなくなってしまうので致命傷です。

前述の通り、少しでもめんどくさい要素があると人間は利用しなくなるものです。できるだけハードルを下げておく必要があります。

ペン収納つきの 3rd party 製保護ケースが必須

Apple 公式の iPad Pro 2020 の保護カバーは側面を保護してくれないほか、Apple Pencil 2 もむき出しのため、上記の「縦持ちすると Apple Pencil 2 が手と干渉して落ちそう」問題が発生します。そこで個人的におすすめなのは、Apple Pencil 2 の収納スペース付き保護ケースです。

今までの写真でも写っていましたが、私は iPad Pro 2017 のときから Apple Pencil 収納機能付き保護ケースを利用していました。初代 Apple Pencil は充電を本体に挿して行わなければならないものの、こういったケースを用いることで常に iPad Pro と Apple Pencil が同時にあるので「絵を描きたい!」と思ったときに Apple Pencil を探すところから始める、みたいなことが必要なく、これが結構体験がよかったです。

検索すると iPad Pro 2020 向けにも Apple Pencil 2 の収納スペース付きの保護ケースがあります。しかも Apple Pencil 2 なら保護ケースに収納しているだけで充電もしてくれます。完璧です。

私はこちらを購入しました。

f:id:biacco42:20200510191330p:plainf:id:biacco42:20200510191347p:plainf:id:biacco42:20200510191356p:plain

f:id:biacco42:20200510191407p:plain

Apple 公式の保護カバーと違い側面も保護してくれる他、Apple Pencil 2 の収納スペースもあるため縦持ちしても Apple Pencil 2 に手を引っ掛けて落としてしまう心配がありません。これで初代 Apple Pencil の問題点をほぼすべてクリアした最強お絵かき・手書きメモ環境になりました。

こちらのケースに関しては Apple 公式のものに近いサラッとした手触りで、カバーの開け閉めによる iPad のウェイク・スリープ機能やスタンド機能にも対応しています。また、側面まで保護するタイプのケースだと電源ボタンや音量ボタンがケースに隠れてしまうことになるのですが、このケースはこのボタン部分の作りが非常によく、力を入れなくても押せて、ボタン本来のクリック感まで感じられる作りになっています。正直感心しました。おすすめです。

お絵描きするならペーパーライクフィルムはあったほうがいい

Apple Pencil と iPad でお絵かきをするのであればペーパーライクフィルムはつけたほうがよいと思われます。私も最初は「画面がギラついて画質が劣化するんじゃないか」等の懸念があって貼っていなかったのですが、一度覚悟を決めてペーパーライクフィルムを貼ってみたら思ったより画質劣化が小さく「なんでもっと早く貼らなかったんだ」と思ったぐらいです。

iPad 標準のガラス面に Apple Pencil のプラスチックなティップだとどうしても「カツカツ」「ツルツル」と滑ってしまう感覚で、紙にペンでかくのとは体験が大きく異なってしまうのですが、ペーパーライクフィルムを貼ることで完璧とは言わないもののそれなりに紙に近い感覚でかけるようになります。ペンが滑ってしまうことがなく動かしたいところに動かして止めたいところで止められるようになるので、手書きメモならともかく絵を描く場合はペーパーライクフィルムを貼ることをおすすめします。

私は、iPad お絵かきパーソンの間で定評のある下記の商品を購入しました。

iPad Pro は画面が大きく、保護フィルムを貼るのも一苦労です。実際このフィルムを貼る際も正直苦戦はして、貼った直後はどうしても少しの気泡が入ってしまい失敗したかなと思ったのですが、1 日程度で気づかないうちにきれいに気泡が消えました。最近の材料技術・ミクロな加工技術はすごいですね…

f:id:biacco42:20200510193405j:plain:w400

使い始めた直後は結構抵抗感が強いのですが、ある程度使うとちょうどいい感じの滑りになります。実際にこのフィルムと iPad Pro 2020 で最近描いたのがこんな感じです。

ラフスケッチ20200503www.pixiv.net

へるへる helltaker 六道冥www.pixiv.net

f:id:biacco42:20210117014131p:plain
描きかけ

まとめ

めちゃめちゃ長くなってしまいましたが、お絵かき / Kindleバイスとしての iPad Pro 2020 12.9 inch モデルのレビューはこんな感じになります。iPad Pro と Apple Pencil のおかげでお絵かきが始められたので、iPad Pro と Apple Pencil は実質無料です。ちょっとしたメモも iPad でとっているため手放せないデバイスになりました。おすすめです。

おしまい。

最新 Apple iPad Pro (11インチ, Wi-Fi, 256GB) - シルバー (第2世代)

最新 Apple iPad Pro (11インチ, Wi-Fi, 256GB) - シルバー (第2世代)

  • 発売日: 2020/03/25
  • メディア: Personal Computers

Apple Pencil(第2世代)

Apple Pencil(第2世代)

  • 発売日: 2018/11/07
  • メディア: Personal Computers

Self-Made Keyboards in Japan こと自作キーボード Discord サーバーでアクセスコントロールとその bot による自動化を導入した話

TL;DR

リモートワーク / WFH や STAY HOME 等でネットワークを介したコミュニケーション手段が非常に求められている中、Discord サーバー運用のノウハウとして カテゴリやチャネルのアクセスコントロールの仕方と bot による自動化 についてまとめます。

Self-Made Keyboards in Japan Discord サーバー

2 年半ほど前から Self-Made Keyboards in Japan という自作キーボードコミュニティのサーバーを Discord で運用しています。

biacco42.hatenablog.com

大変ありがたいことに非常に多くの方に利用していただいており、メンバーはそろそろ 3000 人になろうかというところです。感謝🙏

今春、サーバーメンバーが多くなってきたこともあり、より一層コミュニティとしての機能を拡充したいという思いから、キーボードに直接は関係ないチャネル群とそれをまとめるカテゴリを導入しました。一方で「キーボードサーバー」としての混乱をきたさないことも期待されており、今回カテゴリ単位での可視性・アクセスコントロールを導入しました。

その手順と自動化手法、オマケとして導入経緯や判断について簡単に紹介します。

チャネル・カテゴリ単位でのアクセスコントロール

Discord のチャネルやカテゴリに対するアクセスコントロールの設定法は既存のチャットアプリなどと比べるとやや特殊に見えるかもしれません。

Slack ではプライベートチャネルは「プライベートチャネルを作って」「そこに招待された人だけが見える」という比較的オールドスクールな手法によっています。対して Discord ではこのようなアクセスコントロールをするために role という概念を用います。一見複雑ですが、きめ細やかな権限管理が柔軟にしやすく非常に便利です。

role

Dircord における role というのは、簡単に言うとユーザーにつくラベルです。そしてチャネルやカテゴリに対して、その role というラベルごとに 読み書き等の権限を管理することができます。個人ではなく role に対して権限管理を行うため

  • 個人単位での権限管理が不要で
  • 権限設定の複雑化・意図しない権限付与バグを防ぎやすく
  • 権限のオンオフを role というテンプレート付与剥奪で冪等にできる (←自動化しやすい)

というメリットがあります。

f:id:biacco42:20200509183219p:plain:w300

Biacco42 というアカウントには managers Nitro Booster enable-other-category という role が付与されていることが確認できます。

チャネル / カテゴリを不可視にする

Discord の role による権限管理は非常に柔軟で様々なことができるのですが、基本的に必要なのは 読み書き権限のコントロール かと思います。

Discord では

サーバー全体での role による権限管理 → カテゴリの権限管理 → チャネルの権限管理

のように階層化された権限管理モデルが採用されています。より狭い範囲での権限設定が優先され、変更しないものに関しては上位の権限設定が継承されます。

これを利用して「カテゴリ A の中では role α だけが書き込むことができる」「チャネル B は role β しか見れないし書き込めないプライベートチャネル」というような管理が可能になります。

f:id:biacco42:20200509184510p:plain

f:id:biacco42:20200509185020p:plain

例えば上記画像は、管理者アカウントによる bot の動作テスト用のチャネルの権限設定の一部で、 everyone role の Read Message 権限が剥奪され、 managers role が付与されている人たちのみこのチャネルを見ることができるようになっています。真ん中のグレーの斜め線は上位からの権限の引き継ぎを表しています。

まとめると、カテゴリやチャネルに

  • everyone など全員が該当する role で読み書き権限を制約
  • 許可したい role ごとに読み書き権限を設定

でプライベートチャネルをつくることができます。

role 付与の自動化

会議体のようなある程度固定した組織運用をする場合は上記のカテゴリ / チャネルの権限設定をして管理者権限でユーザーごとに role を付与すればよいのですが、Self-Made Keyboards in Japan のような多人数のオープンコミュニティではそれを人力で管理するのはだいぶ無理があります。また、厳密なアクセスコントロールが必要なわけではなく、「単純に興味がなくて表示を減らしたい」「一部チャネルについては別途規約に同意してほしい」などといった場合には、ユーザーが制限された範囲内で自由に role を付け外しできる方が好ましいです。

ここからは、そういった role 付与の自動化について説明します。

role 付与を管理する bot を導入する

こういったチャットサービスの自動化といえば bot の導入が手軽です。ですが bot を別途サーバーなどでホストするのはインフラ的にも運用的にもコストが重いです。今回のような role 付与をするぐらいであれば botホスティングを含んだサービスを利用することで導入・運用コストを大幅に引き下げることができます。

今回は Dyno というサービスを利用する方法を紹介します。今やろうとしている role の付与程度なら無料プランで利用できる大変太っ腹なサービスです。

dyno.gg

f:id:biacco42:20200509192506p:plain

Dyno のサイトにアクセスして右上の Add To Server を押して、自身の Discord アカウントに紐付いた サーバー管理権限のある サーバーに bot を追加します。その際 bot に付与する権限を確認されます。

f:id:biacco42:20200509194644p:plain:w300

Administrator や Manage Server 等の強い権限はその権限を必要とする機能を利用しなのであれば外しておいたほうが安全です。今回は role の付与とテキストコマンドの削除等だけできればいいのでかなりの権限を制約しておきました。接続が完了すると Discord サーバーの方に bot が参加して、Dyno のページに自動的に遷移します。

Dyno には非常にたくさんの機能があるのですが、不用意に発動されると困るため今回利用する Custom Command 以外は Module / Command ですべて無効にしておきました。利用したい場合は適宜有効化してください。

f:id:biacco42:20200509195301p:plain

めちゃくちゃたくさんの機能があります。

role を付与する Custom Command を設定する

Dyno には標準で role {user} {role} というコマンドがあるのですが、これを誰でも使えるようにしてしまうと好き勝手に role が振れるようになってしまい大変危険です。そのため Custom Command という機能を利用してこの role コマンドを安全にラップします。

Custom Command は bot が読み取ることができるチャネルで {prefix 文字列}{Command 文字列} と入力すると Response が実行されるものです。prefix 文字列はデフォルトで ? ですが変更することが可能です。

Dyno の管理画面の Custom Command のページを開き Add Command でコマンド追加画面を開きます。

f:id:biacco42:20200509200507p:plain

Command 欄には受け付けるコマンドの文字列を、Response には実行する機能やテキストチャネルに書き込む文字列を記述します。今回の設定では、 #enable-other-category チャネルでのみ ?i-agree-to-the-terms-of-use を入力すると enable-other-category role がオン・オフできるようになっています。

上記画像では

  • Command を i-agree-to-the-terms-of-use
  • Response を {require:#enable-other-category}{!role {user} enable-other-category}

設定しました。前述の通り、なにも制限しなければ bot が読み取り可能なすべてのチャネルでこのコマンドが発行されてしまうため、require によってこのコマンドが利用可能なチャネルを #enable-other-category のみに制約しています。 {!{既存コマンド}} で既存のコマンドを呼び出すことができ、{user} などの事前適宜 Variable によって値を埋めることができるので、 role コマンドをこの Custom Command を発行したユーザー {user}enable-other-category を引数として呼び出しすとことで role の付与のオン・オフを制御します。

このように Custom Command で既存コマンドをラップすることで、動作を制限して安全に任意のユーザーに利用してもらうことが可能になります。

利用できるコマンドはこれ以外にもたくさんあります。詳細については Dyno のドキュメントを確認してみてください。

dyno.gg

このようにして、Dyno を利用することで bot サーバーをホスティングすることなく Discord の role 付与を安全に行うことができます。

以上で role 付与、カテゴリ / チャネルのアクセスコントロールの自動化は完了です。お疲れさまでした。

余談

今回、Self-Made Keyboards in Japan Discord サーバーでは、自作キーボードサーバーというトピックサーバーならではの問題として、オフトピックな話題を「コミュニティ」としてどう扱うかという観点で、OTHER カテゴリというカテゴリを導入し、上記の可視性のコントロールを設定しました。

人間、多様な側面を持っているものですから、キーボードのことで意気投合した人々と、キーボードを離れて単に雑談をしたり他の趣味を共有したりといったコミュニケーションを取りたい、人間関係を構築したい、というのは自然な欲求かと思っています。また、そういった刺激が新たな創発の原動力になりうるとも思っています。一方で、Self-Made Keyboards in Japan はキーボードをメイントピックとしたサーバーなので、そういったチャットはノイズという捉え方も可能です。

それらどちらか一方を取るのではなく、個人の自由と裁量に任せてサーバーが運用できたらという思いで今回のような施策を実施しました。他のサーバーでも似たような問題に悩んでいる管理者の方がいるのではないかと思い、今回この記事を書きました。そのような悩めるサーバー管理者の一助になれば幸いです。

おしまい。

自作キーボード入門用語集 2020

この記事では、自作キーボードを構成する要素とよく聞かれる用語を列挙しながら、2020年に自作キーボードに入門するための基礎知識をできるだけ網羅的に紹介します。

  • 外出自粛もあってお家でできる趣味や娯楽を求められている方
  • 新年度・新生活も 1 ヶ月が経ち会社や学校に馴染んで新しいことを始めようという方
  • 自作キーボードって言葉を最近よく聞くな~と言う方
  • 自作キーボードをはじめようと思っている方
  • 自作キーボードのより深い情報を知りたい方

向けに、今自作キーボードの何が「アツい」のか旬のトレンドも交えつつ、自作キーボードでよく聞かれる用語に簡単な文脈と説明を与えることを目標としています。

目次

以下では、知識が広げられるようポイントとなる用語に Google での検索リンク をつけました。また、その用語の 深度 を ♨ マークの数 (1 ~ 3) で表示しています。♨ が 1 個のものは入門する際に知っているといい用語、♨ が 2 個ないし 3 個のものはより深い高度な用語・トピックになっています。♨ が 2 個以上のものについてはよくわからなければ読み飛ばしてもらって大丈夫です。そもそも量が多いので、全てを理解しようとしなくて大丈夫です。

この記事だけではすべての項目について詳細に解説をつけることはできないですが、自作キーボードについてより深く知りたいとき、わからない用語や概念が出てきたときに、調べるためのきっかけや相互の関連性を整理するための道標として役立てていただければ幸いです。

自作キーボードの構成

まずは自作キーボードの一般的な構成要素から紹介していきます。以下の図は自作キーボードの主要なパーツを表した図で、キーボードによって多少の違いはあるものの、基本的にはどのキーボードでもほぼこういった構成になっています。

f:id:biacco42:20200503204306p:plain
キーボードを構成する基本 5 パーツとキーボードキット。どのようなキーボードでも基本はこういった要素の組み合わせになっています。

特にこの中でも、プレート・基板・ケースの3つをまとめてキーボードキットとして取り扱う事が多く、この記事では ♨キーボードキット♨♨プレート♨♨基板 (PCB)♨ケース♨キースイッチ♨キーキャップ の6種について、それぞれの概要や関連用語、カスタマイズのトレンドを紹介していきます。

キーボードキット

♨キーボードキット は、大きく 2 つのカテゴリに分けられます。一つは、日本でよく見られる電子工作キットに近いスタイルの ♨自作キーボードキット で、もう一つは海外で主流となっている、金属筐体など比較的工業生産的な半完成品のパーツを組み合わせて作る ♨♨カスタムキーボード です。

自作キーボードキット

おそらく、読者の方々が思う自作キーボードのイメージは ♨自作キーボードキット の方で、2016 年頃から日本国内で主流のスタイルとなっています。個人製作のものが多いため、アクリルなどの比較的安価かつ加工がしやすい材料を用いていることが多いです。各製作者から、様々な形状や機能を備えた多様な自作キーボードが次々に登場していることが大きな特徴です。

f:id:biacco42:20200504231944p:plain
Googleで「自作キーボード」で検索すれば個性豊かな様々な形状のキーボードが見つかります。

カスタムキーボード

海外キーボードコミュニティ (♨♨reddit♨♨r/MechanicalKeyboards♨♨geekhack♨♨keebtalk ) などで支持が厚いのが ♨♨カスタムキーボード (Custom Keyboard) です。左右分離型など個性的な形状が多い自作キーボードキットに対して、一般的なキーボードの形状を出発点に、よりよい キーボード体験 を求めるムーブメントです。打鍵感や打鍵音、デザイン、設計者のブランドなどを重視して、アルミや真鍮といった金属を加工した高級なものから、カスタマイズ性とコスパに優れた標準的な設計のものまで幅が広いのが特徴となっています。

打鍵音の心地よさからカスタムキーボードの打鍵動画の人気も高く、そういった動画を多数投稿している ♨♨Taeha Types (Nathan Kim)YouTube チャンネルなどが存在しています。

f:id:biacco42:20200504234222p:plain
多数のカスタムキーボードの組み立て・打鍵動画を投稿している Taeha Types の YouTube チャンネル。自作キーボードキットと異なり、カスタムキーボードはオーソドックスなキー配置のものが大多数を占めています。

www.youtube.com

これらのキーボードキットに共通する主要なパラメータに ♨物理キー配列 が挙げられます。物理キー配列は文字通りキーの物理的な並べ方のことで、キー数による分類、♨左右分離型♨一体型 といったキーボードの形状による分類、♨Row Staggered (一般的なキーボードのように行がずれる配列) のようなキースイッチのレイアウトを示す分類、の 3 つに大別できます。

キー数

自作キーボードではキー数についても従来の常識にとらわれず自由に設計できます。フルキーボード等と呼ばれる US 104 キーボードのキー数である 104 キーを 100 % として、パーセンテージでおおよそのキー数を表現することが慣例となっています。例えば、32キーの ♨Treadstone32 であれば ♨30 % キーボード、といった具合です。また、テンキー部分をなくしたものを ♨♨TKL (TenKey Less)Windowsキーに相当する位置のキーをなくしたものを ♨♨WKL (Win Key Less) と呼ぶなど、特殊な呼び方も存在しています。

形状

特徴的な外見で日本で一躍人気を博したのが ♨左右分離型 です。市販品の左右分離型キーボードもなくはないですがまだまだ珍しいため、左右分離型のキーボードといえば自作キーボード、という向きもあるかと思います。両腕を広げ、胸を開いた自然な姿勢で打鍵できることがメリットとされています。

f:id:biacco42:20200505172304j:plain
左右分割型の Ergo42 Towel。キーキャップは MDA BigBang。

逆に「普通のキーボード」となるのが ♨一体型 です。左右分離型の特徴やメリットを考えると、自作でわざわざ一体型を作るにはメリットは一見なさそうに思えますが、膝の上に置いて使えることや、デスク上を従来のキーボードと同じようにレイアウトできるなどの理由から根強い人気があります。

f:id:biacco42:20200505172007j:plain
一体型である GH60 の互換基板 DZ60 と互換アルミ削り出しケース。キーキャップは JTK Purple on White。

キースイッチのレイアウト

キースイッチのレイアウトでは、通常のキーボードのように行方向 (Row) にずれる様に (Staggered) キーを並べた ♨Row Staggered、人間の手の形に配慮して列方向 (Column) にキーをずらした ♨Column Staggered、縦横をずらさず碁盤の目状にキーを配置した ♨Ortholinear (格子配列)、指の動きに着目して行方向にキーをずらした ♨♨Irregular Row Staggered♨♨Symmetric StaggeredMicrosoft Ergonomic Keyboard に似たレイアウトで近年注目の ♨♨Alice配列 と言われるものなどがあります。

f:id:biacco42:20200505182918p:plain
キーボードのキー数、形状、キー配置とその表現の例。

プレート

キースイッチを固定・安定させるためのパーツが ♨♨プレート で、♨♨マウントプレート と呼ばれることも多いです。打鍵感や打鍵音に影響するパーツで、現状では柔らかいほうが打鍵感が優しく、好まれる傾向にあるようです。

材質としてはアルミや真鍮といった金属の他、アクリルやポリカーボネート、基板の材質である繊維強化プラスチック (FR4) などもよく用いられています。柔らかな打鍵感を高めるために、プレートにスリットを入れたり、部分的にプレートのない部分を作ったり、そもそもプレートを入れない場合もあります。

タクタイルやリニアといったスイッチの特性によって相性のいいプレートが異なるとも言われており、キーボードキットによっては複数の選択肢が用意されていることもあるので、マウントプレートのオプションがあるか確認してみるといいでしょう。

f:id:biacco42:20200505185029j:plain
真鍮製のプレートにスイッチをはめた図。適度な強度と柔らかさでマウントプレートに用いられている場合がある。

基板(PCB)

キースイッチの入力状態を読み取るための回路を印刷した板が ♨♨基板 (PCB) です。スイッチと ♨♨マイコン、USB ポートと ♨♨マイコン をつなぐ回路や LED 用の回路が印刷されており、これに各種電子部品をはんだ付けをします。基板自体のカスタマイズ性はほとんどありませんが、同一の形状の基板でも事前に電子部品がはんだ付けしてある ♨はんだ付け不要のキット も最近では出回っているので、はんだ付けに不安がある場合そういった点に着目して選んでみてもいいかもしれません。

f:id:biacco42:20200505190410j:plain
自作キーボード入門用の 4 キーマクロパッド meishi2 の基板。これに電子部品をはんだ付けしていく。

yushakobo.jp

ケース

♨ケース はキーボードの回路を保護する他、外見としての美しさや打鍵感・打鍵音に影響する重要なパーツです。日本では、簡易的ながら比較的安価で実用十分なアクリル製の ♨♨サンドイッチケース が多くなっています。カスタムキーボードではアルミ削り出しで作られた ♨♨トップマウントケース や、最近採用例が増えている ♨♨♨ガスケットマウントケース、その他にもポリカーボネートや木を削り出したケース、最近では3Dプリントケースなど、非常に多様な形状・材質のものがあります。

f:id:biacco42:20200505210955j:plain
日本の自作キーボードキットに多いアクリルサンドイッチケース。安価でシンプルだが、設計次第では打鍵感も悪くなく個人でも製造可能なため、日本の自作キーボードキットではよく用いられています。

f:id:biacco42:20200505211117j:plain
アルミ削り出しケース。アルミケースは質感がよく加工性もいいためこちらもよくケースに用いられています。

基本的には、重く剛性の高いケースほど打鍵時の不要な振動が抑えられて好まれる傾向で、カスタムキーボードでは ♨♨真鍮の重り がわざわざ追加されていることも多いです。最近ではアクリルや繊維強化プラスチック(FR4)など比較的軽量な ♨♨サンドイッチケース♨♨鉛板 を貼るなどのカスタマイズも登場しています。

また、基本的にはキーボードのケースはキーボードキットに固有ですが ♨♨GH60 と呼ばれるキーボード基板とその互換基板 (♨DZ60 など) は交換用のケースが数多く出回っており、手軽にドレスアップすることが可能になっています。

f:id:biacco42:20200505214159p:plain
GH60 とその互換基板は安価かつ交換用のケースが豊富で、簡単にドレスアップできるのが魅力。

Casekbdfans.com

キースイッチ

自作キーボードにおいて打鍵感を左右する非常に重要なパーツが ♨キースイッチ です。もともと「赤軸」など軸の色で種類が呼び分けられていたことから ♨軸 とも呼ばれます。現在主流となっているのは、メカニカルキースイッチのデファクトスタンダードとなっている ♨Cherry MX シリーズの互換製品と、薄型スイッチである ♨Kailh Low Profileスイッチ、通称 Choc スイッチです。

カニカルキースイッチといえば、長らく Cherry MX の赤・茶・青・黒の打鍵感の異なる 4 種類の軸が主流でしたが、最近では中国メーカーを中心に Cherry MX 互換のキースイッチだけでも 100 種類近いものが販売されており、以前とは状況が大きく異なってきています。ここでは、これだけ多くのスイッチを分類するためのパラメータである、♨♨キーの感触 (フォースカーブ)♨荷重♨静音♨♨♨なめらかさ の 4 つについて簡単に紹介していきます。

キーの感触(フォースカーブ)

キースイッチの主要なパラメータとしてキーの感触があり、大きく ♨リニア♨タクタイルクリッキー の 3 種類に分けることができます。

♨リニア はスイッチを押し込む際に引っかかりがなく、他にないなめらかな打鍵感が特徴となっています。名前の通りスイッチを押し込む際の ♨荷重 (スイッチを押すのに必要な力) の変化が一定で、入力される点 (♨♨作動点) での荷重変化がなくフィードバックがない、他のスイッチにはないメカニカルキーボード特有の感触となっています。Cherry MX でいうと赤軸や黒軸が相当します。

対して ♨タクタイル は作動点付近で荷重が一旦重くなった後一気に軽くなって「スコン」と落ちるような感触 (バンプ) が特徴的なスイッチです。打鍵時に明確に押したことがわかるフィードバックがあり、とっつきやすいスイッチになっています。Cherry MX でいうと茶軸が相当します。

♨クリッキー はタクタイル同様にバンプを持ち、バンプを超えると同時にカチッというクリック音が鳴ることが特徴のキースイッチです。キーの感触だけでなく、音によるフィードバックもあるため「入力している感」が強くたのしいスイッチですが、かなりうるさいためオフィスや学校などで使用する際には周りの人にも気を配ったほうがいいでしょう。

これらの感触について定量的に表現したものが「フォースカーブ」で、キースイッチによってはデータが公開されています。横軸はスイッチを押し込んだ距離、縦軸は押し返してくる力 = 荷重です。キースイッチのフォースカーブには向きがあり、右向き矢印が押し込むとき、左向き矢印が離すときを表しています。下のグラフを見ると、リニアの変化の少ない押し心地やタクタイルの「バンプ」を目に見ることができるかと思います。

f:id:biacco42:20200505222240p:plain
リニア、タクタイル、クリッキーそれぞれのフォースカーブ。横軸はスイッチを押し込んだ距離、縦軸は押し返してくる力 = 荷重を表しています。

荷重

キースイッチが入力される ♨♨作動点、または ♨♨底打ち に必要な押し込む力が ♨荷重 で、通常 g (グラム) ないし cN (センチニュートン) で表されます (ものすごく大雑把に言えば g と cN はだいたい同じと思っていいです (あくまでだいたいです))。荷重はキーの感触とは独立で、リニアでもタクタイルでもクリッキーでも、軽いスイッチから重いスイッチまで様々なバリエーションが存在し、「リニアの60g」「タクタイルの45g」のように表現します。荷重は主にスイッチ内のスプリングの強さで決まり、作動点で 45 g 程度、底打ちで 60 g 程度が標準的とされているようです。海外では比較的重め (作動点で 45 ~ 60 g 程度) のものが好まれる傾向にありますが、日本国内では 45 g より軽い 35 g や、人によっては 25 g など非常に軽量なスイッチを好む人もいます。

最近ではこのスプリングにも様々な種類が登場し、スプリング単体での購入も可能となってきました。押し込む距離が長くなると荷重が大きくなる ♨♨♨プログレッシブスプリング や、押し始めから底打ちまで荷重変化が少ない ♨♨♨スロースプリング など、新しい選択肢が増えてきています。

f:id:biacco42:20200505223054j:plain
最近ではキースイッチの交換用スプリングも比較的容易に入手可能になりました。写真は底打ち 62 g のスロースプリングで、押下開始時点から荷重負荷が大きく、底打ちまでの荷重変化が小さいため安定した打鍵感になりやすいです。

静音

通常のメカニカルスイッチはプラスチック部品がぶつかるため少なからず「カチャカチャ」といった音が出ます。それに対して、プラスチックがぶつかる部分にゴムなどを配置した ♨静音スイッチ というバリエーションが存在する場合があります。音が気になる場合には検討してみるとよいでしょう。ただし、ゴムパーツによって底打ちの打鍵感などに影響があるため、実際に触ってみて検討をしたほうがいいかもしれません。クリッキースイッチはもともと意図的に音を出しているため、静音スイッチというものはありません。

なめらかさ

これらのキースイッチに関する最新の、そして最も注目されている要素がなめらかさです。近年非常に多くのスイッチがリリースされている理由の一つが、このなめらかさの追求と言っても過言ではありません。一見どれも同じように見える Cherry MX 互換スイッチですが、メーカーによって設計や部品選定の違い、製造時期による品質のばらつきなどによって打鍵感に差がでてきます。一般に、摩擦感や引っ掛かりのないなめらかな打鍵感がよいとされており、悪い打鍵感のことを「このスイッチはガサガサする」などと表現することがあります。最高の打鍵感を求めて日々なめらかなキースイッチが研究されていますが、なめらかさはスイッチの種別だけでなく複数のパラメーターが絡み合っているため簡単には説明できないことも多いです。

最近では ♨♨♨金型の状態♨♨♨プラスチック素材♨♨潤滑 (ルブ)♨♨♨スイッチパーツの組み合わせ (キメラ)♨♨ガタツキを減らすフィルムの追加 (スイッチフィルム) など、スイッチのなめらかさとそのカスタマイズについて日夜議論がなされています。

なめらかなスイッチとして評価が高いものを一部例示しておくと、比較的高価なスイッチを製造販売する ♨♨ZealPC♨Tealios や、♨Gateron♨Ink シリーズなどが挙げられます。また、最近ではキースイッチを分解して潤滑剤を塗布することで動作をなめらかにする ♨♨潤滑 (ルブ) も非常に人気が高まっており、以前は入手困難だった潤滑剤や、ルブのためにキースイッチを開けるための ♨♨スイッチオープナー♨♨ルブステーション など、ルブに関する様々な製品が登場してきています。

f:id:biacco42:20200505224354j:plain
キースイッチの潤滑 (ルブ) は最近環境が充実してきており Krytox 205 や Tribosys 3203 といった高性能な潤滑剤が入手できるようになってきています。

キーキャップ

キーボードの顔とも言える存在が ♨キーキャップ です。自作キーボードでよく用いられる Cherry MX 互換スイッチはキーキャップを取り付けるための軸部分が共通になっており、多くの個性的なキーキャップを取り付けられます。キーキャップはキーボードの見た目を大きく変えることもさることながら、指が直接触れる場所でもあるため打鍵感にも大きな影響がある重要なパーツとなっています。

キーキャップの材質としては、♨ABS 樹脂♨PBT 樹脂 がよく使われます。ABS 樹脂は発色が鮮やかで加工性がよく二色成形に向くなど見た目に優れた性質を持ちますが、耐久性が低く摩耗してテカテカになりやすい欠点があります。PBT 樹脂は逆に高い耐久性・耐摩耗性をもち美観が保たれやすいですが、加工性が低く高価格になりがちなところがあります。それぞれ手触りや打鍵音に影響し、ABS はツルツルした手触りのものが多く軽めの打鍵音で、PBT はサラサラとしたマットな手触りのものが多く「コトコト」といったやや落ち着いた打鍵音になりやすいのが特徴です。

また、形状として ♨♨DSA♨♨DCS♨♨SA♨♨Cherry♨♨OEM♨♨XDA♨♨MDA♨♨KAT♨♨KAM … などなど、♨プロファイル と呼ばれる多くの分類があります。行ごとに階段のような段差のあるもの (♨ステップスカルプチャ Step Sculpture)、指が触れる面が円筒状にへこんでいるもの (♨シリンドリカル Cylindrical) や球形にへこんでいるもの (♨スフェリカル Spherical)、キートップの面積の広さ、キーキャップの高さなどに特徴があり、それぞれのプロファイルが各々に特色を打ち出しています。指の触覚は想像以上に正確なので、同じキーボード、キースイッチだとしても、プロファイルを変えることで様々な変化が楽しめるポイントです。

f:id:biacco42:20200508003809p:plain
キーキャップの形状による分類。DSA や XDA といったフラットなプロファイルは場所を選ばないため変則的なキー配置になりがちな自作キーボードキットでも扱いやすいです。一方でステップスカルプチャを持つキーキャップにも根強い人気があり、打鍵音を重視する場合には背の高い SA プロファイルのキーキャップなども好まれます。

これらのキーキャップは現在、通常の販売のほか ♨♨Group Buy と呼ばれる共同購入で期間限定・数量限定で生産されることが多く、好みのデザインのものを逃してしまうと入手不可能になってしまう場合もあり注意が必要です。Group Buy を通して高品質なキーキャップを製造しているメーカーとしては ♨♨GMK♨♨Signature Plastics (SP) が、Group Buy の ♨♨プロキシ販売サイト (代理販売) としては ♨♨zFrontier♨遊舎工房 が、Group Buy の企画・告知環境としては ♨♨geekhack 等のコミュニティサイトがそれぞれ有名です。

まとめ

自作キーボードを構成する要素とよく聞かれる用語を、その構成をなぞりながら駆け足で紹介しました。近年盛り上がっているトピックも盛り込んだため、一読しただけではすべての情報を追いかけることは難しいと思いますが、ぜひ自分が気になるトピックを見つけて、2020 年の自作キーボード入門のきっかけ、また新たな領域の開拓の道標として活用していただければ幸いです。

おしまい。

Windows 環境で Blue Yeti のマイクのモニタ (入力フィードバック) が聞こえない場合の設定 / モニタをオフにする方法

問題 - モニタ音声 (フィードバック音声) が聞こえない

USB 接続で高音質な音声入出力が可能な Blue Yeti Microphone。3.5 mm のステレオミニジャックがついており、PC 等の出力デバイスの音を聞きながら 遅延ゼロで入力音声をミックスしてモニタリングできる機能 が非常に便利な使い勝手のいいマイクで、podcast をオンラインで収録したり YouTube 配信したりゲームのボイスチャットに利用したりされています。

↓後継機種

Blue Yeti で検索すると一般的にこの 入力音声をモニタリングする機能はオフにできない と書いてあります。ですが、Windows で場合によってはこのモニタリング音声が聞こえない場合があります。ヘッドホンをした状態で自分の声がフィードバックされないとしゃべるのが非常に困難で問題になります。

解決法 (or Blue Yeti のモニタ音声をオン / オフにする方法)

ポイントは 録音デバイスではなく再生デバイスのプロパティを操作すること です。録音デバイスの入力レベルを操作しても モニタの音量は変更できません。

f:id:biacco42:20200420010838p:plain
1. タスクトレイのサウンドインジケータを右クリックしてサウンドの設定を開く

f:id:biacco42:20200420011001p:plain
2. サウンド コントロール パネル を開く

f:id:biacco42:20200420011347p:plain
3. 再生デバイス Yeti Stereo Microphone のプロパティを開く

f:id:biacco42:20200420011611p:plain
4. レベルタブを開くとマイクの項目があるのでここでモニタの音量調節が可能

以上です。お疲れさまでした。

型クラスの原点 How to make ad-hoc polymorphism less ad hoc を読んだ話

以下の記事は 2018 年 2 月 に Qiita に投稿した記事の移植です

pixivFANBOX にも投稿しています。ご支援頂ける場合はよろしくお願いいたします。

www.pixiv.net


オレオレ型クラスの話をやめろ。原典に当たれ。ということで、Haskell に型クラスの導入を提案した論文とされる Philip Wadler. How to make ad-hoc polymorphism less ad hoc を読んだのでそれをまとめる話です。

型クラス導入の目的

2 つの多相

多相にはアドホック多相、パラメトリック多相の 2 つの種類がある。

アドホック多相

アドホック多相は、ある関数が受け入れる型によって 型に応じた振る舞いをするような多相 。典型的なのは関数や演算子オーバーロード

3 * 3
3.14 * 3.14

同じ演算子 * だけど、整数型と浮動小数点数型どちらでも使えています。

パラメトリック多相

名前の通り、型を表すパラメータを取ることによって 型によらない共通の振る舞いをする多相 。典型的なのは length 関数。

 length :: [a] -> Int

配列であれば中の型に関係なく使うことができる。

Hindley/Milner 型システムと多相

パラメトリック多相については Hindley/Milner 型システムがよく扱えるということで広く用いられていたけど、アドホック多相については良い手法がなかった(ためアドホック多相が導入されていなかった)。

この論文では、型クラスってものを導入して Hindley/Milner 型システムを拡張することでオーバーロード = アドホック多相ができるようにするよ。

アドホック多相の制約

算術演算の場合

整数型と浮動小数点数型にオーバーロードされた乗算演算子 * を考える。この演算子 *アドホック多相にできるけど、それを利用した関数はアドホック多相にできない。

-- これはできない
square x = x * x

square 3
square 3.14

関数自体を Int -> IntFloat -> Floatオーバーロードさせることもできるけど

squares (x, y, z) = (square x, square y, square z)

みたいな関数を考えるとき、組み合わせが爆発する。

同値評価の場合

同値性を評価する場合にも同じような問題がある。

-- これはできない
member [] y = False
member (x:xs) y = (x == y) \/ member xs y

member [1, 2, 3] 2
member "Haskell" 'k'

そこで同値性についてはパラメトリック多相で対応するアプローチが試された。

(==) :: a -> a -> Bool
member :: [a] -> a -> Bool

ただ、この方法だと関数型や抽象データ型などの同値評価ができない型を渡しても静的に型エラーを起こすことができず、実行時に評価しようとしてエラーすることになってしまった。

そこで同値評価できる型だけに同値性評価できるように制約を書ける方法が Standard ML で導入された。

(==) :: a(==) -> a(==) -> Bool
member :: [a(==)] -> a(==) -> Bool

この方法では、関数型や抽象データ型などを member 関数に渡すと型エラーにすることができるようになったけれど、Standard ML では参照型の同値性評価は他の型の同値性評価と異なるように設計されていたので、同値性評価する型の峻別のためにランタイムシステムを構築する必要が出てきてしまった。

これに対してオブジェクト指向では、データに対して実装を与えるメソッドのポインタの集合 = 辞書 を持っているので、同値性評価関数に渡ってくる引数のいずれかの辞書に含まれる同値性評価のメソッドを用いて評価することができる。

このメソッドの集合をデータと一緒に渡すのがいい感じだから、型クラスでもこのアイディアが背景としてあるよ。

型クラスの導入例

まず、型クラスを実際に導入したコード例を示す。

class Num a where
    (+), (*) :: a -> a -> a
    negate   :: a -> a

instance Num Int where
    (+)    = addInt
    (*)    = mulInt
    negate = negInt

instance Num Float where
    (+)    = addFloat
    (*)    = mulFloat
    negate = negFloat

square            :: Num a => a -> a
square x          = x * x

squares           :: Num a, Num b, Num c => (a, b, c) -> (a, b, c)
squares (x, y, z) = (square x, square y, square z)

型クラスの定義

このコードで型クラス Numclass キーワードによって定義されている。Num 型クラスは (+)(*)negate が定義されている型 a が所属している。

続いて、instance として型 a を具体化した実装を与えている。これらは Num の要求する関数を実装しているか型検査される。たとえば

addInt :: Int -> Int -> Int

が検査される。

型クラス制約の利用

型クラスの導入によって、前述の squaresquares が実装できた!

ここで square の型は Num a => a -> a となっているが、Num a => によって型 aNum が実装されていることが要求されている。この square は次のように利用できる。

square 3
square 3.14

そしてもちろん

square 'x'

Num Charinstance が存在しないからコンパイル時に静的に型エラーにできる!

アドホック多相が実現できていることが確認できる。

型クラスの 変換

実際にはこの型クラスの仕組は、コンパイル時に型クラスを用いない形に変換されて Hindley/Milner 型システムで扱えるようになる。つまりは型クラスはある種の糖衣構文になっている。

上記のコード例を型クラスを用いない形に変換したものが下記のコード。

data NumD a = NumDict (a -> a -> a) (a -> a -> a) (a -> a)
add (NumDict a m n) = a
mul (NumDict a m n) = m
neg (NumDict a m n) = n

numDInt     :: NumD Int
numDint     =  NumDict addInt mulInt negInt

numDFloat   :: NumD Float
numDFloat   =  NumDict addFloat mulFloat negFloat

square'          :: NumD a -> a -> a
square' numDa x  =  mul numDa x x

squares'  :: (NumD a, NumD b, NumD c) -> (a, b, c) -> (a, b, c)
squares' (numDa, numDb, numDc) (x, y, z)
          =  (square' numDa x, square' numDb y, square' numDc z)

class 宣言の代わりに、class で定義されていた関数の型を持つデータ型の辞書 NumDict とそれらのメソッドにアクセスするための関数が定義されている。1

instance で宣言されていた部分は、上記の NumD a 型のデータ型の値として変換される。

numDInt     :: NumD Int
numDInt     =  NumDict addInt mulInt negInt

やってることは単純で、NumDict 値コンストラクタで値を埋めているだけ。Float も同じ。こうやって作った辞書をもとに演算は以下のように書き換えられる。

x + y     -->  add numD x y
x * y     -->  mul numD x y
negate x  -->  neg numD x

ここで引数に渡っている numD は適当な演算の入った辞書。

ここからが型クラスのマジックで、適当な演算の入った辞書が型によって自動的に挿入される!

3 * 3       -->  mul numDInt 3 3
3.14 * 3.14 -->  mul numDFloat 3.14 3.14

これによって型によって処理を切り替えるというアドホック多相が実現された。

square'          :: NumD a -> a -> a
square' numDa x  =  mul numDa x x

square 3     -->  square' numDInt 3
square 3.14  -->  square' numDFloat 3.14

力尽きたので Eq 型クラスからの話(型クラスの継承等)については割愛。変換のルール自体は上記に示されたもの。あとで書くかもしれない。

まとめ

というわけで、型クラスはオブジェクト指向でない静的型付け言語、特に Hindley/Milner 型システムを持った言語において、Hindley/Milner 型システムを用いてアドホック多相を実現する手法として導入されました。その型クラスのお手本になったのは実はオブジェクト指向言語のサブタイピング多相であり、オブジェクト指向における関数ポインタの辞書を文字通り Haskell におけるデータ型の辞書として読み替えて この辞書を型に応じて挿入する というものでした。

型クラスの原典に当たって、これで一安心ですね。

実は、全く同様の実装が Scala で借用されており、Haskell における型クラスが脱糖されたような状態で書けるようになっています。また、最近登場した Swift や Rust でも型クラスと似たようなことができるようになっています。おもしろいですね。

おしまい

参考

Philip Wadler. How to make ad-hoc polymorphism less ad hoc Instances and Dictionaries - School of Haskell


  1. 現在の GHC の実装ではレコードによって一括定義するようにしているらしい data NumD a = NumDict {add :: a -> a -> a, mul :: a -> a -> a, negate :: a -> a} みたいな

【Haskell や圏論が出てこない】Scala で型クラスを完全に理解した話

以下の記事は 2018 年 2 月 に Qiita に投稿した記事の移植です

pixivFANBOX にも投稿しています。ご支援頂ける場合はよろしくお願いいたします。

www.pixiv.net


TL;DR

結論から。

型クラスはちょっとすごいオーバーロード であり 型に対して静的にメソッドを挿入する方法 です。

この記事は

  • Haskell圏論等の難しそうな知識・議論なしで型クラス概念を具体的に理解する
  • 型クラスが実は単なるすごいオーバーロードで、ちょっとした便利なものだと実感できる
  • 型クラスとインターフェイスの違い論争で消耗しなくなる
  • 型クラスを知っているとなにがオトクなのかわかる
  • Haskell 等の高度な抽象化の恩恵を Java 的な慣れ親しんだシンタックスで書ける Scala は便利でたのしいと感じる

ことを目的としています。

対象読者

  • 関数型とか型クラスとかモナドとかよく聞くけど全然わからない…でも理解したい気持ちはある
  • 型クラスの概念はいろいろ読んでなんとなく分かるけど説明が Haskell ばかりで実感がわかない
  • 型クラスがなんなのかはぶっちゃけどうでもいいが、便利なのかどうか、どう便利なのかを知りたい

Scala 自体はわからなくても大丈夫です。

まえおき

最近またにわかに 型クラス が話題になっていました。

モナド圏論同様、Haskell の神秘じみた概念の一つして定期的に話題になっていて、なんだかすごくてかっこよさそうなんだけれど、そもそも Haskell をよく知らないし、理論色の強い解説が多くなかなかスッキリ消化できない方が多いのではないかと思います。私もその一人です。Haskell とかさっぱりです。

しかし 型クラスが本当に素晴らしいものなら Haskell を使わなくても素晴らしいといえるはず ということで、この記事では Pure Scala で型クラスを具体的に理解することを目指します。

"型クラス" とはなにか

最初に、型クラスそのものではなく、"型クラスという誤解を招きやすい名称" について整理したいと思います。JavaC++ 等にはじまる多くのオブジェクト指向言語では クラス は特別なキーワードとして扱われてきました。そして、それは と呼ばれる概念とほぼ一対一対応しています。

この慣習がまず 型クラス という言葉をわかりづらくしてしまっているように思います。そこで一旦、OOPL での型やクラスという概念を忘れて、本来的な言葉の定義を考え直してみます。

けい【型】[漢字項目]の意味 - goo国語辞書 - goo辞書 1. 同形のものをいくつも作るとき元になるもの。いがた。「原型・紙型・母型」 2. 基準となる形。タイプ。「型式/定型・典型・模型・類型」

となっており、鋳型等、同じ形をしたものを作るもの・その枠といった意味です。もうちょっとプログラム的に言えば 一定の決まったデータ構造の約束 と言えそうです。

一方 クラス

Weblio - class (共通の性質を有する)部類、種類、(学校の)クラス、学級、組、(クラスの)授業(時間)、(編物教室などの)講習、クラスの生徒たち、同期卒業生、同年兵

となっており、共通の性質をもった集まり であると言えそうです。

つまり、型クラスとは型のクラス = 似たような性質を持った型の集まり、というふうに読み取ることができ、これは実際型クラスをよく説明する表現になっています。

型クラス = 共通の性質を持った型の集まり

というわけで前置きが長くなりましたが、言葉の意味を明らめたところで実際に型クラスとはどういうもので、型クラスが一体何を実現してくれるのか見ていきたいと思います。

型クラスを見て体験する

まずは、型クラスがどんなコードの見た目をしていて何ができるのかを確認してみます。Scala のコードですが、Scala を知らない人でも雰囲気がつかめれば大丈夫ですので、細かいシンタックスは気にせず見てみてください。

まずは、以下のような仕様の実装について考えます。

sum という List を受け取ってその合計を返す関数を定義したいとします。この際、sum が引数に受け取る型は、ただ 1 つの List[Int] などではなく、合計という概念が適用できる型 を幅広く受け取れるようにしたいでしょう。このように多くの型を受け取れることを「多相」と呼びます。

言い換えると、合計という概念が適用できるある型 A を要素に持つ List[A] を引数に取る多相なメソッド sum を作りたい、です。ここで、型 A は拡張できない or 拡張したくない既存の型とします。

ここでいったん合計という概念の詳細は考えずに、まずは IntString の2つに対応してみましょう。自然に実装できるでしょうか?

sum メソッドのナイーブな実装

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}

def sum(xs: List[String]): String = xs match {
  case Nil => ""
  case head :: tail => head + sum(tail)
}

sum(List(1, 2, 3))        // => 6
sum(List("a", "b", "c"))  // => "abc"
sum(List(true, false))    // Kaboom! Can't compile

型クラスがでてくるかと思いきや拍子抜けしたかもしれませんが、実はオーバーロード / アドホック多相で上記の仕様を満たすことができます。簡単にコードの説明をすると

  1. 引数の List を match (switch のようなものです) で Nil と それ以外で場合分け
  2. Nil だったら受け付ける型のデフォルト値を返す
  3. List が要素を持つならその先頭の値 head と List の残りの要素 tail の総和を足す

ということをやっています。

オーバーロードの引数の型として、合計という概念が適用できる型を列挙し、それぞれの実装を与えることで自然に型にそった処理を実現できています。

この実装によって明らかになった性質がいくつかあります。

  • 合計という概念が適用できる型を明に列挙することで、それ以外の型で利用しようとした場合は静的にコンパイルエラーにすることができる
  • オーバーロードの制約となる型は既存の型であり、既存の型を修正したり拡張したりせずに新たに振る舞いを追加できる
  • 合計というセマンティクスは共通だが、型ごとに実装は異なる

同時に、この実装にはいくつか問題を指摘することができるかと思います。

まず、sum(xs: List[Int]): Intsum(xs: List[String]): String でほとんど実装が重複してしまっています。DRY じゃないですね。

また、今の sumIntString のみにしか対応できていません。もちろんこれら以外の合計が適用できる型には対応できていないですし、仮に多くの型のオーバーロードを用意したとしても、 sum の利用者が追加する独自の型に対応することはできません。つまり、拡張性・再利用性が無い状態になってしまっています。

これらの問題を解決して拡張性を得るためには、合計という概念が適用できる型 をきちんと取り扱う必要がありそうです。

そこで少し今の sum の実装を見直してみます。問題点として挙げた 実装の重複 という観点から、IntString で共通な部分と異なる部分を抽出してみます。

共通な部分は

  • リストの要素の たぐり方
  • リストが Nil だったときには、引数の型のデフォルト値を返す
  • 引数の型同士の加法を利用した合計の実装

異なる部分は

  • IntString それぞれのデフォルト値
  • IntString それぞれの加法の具体的な実装

です。

ここから List に内包される 合計という概念が適用できる型 の条件として 加算が定義できる型 であることが言えそうです。このような加算が定義できる型を Addable な型と呼ぶことにしましょう。そうすると、sum メソッドは Addable な性質を定義できる型の集合 を要素として持つ List を受け入れたいと言えます。

それができる すごいオーバーロード型クラス ということになります。

早速上記の仕様の型クラスでの実装をみてみましょう。

sum メソッドの型クラスでの実装

def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match {
  case Nil => addable.unit
  case head :: tail => addable.add(head, sum(tail))
}

trait Addable[A] {
  val unit: A
  def add(x: A, y: A): A
}

implicit object AddableInt extends Addable[Int] {
  override val unit: Int = 0
  override def add(x: Int, y: Int): Int = x + y
}

implicit object AddableString extends Addable[String] {
  override val unit: String = ""
  override def add(x: String, y: String): String = x + y
}

sum(List(1, 2, 3))        // => 6
sum(List("a", "b", "c"))  // => "abc"
sum(List(true, false))    // Kaboom! Can't compile

Scala を知らない方は traitobjectimplicit 等の見慣れないキーワードが出てきてちょっと混乱するかもしれませんが、落ち着いてオーバーロードの実装と見比べてみると概形が見えてくるのではないかと思います。Scala の説明もしながら、順を追って見ていきます。

まず sum メソッドの実装を 1 ヶ所に集中するために、型パラメータ A を導入しました。

def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match {
  case Nil => addable.unit
  case head :: tail => addable.add(head, sum(tail))
}

早速いくつか Scala 特有の記法があるため簡単に補足します。 def はメソッドを定義するキーワードで、def hoge(): Piyo のような形で使います。最後の Piyo が戻り値の型です。Scala ではメソッドのパラメータを複数の パラメータリスト として記述することができます。なので () が 2 つありますが、単に引数が並んでいるだけだと思ってもらって大丈夫です1。2 個目のパラメータリストにあるキーワード implicit は型クラスを実現する重要な機能で、オーバーロードと同様に 型情報を元にコンパイラが暗黙的に挿入すべき値を解決します。つまり、Addable[A] の型を見て自動的にその型に適合した addable が挿入されます。型によって実際に呼び出すメソッドが決まるオーバーロードそっくりですね。

ここで、あらためて sum の型 A に関する実装について、共通の部分と異なる部分を再確認してみます。

共通な部分は

  • リストの要素の たぐり方
  • リストが Nil だったときには型 A のデフォルト値を返す
  • A 同士の加法を利用した合計の実装

異なる部分は

  • A のデフォルト値
  • A の加法の実装

です。

sum[A](xs: List[A])(implicit addable: Addable[A]): A メソッドには共通の List のたぐり方が実装されています。対して型 A に共通の振る舞いが sum の次の Addable[A] に定義されています。

trait Addable[A] {
  val unit: A
  def add(x: A, y: A): A
}

trait というのは実装の持てる interface のようなものです。ここには見ての通り、ある型 A のデフォルト値が unit という変数で表され、加法が add(x: A, y: A): A というシグネチャで宣言されています。ここで宣言した Addable = 加算が定義できる 性質が、合計という概念を適用できる型の条件でした。

さて、ここまでで sum に共通な要素を記述することができました。ここからはオーバーロード同様に、合計という概念が適用できる型それぞれについて Addable の実装を与えていきましょう。

implicit object AddableInt extends Addable[Int] {
  override val unit: Int = 0
  override def add(x: Int, y: Int): Int = x + y
}

implicit object AddableString extends Addable[String] {
  override val unit: String = ""
  override def add(x: String, y: String): String = x + y
}

またしても implicit キーワードが出てきましたが、ここまでくればもう分かる通り、この implicitsum メソッドの引数における implicit とこの実装を紐付けるための目印になっています。オーバーロードでは、共通のメソッド名で異なる引数型であれば自動的にオーバーロードであることがコンパイラに伝わりますが、implicit を利用する場合にはコンパイラに明示的に教えてあげる必要があります。

object というのは シングルトンオブジェクト のことで、Scala では言語組み込みの仕様になっています。object は定義と同時に型の名前でアクセスできる static なクラスのようなものだと思ってください。このシングルトンオブジェクトで、IntString それぞれの型に固有の処理を実装として与えています。

さて、一通り眺めてきましたが、いまここを読んでいる方は上記のシングルトンオブジェクトの実装がオーバーロードのように見えてきているのではないでしょうか?もう一度、実際に並べて比べてみます。

オーバーロードと型クラスの比較

オーバーロード

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}

def sum(xs: List[String]): String = xs match {
  case Nil => ""
  case head :: tail => head + sum(tail)
}

型クラス

def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match {
  case Nil => addable.unit
  case head :: tail => addable.add(head, sum(tail))
}

trait Addable[A] {
  val unit: A
  def add(x: A, y: A): A
}

implicit object AddableInt extends Addable[Int] {
  override val unit: Int = 0
  override def add(x: Int, y: Int): Int = x + y
}

implicit object AddableString extends Addable[String] {
  override val unit: String = ""
  override def add(x: String, y: String): String = x + y
}

型クラスのほうがちょっと実装が長くなってしまっていますが、オーバーロードの実装で問題の一つだった重複した実装が、ジェネリックsum メソッドによって一本化できています。

さらに、オーバーロードのもう一つの問題点だった拡張性についても、型クラスでは改善しています。今、合計という概念が適用できる型として Double を発見し、Double についても sum メソッドが利用したくなったとします。この時、sum の実装については手を加えずに、sum を拡張することができます。

implicit object AddableDouble extends Addable[Double] {
  override val unit: Double = 0.0
  override def add(x: Double, y: Double): Double = x + y
}

sum(List(1.0, 2.0, 3.0)) // => 6.0

さらにさらに、自作の有理数を扱う Rational クラスをつくったとしましょう。Rational クラスには Rational 同士を加算する add メソッドが定義されているとします。この時、sum の実装については手を加えずに

implicit object AddableRational extends Addable[Rational] {
  override val unit: Rational = Rational(0, 1)
  override def add(x: Rational, y: Rational): Rational = x.add(y)
}

sum(List(Rational(1, 2), Rational(1, 3), Rational(1, 4))) // => 13/12

気持ちよくなってきた。

というわけで型クラスを見てきましたが、もう型クラスについては怖くなくて、オーバーロードのようにすっかり手に馴染むようになってきたのではないでしょうか?

型クラスとは

ここで、型クラスの性質をまとめてみます。

  • あるジェネリックなメソッドが 受け入れられる型の集まり を型クラスの実装として明に列挙することで、それ以外の型では静的にコンパイルエラーにすることができる(型クラス制約)
  • 型クラス制約を与える型は既存の型でよく、既存の型を修正したり拡張したりせずに新たに振る舞いを追加できる
  • 型クラス制約を受ける型の振る舞いは共通だが、型ごとに実装は異なる
  • 既存の型クラスに適合させたい型が増えた場合には、その型クラスの実装を提供するだけでよい(たとえば sum メソッドに手を入れる必要がない)

これは前述のオーバーロードの満たす性質と近く、より抽象的な / 拡張に対して開いたものになっています。特に、ライブラリ実装者が型クラスを用いることで、利用者はライブラリによってなされた実装に対して手を加えることなくそれを拡張することができます。

実際、今回扱った sum メソッドでは DoubleRational といったすでに存在する一般の型について、sum メソッドだけでなく型そのものについても直接変更を加えず、後付で Addable な性質を実装することができました。

これらが、型クラスが便利で、コードをより抽象的で整理されたものにしてくれるといわれる所以ではないかと思います。

そもそも実は、型クラスという仕組み自体が この記事で名前の出せないあの言語 で一般性の高いアドホック多相 / オーバーロードを実現するための仕組みとして導入されました。これについては別記事で扱っていますが、型クラスがちょっとすごいオーバーロード というのは言い過ぎではないはずです。

型クラスがわかってきたところで、よくある 型クラスとインターフェースの違い について考えてみます。

型クラスとインターフェースとの違い

型クラスは、ある型に対するメソッドの存在を約束するという点でインターフェースと似ており、よく比較されています。実際、Scala の型クラスの実装でも trait Addable[A] のようなかたちで、trait による抽象化が行われていました。

では、インターフェース(トレイト)と型クラスではなにが違うのか、sumAddable の実装についてインターフェースを用いたサブタイプ多相による実装を考えてみます。

Addable[A] インターフェースを継承したクラスの値を扱う sum

trait Addable[A] {
  val value: A
  def add(x: Addable[A]): Addable[A]
}

class AddableInt(override val value: Int) extends Addable[Int] {
  override def add(x: Addable[Int]): Addable[Int] = new AddableInt(value + x.value)
}

def sum[T](xs: List[Addable[T]]): Addable[T] = xs match {
  case Nil => null
  case head :: Nil => head
  case head :: tail => head.add(sum(tail))
}

sum(List(1, 2, 3).map{ new AddableInt(_) }).value // => 6

渡された List が空の場合はデフォルト値が取得できないので null を返してしまったり、そういう設計のため addsum の引数・戻り型が A でなく Addable[A] になってしまっていたり、 List[Int] を包み直したりなかなか難儀な感じになってしまいましたが、ほぼ似たような実装ができています。が、型に紐づく処理を挿入するためだけにすべての値をインターフェースに適合した型にラップしており無駄が多いし素直じゃないですね。

Java だとこういうときは ストラテジーパターン が自然かもしれません。

AddStrategy[A] インターフェースをストラテジーとする sum

def sum[A](xs: List[A])(addStrategy: AddStrategy[A]): A = xs match {
  case Nil => addStrategy.unit
  case head :: tail => addStrategy.add(head, sum(tail))
}

trait AddStrategy[A] {
  val unit: A
  def add(x: A, y: A): A
}

object AddStrategyInt extends AddStrategy[Int] {
  override val unit: Int = 0
  override def add(x: Int, y: Int): Int = x + y
}

sum(List(1, 2, 3))(AddStrategyInt) // => 6

もうおわかりだと思いますが、これはほぼ完全に型クラスによる実装と同じです。implicit による自動挿入だけがない状態になっています。

インターフェース・サブタイピング、Scala でできないこと

インターフェースの実装でできていなこと、ひいては Scala でできないことは、結局のところジェネリックなメソッドの型パラメータ A からスタティックなメソッドが呼べないことです。(既存の型 A に対して unit 等の拡張ができないというのもありますが...2)

// これはできない
def sum[A](xs: List[A]): A = xs match {
  case Nil => A.unit
  case head :: tail => A.add(head, sum(tail))
}

Scala は pure な OOP を標榜しており、method は常にそれの所属するインスタンスを必要としますScalastatic キーワードがなく、実質的な static として機能するのがシングルトンオブジェクトである object であることからも、Scala がメソッドの呼び出しに常にインスタンスを必要とするオブジェクト指向を目指していることがわかります。

そこで、型に紐付いた static なメソッドを呼び出すためのプレースホルダimplicit object だったわけです。上記の A と下記の addable: Addable[A] が対応していることがわかるかと思います。

// 型クラスによる A の addable への置き換え
def sum[A](xs: List[A])(implicit addable: Addable[A]): A = xs match {
  case Nil => addable.unit
  case head :: tail => addable.add(head, sum(tail))
}

型クラスとインターフェースの違いまとめ

まとめると、インターフェースと型クラスは似ているようですが、そもそも目的の違うもので比べるものではない ということです。

インターフェースはそれを継承したものにその 実装を強制する仕組み です。この仕組自体は型クラスの実装でも用いられていました。

一方型クラスは、その約束された実装を 型情報に基づいて自動的に挿入する 仕組みでした。型クラスを実現するためには挿入する実装を約束するために、インターフェースが必要 でした。

型クラスとインターフェースは似たような性質を見かけ上持っているため比較されたりしていますが、そもそも対置して比較できるものではなく、むしろ型クラスがインターフェースに依存している、協調的な関係になっています。

インターフェースでの実装でも示した通り、インターフェース自体は様々な使い方ができ、ポイントだったのは 型に対する実装をデータ型と同じ場所に置くか、違う場所に置いて適宜挿入するか ということでした。

この、型に対する実装をインターフェースで約束して、データ型と違う場所に置いて自動的に挿入してくれる仕組みこそが型クラスの正体でした。

その点でも、型クラスが アドホック多相・オーバーロードのための仕組み = 型情報に基づいて自動的に実装を挿入する仕組み であるということが納得できるかと思います。

長々とやってきましたが、最後に型クラスを知っているとなにが嬉しいかを紹介して終わりにしようと思います。

おまけ - なぜ型クラスとインターフェースが混同されてしまったか

おそらく、型クラスの原点である Haskell において、型クラスがインターフェースの仕組みを暗黙に内包していることが原因ではないかと思います。

前述の通り、型クラスは仕組み上、実装の約束をするためのインターフェースを必ず必要とします。しかし、Haskellオブジェクト指向な言語ではないため、そもそもインターフェースというオブジェクト指向の仕組みを持ちませんでした。継承とかないですからね。

そのため、型クラス導入の際に、型クラスを実現するキーワード class が実質的にインターフェースの部分を担うことになりました。

このことが型クラスとインターフェースが混同されて議論される原因の一端ではないかと思っています。おまけおわり。

型クラスを理解すると嬉しいこと

型クラスを理解しているとなにが嬉しいでしょうか。個人的には、ライブラリやフレームワーク等の抽象的なコードを書く側でない限り 型クラスによる実装を書く機会はめったにない と思います。

では、型クラスの知識はムダなのかというとそんなことはありません。ライブラリを作成する際はもちろん、 ライブラリを利用する・コードを読む際にとても役立ちます

Scala のライブラリでは、標準ライブラリも含めて型クラスによる実装が様々なところに見つけられます。Scala のコレクションのように 高階型 を伴う実装や Json パーサー等の型によって異なる実装を挿入したい場合において型クラスは非常によく用いられているため、それらを利用する際に型クラスの知識があると シグネチャから実装の意図が容易に汲み取れます

たとえば、ScalaJson パーサーの 1 つである play-jsonJson.fromJson メソッドのシグネチャを見てみます。

def fromJson[T](json: JsValue)(implicit fjs: Reads[T]): JsResult[T]

型クラスの知識があると、ドキュメントがなくても Reads[T] 型の意図するところ、求められる実装がこのシグネチャからわかるのではないでしょうか?実際にこのメソッドを使うコードを示します。

case class Cat(name: String, age: Int)
object Cat {
  implicit val jsonReads: Reads[Cat] = Json.reads[Cat] // 型クラスの実装の提供
}

val json = Json.parse(
  """{
    "name" : "Tama",
    "age" : 4
  }"""
)

val cat = Json.fromJson[Cat](json)  // ここに implicit に jsonReads が渡されている
// --> val cat = Json.fromJson[Cat](json)(jsonReads) と同じ

Json.reads[Cat] は実はマクロで、Cat 型の実装から JsonCat 型にパースする Reads[Cat]インスタンスを生成してくれます。Cat 型に特殊化した実装を型クラスのインスタンスとして提供しているわけですね。

このように、ユーザー側で定義した型について、型クラスを利用することで型固有の処理をあとから利用者が差し込めるようになっています。型の定義場所にその型のパーサを implicit に提供することで、実際に Json をパースするところでは型に固有のパーサーを意識しなくて済むようになっています。

というわけで、型クラスを知っていると利用者としてもメリットが有ることがわかったかと思います。自分で積極的に使わなくても、コードを読む際に重宝するという点で、Scala におけるデザインパターンの一つとして捉えても差し支えないでしょう。

どうでしょうか?型クラスがどんなものか具体的に理解できたでしょうか。

本当に型クラスの指すものがこれであっているのか気になる、型クラスの定義自体を知りたい、という場合は型クラスの原点 How to make ad-hoc polymorphism less ad hoc を読んだ話も参照してみてください。

型クラス完全に理解した。

補足1 - sum メソッドと fold

今回の sum メソッドの実装は、デフォルト値と加算という処理を使って List の値を走査して単一の戻り値を作っていました。これについて、関数型と呼ばれる言語を扱ったことがある人は fold が思いついたのではないかと思います。まさに foldsum のようなリスト構造をたぐる処理の一般的表現になっています(より一般性があるのは foldLeft / foldRight ですがここでは簡単のため fold とします)。

def fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1

Scala のコレクションの根幹になっている GenTraversableOnce[+A] にこの定義がありますが、これはデフォルト値 z と、 A1 型の 2 つの値を取って A1 型の値を返す関数オブジェクトを引数にとっており、それぞれが Addableunitadd に相当してることがわかると思います。fold を使って sum と同様の実装を与えてみます。

List(1, 2, 3).fold(0){ (acc, i) => acc + i }
List(1, 2, 3).fold(0){_ + _}
List(1, 2, 3).fold(AddableInt.unit){ AddableInt.add }

2 行目は省略記法で、1 行目と意味するところは同じです。fold では型クラスやオーバーロードよりさらに柔軟に、利用するその場で処理を記述できるようになっています。これは関数を一級市民として扱うことで、関数型という型 の宣言ができるようになり、それがインターフェースとして機能しているわかりやすい例だと思います。3 行目はわかりやすさのため、という名の悪ふざけです。

fold に対して型クラスが優れている点は、型に応じて自動的に処理が挿入される点ですが、概ねの場合はこういった関数オブジェクトを渡すことでなんとかなるのではないかと実は思っています。実際、ここで挿入される関数オブジェクトは List(1, 2, 3) から推論される型 Int により (Int, Int) => Int であることが静的に解決されるので、型的に安全になっています。

補足2 - Context bound な型クラス制約の書き方

Scala の型クラスについてはもうちょっとオシャレに書く方法が用意されています。Context bound というやつです。

def sum[A: Addable](xs: List[A]): A = xs match {
  case Nil => implicitly[Addable[A]].unit
  case head :: tail => implicitly[Addable[A]].add(head, sum(tail))
}

引数として渡されていた (implicit addable: Addable[A]) をなくして、implicitly[Addable[A]] というかたちで Addable[A] の実態を参照できるようになりました。おしゃれではあるのですが、見た目が直感的じゃないので賛否両論かもしれません。

参考

Martin Odersky. Poor man's Type Classes Philip Wadler. How to make ad-hoc polymorphism less ad hoc JSON automated mapping


  1. カリー化などで調べると説明があるかと思います

  2. implicit を用いた暗黙の型変換(Pimp my liblary パターンとも呼ばれるらしい)で既存型に対して拡張は可能ですが、わかりづらさ等の理由で最近はあまり推奨されていないようです。

Scala の implicit parameter のスコープと左辺値型推論で困った話

以下の記事は 2016 年 7 月 に Qiita に投稿した記事の移植です

pixivFANBOX にも投稿しています。ご支援頂ける場合はよろしくお願いいたします。

www.pixiv.net


Scala の特徴的言語仕様の一つであり、憎まれたり便利だったりする implicit の小ネタです。いろいろ検証過程を書いてるので、結論だけ知りたい人は最後を見てね。

便利な implicit parameter

implicit には何種類かあるのはご存知という前提で。implicit parameter 便利ですよね。

implicit val g = "Hello "

def hoge(name: String)(implicit greet: String) {
  println(greet + name)
}

hoge("Biacco") // 2番めのパラメタ群については implicit に渡される

ただ、この implicit が解決されるスコープと型推論が組み合わさるとややこしいことになるようです。

implicit parameter の解決スコープと型推論がうまく動かない例

今回ハマったのはこれ。

object Main extends App{
    import Hoge._
    val h = new Hoge
    h.hoge
}

class Hoge {
    def hoge(implicit s: String) { println(s) }
}

object Hoge {
    implicit val hs = "hoge!"
}

一見動きそう。でもコンパイルすると

error: could not find implicit value for parameter s: String

なるほどわからん

検証1: スコープを確認する

スコープに正しく入ってないのかな?と思って以下を試す。

object Main extends App{
    import Hoge._
    val h = new Hoge
    h.hoge(hs) // 明示的に hs を渡してみた
}

class Hoge {
    def hoge(implicit s: String) { println(s) }
}

object Hoge {
    implicit val hs = "hoge!"
}

動くんですよねこれが。コンパイル通ります。おっかしいな〜〜〜〜〜〜〜〜〜〜

検証2: ローカルスコープにおいてみる

というわけで、スコープに存在しないわけではないというのがわかった。じゃあ間違いなくアクセスできるということで試しにローカルスコープにおいてみる。

object Main extends App{
    import Hoge._
    
    implicit val hs = "hoge!"
    
    val h = new Hoge
    h.hoge
}

class Hoge {
    def hoge(implicit s: String) { println(s) }
}

object Hoge {
}

これもOK. この辺でかんべんしてくれという気持ちになり始める。

問題の定義: implicit な変数がスコープ内にいるにもかかわらず implicit parameter で解決されない

というわけで、現在の問題は

  • implicit な変数はスコープ内にいる
  • implicit parameter がそれを解決できない
  • implicit な変数はローカルスコープではなく import のスコープにいる

になります。一応 暗黙のパラメータ解決優先順位 なんかを見てみるものの、import した場合は Prefix なしの状態で参照できることとなっているが、 import Hoge._ しているのでそこも問題なさそうである。

解決法

まぁ、タイトルにもしているのでお気づきかと思いますが、この問題は implicit parameter と左辺値型推論が組み合わさると発生するようです。なので明示的に型注釈を与えてみます。

object Main extends App{
    import Hoge._
    
    val h = new Hoge
    h.hoge
}

class Hoge {
    def hoge(implicit s: String) { println(s) }
}

object Hoge {
    implicit val hs: String = "hoge!" // implicit な変数に型注釈をつける
}

と、コンパイルできるようになりました!出力もちゃんと hoge! になる。なんだったんだこれ...

解決した後に見つけたんですが OE さんがもう書いてた。死にたい。

Scala コミュニティでは定期的に話題になる問題のようです。きびしい。

Amazon.co.jpアソシエイト