2022年に読んだ技術書

年の瀬なので、2022年に読んだ技術書をまとめてみたいと思います。

ここでいう技術書とは、プログラマやソフトウェアエンジニアが読む、プログラミングやコンピューターサイエンスに関する書籍のことを指します。

ここに挙げる本は、最初から最後まですべてを通読したとは限りません。というか一部分しか読んでいない本の方が多いです。

実践ドメイン駆動設計

www.shoeisha.co.jp

個人開発で以下の記事のようなことをやっていたので、DDDやClean Architectureに関するものは結構読みました。エリック・エヴァンスやUncle Bobの原典も参考にしましたがたぶん初めて読んだのは去年以前で、『実践ドメイン駆動設計』は今年に初めて読んだので挙げました。

zenn.dev

zenn.dev

読みやすいコードのガイドライン

gihyo.jp

読みやすいコードに関する書籍は数ありますが、その中でも内容の納得度が高いと感じました。どう変えたら良くなるのかという指示も具体的で、実践もしやすそうです。

徹底攻略 データベーススペシャリスト教科書

book.impress.co.jp

10月にデータベーススペシャリストを受験し、無事合格しました。知識を入れるための読み物としてはこちらが良かったです。

OS、ネットワーク、etc.

bookplus.nikkei.com

www.ohmsha.co.jp

gihyo.jp

bookplus.nikkei.com

自分の知識が足りていない分野を潰していきました。まだ十分とは言えないですが、やはりこういった基礎的な部分は知っておいて損はないと思いました。

ちょうぜつソフトウェア設計入門

gihyo.jp

ソフトウェア設計について勉強するなら、第一に薦められる本だと思いました。デザインパターンからClean Architecture、TDDまで幅広く解説されていて、解説もとてもわかりやすいです。私はPHPをほとんど書けないのですが、特に問題なく読めています。

並行プログラミング入門

www.oreilly.co.jp

並行プログラミングについて興味が出たので読み始めました。並行と並列の定義から説明されています。そのうえアセンブリによる実装からRustによるモダンな実装まで、さらには数学的な理論まで解説してくれます。かなり良い本だと思いました。

型システム入門

www.ohmsha.co.jp

型システムの理論体系を勉強したくなったので読みました。初学者だと導出木に面食らうと思いますが、それさえ乗り越えてしまえば読めます。簡単な型付きプログラミング言語なら実装できるようになる…かもしれません。

Webブラウザセキュリティ

Webブラウザセキュリティ ― Webアプリケーションの安全性を支える仕組みを整理する – 技術書出版と販売のラムダノート

CORSは何のためにあるのか、が気になって読みました。

n月刊ラムダノート Vol.3, No.1(2021)

n月刊ラムダノート Vol.3, No.1(2021) – 技術書出版と販売のラムダノート

「継続」の記事が面白かったです。実は並行プログラミングとも関連がありそうです。

Software Design, WEB+DB PRESS

どちらも技術評論社から出ている雑誌ですが、面白い記事が多かったです。

gihyo.jp

データモデリングの記事が面白かったです。

gihyo.jp

Reactに関する記事が面白かったです。

gihyo.jp

リファクタリングの記事が面白かったです。凝集度と結合度といった指標について学ぶことができます。

gihyo.jp

Web APIの特集で、OpenAPIについて学ぶことができました。

gihyo.jp

JavaScriptの関数に関する記事が面白かったです。どれを使うか判断する参考になります。

gihyo.jp

あとは特定の号ではありませんが、WEB+DB PRESSで連載されている和田卓人さんの「サバンナ便り」が面白かったです。少し遅れてWeb上でも公開されるようです。

おわりに

思い出せる範囲で書きました。年末に思い出すのは大変なので、来年は読んだら書き留めておくと良さそうです。

EPUBをKindle用にできるだけ読みやすく変換する

EPUBファイルを手に入れた際、それをKindleで読むためにはmobiファイルに変換する必要がある。 しかし適当に変換してみると、読みにくいものができてしまうことも珍しくない。 今回はチューニング方法や、注意すべき点を簡単に説明する。

なお、DRMフリーの電子書籍を購入できるサイトについては以下にまとめてある。

htlsne.hatenablog.com

前提

「Send to Kindle」は使わない方がよい。

www.amazon.co.jp

試してみると、優先で転送したものとSend to Kindleで転送したものの見た目が異なっている。 Amazon側が再変換か何かをしてしまうらしい。 Send to Kindleは便利だが、期待した結果が得られないようなら使用を諦めたほうが良いだろう。

