oinume journal

Scratchpad of what I learned

Audibleにハマったのでオススメする記事

AmazonでAudibleが3ヶ月無料キャンペーンをやっていたので試しに使ってみたところ、かなり自分のツボに刺さったので良いポイントをまとめてみた。

自分的にAudibleが刺さったポイント

「本を読みたいけど時間がない...」というのが、Audibleに出会う前の自分だった。いや、正確には「読書したいけどスマホのKindleで読んでいると気付いたら他のことをしている」というのが実態だった。Audibleであれば、ウォーキング中や通勤時間などのスキマ時間に「耳」から本の内容をインプットできるので、「気付いたらスマホで違うことをしていた」ということが起きにくい。そのため、Audibleで本を聴くようになってから圧倒的に読書習慣が根付いた。

豊富なコンテンツ

詳しくは Audibleのサイト を見てもらうとして、対象の書籍の幅の多さに驚く。たとえば小説であれば、「三体」「プロジェクトヘイルメアリー」などヒットしたものは大体あるイメージ。ビジネス書も「イシューからはじめよ」「コンサル一年目が学ぶこと」などそれなりのラインナップがある。

もともと自分はウォーキング中にはPodcastを聴いていたのだけど、あまり興味のあるPodcastもなく惰性で聴いていた。Audibleであれば書籍が対象なので得られる情報の質がPodcastに比べて高いし、密度が濃いと感じる。

なお、Audibleは本の内容を人間が読み上げているため、小説だと「その声じゃないんだよなぁ」と思うこともあるがこれはしょうがない。

Audibleアプリのクオリティが高い

Audibleアプリは、単純にただのAudiobookよりさらに良い体験を提供していると思っている。具体的には、再生速度が0.05刻みで細かく調節できるのと、クリップ&メモ機能が便利だ。

クリップ機能は、読まれている本の位置を保存する機能でいわゆるしおりみたいなもの。クリップしておいた場所に後からジャンプできるのと、そのクリップに対して自分用のメモを保存することができる。メモをする場合は自動で再生が止まるようになっているのも気が利いていて良い。

月1500円で聴き放題

中には別途お金を払う必要がある本もあるが、大体のものは1500円で聴けるので、聴いてみて「これはイマイチだな」と思ったらやめればいいし、逆に気に入ったら書籍を購入するのも良い。本屋での立ち読み感覚で色々聴くことができるのがグッド。

目にやさしい

Audibleを再生している間はなるべくスマホは見ないで外界を見るようにしている。本を読んでいるとどうしても目が疲れてしまう問題があるけど、Audibleではスマホさえ見なければ目は疲れない。

イマイチな点

再生速度を2倍にするなどして速度を上げることでカバーできるが、書籍を読む速読に比べると遅いという問題はある。あと上で書いたように小説はナレーターの声が自分のイメージと違うことが多いので、Audibleで聴くには向かないかもしれない。

まとめ

「Amazonから金でももらっているのか?」というような記事になってしまったけど、Audibleは自分の読書週間を大幅に改善してくれたプロダクトなのでオススメ記事を勢いで書いた。以前Audibleを試して「コンテンツが少ない」と思って使わなくなった人ももう1回見てみるといいかも。

Go言語でcodemod

大規模なコードベースでリファクタリングを省エネ化するためにcodemodを最近調べていて、軽く試行錯誤したのでそのメモ。

やりたいこと

例えば以下のようなTable Driven TestなコードをBEFOREからAFTERに書き換えたい。コード量が多いため人間がやるのは現実的ではなく、codemodで機械的に書き換えたい。

BEFORE

package main

import (
    "slices"
    "testing"
)

func TestContains(t *testing.T) {
    type args struct {
        ss []string
        s  string
    }
    tests := []struct {
        name string
        args args
        want bool
    }{
        {
            name: "empty: false",
            args: args{[]string{}, ""},
            want: false,
        },
        {
            name: "found: true",
            args: args{[]string{"a", "b", "c"}, "b"},
            want: true,
        },
        {
            name: "not found: false",
            args: args{[]string{"a", "b"}, "c"},
            want: false,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := contains(tt.args.ss, tt.args.s); got != tt.want {
                t.Errorf("contains(): want=%v, got=%v", got, tt.want)
            }
        })
    }
}