ツール

EPUBをmobiに変換する方法には主に2通りある。 Amazon公式のkindlegenツールと、OSS電子書籍管理ツールCalibreである。 kindlegenは細かい調整ができないため、今回はCalibreを用いる。

チューニング

私は製本の専門知識は持っていないので、それほど難しいことはできない。 読みにくいと感じるときは、フォントサイズか行間の広さに問題があることが多いように思う。 どちらもCalibreの変換ダイアログの「外観」タブから設定できる。

フォントサイズは「最小フォントサイズ」で指定できる。 入力ファイルのフォントサイズが小さすぎる場合も大きすぎる場合も、ここで調整ができるようだ。 14ポイントか15ポイントあたりに設定するのが良いように思われる。

行間の広さは「最小の行高さ」で指定できる。 デフォルトでは120%だが、150%から180%の間くらいを指定するのが良いように思われる。

RSA秘密鍵について

PEMとDERというものがあるがどういう違いがあるのか、ssh-keygenで作成される鍵とopenssl genrsaで作成される鍵は違うのか、など気になったので調査してまとめてみる。

形式について

PKCSという規格群が存在する。

PKCS - Wikipedia

この中でPKCS#1という規格があり、この中でRSA暗号秘密鍵や公開鍵のフォーマットが規定されている。 openssl genrsaで生成されるのはこちら。

RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2

PKCS#8という規格も存在し、RSA暗号に限らない秘密鍵のフォーマットが規定されている。こちらも使用されることがある。例えばJavaの標準クラスで読み込みが可能なのはこちらの形式。

PKCS8EncodedKeySpec (Java Platform SE 8)
RFC 5958 - Asymmetric Key Packages

PKCS#1やPKCS#8の中ではASN.1 (Abstract Syntax Notation One) という形式でフォーマットが記述されている。 ASN.1は抽象的な形でフォーマットを記述する記法であり、具体的なバイナリ列にするエンコード方法は定義されない。 ASN.1は暗号技術の文脈に限らず利用される。

Abstract Syntax Notation One - Wikipedia
RFC 6025 - ASN.1 Translation

具体的にはこのような形式で記述される。 (RFCのAppendix Aを参照。)

RSAPrivateKey ::= SEQUENCE {
    version           Version,
    modulus           INTEGER,  -- n
    publicExponent    INTEGER,  -- e
    privateExponent   INTEGER,  -- d
    prime1            INTEGER,  -- p
    prime2            INTEGER,  -- q
    exponent1         INTEGER,  -- d mod (p-1)
    exponent2         INTEGER,  -- d mod (q-1)
    coefficient       INTEGER,  -- (inverse of q) mod p
    otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

ASN.1で記述されたものをエンコードする方法として、DER (Distinguished Encoding Rules) というものが存在する。 他にもBERなどエンコード方法は存在するが、DERは一通りにエンコード方法が定まる点が優れている。 DERはテキストとは限らないバイナリ列になる。 DERをBASE64化して、改行を入れたりヘッダを入れたりしてテキスト形式にしたのがPEM形式 (Privacy Enhanced Mail) 。 テキストの方が扱いやすいためか、PEMの方が見ることが多い。

つまりPKCS#1のPEMとか、PKCS#8のDERとか、これだけで2 * 2 = 4通りある。

ややこしいことに、OpenSSHはバージョン7.8以降から別の形式を採用している。 このバージョンから、ssh-keygenで生成される鍵フォーマットがPEM形式から独自形式 (OpenSSH format) に変更になった。これはPKCS#1ともPKCS#8とも異なる形式で、RFC4716で定義されている。見た目はBASE64化されておりPEMに似ているが、厳密にはPEMと異なる形式らしい。

https://www.openssh.com/txt/release-7.8
https://tools.ietf.org/html/rfc4716#section-3.5

ここまでのまとめ

一旦まとめる。 フォーマットとして大きく異なるPKCS#1とPKCS#8がある。PEMやDERはそのエンコード(符号化)方法。 全く違う形式としてOpenSSH形式がある。

鍵の中身を見てみる

opensslコマンドで鍵の中身を確認することができる。 -nooutオプションでPEM形式の出力を抑制し、-textオプションで内容をテキスト形式で表示する。 PKCS#1でもPKCS#8でも可能だが、以下はPKCS#1の例。なおOpenSSH形式は無理そう。 (長いので...部分は省略している)

$ openssl rsa -in a.pem -noout -text
RSA Private-Key: (4096 bit, 2 primes)
modulus:
    00:b0:e7:d4:47:2c:63:9c:f6:a8:20:b2:8b:f0:3f:
    ...
publicExponent: 65537 (0x10001)
privateExponent:
    31:a8:21:72:09:07:bd:1b:8f:7d:fe:20:41:c3:b8:
    ...
prime1:
    00:eb:94:66:41:e2:e8:42:af:4d:dc:b3:3b:bc:f0:
    ...
prime2:
    00:c0:3d:6d:ff:98:4c:6c:39:e9:5b:fe:47:ab:20:
    ...
exponent1:
    00:b9:96:e9:90:52:60:43:d8:b3:70:81:4b:38:a8:
    ...
exponent2:
    00:b8:b5:cd:8d:3a:ce:a3:66:79:7c:74:b4:74:0e:
    ...
coefficient:
    64:8d:8c:58:fb:fe:8e:43:98:89:c1:2e:16:69:4d:
    ...

RSA暗号について知識があれば、原理通り主に2つの素数からなっていることが見て取れる。

生成する

PKCS#1

$ openrsa genrsa

これだと標準出力に出力される

$ openrsa genrsa 4096 -out a.pem

鍵の長さを指定したり、-outで出力ファイルを指定したりすることが多い。 パスフレーズを指定したい場合は、-aes256 オプションをつければ良い。

ssh-keygenでもオプションをつければ作れる。

$ ssh-keygen -m PEM

PKCS#8

ssh-keygenを使えば一応可能。

$ ssh-keygen -m PKCS8 -f openssh.key

OpenSSH形式

バージョン7.8以降のssh-keygenをそのまま使うだけ。-fは出力ファイル。

$ ssh-keygen -f openssh.key

形式を変換する

必要そうなものだけにして、網羅はしない。

PKCS#1からPKCS#8

opensslコマンドでできる。

$ openssl pkcs8 -in a.pem -topk8

パスフレーズが不要な場合は-nocryptオプションを付ける。

$ openssl pkcs8 -in p1.key -topk8 -nocrypt

-outformオプションでPEMかDERか指定できる。デフォルトはPEM。

$ openssl pkcs8 -in a.pem -topk8 -nocrypt -outform DER

これだと標準出力に出力されるので、ファイルに保存したい場合はリダイレクトするか-outオプションで出力ファイルを指定する。

PKCS#8からPKCS#1

openssl rsaでできる。

$ openssl rsa -in p8.key

-outformオプションでPEMかDERか指定できたり、-outで出力ファイルが指定できるのは同じ。

OpenSSH形式からPKCS#1

素直にはできない。ssh-keygenの、形式を指定する-mオプションと、パスフレーズを変更する-pオプションを組み合わせるとできる。 ただし、元のファイルを置き換えてしまうので注意。

$ cp a.key b.key    # 置き換えてしまうので一旦コピー
$ ssh-keygen -f b.pem -m PEM -p

なおこの場合の-fオプションは入力ファイルを表す。

形式を見分ける

どこまで厳密化はわからないが、PEMのヘッダを見ればある程度わかる。

PKCS#1

-----BEGIN RSA PRIVATE KEY-----

PKCS#8(暗号化なし)

-----BEGIN PRIVATE KEY-----

PKCS#8(暗号化あり)

-----BEGIN ENCRYPTED PRIVATE KEY-----

OpenSSH形式

-----BEGIN OPENSSH PRIVATE KEY-----

Javaで読み込む

標準ライブラリで読めるのはおそらくPKCS#8だけ。DER形式のPKCS#8にすると読める。 詳しくは以下を参照。

https://stackoverflow.com/a/19387517

Bouncy Castleを使えばPKCS#1を直接読める。

https://www.bouncycastle.org/

参考

DRMフリーのIT技術書が買えるストア

よくあるネタだけど、自分が知っている範囲でまとめてみる。

O'Reilly Japan

www.oreilly.co.jp

EPUB or PDF。ご存知オライリー。日本のオライリーなので、洋書はない。価格は紙で買うより若干安めに設定されている。

技術評論社

gihyo.jp

EPUB or PDF。紙書籍の税抜き価格で買える。

インプレス

book.impress.co.jp

EPUB or PDF。自社出版物を売っている。

達人出版会

tatsu-zine.com

EPUB or PDF。自社の出版物の他、インプレスオーム社など他社の出版物も委託販売している。とりあえずここで探してみると良い。

Tech Book Zone Manatee

book.mynavi.jp

EPUB or PDF。マイナビが運営している。インプレスなど他社出版物もあり。

Google Play

play.google.com

意外に穴場。オライリーの洋書など、DRMフリーで買えるものもある。

翔泳社

www.seshop.com

どうやら形式はPDFのみ。なのでリフロー形式で読みたい場合には、DRMはかかってしまうがKindleなどで買うのも視野に入る。

マイナビブックス

book.mynavi.jp

EPUB or PDF。同じくマイナビが運営している。多分上記Manateeが上位互換なので忘れてよし。

MacでThinkpad Trackpoint Keyboard IIを使う

www.lenovo.com

MacBook Proのバタフライキーボードは全く好きになれないので、Trackpoint Keyboard IIを購入して使っている。しかし快適に使うのには設定が必要で、それなりに苦労したのでまとめておく。

なお単純に使用するだけなら、Bluetooth接続、USB接続ともに何の設定もなく使える。トラックポイントも動く。

今回の対象モデルは日本語配列で、macOSのバージョンはMojave。

変換・無変換キーやControlの位置

デフォルトでは変換・無変換キーはかな・英数キーとして使えない。標準でそれを設定する項目も見当たらない。またAの左隣のキーはCapsLockになっているが、ここはControlにしたい。 これらの問題はKarabiner-Elementsを導入することで解決できる。

karabiner-elements.pqrs.org

スクロール方向の問題

ナチュラルスクロールを有向にしていると、トラックポイント+センターボタンでのスクロールの向きが通常のThinkpadとは逆向きになってしまう。 理想としてはトラックパッドナチュラルスクロールを維持しつつ、トラックポイントのスクロールは逆向きにしたい。 Scroll Reverserというアプリケーションを導入することで解決することができる。"Reverse Mouse"だけオンにしておくと良い。

pilotmoon.com

トラックポイントのカーソル速度の問題

トラックポイント使用時のカーソル速度は、Macのシステム設定のマウス設定で調整できるが、上限いっぱいにしてもそれほど速くならない。 ステアーマウスというアプリケーションを導入し、「感度」を設定することで調整することができる。

plentycom.jp

しかしKarabiner-Elementsと併用しようとすると罠がある。 Karabiner-Elementsの"Modify events from this device"をオンにしていると、感度や加速度の設定が反映されない。 このことは、ステアーマウス上の「サポートに問い合わせ」ボタンを押した際に表示されるメッセージからも確認することができる。

Caution: マウスが機能しない場合は、Karabiner-Elements PreferencesのDevicesタブで、このマウスの "Modify events from this device" オプションをオフにする必要があります。

しかし"Modify events from this device"をオフにしてしまうと、キーボードの設定も反映されなくなってしまい、これでは困ってしまう。

回避策としては、USB無線レシーバーを使って接続すると良い。 Bluetooth接続だと前述の"Modify events from this device"でマウスとキーボードが一緒になってしまい一括変更しかできないが、USB接続だとマウスとキーボードを別々に設定できるようになる。 そのためマウスのみチェックを外すことで、Karabiner-Elementsのキーボード設定を有効にしつつ、マウスの感度はステアーマウスで設定できるようになる。

USBレシーバーをつけるのは面倒だが(USB Type-Aの端子がない場合は特に)、サポートに連絡してみても他に解決方法はないようだ。残念だがこの設定でしばらく使ってみようと思う。

ABC 161D Lunlun Number

本番では解けなかった問題。敗因は全列挙できることに気づけなかったこと。

別解ができたので貼っておく。

考えたこと

ルンルン数が十分密に存在するなら1から順に判定していけるかと思った。しかし次のようなコードでルンルン数の総数を調べたところ無理そうだとわかった。 今になって雑に見積もってみると、解説の解法から考察すると、上限が一桁増えるとルンルン数の総数は大体3倍くらいに増える。 そのため107まで調べても1万個に満たないルンルン数しか発見できず、この解法では間に合わない。

fn main() {
    let n = 100_000_000;
    let mut count = 0;
    for i in 1..=n {
        if check(i) {
            count += 1;
            // eprintln!("{}", i);
        }
    }
    println!("{}", count);
}

fn check(n: u64) -> bool {
    let mut ok = true;
    let s: Vec<u32> = n.to_string().chars().map(|x| x.to_digit(10).unwrap()).collect();
    let prev = s[0];
    for d in s {
        if (d as i64 - prev as i64).abs() > 1 {
            ok = false;
            break;
        }
    }

    ok
}

そこでルンルン数の総数が高速に計算できれば、二分探索と組み合わせてk番目のルンルン数が見つけられるのではないかと考えた。 総数を数えるには、桁DPの考え方が適用できる。

本番中には実装が終わらなかったが、その後仕上げたのが次のコード。

use proconio::input;

#[allow(unused_macros)]
macro_rules! multi_vec {
    ( $elem:expr; $num:expr ) => (vec![$elem; $num]);
    ( $elem:expr; $num:expr, $($rest:expr),* ) => (vec![multi_vec![$elem; $($rest),*]; $num]);
}

fn main() {
    input! {
        k: u64,
    }

    let mut mx = 1;
    while calc(mx) < k {
        mx *= 10;
    }
    // dbg!(mx, calc(mx));
    let mut l = 0;
    let mut r = mx;
    while l + 1 < r {
        let mid = (l + r) / 2;
        if calc(mid) < k {
            l = mid
        } else {
            r = mid
        }
    }
    let ans = r;
    println!("{}", ans);
}

fn calc(m: u64) -> u64 {
    let s: Vec<u8> = m.to_string().chars().map(|x| x as u8 - b'0').collect();
    let l = s.len();
    // dp[i][j] = i桁目まで、jは超えないことが確定したか、kは今の桁数字
    let mut dp = multi_vec![0; l + 1, 2, 10];
    for i in 1..=l {
        let d = s[i - 1] as usize;
        for k in 0..=9 {
            // 超えない確定
            if k > 0 {
                dp[i][1][k] += dp[i - 1][1][k - 1];
            }
            dp[i][1][k] += dp[i - 1][1][k];
            if k < 9 {
                dp[i][1][k] += dp[i - 1][1][k + 1];
            }

            // dbg!(dp[1][1][1]);
            // dbg!(i, k, dp[2][1][2]);

            // 超えるかわからない
            if k > 0 {
                if k < d {
                    dp[i][1][k] += dp[i - 1][0][k - 1];
                } else if k == d {
                    dp[i][0][k] += dp[i - 1][0][k - 1];
                }
            }
            if k < d {
                dp[i][1][k] += dp[i - 1][0][k];
            } else if k == d {
                dp[i][0][k] += dp[i - 1][0][k];
            }
            if k < 9 {
                if k < d {
                    dp[i][1][k] += dp[i - 1][0][k + 1];
                } else if k == d {
                    dp[i][0][k] += dp[i - 1][0][k + 1];
                }
            }

            // dbg!(i, k, dp[2][1][2]);

            // この桁から始まる数字
            if k > 0 {
                if i == 1 {
                    if k < d {
                        dp[i][1][k] += 1;
                    } else if k == d {
                        dp[i][0][k] += 1;
                    }
                } else if i > 1 {
                    dp[i][1][k] += 1;
                }
            }

            // dbg!(i, k, dp[2][1][2]);
        }
    }

    // dbg!(m, &dp);

    let mut ret = 0;
    for i in 0..=9 {
        ret += dp[l][0][i] + dp[l][1][i]
    }

    ret
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calc_test() {
        assert_eq!(calc(1), 1);
        assert_eq!(calc(2), 2);
        assert_eq!(calc(9), 9);
        assert_eq!(calc(10), 10);
        assert_eq!(calc(11), 11);
        assert_eq!(calc(12), 12);
        assert_eq!(calc(21), 13);
        assert_eq!(calc(23), 15);
    }
}

powerlevel10kとfzf-tmux

zshのプロンプトをpureからpowerlevel10kに移行しようとしたところ、fzf-tmuxと食い合わせが悪いことがわかった。 以下のような関数で再現する。

func() { var=$(fzf-tmux); print -z $var }

例えばREADME.mdを選択すると、

❯ README.md

となってほしいところ、何故か表示上では

❯ README.mdREADME.md

となってしまう。なお表示がおかしいだけで、実際のコマンドライン内容には問題ない。

デフォルトオプションのfzf-tmux -dだとほぼ確実に起こり、fzf-tmux -rだとまず起こらない。fzf-tmux -uだと起こったり起こらなかったり。

原因はおそらく、tmuxでウインドウのリサイズが走るため。 https://github.com/romkatv/powerlevel10k/blob/master/README.md#horrific-mess-when-resizing-terminal-window 公式ドキュメントにもリサイズはおかしくなると書いてあるので、Issue等は作成していない。

対処法としてはfzf単体を使うか、fzf-tmux -rなどとして横分割にするか。 横分割は画面の描画が崩れてしまい辛いので、私はとりあえずfzf単体で使ってみることにした。