func contains(ss []string, s string) bool {
    return slices.Contains(ss, s)
}

AFTER

func TestContains(t *testing.T) {
    type args struct {
        ss []string
        s  string
    }
    tests := map[string]struct {
        args args
        want bool
    }{
        "empty: false": {
            args: args{[]string{}, ""},
            want: false,
        },
    }
    ...
}

DIFF

--- a/tools_eg/table_driven_test.go
+++ b/tools_eg/table_driven_test.go
@@ -10,29 +10,25 @@ func TestContains(t *testing.T) {
        ss []string
        s  string
    }
-  tests := []struct {
-      name string
+   tests := map[string]struct {
        args args
        want bool
    }{
-      {
-          name: "empty: false",
+       "empty: false": {
            args: args{[]string{}, ""},
            want: false,
        },
-      {
-          name: "found: true",
+       "found: true": {
            args: args{[]string{"a", "b", "c"}, "b"},
            want: true,
        },
-      {
-          name: "not found: false",
+       "not found: false": {
            args: args{[]string{"a", "b"}, "c"},
            want: false,
        },
    }
-  for _, tt := range tests {
-      t.Run(tt.name, func(t *testing.T) {
+   for name, tt := range tests {
+       t.Run(name, func(t *testing.T) {
            if got := contains(tt.args.ss, tt.args.s); got != tt.want {
                t.Errorf("contains(): want=%v, got=%v", got, tt.want)
            }

試したこと

eg

egというのは golang.org/x/tools 配下で提供されているコード書き換えのコマンドのこと(link)。例えば以下のようにdeprecatedな ioutil.ReadAll を呼び出している箇所をtemplateのGoコードを用いて io.ReadAll に書き換えることができる。

http_get.go

package main

import (
    "fmt"
    "io/ioutil" //nolint:staticcheck
    "net/http"
)

func main() {
    resp, err := http.DefaultClient.Get("https://github.com/golang/go")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}

http_get.template

package main

import (
    "io"
    "io/ioutil"
)

func before(r io.Reader) ([]byte, error) {
    return ioutil.ReadAll(r)
}

func after(r io.Reader) ([]byte, error) {
    return io.ReadAll(r)
}

egを使って書き換える

$ eg -t http_get.template -w http_get.go

ただ、egでは先ほどのようなTable Driven Testの struct -> map[string]struct にするのは eg: map[string]struct{} is not a safe replacement for struct{name string} のエラーでできなかった(ref)。

なので、次に紹介するast-grepを試してみた。

ast-grep

ast-grepは様々な言語に対応した、ASTベースでコードの検索、Linterの作成、コード書き換えを行えるコマンドラインツールである。YAMLファイルによる独自のルールを定義することによって、かなり複雑なパターンマッチングを行うことができる。

ASTツリーを表示しながらパターンにマッチした部分をハイライトするPlaygroundも用意されているので、AST初心者でも試行錯誤すればルールが定義できそうな雰囲気を感じる。自分はまだ使いこなせてないので、とりあえず紹介だけ。

これを使えばおそらく先ほどのTable Driven Testのコードは書き換えられそうな気がする。

その他のツール

  • TypeScriptだとts-morphというものがあり、これのGo版が欲しい...!!!
  • Codemodというコード書き換えのためのSaaSがある。内部的にはast-grepなどを呼び出すっぽい

最後に

とりあえずast-grepで試行錯誤して上手くいったらまた記事を書きます。

Raycastを使い始めて1年経った

Raycastを使い始めて1年経ったので、どういうことに使っているかを振り返ってみる。去年書いた AlfredからRaycastに移行した - oinume journal の記事から少し使い方が変わっているところもあるのでメモがてら。

基本的な使い方

  • Cmd + QをRaycast起動のショートカットとして割り当てている。Pro版は使っていないのでAI機能などは使ったことがない。
  • ブラウザのブックマーク検索など、よく使うけどHotKeyを割り当てるほどでもないRaycastコマンドはbmのようにAliasを設定している。
    • Cmd + QでRaycastを起動してbmと入力するとブックマークの検索ができるので楽ちん

アプリケーションランチャー機能

アプリケーションを起動するときのランチャーとして使っている。よく使うアプリにはHot Key(ショートカット)を割り当ててる。

Clipboard History

クリップボードの履歴を溜めておいて、後から検索してペーストできる機能。Ctrl + Cmd + CをHotKeyとして割り当てて瞬時に起動できるようにしている。

絵文字検索

正確にはSearch Emoji & Symbolsという機能。これが地味に便利な機能で、こんな感じで英語で絵文字を検索して選択するとクリップボードにコピーされるのですぐにペーストできる。

スクリーンショットの検索&クリップボードへのコピー&SlackやGitHubへ貼り付け

これも便利な機能で、以下のような流れで使うのだけど、スクショを貼り付けるのがキーボード操作だけでできるようになる。

  • スクリーンショットを撮った後にRaycastを起動
  • screenshotで検索して機能を起動して、スクショをサムネイルで検索して選択
  • するとクリップボードにスクショがコピーされるので、SlackやGitHubなどにペーストするとスクショが貼り付けられる。

ブラウザのブックマーク検索

正式名称はSearch Browser Bookmarks。ブラウザのブックマークを検索することができる。SafariとChromeがある場合は、Cmd + KでSelect Browsersを選ぶと対象のブラウザを選択することができる(Safari, Chromeの両方を対象にすることも可能)。

開いているウィンドウの検索・切り替え

正式名称はSwitch Windows。今開いているウィンドウをfuzzy検索できる。自分はIntelliJ IDEAを使っていてたくさんウィンドウを開いているので、これで検索してウィンドウを切り替えることが多い。

Chromeの履歴検索

「あの閉じてしまったタブをもう1回開きたいなぁ」という時に意外と重宝するのがChromeの履歴検索である。Chrome Extensionをインストールすると使えるようになる。

使わなくなった機能

最初便利だなと思っていたけど使わなくなった機能もある。

  • Quicklink: ブラウザのブックマークで十分だと感じた
  • カレンダー連携(My Schedule): ChromeでGoogleカレンダーを固定タブで開いているのでRaycastから飛べなくてもいいかなと思った。あと、MacのカレンダーとGoogle Calendarを連携するのが嫌になったというのもある。

Raycastの使いこなせていない機能

Raycast便利に使っているけど、Script CommandsやExtensionを作れるほどのレベルにはなっていないので、今年はそれらに挑戦してみたい。

HasuraをDokku上で動かす

VPS上にDokkuを構築したので、そこにHasuraを動かしてみるテスト。Hasuraとは簡単に言うとPostgreSQLのテーブルスキーマからGraphQLサーバーを構築してくれるミドルウェア。

前提として、Dokkuはすでに構築済みとする。(自分の場合はUbuntu 22.04 上にDokku 0.32.3を構築済み)

手順

まずは最初にhasuraという名前のアプリケーションを作成する

$ dokku apps:create hasura

次に、HasuraのためのPostgreSQL databaseを建てる。

$ dokku postgres:create hasura-db
       Waiting for container to be ready
       Creating container database
       Securing connection to database
=====> Postgres container created: hasura-db
=====> hasura-db postgres service information
       Config dir:          /var/lib/dokku/services/postgres/hasura-db/data
       Config options:
       Data dir:            /var/lib/dokku/services/postgres/hasura-db/data
       Dsn:                 postgres://postgres:<redacted>@dokku-postgres-hasura-db:5432/hasura_db
       Exposed ports:       -
       Id:                  272a6f8b722310887317b3d7e10821ed3cfcc88dae7911945c10590be338c09c
       Internal ip:         172.17.0.4
       Initial network:
       Links:               -
       Post create network:
       Post start network:
       Service root:        /var/lib/dokku/services/postgres/hasura-db
       Status:              running
       Version:             postgres:15.4

上で構築したhasura-dbをhasuraアプリケーションにリンクする。ただし、この時点ではまだhasuraはアプリケーションの設定がされていないのでApp image (dokku/hasura:latest) not foundのエラーが出るけどOK。

$ dokku postgres:link hasura-db hasura
-----> Setting config vars
       DATABASE_URL:  postgres://postgres:<redacted>@dokku-postgres-hasura-db:5432/hasura_db
-----> Restarting app hasura
 !     App image (dokku/hasura:latest) not found

次にhasura向けの環境変数を設定する。HASURA_GRAPHQL_DATABASE_URLで指定するDSNは上のコマンドで得られたDATABASE_URLをコピーしてくる。また、your_admin_secretはパスワードみたいなものなので、ランダム文字列にするのが良い。

$ dokku config:set hasura \
 HASURA_GRAPHQL_DATABASE_URL="postgres://postgres:<redacted>@dokku-postgres-hasura-db:5432/hasura_db" \
 HASURA_GRAPHQL_ADMIN_SECRET="your_admin_secret" \
 HASURA_GRAPHQL_ENABLE_CONSOLE="true" \
 HASURA_GRAPHQL_SERVER_PORT=5000

次にhasuraアプリケーションのためのdocker imageをpullしてくる。

$ docker pull hasura/graphql-engine:v2.36.1
FAIL

上のコマンドを実行するユーザーがdockerグループに所属していないと権限がなくてpullできないので、自分の場合はdokkuユーザーになってpullした。

$ sudo su - dokku
$ docker pull hasura/graphql-engine:v2.36.1

そして最後に dokku git:from-imageコマンドでhasuraアプリケーションにpullしてきたimageを指定してアプリケーションとしてデプロイする。

$ dokku git:from-image hasura hasura/graphql-engine:v2.36.1

これで http://hasura.<your_dokku_domain>/ のURLにアクセスするとHasura Consoleが表示されるはず。もしエラー画面が出ている場合は、/var/log/nginx/hasura-error.log を見てみると何かヒントがあるかもしれない。

ハマった点

Dokku上のコンテナのlisten portはデフォルトだと5000になるということを知らずに、環境変数HASURA_GRAPHQL_SERVER_PORTを指定していなかったため、Hasuraはデフォルトの8080でlistenしていてDokkuのNginxからDocker containerへのproxyがうまくできていなかった。

そのため

We're sorry, but something went wrong. If you are the application owner check the logs for more information.

のエラーがずっと出ていたけど(スクショ)、解決方法がなかなかググっても出てこずにハマった。

DokkuのPort Managementのドキュメントに書いてはあるのだが、正直そんなの知らんわって思った。でもおかげでDokkuのport mappingの仕組みが理解できたので良かったけど。

成功したら...

以下のような画面が出るので、HASURA_GRAPHQL_ADMIN_SECRETの環境変数で指定したシークレットを入力するとめでたくHasuraのConsoleが見れるようになる。

※この記事を書き終わってHasura on Dokkuの構築自体に満足してしまい、肝心のアプリケーションの作成はまだできていない。

squash mergeの環境でCascading PRsでコンフリクトした時

最近以下のような記事を目にすることがあり、ちょうど自分もsquash mergeの環境でひとつ問題を抱えていたので、その話を自分のメモ代わりに書きたくなった。

問題とは、上にある記事のcascading PRsの話で、feature1 -> feature2 というようにbranchを派生して作った場合に、feature1 をmainにマージした後に作業ブランチを feature2 にして git rebase main すると、必ずコンフリクトが発生するので悩んでいた、ということ。

解決策としては、以下にあるようにgit rebase --onto main <last commit on feature1> feature2 をすればいいらしい。「らしい」というのはまだ自分は試してないけど、同僚がこれで「うまくいけた」と言っていたので。

stackoverflow.com

もしくはJun Mukai's blogに書いてあるような以下の方法でもいいのかもしれない。そういえば git rebase --skip って使ったことなかったな。

PR1に由来するコンフリクトは自分にとっては自明だし、ほとんどの場合にはgit rebase –skipするだけだ。